本篇主要介绍:指针和变量的关系
、指针类型、指针的运算符
、空指针和野指针、指针和数组
、指针和字符串
、const 和指针、以及gdb 调试段错误
。
(资料图片)
基础概念指针是一种特殊的变量。存放地址的变量就是指针。
int num = 1;
会申请4个字节的内存来存放数字1,每次访问 num 就是访问这4个字节。
访问内存中的这4个字节,不仅可以通过名称(例如 num),还可以通过地址
。
Tip:&
不仅是位运算符,还是取地址
操作符。例如 int* ptr = #
,就是取变量 num 的地址并将其保存到指针变量 ptr 中
请看示例:
#include int main() { int num = 10; // num 的地址:0x7fff4dbf01d8 printf("num 的地址:%p\n", &num); // num 的地址加1 :0x7fff4dbf01dc。 printf("num 的地址加1 :%p\n", &num + 1); // j 存放连续内存的第一个字节地址 int *j = # // 10。通过地址访问 printf("%d", *j); return 0;}
&num
和 &num + 1
相差4个字节,说明 &num 表示整数。
普通变量存放值,而指针用于存放地址。
通过 int *j = &num
将变量num的首地址给到指针 j(j的类型是 int *
),最后通过地址(*j
) 访问整数1。
int *j
是一个int类型的指针,还有 char、float等指针类型。指针类型必须匹配,比如将 j 的指针类型换成 char,则会警告。就像这样:
- int *j = #+ char *j = #
运行:
/workspace/CProject-test/main.c:12:11: warning: incompatible pointer types initializing "char *" with an expression of type "int *" [-Wincompatible-pointer-types] char *j = # ^ ~~~~1 warning generated.num 的地址:0x7ffddcfe5328num 的地址加1 :0x7ffddcfe532c10
Tip: 指针 j 也有地址,也就是指针的指针。现在不研究
练习题目
:请问输出什么?
#include int main() { int num = 10; int *p = # printf("用指针访问数据 num :%d\n", *p); *p = 11; printf("用过指针修改 num 数据:%d\n", num); return 0;}
提示:数据可以通过变量访问,也能使用地址(指针)访问。就像通知同学去嵌入式实验室上课,或者是 303 上课。其中*p = 11;
等价于 num = 11;
输出:
用指针访问数据 num :10用过指针修改 num 数据:11
星号的作用指针 *
有两个主要作用(根据*
前面有无类型做区分):
请看示例:
#include int main() { int num = 10; // 指针类型声明 int *p = # // 取值 printf("%d\n", *p); // 10 // 取值 *p = 11; printf("%d\n", num); // 11 return 0;}
指针类型所占字节在32位系统上,指针通常占用4个字节;而在64位系统上,指针通常占用8个字节。请看示例:
#include int main() { printf("char类型指针所占字节数为:%zu\n", sizeof(char*)); printf("short类型指针所占字节数为:%zu\n", sizeof(short*)); printf("int类型指针所占字节数为:%zu\n", sizeof(int*)); printf("long类型指针所占字节数为:%zu\n", sizeof(long*)); printf("float类型指针所占字节数为:%zu\n", sizeof(float*)); printf("double类型指针所占字节数为:%zu\n", sizeof(double*)); printf("long long类型指针所占字节数为:%zu\n", sizeof(long long*)); return 0;}
输出:
char类型指针所占字节数为:8short类型指针所占字节数为:8int类型指针所占字节数为:8long类型指针所占字节数为:8float类型指针所占字节数为:8double类型指针所占字节数为:8long long类型指针所占字节数为:8
练习题目
:请问整数类型的指针和字符类型的指针加1分别是几个字节?
#include int main() { int num = 10; printf("num 的地址:%p\n", &num); printf("num 的地址加1 :%p\n", &num + 1); char ch = "a"; printf("ch 的地址:%p\n", &ch); printf("ch 的地址加1 :%p\n", &ch + 1); return 0;}
输出:
num 的地址:0x7fffe8244288num 的地址加1 :0x7fffe824428cch 的地址:0x7fffe8244287ch 的地址加1 :0x7fffe8244288
答案
:int *
加1是4个字节;char *
加1是1个字节。&num 和 &ch 分别代表该变量的全部字节。
比如这段代码是不能实现 a、b 两数交换。请看示例:
#include void swap(x, y){ int tmp = x; x = y; y = tmp;}int main() { int a = 1; int b = 2; swap(a, b); printf("a:%d\n", a); printf("b:%d\n", b); return 0;}
a:1b:2
分析:调用 swap(a, b)
这里是一个值传递,找到函数入口地址,对参数 x、y 申请空间和赋值,通过 tmp 变量完成了 x和y的交换,最后回收局部变量 x、y和tmp,释放空间。而 a,b数据没有变化。
可以通过指针来实现两数的交换。请看示例:
#include void swap(int* x, int* y){ int tmp = *x; *x = *y; *y = tmp;}int main() { int a = 1; int b = 2; swap(&a, &b); printf("a:%d\n", a); printf("b:%d\n", b); return 0;}
a:2b:1
分析:通过 swap(&a, &b)
将 a b 的地址传给 x 和 y,通过 x 和 y 指针对 a 和 b 进行交换,虽然最后会销毁swap中的局部变量,但 a 和 b的值已经完成了交换。
指针和变量的关系
练习1题目
:输出什么?
#include int main() { int a = 10, *pa = &a, *pb; printf("%d\n", *pa); pb = pa; printf("%d\n", *pb); return 0;}
输出:10 10
分析:
int a = 10, // pa 指向变量 a*pa = &a, // 定义一个整数型的指针 pb*pb;printf("%d\n", *pa);// pb 也指向变量 apb = pa;printf("%d\n", *pb);return 0;
练习2题目
:输出什么?
#include int main() { int x = 3, y = 0, *px = &x; y = *px + 5; printf("%d\n", y); y= ++*px; printf("%d\n", y); printf("%p\n", px); y = *px++; printf("%p\n", px); printf("%d\n", y); return 0;}
输出:
840x7ffc48b9be380x7ffc48b9be3c4
分析:
y= ++*px;
等效 ++(*px)
。如果是 ++* 是不对的类似 y = ++i,等于先执行 ++,在执行 y = i,这里先对 (*px) 执行 ++,在返回 *px 的值
y = *px++;
先执行 y = *px,然后是 px++。px是整数类型的地址,加1就是加4个字节。
练习3题目
:输出什么?
#include int main() { int x = 3, y = 0, *px = &x; printf("%p\n", px); y = (*px)++; printf("%p\n", px); printf("%d\n", x); return 0;}
输出:
0x7ffef1dc4d580x7ffef1dc4d584
分析:*px++
表示指针加1,(*px)++
表示值加1。
指针初始化有两种方法:已经存在的空间和自己申请空间。
已经存在的空间,例如:
#include #include int main() { int num; int* p = # *p = 10; char *str = "abc"; printf("%s\n", str); // abc。把字符串的地址赋值给指针变量 return 0;}
自己申请空间可以使用 malloc
函数。申请的是 void 类型指针,也称为通用类型指针。请看示例:
#include // malloc 需要引入 #include int main() { // 申请16个字节 int* q = malloc(sizeof(int) * 4); // 在堆里申请了16个字节 // int* q = (int *)malloc(sizeof(int) * 4); // 推荐 *q = 10; // 释放申请的16个字节 free(q); return 0;}
申请空间,使用完需要使用 free()
释放。
Tip:根据 C99 标准以及更高版本的标准,显式的类型转换是建议的做法,以确保类型的安全性和可读性。
空指针和野指针下面这段代码 p 就是一个野指针,运行报错:段错误 (核心已转储)
:
#include int main() { int* p; *p = 1; return 0;}
这里声明一个指针 p,里面是一个随机数,例如 0x7ffe71df3f40
,接着往指向的内存放1,由于这块内存不知道是否存在,即使存在也不能访问,于是报段错误
。
直接手写一个地址也不可以。就像这样:
#include int main() { // warning: incompatible integer to pointer conversion initializing "int *" with an expression of type "long" [-Wint-conversion] // 这个警告是因为你正在将一个 long 类型的表达式赋值给一个 int* 类型的指针变量,导致类型不匹配。 // int* p = 0x7ffe71df3f40; int* p = (int *)0x7ffe71df3f40; *p = 100; return 0;}
// 分段错误 (核心已转储)"Segmentation fault (core dumped)
空指针
也不能使用:
int* p = NULL;*p = 100;// 输出:`Segmentation fault (core dumped)`
但空指针会让你可控。就像这样:
int* p = NULL;if (p != NULL) { printf("p is not NULL\n");}else{ printf("p is NULL\n");}// 输出:p is NULL
指针和数组指针当数组用
遍历一个数组,可以这样:
#include int main() { int arr[] = {1, 2, 3, 4, 5}; int length = sizeof(arr) / sizeof(arr[0]); // 计算数组的长度 // 1 2 3 4 5 for (int i = 0; i < length; ++i) { printf("%d ", arr[i]); } return 0;}
使用指针遍历数组有两种方式(效果相同)。请看示例:
#include int main() { int arr[] = {1, 2, 3, 4, 5}; int length = sizeof(arr) / sizeof(arr[0]); // 计算数组的长度 // 指针遍历方式1 /* int* pArr = arr; for (int i = 0; i < length; ++i) { printf("%d ", *(pArr + i)); } */ // 指针遍历方式2 int* pArr = arr; for (int i = 0; i < length; ++i) { printf("%d ", pArr[i]); } return 0;}
Tip:在数组一文中我们知道数组名表示首元素地址,这里*(pArr + i)
会依次遍历数组或许是因为指针是int类型吧!
总结:pArr[i]
等于 *(pArr + i)
。在这里[]
不再是取某个索引,而是表示取值。
题目
:分析 char a[] = "Hello";
和 char *b = "World";
#include int main() { char a[] = "Hello"; char *b = "World"; // Iterating over "a" printf("Characters in "a":\n"); for (int i = 0; a[i] != "\0"; i++) { printf("%c\n", a[i]); } // Iterating over "b" printf("\nCharacters in "b":\n"); for (int i = 0; b[i] != "\0"; i++) { printf("%c\n", b[i]); } return 0;}
输出:
开始运行...Characters in "a":HelloCharacters in "b":World运行结束。
为什么指针也可以通过索引访问特定字符?比如 char *b = "World";
,可以将字符串视为字符数组,使用指针来指向该数组的首地址,指针可以通过偏移来访问特定位置的元素,包括字符串中的字符。练习题目
:下面代码中 p1[0]
、p2[0]
、p3[0]
的值分别是多少?
// 申请4*4个字节,每个字节地址假如是:0x100(存放1) 0x104(存放2) 0x108 0x10cint a[] = {1,2,3,4};int *p1 = (int*)(&a + 1);int *p2 = (int*)((int)a + 1);int *p3 = (int*)(a + 1);
分析:
(int*)(&a + 1)
- &a 表示整个数组,加1则到下一个数组,然后将数组指针强转成整数指针,指向第5个元素,其实已经越界了。(int*)((int)a + 1)
- a 表示数组首元素地址,(int)a 将地址转为整数,以前是加1个元素,现在就是加1,然后又将整数转为整数指针,乱了(就好比访问 0x101 0x102 0x103 0x104
)(int*)(a + 1)
- a 表示数组首元素地址,加1则是第二个元素地址 0x104,不强转也可以。结论:只有p3[0]
(等价于 *(p3 + 0)
)是一个正常的元素,也就是2.
题目
:用数组和指针定义字符串有什么区别?
#include int main() { char str[] = "HelloWorld"; // HelloWorld printf("%s\n", str); char* s = "HelloWorld"; // HelloWorld printf("%s\n", s); return 0;}
Tip: 字符串的输出都是首地址,比如这里的 str 是数组的首地址,s 指针指向的也是首地址。
分析:char str[] = "HelloWorld";
在栈中定义一个数组,用11个字节存储HelloWorld
(还有一个 \0)。请看示例:
#include int main() { char str[] = "HelloWorld"; str[0]++; // IelloWorld printf("%s\n", str); // error: cannot increment value of type "char[11]" str++; // printf("%s\n", str); return 0;}
数组名(str++
)不可以修改,str 就是数组首元素地址,已经固定了,可认为它是常量。但数组内容可以修改。
char* s = "helloWorld";
将 helloWorld
放在只读数据区,s 是局部变量,放在栈中,占8个字节。请看示例:
#include int main() { char* s = "helloWorld"; s++; // elloWorld printf("%s\n", s); // 报错:Segmentation fault (core dumped) s[0]++; return 0;}
指针可以加加,但指针指向的内容不能修改。
str 只是个名字,不占空间,如果一定要说占多少,那就是它执行的数组占11个字节。而 s 是8个字节,指向一个只读区,占 11 个字节。
练习题目
:分析以下示例。
#include int main() { char str[20]; str = "HelloWorld"; char* s; s = "HelloWorld"; // HelloWorld printf("%s\n", s); return 0;}
分析:
// 分配20个字节的内存,并把首地址给 strchar str[20];// str 是只读的,不能再赋值。报错:`error: array type "char[20]" is not assignable`str = "HelloWorld";// 定义一个指针 schar* s;// 将 HelloWorld 的首地址给 ss = "HelloWorld";
扩展自定义strcpy()函数题目
:实现原生字符串拷贝方法strcpy。strcpy 其用法如下:
#include #include int main() { char source[] = "Hello"; char destination[10]; // 目标字符串需要足够的空间来容纳 source 字符串 strcpy(destination, source); printf("Source string: %s\n", source); printf("Destination string: %s\n", destination); return 0;}
实现:
#include char* strcpy_custom(char* destination, const char* source) { // 字符串数组末尾有一个特殊的空字符 "\0" 来表示字符串的结束。逐个复制字符,直到遇到源字符串的结束标志 "\0" while (*source != "\0") { *destination = *source; destination++; source++; } *destination = "\0"; // 在目标字符串末尾添加结束标志 "\0" return destination;}int main() { // 定义两个字符数组 char source[] = "Hello"; char destination[10]; // 目标字符串需要足够的空间来容纳 source 字符串 // 数组名。表示首元素的地址,加 1 是加一个元素(比如这里1个字节) strcpy_custom(destination, source); printf("Source string: %s\n", source); printf("Destination string: %s\n", destination); return 0;}
Tip:const char* source
中 const 的作用请看const 和指针
输出:
开始运行...Source string: HelloDestination string: Hello运行结束。
将 while 替换成下面一行代码效果也相同:
char* strcpy_custom(char* destination, const char* source) { /* while (*source != "\0") { *destination = *source; destination++; source++; } *destination = "\0"; */ // 替换成 while((*destination++ = *source++) != "\0"); return destination;}
分析:(*destination++ = *source++) != "\0"
:
之前的是首先判断,在赋值。`*source != "\0"`、`*destination = "\0";`,这里是先赋值后置++会放在表达式最后,所以等于:(*destination = *source) != "\0";destination++;source++;
const 和指针首先补充下(int*)
的作用。之前说到 const 定义的变量可以被修改,我们写了如下代码:
#include int main() { const int val =5; int *ptr= (int*)&val; *ptr=10; printf("val = %d\n",val); printf("*ptr = %d\n", *ptr); return 0;}
其中 int *ptr= (int*)&val;
是将一个 const int 类型的变量 val 地址强制转换为 int* 类型的指针,并将指针存储在 ptr 中。这种类型转换是不安全的,因为它丢失了 val 的常量性质。
const char* source
声明一个常量指针,以下代码仅做示意:
#include int main() { const char* source = "Hello"; char* mutableSource = "World"; printf("%c\n", source[0]); printf("%c\n", mutableSource[0]); // 以下操作是非法的,会导致编译错误 // source[0] = "h"; // 不能修改字符数据 // 合法 // 尽管mutableSource是一个非常量指针,看起来可以进行修改,但修改字符串常量是不被允许的,并且这可能导致未定义行为。 mutableSource[4] = "w"; // 可以修改字符数据 return 0;}
运行:
开始运行...HWSegmentation fault (core dumped)运行结束。
就近原则const 有个就近原则
:
const int* p1 = #
,const 修饰的是 *,所以 *p1 不能修改, p1 可以修改比如:int* const p2 = #
,const 修饰 p2,所以 p2 不能修改,*p2 可以修改请看示例:
#include int main() { int num = 1; const int* p1 = # // const 修饰的是 *,所以 *p1 不能修改, p1 可以修改 p1++; // (*p1)++; int* const p2 = # // const 修饰 p2,所以 p2 不能修改,*p2 可以修改 // p2++; (*p2)++; const int* const p3 = # // 两个都不能修改 // p3++; // (*p3)++; return 0;}
gdb 调试段错误GDB(GNU Debugger)是一款强大的调试器,用于帮助开发者查找和解决程序中的错误。通过与源代码交互,并提供诸如断点设置、变量观察、内存检查等功能,GDB允许开发者逐行执行程序并分析其运行状态。除了上文使用的 run,还有如下操作
run:运行程序。break
:在指定行设置断点。break
:在指定函数设置断点。continue:继续执行程序直到下一个断点或程序结束。next:逐过程地执行程序。step:逐语句地执行程序。print
:打印变量的值。backtrace:显示函数调用的堆栈跟踪信息。quit:退出GDB调试会话。使用 gdb 调试段错误的过程如下:
编写代码:
pjl@pjl-pc:~/pjl$ cat demo-3.c#include int main() { int* p; *p = 1; return 0;}
编译运行发现段错误:
pjl@pjl-pc:~/pjl$ gcc demo-3.c -o demo-3pjl@pjl-pc:~/pjl$ ./demo-3段错误 (核心已转储)
将代码编译为可调试
的可执行文件。在gcc或g++编译时,添加"-g"选项可以生成包含调试信息的可执行文件。
// 增加 -gpjl@pjl-pc:~/pjl$ gcc demo-3.c -o demo-3 -g// 启动GDB并加载可执行文件pjl@pjl-pc:~/pjl$ gdb demo-3GNU gdb (Ubuntu 9.1-0kylin1) 9.1Copyright (C) 2020 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law.Type "show copying" and "show warranty" for details.This GDB was configured as "x86_64-linux-gnu".Type "show configuration" for configuration details.For bug reporting instructions, please see: .Find the GDB manual and other documentation resources online at: .For help, type "help".Type "apropos word" to search for commands related to "word"...Reading symbols from demo-3...(gdb)
输入 run(还有其他操作) 找到是第5行代码报错:
...// run:运行程序。(gdb) runStarting program: /home/pjl/pjl/demo-3Program received signal SIGSEGV, Segmentation fault.0x0000555555555135 in main () at demo-3.c:55 *p = 1;(gdb)
高级指针提前透露:指针遇上数组
题目
:以下代码输出什么?
#include int main() { char * string[] = {"Hello", "World" }; printf("%s\n", string); return 0;}
分析:我们知道定义字符串有以下两种方法:
char str[] = "HelloWorld";char* s = "HelloWorld";
Tip: string 在 C 中不是关键字,也不是保留字,就是一个普通变量名。
[]
的优先级是非常高的,这里首先是定义一个数组(string[]
),其次就是指针,合起来就是一个指针数组。
首先在只读区分配两块内存分别存放 Hello(地址比如是 0x100) 和 World(地址比如是 0x200),指针数组是16个字节,本质就是数组,只不过里面放的是指针,比如前8个字节的地址是0x1000,那么 string 就是 0x1000,因为数组名就是数组首元素地址。
所以要输出这两个字符串,可以这么写:
#include int main() { char * string[] = {"Hello", "World" }; // Hello printf("%s\n", string[0]); // World printf("%s\n", string[1]); return 0;}
关键词: