指针 = 存地址的变量;理解它,先理解内存模型。
进程的地址空间是一条字节数组,每个字节有唯一编号,这个编号即地址。指针本身也是一个对象,占自己的字节,存的内容是另一个对象的地址。
地址 内容
0x1000 ┌──────────┐
0x1001 │ │ 每个格子 1 字节
0x1002 │ │
... │ │
0x1008 │ 42 │ ← 变量 x 占 4 字节,首地址 0x1008
0x100C │ ... │
int x = 42; // x 是 int,占 sizeof(int) 字节
int *p = &x; // &x 取 x 的首地址,存进 p
printf("%p\n", (void*)p); // 打印地址,如 0x7ffc...
printf("%p\n", (void*)&x); // 与 p 相同
&x 取地址,*p 解引用(按 p 里存的地址读写)。*p = 10; 等价于改 x,因为 *p 与 x 是同一块内存的两个名字。指针除地址外还带类型。类型决定两件事:解引用读写的字节数(int* 读 sizeof(int) 字节,char* 读 1 字节),以及这些字节的解释方式(有符号、浮点等)。
int x = 0x41424344; // 小端机上内存字节是 44 43 42 41
char *c = (char*)&x; // 强制按字节看
printf("%c\n", c[0]); // 'D'(小端:最低字节在前)
同一块内存,int* 看是整数,char* 看是一串字节;位模式不变,变的只是解释方式。
printf("%zu %zu %zu\n", sizeof(int*), sizeof(char*), sizeof(void*));
64 位平台上三种指针通常都是 8 字节(地址为 64 位)。指针大小与其指向的类型大小无关:char* 与 int* 一样大。
指针有对齐约束:int* 通常要求 4 或 8 字节对齐,即地址须为 _Alignof(int) 的整数倍。把不对齐的地址强转成 int* 再解引用,多数架构上触发 SIGBUS,至少是未定义行为。
#include <stdalign.h>
printf("%zu\n", alignof(int)); // 通常 4
printf("%zu\n", alignof(double)); // 通常 8
指针不指向任何合法对象时,赋为空指针。空指针是明确的无效值;未初始化是垃圾值,两者不同。
int *p = NULL; // C 传统写法(NULL 是宏,通常 ((void*)0))
int *q = nullptr; // C23 起引入的真正的关键字,类型是 nullptr_t
if (p == NULL) { /* 未初始化/未分配 */ }
if (p) 或 if (p != NULL);这只排除空指针这一种情况,不保证指向的地址合法。void* 是通用指针:任何对象指针都能隐式转成 void*,用于只传递地址、不关心类型的场合(如 malloc 返回值、回调参数)。
void *mem = malloc(64); // malloc 返回 void*,不关心你要存什么
int *arr = mem; // C 中 void* 隐式转 int* 合法(C++ 需要强转)
void* 不能直接解引用(编译器不知道读几个字节),也不能做算术(见第 3 章)。void* 保证存得下任何对象指针,但不保证存得下函数指针(POSIX 保证,C 标准不保证)。*p = 10 编译成 mov [reg], 10 之类——寄存器里放地址,方括号表示去这个地址。C 的 * 对应汇编的间接寻址,& 对应 lea(取有效地址)。id() 只是近似。指针是存地址的变量;类型决定对这块内存的解释方式;解引用即按类型读写。