指针(pointer)也是变量,但在 C 语言里地位特殊,它几乎撑起了 C 语言的整片天。指针的用途肉眼可见处:字符串、动态分配内存、按引用传参、构造链表二叉树这样的复杂数据结构、指向函数……说指针是 C 语言的中枢神经也不过分。
指针到底是什么
计算机的内存是一长串连续排列的数据,每一格都有地址。普通变量直接装值;指针装的是某个值所在的内存地址,它指向那块地方。我们用指针,就是告诉程序:你要的东西在那个地址上,顺着去找。
声明指针的语法是在类型后加一个星号:
int var = 10;
int *ptr = &var; // ptr 存的是 var 的地址
printf("%p\n", ptr); // 打印地址,形如 0x7fffffffe9cc
printf("%d\n", *ptr); // 解引用,取出 var 的值:10两处要点:
- 取地址
&(address-of):&var得到 var 的地址,用来给指针赋值。声明完指针记得初始化,什么都不指就指向NULL。 - 解引用
*(dereference): 取出指针指向的那个值。声明里的*和解引用共用一个符号,但意思不同,声明里是这是指针,表达式里是把指针指的地方拿来。看*ptr读作 ptr 所指的值。
指针存的是地址,打印地址统一用 %p,不要用 %d。早期教程有人用 %d 打指针,严格说是未定义行为。
只要地址对,改它的值就改到原变量
int a = 1;
int *pointer_to_a = &a;
a += 1; // 直接改 a
*pointer_to_a += 1; // 通过指针改 a,效果一样
printf("a 现在等于 %d\n", a); // 3关键在 *pointer_to_a += 1;,它等价于把 a 里面存的值加 1。因为指针存着 a 的地址,对这个地址上的内容动手,改的就是 a 本人。这正是后面函数里改外面的变量的通行证。
字符串其实是指针
回看字符串篇的 char * name = "John";,其实干了三件事:
- 在栈上分配一个局部变量
name,它是个指向字符的指针。 - 把字符串 "John" 放到程序的某个内存位置。
- 让
name指向 J 所在的位置(后面跟着 o、h、n,最后是\0)。
因为内存连续,从 J 往前扫就是 o、h、n,直到撞上结束符 \0。所以 char * 导航字符串这件事,本质就是指针在内存里沿街扫描。
解引用=方括号的隐藏版
数组访问其实一直在悄悄用指针。你有两个等价写法:
char vowels[] = {'A', 'E', 'I', 'O', 'U'};
char *pv = vowels; // 数组名就是首元素地址
vowels[0]; // 方括号写法
*(pv + 0); // 解引用写法,两者一样
printf("%c\n", *pv); // A,也就是 vowels[0]vowels[0] 访问数组第一项,就是解引用指向首元素的指针。指针篇之后的数组与指针一节会把这个关系串起来讲。
指针的大小:跟类型无关
指针存的是地址,而地址所需空间由机器架构决定,跟它指向什么类型没有关系:
int *ptr1;
char *ptr2;
printf("%zu\n", sizeof(ptr1)); // 64 位系统上是 8
printf("%zu\n", sizeof(ptr2)); // 也是 832 位系统统一 4 字节,64 位系统统一 8 字节,同一个系统上所有类型的指针大小都相等,因为它们存的都是一个内存地址。
四类特殊指针:分清少出错
- NULL 指针: 不指向任何有效内存,用
int *p = NULL;宣告我还没对象。用之前先判空,能防住一大类崩溃。 - void 指针: 不带类型的万能指针,能接任何类型的地址,但解引用前必须先转型(后面动态分配会见到)。
- 野指针(wild pointer): 声明了却没初始化,指向一团乱内存。别碰,一碰就是未定义行为,所以声明后要么给地址,要么给 NULL。
- 悬垂指针(dangling pointer): 指向的内存已被释放(free 过或函数返回),再用就是访问已回收的区域。经验:free 之后把指针置 NULL。
指针的优点与代价
优点:动态分配/释放内存、高效访问数组和结构体、直达任意内存位置、搭链表/图/树。代价也明显:乱给值就内存踩踏,没初始化就段错误,滥用还是内存泄漏的头号制造者。指针是 C 语言的元老级双刃剑,顺手时天下无敌,出错时无从查起。
先把"指针存地址、& 取地址、* 解引用"啃明白,下一篇看它和数组怎么互相打通。