5.1 指针的本质:地址加类型


5.1 指针的本质:地址加类型

本节摘要:指针变量存的就是一个地址(64 位平台上 8 字节),外加编译期记录的"指向类型"。类型决定两件事:解引用时一次读几字节、运算时一步跨几字节。本节用最小实验拆开这两个作用,认识空指针与野指针,并把段错误的机制第一次完整闭环。

两个动作:取地址与解引用

#include <stdio.h> int main(void) { int a = 42; int *p = &a; /* & 取地址:p 里存的是 a 的地址 */ printf("a 的地址 %p\n", (void *)p); printf("p 自己的地址 %p\n", (void *)&p); printf("p 指向的值 %d\n", *p); /* * 解引用:按 int 读那 4 字节 */ *p = 100; /* 通过地址写:a 变成 100 */ printf("a 现在 = %d\n", a); return 0; }

典型输出:

a 的地址 0x7ffe4a3c2c44 p 自己的地址 0x7ffe4a3c2c48 p 指向的值 42 a 现在 = 100

三个观察:p 自己也是变量(也有地址,且与 a 相邻,都在栈上);*p 读到了 42;通过 *p 写入改掉了 a。指针没有魔法,它只是"记下了别人家的门牌号", 号是"按这个门牌号去访问"*。

图 1 指针与被指对象的内存关系

图 1 指针与被指对象的内存关系

类型决定解引用的"读法"

同一块内存,不同类型的指针读出不同的值——第 2 章"滤镜"的指针版:

#include <stdio.h> int main(void) { double d = 3.14; char *bytes = (char *)&d; /* 按 1 字节看 double 的 8 个字节 */ double *pd = &d; printf("按 double 读 %f\n", *pd); for (int i = 0; i < 8; i++) printf("字节%d = %02X\n", i, (unsigned char)bytes[i]); return 0; }

输出里能看到 IEEE 754 的真实字节(1F 85 EB 51 B8 1E 09 40,小端)。char *double * 存的地址完全相同,差别只在编译器生成的读指令宽度。这就是"指针 = 地址 + 类型"的完整含义。

空指针与野指针:三类危险指针

空指针 NULL:明确表示"谁也不指"。解引用空指针是"好崩溃"——地址 0 所在页被操作系统永久封死(第 1 章地图的最底部),访问立刻段错误,gdb 一眼定位。工程上,指针初始化为 NULL、用完置 NULL,是成本最低的防御。

野指针:未初始化的指针,内容是栈/寄存器残留的随机字节。它可能指向任何地方——运气差踩到只读页崩溃,运气"好"写坏了别的数据还在正常运行。比空指针危险得多,因为它"不一定崩"。

悬垂指针:指向已死对象(已返回的栈帧、已 free 的堆块)。第 4 章见过它的栈版本,5.4 节看堆版本。

判据由此得来——用指针前问三句:非空吗?在界吗?对象还活着吗?

段错误的完整闭环

现在可以把全书到此为止的线索拼成完整因果链:

解引用非法地址(空指针/野指针/越界/悬垂) ↓ MMU 查页表:该虚拟地址无合法映射 ↓ CPU 触发页错误异常,内核判定为非法访问 ↓ 内核向进程发送 SIGSEGV 信号 ↓ 默认处理:终止进程,shell 打印 Segmentation fault

段错误不是 C 语言的报错,是硬件与操作系统联手维持秩序的结果。C 只是把"直接访问内存"的权力交给了你,代价是你必须自己保证每次访问合法。

声明语法:读法比记法重要

int *p; /* p 是指针,指向 int */ int **pp; /* pp 是指针,指向 int 指针 */ int *a, b; /* a 是指针,b 是 int!星号跟名字不跟类型 */

第三行是经典陷阱:一个星号只修饰紧跟的名字。一行声明多个变量时,指针与非指针混写是事故源。复杂声明从名字读起:int *f(void) 是"函数 f 返回 int 指针";int (*f)(void) 是"指针 f 指向返回 int 的函数"——括号改变结合方向,5.3 节函数指针见分晓。

⚠️ 常见坑:以为强制类型转换能"修好"指针。转换只改滤镜不改地址——把不匹配的地址转过去再用,崩得更快。转换的正确用途是"我知道这块内存确实有两种合法解读"(比如把任何指针转 char 星看字节),不是"让编译器闭嘴"。

💡 关键直觉:指针恐惧症的解药是记住它只有两个信息——地址和类型滤镜。写每一行指针代码时能回答"这个地址是谁的家、还住着人吗",就不会翻车。

本节要点回顾

  • 指针变量 = 存地址的 8 字节盒子 + 编译期类型信息:类型决定读写宽度与运算步长,运行时不存在。
  • 解引用就是"按门牌号访问":读、写都通过地址直达内存。
  • 空指针是"好崩溃",野指针是"随机灾难",悬垂指针是"延时爆炸":初始化 NULL、用后置 NULL。
  • 段错误链路:非法地址 → MMU 页错误 → SIGSEGV → 进程终止。
  • 星号跟名字不跟类型:一行多声明是陷阱;复杂声明从名字往外读。

下一节给指针装上"步长":运算规则与数组名的真面目。

常见疑问

问:NULL 到底是什么值? 标准只保证它是指针空值常量,多数实现是地址 0。操作系统故意把低地址页设为不可访问,空指针解引用因此总能立刻崩溃——这是硬件与系统联手提供的保护,不是 C 语言的功劳。

问:指针可以比较大小吗? 指向同一数组元素(含尾后哨兵)的指针可以比较先后;两个无关对象的指针比较大小是未定义行为。相等比较(是否指向同一对象)总是合法的。

问:为什么打印指针要强制转成 void 指针? 因为函数指针与数据指针的表示可能不同,而 void 指针是打印接口约定的通用指针形态。养成打印地址统一转 void 指针的习惯,可移植性最好。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U