一、这一章在讲什么
这一章的主题非常集中:指针、引用、动态内存分配。
如果说前几章主要是在建立 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 new 和 delete
int* ptr = new int;
*ptr = 10;
delete ptr;
含义是:
new int:在自由存储区申请一块能放int的空间- 返回这块空间的地址
- 用指针接住这个地址
- 用完后必须
delete
5.3 动态数组要配对 delete[]
int* nums = new int[10];
delete[] nums;
这一组配对关系必须牢记:
new对应deletenew[]对应delete[]
配错是未定义行为,不是“小问题”。
六、指针运算为什么不是“每次加 1 个字节”
这一章另一个关键点是:指针递增不是简单地地址 +1。
int* ptr = nums;
+ptr;
如果 ptr 是 int*,那它前进一步其实是跳过一个 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 出来的。
九、这一章可以提炼出的原始指针最佳实践
结合书里的内容,可以总结成下面几条:
- 指针定义后立刻初始化,不能先放着不管。
- 不确定有没有对象可指时,用
nullptr表示空指针。 - 解引用前先确保指针有效。
new和delete、new[]和delete[]必须严格配对。- 释放后尽量立刻置空,避免误用悬空指针。
- 能不用裸
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;
表示只能读,不能通过 ref 改 value。
十三、指针和引用怎么选
我觉得这一章最适合落地成下面这张判断表。
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 和智能指针能解决资源管理问题
换句话说,不理解这一章,后面学类、构造函数、拷贝控制、移动语义、智能指针时都会发虚。
特别是到了后面的:
- 类和对象
- 拷贝构造
- 运算符重载
- 智能指针
你会发现它们都在回答同一件事:
一块资源由谁拥有,什么时候创建,什么时候释放,能不能共享,能不能转移。
而第八章就是这个问题的起点。
十五、这一章最值得背下来的结论
- 指针是存地址的变量,
*ptr才是地址里的值。 &取地址,*解引用。- 指针未初始化最危险,因为垃圾值会被当成地址。
new申请的资源必须由匹配的delete释放。new[]必须配delete[]。- 指针递增按“元素大小”移动,不是按 1 字节移动。
- 数组在很多表达式里会退化成首元素指针。
- 引用是别名,必须初始化,通常不为空,也不改绑。
- 只读输入参数优先使用
const T&。 - 现代 C++ 要理解裸指针,但尽量少手写裸内存管理。
十六、我对这一章的学习建议
如果你第一次学 C++,建议把这章按下面顺序吃透:
- 先完全搞懂
&、*、地址、解引用。 - 再理解“指针为什么会崩”,重点盯生命周期。
- 然后再学
new/delete,不要只背语法。 - 最后把引用和
const T&用到函数参数里。
真正掌握的标志不是能背概念,而是你能清楚回答:
- 这个变量是值,还是地址?
- 这块内存是谁申请的?
- 谁负责释放?
- 这里该传值、传引用,还是传指针?
如果这四个问题都能答清楚,第八章就算真正吃进去了。
十七、参考说明
本文是基于《21天学通 C++(第8版)》第八章“阐述指针和引用”的学习整理笔记,内容以知识点归纳和个人讲解为主,没有照搬原书示例全文,而是按理解重新组织。