一、这一章在讲什么

这一章的主题非常集中:指针、引用、动态内存分配

如果说前几章主要是在建立 C++ 的语法直觉,那么这一章开始真正进入 C++ 的底层能力区。它回答的是几个核心问题:

  • 变量在内存里到底怎么放
  • 地址为什么能被“保存起来”
  • 为什么 new / delete 容易写出 bug
  • 引用和指针到底有什么区别
  • 函数传参时,什么时候该传值,什么时候该传引用,什么时候才该传指针

这章读完之后,最大的收获不是会写几个 *&,而是会开始把程序看成“值 + 地址 + 生命周期”的组合。


二、指针的本质

2.1 指针不是“特殊魔法”,它本质上也是变量

指针本质上就是一个变量,只不过它保存的不是普通数据,而是内存地址

例如:

int age = 30;
int* ptr = &age;

这里发生了三件事:

  • age 是一个普通整数变量
  • &age 取出 age 所在的内存地址
  • ptr 把这个地址保存起来

所以可以把指针理解成:

“一个知道另一个对象住在哪里的变量”

2.2 &* 分别干什么

  • & 是取地址运算符
  • * 在“定义时”表示声明指针,在“使用时”表示解引用

看一个最小例子:

int age = 30;
int* ptr = &age;

cout << ptr << endl;   // 输出地址
cout << *ptr << endl;  // 输出地址处存的值,即 30

这里一定要分清:

  • ptr 是地址
  • *ptr 是地址对应位置上的数据

这是整章最基础、也最容易混掉的一层。


三、指针最重要的三个基本动作

3.1 声明指针

int* ptr = nullptr;

书里使用的是 NULL,但如果你写的是现代 C++,更推荐使用 nullptr。它是专门为空指针引入的关键字,类型更明确,也更安全。

3.2 让指针指向某个对象

int age = 30;
int* ptr = &age;

此时 ptr 保存的是 age 的地址。

3.3 通过指针访问或修改对象

*ptr = 40;
cout << age << endl;   // 40

这说明:

  • 指针不只是“看地址”
  • 它还能通过地址直接改原对象

这也是指针既强大又危险的原因。


四、为什么 sizeof(int*) 不等于 sizeof(int)

这一章有个特别容易考察理解的点:

  • sizeof(int) 取决于整数类型本身
  • sizeof(int*) 取决于“地址”在当前平台上需要多少字节

也就是说:

  • 指针指向 int
  • 指针指向 double
  • 指针指向 char

这些指针变量本身在同一平台上通常一样大,因为它们存的都是地址。

常见情况:

  • 32 位环境下,指针通常是 4 字节
  • 64 位环境下,指针通常是 8 字节

结论是:指针大小通常和“它指向什么类型”无关,而和“平台地址宽度”有关。


五、动态内存分配是这一章的核心应用

5.1 为什么需要动态内存

静态数组的问题是:大小提前写死。

int nums[100];

这会带来两个现实问题:

  • 如果用户只输入 3 个数,空间浪费
  • 如果用户要输入 1000 个数,空间不够

所以需要运行时按需申请内存,这就是动态内存分配。

5.2 newdelete

int* ptr = new int;
*ptr = 10;
delete ptr;

含义是:

  • new int:在自由存储区申请一块能放 int 的空间
  • 返回这块空间的地址
  • 用指针接住这个地址
  • 用完后必须 delete

5.3 动态数组要配对 delete[]

int* nums = new int[10];
delete[] nums;

这一组配对关系必须牢记:

  • new 对应 delete
  • new[] 对应 delete[]

配错是未定义行为,不是“小问题”。


六、指针运算为什么不是“每次加 1 个字节”

这一章另一个关键点是:指针递增不是简单地地址 +1。

int* ptr = nums;
+ptr;

如果 ptrint*,那它前进一步其实是跳过一个 int 的大小,也就是:

ptr = ptr + sizeof(int)

