程序里的每个变量都存放在内存的某个位置,而每个“位置”都有一个编号,也就是它的地址。指针只是保存这种编号的变量。这就是全部的概念,本页其余内容都是语法和由此带来的结果。
指针在 C 语言中如此重要,是因为 C 交给每个函数的都是参数的副本。拿到数字 7 的函数可以随意修改它的副本,调用者的变量纹丝不动。改为把地址交给函数,它就能顺着地址回去修改原来的变量。数组、字符串、动态内存,以及你将来用 C 构建的每一种数据结构,都建立在这一个能力之上。
内存、地址,以及为什么每样东西都有地址
当你写下 int score = 42; 时,编译器会预留几个字节的内存,并给它们起一个你可以使用的名字。这些字节还有一个数字地址。把内存想象成一条很长的街道,房子都有门牌号:score 就是住在 0x7ffd4c2a 号的那户人家,名字是给你看的,号码是给机器用的。
& 运算符用来取得这个号码:
运行一下。三个地址各不相同,因为三个变量占据不同的字节,而且每次运行地址都会变化。这很正常,也是你永远不该把地址写死的原因。注意 (void *) 强制转换:标准规定 %p 接收的是 void *,传入其他类型的指针严格来说是未定义行为。养成转换的习惯。
地址不可预测并不是问题,因为你几乎从不关心地址具体是多少,你只关心自己拿到了它。
声明指针
指针的声明要写明它指向的类型:
int *p; // p can hold the address of an int
double *q; // q can hold the address of a double
char *name; // name can hold the address of a char
把 int *p; 读作“*p 是一个 int”,即对 p 解引用得到一个 int。以后 C 再抛给你多复杂的声明,这种读法都适用。
* 写在哪里是风格问题,编译器并不在意:int* p、int *p 和 int * p 声明的是同一个东西。大多数 C 代码写成 int *p,这有一个实际的理由:
int* a, b; // a is a pointer to int; b is a PLAIN int, not a pointer
int *a, *b; // both are pointers - the * binds to the name, not the type
* 属于声明符,而不是类型。把它写在名字旁边能让这一点一目了然。
& 和 *:两个运算符
所有工作都由两个运算符完成,而且它们恰好相反。
&x:取地址。得到x所在的地址。*p:解引用。到p中的地址去,取出住在那里的对象。
最后两行就是指针存在的全部意义。没有任何语句直接给 age 赋值,age 却变了,因为 *p = 31 的意思是“把 31 存到 p 保存的地址上”,而那个地址正是 age 的地址。
下面是它在内存中的样子:
p age
+------------+ +----------+
| 0x7ffd1c40 | --------> | 31 |
+------------+ +----------+
at 0x7ffd1c38 at 0x7ffd1c40
p -> the address 0x7ffd1c40
*p -> the int stored there, 31
&age -> 0x7ffd1c40, the same address p holds
&p -> 0x7ffd1c38, where p itself lives
注意最后一行:指针本身也是内存中某处的一个变量,所以它也有自己的地址。这不是脑筋急转弯,只是规则一致而已,指向指针的指针(int **)就是这样工作的。
因为 & 和 * 互相抵消,*&age 就是 age,&*p 就是 p。
为什么指针类型很重要
指针保存地址,而地址只是一个数字,那编译器为什么非要知道它指向的是 int 还是 double?原因有两个:
- 要读多少字节。 解引用
int *读取 4 个字节(通常如此);解引用double *读取 8 个字节。没有类型,*p就毫无意义。 - 如何解释这些字节。 同样的位模式,当作
int是一个数,当作float就是完全不同的另一个数。
而指针本身的大小并不取决于它指向什么,地址就是地址:
在 64 位机器上,三个指针都是 8 字节,而它们指向的东西分别是 4、8 和 1 字节。正是被指向的类型让 *p 和指针运算有了意义。
在不同指针类型之间赋值是编译器会报的错误,而且理由充分:pi = &d; 会让你读取 double 的 4 个字节,再把结果当成整数。
经典动机:swap()
下面是每门 C 课程都会拿出来的函数,因为它失败的方式恰好解释了指针。
函数报告说它交换了两个值,而 main 报告说什么也没发生。broken_swap 收到的是 x 和 y 的副本。它完美地交换了这两个副本,然后函数一返回,副本就不复存在了。
改为把地址交给它:
现在 a 和 b 仍然是副本,只不过是两个地址的副本,而地址的副本和原地址指向的是同一个变量。无论函数在哪里被调用,*a 都是 x。
这就是每个修改调用者数据的 C 函数背后的模式,也是函数参数背后的一般规律:C 语言没有引用传递,所以你传入指针再解引用。 这也是 scanf 需要 & 的原因:
int n;
scanf("%d", &n); // scanf must be able to write into n, so it needs n's address
返回多个值
C 函数只能返回一个值。指针就是绕过这个限制的方法:为额外的结果传入地址。
返回值负责表示状态,指针负责带回结果。这种分工在 C 自己的标准库和操作系统 API 中随处可见。
指向指针的指针
既然指针也是变量,你就可以取它的地址,这个地址的类型是“指向 int 指针的指针”:
pp p value
+------+ +------+ +------+
| &p | ----> | &val | -----> | 8 |
+------+ +------+ +------+
当函数需要修改调用者持有的指针时,你就会真正用到 **,例如让调用者的指针指向新分配内存的分配函数;命令行参数中的 char *argv[] 也是如此。
四个值得提前了解的错误
使用从未指向任何地方的指针。 未初始化的指针保存的是那里原本就有的字节。对它解引用会读写一个随机地址。
int *p; // p holds garbage
*p = 10; // undefined behavior - likely a crash
给每个指针初始化,要么是真实的地址,要么是 NULL。相关的规范请参阅空指针。
混淆 *p 和 p。 p = 5; 把指针设为地址 5(毫无意义);*p = 5; 把 5 存到它指向的位置。编译器会对前者发出警告,警告值得认真看。
在 scanf 中忘了写 &。 scanf("%d", n) 把 n 的值当作地址传进去。它能编译通过(带一个警告),然后往那个数字所代表的内存里写数据。
返回局部变量的地址。 函数一返回,局部变量就消失了;这个地址就像一张已被拆除房屋的收据。
int *bad(void) {
int local = 42;
return &local; // the caller gets a dangling pointer
}
如果你需要在调用结束后依然存在的内存,就去分配它,这正是动态内存的用途。
接下来学什么
指针不止于“保存一个地址”。给指针加上一个数,它会以所指类型为单位在内存中前进,这就是指针运算的主题;而在 C 中,数组名会退化为指向首元素的指针,所以一旦两者都了解了,指针和数组实际上是同一个话题。接下来读这两篇,C 语言的其余部分就不再像魔法了。
常见问题
C 语言中的指针是什么?
指针是一个变量,它的值是一个内存地址,也就是内存中另一个对象所在的位置。int *p = &age; 让 p 保存 age 的地址,之后 *p 就通过这个地址读写 age 变量。
C 语言中 * 和 & 有什么区别?
&x 是取地址运算符:它得到 x 所在的地址。*p 是解引用运算符:它到 p 中保存的地址去,取出那里的对象。两者互相抵消,所以 *&x 就是 x。
为什么 * 会出现两次,一次在声明里,一次在使用指针时?
它们是同一个符号的两种不同用法。在 int *p; 中,* 是类型的一部分,表示“p 是指向 int 的指针”。在 *p = 5; 中,* 是解引用运算符,表示“把 5 存到 p 保存的地址上”。把声明读成“*p 是一个 int”,两种用法就都说得通了。
C 语言为什么需要指针?
C 语言的所有参数都是按值传递的,所以函数无法修改调用者的变量,除非把地址交给它。指针还能让你高效地遍历数组、返回多个结果、构建链式结构,以及使用 malloc 在运行时分配的内存。