这门课的起点,是我大一被卡了一整周的一句话:「指针就是存地址的变量」。字面意思全都认识,连起来就是不明白,直到有天在调试器里第一次亲眼看了一眼内存,才豁然开朗。本章的任务就是把那次「看见」复刻给你:内存到底长什么样、地址是什么、指针存的是什么、* 和 & 各自在干什么。后面十三章的一切(栈帧、堆、野指针、函数指针)都建立在这一章的图景上,值得慢读。
把进程的内存想象成一条极长的街道,每个格子 1 字节,每格有唯一门牌号:
地址 内容
0x1000 [ ]
0x1001 [ ]
... ← 每个格子 1 字节,门牌号就是地址
0x1008 [44 43 42 41] ← 变量 x(int,4 字节)住在这
0x100C [ ]
一个 int 要占 4 格,它住在哪?住首格,它的地址就是首格的门牌号 0x1008。「取地址」&x 问的就是这件事:
int x = 42;
printf("%p\n", (void*)&x); // 输出类似 0x7ffd12345678
(地址每次运行都可能不同,那是操作系统的 ASLR 在随机化,计组课第 10 章提过。你自己的机器上跑,数字必然和我这里的例子不同,这正说明它是个真实的物理位置,不是编出来的概念。)
指针就是存这个门牌号的变量:
int x = 42;
int *p = &x; // p 里装着 x 的地址
*p = 10; // 「顺着 p 里存的地址找过去,把那里改成 10」——x 变成 10
printf("%d\n", x); // 10
*p = 10 和 x = 10 改的是同一块内存:*p 就是「x 的另一个名字」。初次理解这句话,建议用调试器(gdb 的 p &x、p p)亲眼看一次两个地址相等,胜过读十遍定义。
地址本身只是一串编号,「从这里开始读几个字节、当成什么读」全由指针的类型说了算:
int x = 0x41424344;
char *c = (char *)&x;
printf("%c\n", c[0]); // 输出 'D'
x 在内存里的字节(小端机器)是 44 43 42 41,int* 读它解释成整数,char* 读同一个地方只取 1 字节解释成字符。内存里的位模式纹丝没动,变的只是解释方式。这个实验是「类型系统是内存的解释协议」的最短证明,也是后面所有强转、别名(第 14 章)话题的种子。
一个常被忽略的事实:指针变量本身也是个对象,要占内存。64 位平台上不管指向什么:
printf("%zu %zu %zu\n", sizeof(int*), sizeof(char*), sizeof(void*));
// 8 8 8
三种指针都是 8 字节(存 64 位地址),和指向的类型大小无关。「int* 比 char* 大」是初学者的高频误解,顺手破掉。数据结构课第 12 章算的「指针税」(每个链表节点多 8 字节)就是这笔账的应用。
指针还有对齐要求:int* 指向的对象应落在 4 字节对齐的地址上。把奇数地址强转成 int* 再解引用,在多数架构上直接 SIGBUS(总线错误),第 14 章会作为未定义行为正式立案。
指针可能不指向任何合法对象,这时赋 NULL(C23 起也有真正的 nullptr 关键字):
int *p = NULL;
if (p) { ... } // 惯用法:NULL 的假值判断
两个必须分清的概念:空指针(明确的「不指向」,值确定)和未初始化指针(垃圾值,可能指向任何地方)。后者比前者危险得多,解引用 NULL 大概率干净利落地段错误,解引用野指针可能「看起来没事」(垃圾地址恰好在可读区域),把雷埋给未来(第 14 章)。所以 C 的纪律是:指针声明即初始化,暂时没有目标就先置 NULL,让「没指向」变成可检测的状态。
void* 只存地址、不带类型,任何对象指针都能隐式转成它,它也只负责「搬运地址」,不能解引用(编译器不知道该读几格)也不能做算术:
void *mem = malloc(64); // malloc 返回 void*:「一块 64 字节的空地,你自己解释」
int *arr = mem; // C 里隐式转回 int*,合法(C++ 必须显式强转)
它是 C 语言实现「泛型」的土办法:qsort 的比较函数收 void*、线程函数参数是 void*,都是同一招。代价是类型安全全靠程序员自觉,转错类型编译器不拦,这是第 14 章 UB 清单上的常客。
最后把指针翻译回你已经认识的东西。计组课第 10 章的汇编里,mov eax, [rbx] 的方括号就是解引用,r 里放的就是指针;lea 取的是「算出来的地址」正是 &。Java 的对象引用其实也是指针,只是 JVM 把地址藏起来不让运算、配上 GC 管生死;Python 的变量是「名字到对象的绑定」,id() 看到的就是那个地址。所以「指针」从来没有离开过你,只是 C 把它交到你手里,别的语言把它锁在柜子里。这门课的任务就是练好「拿着裸指针不伤手」的基本功。
* 的一词多义没分清。int *p = &x; 里的 * 是「声明 p 是指针」(类型的一部分),*p = 10 里的 * 是解引用(一个操作)。两个 * 语义不同,初读最绕,写的时候想着「int* 是类型」就顺了。int *p; *p = 1; p 里是垃圾,写进随机地址,轻则段错误,重则悄悄改了别的数据。声明即初始化。%p 且强转 (void*),用 %d 在 64 位机上截断地址,输出的「地址」是错的。%p)和 sizeof,画一张「谁住哪」的示意图,验证同类型变量的地址间距。char * 逐字节打印一个 int 的 4 个字节(十六进制),判断你的机器是大端还是小端,和计组课第 2 章的理论对上。int *p = NULL; printf("%d", *p); 的运行结果是什么?哪一层(编译、链接、运行)出错?错误信息长什么样(跑一次记录原文)。