逻辑原因很简单:

  • 指针前进的目标不是“下一个字节”
  • 而是“下一个同类型元素”

所以:

  • char* 前进一步常常是 1 字节
  • int* 前进一步常常是 4 字节
  • double* 前进一步常常是 8 字节

这也解释了为什么数组和指针可以紧密配合。


七、数组和指针为什么看起来很像

书里这一节非常重要,因为很多 C/C++ 初学者都会在这里产生误解。

例如:

int numbers[5] = {24, -1, 365, -999, 2011};
int* ptr = numbers;

这里 numbers 在很多表达式里会退化成指向首元素的指针,所以:

numbers[2]

*(ptr + 2)

效果相同。

但要注意,数组不是普通指针变量。它和指针“很像”,但并不完全相同:

  • 数组名通常代表首元素地址
  • 但数组本身是固定的一整块存储
  • 数组名不能像普通指针那样随便改指向

所以更准确的说法是:

数组在很多场景下会退化成指向首元素的指针,但数组本身不是一个可随意改值的普通指针变量。


八、使用指针时最容易犯的错误

这是这一章最值得反复复习的部分,因为很多崩溃都不是“语法错”,而是“生命周期错”。

8.1 未初始化指针

int* ptr;
cout << *ptr << endl;  // 危险

这里 ptr 里是垃圾值。解引用垃圾地址,程序可能直接崩。

8.2 内存泄漏

int* ptr = new int[5];
ptr = new int[10];

第一块内存的地址丢了,再也释放不到,这就是泄漏。

本质是:

  • 申请了资源
  • 却失去了回收它的能力

8.3 悬空指针

int* ptr = new int(10);
delete ptr;
cout << *ptr << endl;  // 危险

delete 之后,ptr 还保留着旧地址,但那块内存已经不再归你安全使用。

这时 ptr 就成了悬空指针。

8.4 重复释放

delete ptr;
delete ptr;  // 错误

同一块资源只能释放一次。

8.5 释放了不该释放的内存

只有 new 返回的地址,才能交给对应的 delete

例如下面这种就不行:

int age = 30;
int* ptr = &age;
delete ptr;  // 错误

因为 age 不是 new 出来的。


九、这一章可以提炼出的原始指针最佳实践

结合书里的内容,可以总结成下面几条:

  1. 指针定义后立刻初始化,不能先放着不管。
  2. 不确定有没有对象可指时,用 nullptr 表示空指针。
  3. 解引用前先确保指针有效。
  4. newdeletenew[]delete[] 必须严格配对。
  5. 释放后尽量立刻置空,避免误用悬空指针。
  6. 能不用裸 new / delete 时,尽量不用。

最后这一条虽然超出了本章原书写法,但非常重要。现代 C++ 更提倡:

  • std::vector 管动态数组
  • std::string 管字符串
  • std::unique_ptr / std::shared_ptr 管动态对象

也就是说,理解原始指针是必须的,但实际业务代码里要尽量减少手写内存管理。


十、引用的本质:变量的别名

引用可以理解成“对象的另一个名字”。

int original = 30;
int& ref = original;

ref = 40;
cout << original << endl;  // 40

这里 ref 不是新对象,也不是独立副本,而是 original 的别名。

这一点和指针非常不同:

  • 指针存的是地址,本身可以单独存在
  • 引用本质上绑定到某个对象

所以引用有两个鲜明特征:

  • 声明时必须初始化
  • 一旦绑定,通常不能再改绑到别的对象

这使它比裸指针更“稳定”,也更适合作为函数参数。


十一、为什么函数参数里经常优先使用引用

如果按值传递一个大对象,会发生复制:

void Foo(BigObject obj);

复制可能很贵。

而按引用传递:

void Foo(BigObject& obj);

函数直接操作原对象,不用再拷贝一份。

如果函数只是读取,不应该修改参数,那就再加 const

void Foo(const BigObject& obj);

