本节摘要:指针就是一个普通变量,只是它的值被解释为另一个对象的地址。解引用、取地址、指针算术、多级指针,全部围绕"值是地址"这一个事实展开。本节训练拿地址画图的基本功,把悬垂指针、野指针、空指针三大凶案现场逐一复原,为第 3 章智能指针的所有权模型打好地基。
阅读完本节,你应当能够:
const 修饰指针时的三种组合含义。先破除神秘感:
int x = 42; int* p = &x; // p 是一个变量 值为 x 的地址
p 自己也要占内存——64 位平台上 8 字节,和 int 变量一样老老实实住在栈帧里。它唯一的特别之处是"值被当作地址解释"。&x 取出 x 的地址存进 p,*p 顺着地址找到 x。用一张地址表把状态写清楚:

二级指针与指针的引用(int*&)服务的场景相同:函数想改"调用者的指针指向谁"(比如分配缓冲后回传地址)。日常代码里 int*& 更直观,C 风格接口里 int** 更常见。
int arr[4] = {10, 20, 30, 40}; int* p = arr; p + 1; // 地址增加 4 即 sizeof(int) *(p + 2); // 30 即 arr[2]
p + 1 不是地址加 1 字节,而是加"一个元素的宽度"——这是数组下标与指针算术能互换的根基:arr[i] 在机器层面就是 *(arr + i)。也正因如此,指针算术只在数组(或同一分配块)内部有意义,跨对象比较与加减是未定义行为。void* 不能做算术(不知道步长),char* 步长为 1,常用于逐字节查看内存。
野指针(未初始化):
int* p; // 栈上的值是随机图案 *p = 1; // 往一个随机地址写入 未定义行为
防御很简单:声明即初始化,暂时没有目标就给 nullptr。
悬垂指针(目标已死):
int* p = new int{42}; delete p; // 堆内存归还 但 p 的值没变 *p = 7; // 悬垂:写一块已归还的堆内存 未定义行为 delete p; // 双重释放 同样未定义 堆管理器状态被破坏
delete 只做两件事:调用析构、归还内存;它不会顺手把指针置空。好习惯是 delete 后立刻 p = nullptr;——对空指针 delete 是安全的无操作。但真正的解法在第 3 章:让栈上的智能指针对象在离开作用域时自动释放堆内存,悬垂问题从源头消失。
空指针:nullptr(C++11)取代 NULL,它有专属类型不会与整数混淆。解引用空指针几乎必然立刻段错误——这其实是三大凶案里最"仁慈"的一种,因为崩溃现场离案发地最近。
int* p = nullptr; if (p) { /* 非空才使用 */ } // C++17 提供了更直的表达 if (p != nullptr) { }
const 修饰指针时,看它站在星号前还是星号后:
int a = 1, b = 2; const int* p1 = &a; // 指向常量的指针:不能通过 p1 改 a 但 p1 可改指 int* const p2 = &a; // 常量指针:p2 只能指向 a 但可通过 p2 改 a const int* const p3 = &a; // 双重锁定:既不能改指也不能改值 p1 = &b; // 可以 // *p1 = 10; // 编译错误 // p2 = &b; // 编译错误 *p2 = 10; // 可以 a 变为 10
读法窍门:从右往左读,const 修饰谁谁不可变。工程含义更重要——接口用 const int* 或 const int& 是一份契约:承诺只读不改。标准库里 const char* 返回值、const& 传参,全是这个契约的应用。函数后置的 const(int get() const;)承诺不改对象自身,同一逻辑在成员函数层面的延伸,第 2 章细讲。
顺带梳理几组易混操作,全部能用"值是地址"一句话复原:*p++ 是 *(p++),先取当前值再后移;(*p)++ 是把所指值加一;&arr[0] 与 arr 同值但类型分别是 int* 与 int[4]。拿不准时加括号,别赌优先级。
💡 关键直觉:指针给了你直接搬弄内存的权限,但语言不再替你保管任何事。C++ 的现代答案不是没收权限,而是给权限上锁——引用消灭"可空"、
span消灭"丢长度"、智能指针消灭"忘释放",它们都是包着安全外衣的指针。理解裸指针,是为了真正看懂这些外衣。
arr[i] 即 *(arr + i),跨界运算是未定义行为。第一章到此收官。你已经能画出程序的内存分区、推算类型宽度、回放栈帧轮回、追踪指针指向。下一章把镜头转向"复合对象":类在内存里长什么样,继承如何拼接,多态怎样经由虚表完成一次间接跳转。