这一章在干嘛?
书名的主角登场。从「内存和地址」这个最朴素模型出发,讲透间接访问 *、左值右值的联动、NULL 与野指针、指针的指针,最后到指针运算——它在数组章节会大放异彩。
6.1 内存和地址:值与类型
把内存想成一排编了号的房间:每个字节一个门牌号(地址)。变量占据一个或多个房间,房间号就是它的地址。
值和类型是两回事:同一块 4 字节内存,按 int 读是 65,按 float 读是另一个数,按 char* 读是 4 个房间中的第 1 个门牌。类型是编译器给地址附加的「解读说明书」——所以 int *p 声明的是「p 存地址,且按 int 格式解读目标」。
心智模型:
变量名是「房间标签」,编译后消失;指针变量是「写了门牌号的纸条」。拿着纸条上门(解引用)才能拿到屋里的人。
6.2 间接访问:* 与 & 的联动
int a = 10;
int *p = &a; /* & 取地址:p 存下 a 的门牌号 */
printf("%d\n", *p); /* * 间接访问:按门牌上门取货,打印 10 */
*p = 20; /* 上门「改货」:a 变成 20 */
printf("%d\n", a); /* 20 */
& 和 * 互为逆操作:*&a 就是 a 本身。结合左值概念理解第二处赋值——*p 标识了「a 的存储位置」,所以它是左值,能被赋值。这正是函数内改外部变量、数组遍历、链表操作的全部底层原理:
void swap(int *x, int *y) /* C 只能传值:传地址的值才能改外面 */
{
int t = *x;
*x = *y;
*y = t;
}
swap(&a, &b); /* 交出两块房间的门牌号 */
指针三宗罪(编译器基本不报错):
① 未初始化就解引用(野指针乱写内存);② 解引用 NULL(崩溃是最好的结局);③ 解引用已 free 的指针(悬空指针)。配方:声明即初始化(暂时没有就置 NULL),使用前判空。
6.3 指针的指针与指针表达式
指针变量自己也有地址,于是可以存进另一个「更高级」的指针:
int a = 5;
int *p = &a;
int **pp = &p; /* pp 指向「指向 int 的指针」 */
printf("%d\n", **pp); /* 两次解引用:pp -> p -> a */
*pp = NULL; /* 能改 p 本身!这是二级指针的杀手锏 */
二级指针的经典用途:在函数里修改指针本身。比如「往链表头部插入节点」要把头指针换成新节点,调用方必须交出头指针的地址(第 12 章实战)。读复杂指针声明用右左法则:从标识符出发,先右后左交替结合:
int **p; /* p 是指针,指向「int*」:二级指针 */
int *v[5]; /* v 先结合[]:数组,元素是 int* —— 指针数组 */
int (*w)[5]; /* 括号先结合 *:w 是指针,指向「int[5]」—— 数组指针 */
6.4 指针运算:以元素为单位
指针 ± 整数,不是门牌号加减,而是按所指向类型的大小跳跃:
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; /* 数组名退化为首元素地址 */
printf("%d\n", *p); /* 10 */
printf("%d\n", *(p + 2));/* 30:跨过 2 个 int = 8 字节 */
int *q = &arr[4];
printf("%td\n", q - p); /* 4:同数组内相减得元素个数(类型 ptrdiff_t) */
if (p < q) { /* 同数组内可比较大小:指向靠前者更小 */ }
| 运算 | 合法性 | 说明 |
|---|---|---|
| p + n / p − n | 限定于数组内(含末尾后一格) | 越出范围是未定义行为 |
| q − p | 仅同一数组 | 结果为元素个数差,不是字节差 |
| p < q / p == q | 仅同一数组(NULL 除外) | 跨数组比较无意义且未定义 |
| p + q | 非法 | 两个指针相加毫无意义 |
本章通关标准:
看到 int *p = &a; 能默画「纸条-房间」关系图;知道 *p 当左值意味着什么;明白指针运算以 sizeof(*p) 为步长且只在数组内有定义。
1. *p 出现在赋值号左边意味着什么?
*p 是间接访问表达式,它标识了 p 所指向的存储位置,因此是左值、可被赋值——写入的是 p 指向的那块内存。这就是 swap 通过传地址修改外部变量的原理。
2. 指针运算 p+1 的实际地址增量是多少?
不是 +1 字节,而是 +sizeof(p) 字节:int 前进 4 字节、char* 前进 1 字节、double* 前进 8 字节。指针运算的单位是「元素」而非「字节」。
3. 野指针、NULL 指针、悬空指针分别怎么防范?
野指针:声明即初始化(暂无目标就置 NULL);NULL:解引用前判空(if (p) …);悬空指针:free 后立即置 NULL,不复用旧值。三者共同点是解引用前必须确认「指向有效目标」。