这类参数形式在现代 C++ 中极常见,因为它兼顾了:

  • 性能
  • 语义清晰
  • 安全性

书中用平方函数举例,本质想说明的是:

  • const 引用可用于“输入/输出参数”
  • const 引用适合“只读输入参数”

十二、const 指针和 const 引用一定要分清

12.1 指向可变数据的常量指针

int value = 10;
int* const ptr = &value;

含义:

  • ptr 自己不能改指向
  • *ptr 可以改

12.2 指向常量数据的指针

const int* ptr = &value;

含义:

  • ptr 可以改指向
  • 但不能通过 ptr 改数据

12.3 指向常量数据的常量指针

const int* const ptr = &value;

含义:

  • 指向不能改
  • 数据也不能通过它改

引用上的 const 反而更直观:

const int& ref = value;

表示只能读,不能通过 refvalue


十三、指针和引用怎么选

我觉得这一章最适合落地成下面这张判断表。

13.1 优先用引用的场景

  • 参数一定非空
  • 语义上就是“这个对象本身”
  • 不需要表达“可能没有对象”
  • 不需要重新绑定到别的目标

例如:

void Print(const string& name);
void Swap(int& a, int& b);

13.2 需要用指针的场景

  • 需要表示“可以为空”
  • 需要动态分配对象
  • 需要处理数组/内存块
  • 需要改变指向
  • 需要和底层接口、C API 打交道

例如:

int* ptr = nullptr;
char* buffer = new char[1024];

一句话总结:

能明确表达“别名关系”时优先用引用;需要表达“地址、可空、可变指向、动态内存”时再用指针。


十四、这一章对现代 C++ 的现实意义

虽然今天很多业务代码已经尽量避免直接写裸指针,但这章仍然非常重要,因为它决定了你是否真的理解:

  • 变量和对象的内存语义
  • 传值、传引用、传指针的成本差异
  • 为什么会有悬空引用、悬空指针、未定义行为
  • 为什么 RAII 和智能指针能解决资源管理问题

换句话说,不理解这一章,后面学类、构造函数、拷贝控制、移动语义、智能指针时都会发虚。

特别是到了后面的:

  • 类和对象
  • 拷贝构造
  • 运算符重载
  • 智能指针

你会发现它们都在回答同一件事:

一块资源由谁拥有,什么时候创建,什么时候释放,能不能共享,能不能转移。

而第八章就是这个问题的起点。


十五、这一章最值得背下来的结论

  1. 指针是存地址的变量,*ptr 才是地址里的值。
  2. & 取地址,* 解引用。
  3. 指针未初始化最危险,因为垃圾值会被当成地址。
  4. new 申请的资源必须由匹配的 delete 释放。
  5. new[] 必须配 delete[]
  6. 指针递增按“元素大小”移动,不是按 1 字节移动。
  7. 数组在很多表达式里会退化成首元素指针。
  8. 引用是别名,必须初始化,通常不为空,也不改绑。
  9. 只读输入参数优先使用 const T&
  10. 现代 C++ 要理解裸指针,但尽量少手写裸内存管理。

十六、我对这一章的学习建议

如果你第一次学 C++,建议把这章按下面顺序吃透:

  1. 先完全搞懂 &*、地址、解引用。
  2. 再理解“指针为什么会崩”,重点盯生命周期。
  3. 然后再学 new/delete,不要只背语法。
  4. 最后把引用和 const T& 用到函数参数里。

真正掌握的标志不是能背概念,而是你能清楚回答:

  • 这个变量是值,还是地址?
  • 这块内存是谁申请的?
  • 谁负责释放?
  • 这里该传值、传引用,还是传指针?

如果这四个问题都能答清楚,第八章就算真正吃进去了。


十七、参考说明

本文是基于《21天学通 C++(第8版)》第八章“阐述指针和引用”的学习整理笔记,内容以知识点归纳和个人讲解为主,没有照搬原书示例全文,而是按理解重新组织。