C++ 内存管理进阶学习笔记
核心哲学:RAII (Resource Acquisition Is Initialization)
核心思想:将资源的生命周期与对象的生命周期绑定。
- 获取资源在构造函数中完成
- 释放资源在析构函数中完成
- 优势:无论函数正常结束还是异常退出,资源都能被正确释放
一、智能指针:RAII的典范实现
1. std::unique_ptr - 独占所有权
哲学:“一个对象只能有一个主人”== (同一时刻只有一个指针实例指向对象)==
基本用法
#include <memory>
// 推荐创建方式 (集中内存分配,更加高效)
auto ptr = std::make_unique<MyClass>(42);
// 使用方式与原始指针类似
ptr->doSomething();
(*ptr).doSomething();
// 所有权转移(不能复制)
auto new_owner = std::move(ptr);
特性
- 禁止拷贝,允许移动
- 几乎零开销
- 默认使用
delete释放资源,可自定义删除器
应用场景
- 工厂模式返回值
- 类的独占成员变量
- 容器中的动态对象管理
2. std::shared_ptr - 共享所有权
哲学:“多个指针可以共享同一个对象”
基本用法
auto shared1 = std::make_shared<MyClass>(100);
auto shared2 = shared1; // 引用计数+1
std::cout << shared1.use_count(); // 输出引用计数
实现原理
- 使用引用计数机制
- 控制块包含:引用计数、弱引用计数、删除器、分配器等
- 当引用计数归零时自动释放对象
3. std::weak_ptr - 弱引用
哲学:“我想观察你,但不想拥有你”
基本用法
std::weak_ptr<MyClass> weak_obs = shared1;
// 安全访问方式
if (auto temp = weak_obs.lock()) {
temp->doSomething();
}
主要用途
- 解决
shared_ptr的循环引用问题 - 缓存实现
- 观察者模式
二、为什么优先使用 make_unique 和 make_shared
1. 异常安全(主要优势)
问题场景:
void risky() {
MyClass* p1 = new MyClass(1); // 可能泄漏
MyClass* p2 = new MyClass(2); // 如果这里抛出异常
delete p1; // 这行不会被执行!
delete p2;
}
- 因为抛出了异常,正常的执行流程立即中断!程序不会继续执行第3行剩下的部分(不会给
ptr2赋值),也不会执行第4、5、6、7行。 - 程序进入异常处理模式:它开始栈展开(Stack Unwinding)。这个过程就是沿着函数调用链向上回溯,寻找能捕获这个异常的
catch块。同时,销毁当前函数栈帧中的所有局部对象。 - 在
riskyFunction的栈帧中,有哪些局部变量?
-> 只有ptr1和ptr2这两个原始指针。 - 销毁局部变量
ptr1和ptr2意味着什么?
-> 意味着回收它们在栈上占用的那4或8个字节的内存。这仅仅是把存储地址值的那个“小纸条”给扔了。
-> 它绝对不会自动触发delete ptr1;! 编译器只知道ptr1是个指针,不知道它指向的内容需不需要、该怎么释放。释放堆内存是程序员的责任。 - 栈展开完成,程序跳转到
catch块继续执行。 - 最终结果:
- 第一个
MyClass对象(第2行创建的)永远地留在了堆上,没有任何指针记得它的地址,无法再被访问,也无法被释放。这就是内存泄漏。 - 第二个
MyClass对象(第3行尝试创建的)呢?它的内存分配成功了,但构造没完成。C++运行时库会负责释放这块原始内存,因为构造没有完成,谈不上有一个需要析构的对象。所以第二次分配的内存不会泄漏。
- 第一个
安全方案:
void safe() {
auto p1 = std::make_unique<MyClass>(1); // 要么完全成功
auto p2 = std::make_unique<MyClass>(2); // 要么完全失败且自动清理
}
对象的构造分 内存分配 和 资源初始化 两部分, 并非一个原子操作
智能指针相当于将可分的两部分重新"封装"成一个整体, 要么一起完成, 要么集中销毁
2. 性能优化(特别是make_shared)
new+shared_ptr:2次内存分配(对象 + 控制块)make_shared:1次内存分配(对象和控制块连续存放)- 更好的缓存局部性,访问引用计数更快
三、关键概念深入理解
1. 栈内存 vs 堆内存
| 特性 | 栈内存 | 堆内存 |
|---|---|---|
| 管理方式 | 编译器自动管理 | 手动管理(或通过智能指针) |
| 分配/释放 | 自动(作用域结束) | 手动(new/delete) |
| 生命周期 | 与作用域绑定 | 显式控制 |
| 内存泄漏 | 不可能发生 | 主要发生地 |
2. 智能指针的RAII实现机制
// 简化版unique_ptr析构函数
~unique_ptr() {
if (ptr != nullptr) {
delete ptr; // 关键:在析构函数中释放托管资源
}
}
3. 所有权转移流程
工厂函数内部 unique_ptr --移动--> 临时unique_ptr --移动--> 接收者unique_ptr
(拥有资源) (拥有资源) (拥有资源)
六、最佳实践总结
- 默认首选
std::unique_ptr- 开销最小,表达意图最明确 - 明确需要共享所有权时才用
std::shared_ptr- 注意性能开销 - 需要观察或解决循环引用时用
std::weak_ptr - 优先使用
make_unique和make_shared- 更安全、更高效 - 避免混合使用智能指针和原始指针 - 统一管理策略
- 理解底层机制 - 明白RAII、栈展开、移动语义等原理
七、常见误区澄清
❌ 误区:智能指针完全避免了内存管理
✅ 正解:智能指针改变了内存管理的方式,从"手动管理"变为"通过 ownership 语义管理"
❌ 误区:shared_ptr可以随便使用
✅ 正解:shared_ptr有性能开销,应仅在需要共享所有权时使用
❌ 误区:make_shared在任何情况下都比new好
✅ 正解:make_shared会使对象和控制块生命周期绑定,可能延迟内存释放
总结
C++内存管理的进阶之路在于从"手动管理"上升到"语义管理":
- 基础:理解栈/堆区别,掌握new/delete
- 进阶:掌握RAII理念和智能指针
通过智能指针和RAII,我们将容易出错的堆内存管理托付给绝对不会出错的栈内存生命周期机制,从而编写出既安全又高效的C++代码。
1120

被折叠的 条评论
为什么被折叠?



