适用读者:零基础小白,有 C++ 基本语法概念即可。每段代码均附带中文注释,易错环节有 预警。
第一步:理解 C++ 中的"资源黑洞"——为什么需要 RAII
1.1 从一个血泪 Bug 讲起
想象你借了一本书(资源),看完后必须归还。但如果你看到一半突然被叫去开会(异常抛出),书没还就走了——这就是 C 风格手动管理资源的最大痛点。
void risky_function() { // 第1步:向系统"借"资源——在堆上分配 100 个 int int* data = new int[100]; // new:相当于"借书" // 第2步:使用资源 process(data); // 假设这里可能抛出异常 // 第3步:归还资源 delete[] data; // delete:相当于"还书" // 如果第2步抛异常,程序直接跳过 delete,造成内存泄漏! }类比:这就像图书馆借书——你借了书,但中途被突发事件打断,书没还就走了。RAII 就是"图书管理员",不管你因什么原因离开(正常返回 / 异常抛出 / 提前 return),它都保证把书收回来。
1.2 RAII 三个字母的含义
| 字母 | 全称 | 含义 | 生活类比 |
| R | Resource | 资源 | 你借的那本书 |
| A | Acquisition | 获取(即初始化) | 借书的动作(构造函数执行) |
| I | Initialization | 初始化 | 把书拿到手的同时登记(构造即获取) |
| I | Is | —— | (连接词,无特殊含义) |
| (隐含) | Release | 释放(即析构) | 还书的动作(析构函数执行) |
RAII 的核心公式:
资源生命周期 = 对象生命周期 获取资源 → 构造函数 // 对象诞生时拿到资源 释放资源 → 析构函数 // 对象消亡时归还资源
1.3 用 RAII 改写上面的 Bug 代码
#include <memory> // 智能指针头文件 void safe_function() { // 构造时获取资源,unique_ptr 对象诞生 = 资源到手 std::unique_ptr<int[]> data = std::make_unique<int[]>(100); process(data.get()); // 使用 .get() 获取原始指针 // 无需手动 delete!函数结束时 unique_ptr 自动析构,释放内存 // 即使 process() 抛出异常,栈展开也会调用析构函数 }一句话总结:RAII 把"资源"绑定到"栈对象"的生命周期上。栈对象销毁时 C++ 保证调用析构函数,因此资源一定被释放。简单来说就是对象构造时获取资源,对象销毁时释放资源。
第二步:智能指针三兄弟总览
C++11 引入了三种智能指针,各司其职:
| 指针类型 | 核心语义 | 何时使用 | 头文件 |
| unique_ptr | 独占所有权,不可拷贝,只能移动 | 默认首选,90% 的场景用它 | <memory> |
| shared_ptr | 共享所有权,引用计数,最后一个销毁时才释放 | 多个对象共享同一资源 | <memory> |
| weak_ptr | 弱引用,不增加引用计数,可"升级"为 shared_ptr | 打破循环引用、缓存观察者 | <memory> |
记忆口诀:unique 独占一人走,shared 共享大家有,weak 旁观不插手。
第三步:unique_ptr — 独占资源的"独行侠"
3.1 基本用法
#include <memory> #include <iostream> // 第1步:创建 unique_ptr(推荐用 make_unique) auto ptr1 = std::make_unique<int>(42); // C++14 起可用 std::cout << *ptr1 << "\n"; // 输出: 42 // 第2步:修改指向的值 *ptr1 = 100; // 像普通指针一样解引用 // 第3步:移动所有权——"交棒" auto ptr2 = std::move(ptr1); // ptr1 变为 nullptr,ptr2 接管资源 // ⚠️ 此后 ptr1 不能再使用!解引用空 unique_ptr 是未定义行为 assert(ptr1 == nullptr); // 通过 // 第4步:手动提前释放(通常不需要) ptr2.reset(); // 立即释放资源3.2 为什么 unique_ptr 不能拷贝?
auto p1 = std::make_unique<int>(10); // auto p2 = p1; // ❌ 编译错误!unique_ptr 的拷贝构造被 = delete auto p2 = std::move(p1); // ✅ 只能移动设计哲学:独占所有权就像房产证,世上只有一份原件。你可以转让(移动),但不能复印(拷贝)。
3.3 unique_ptr 的数组特化
// 第1步:创建包含 5 个 int 的数组 auto arr = std::make_unique<int[]>(5); // C++14 起支持 // 第2步:像数组一样使用下标 for (int i = 0; i < 5; ++i) { arr[i] = i * 10; // 不需要 *(arr.get() + i) } // 第3步:自动调用 delete[](而非 delete),无需操心 // 函数结束自动释放 // ⚠️ 注意:unique_ptr<T[]> 没有 operator* 和 operator-> // 因为解引用数组首元素通常没有意义3.4 自定义删除器
有时你管理的不是 new 出来的内存,而是文件句柄、socket 等。unique_ptr 支持自定义"怎么还"的方式:
#include <cstdio> // 第1步:定义删除器——告诉 unique_ptr "怎么关闭文件" auto file_deleter = [](std::FILE* f) { if (f) { std::fclose(f); // RAII 包装 C 语言文件操作 std::puts("文件已关闭"); } }; // 第2步:创建 unique_ptr,并传入删除器类型 // ⚠️ 自定义删除器是 unique_ptr 类型的一部分 std::unique_ptr<std::FILE, decltype(file_deleter)> file_ptr(std::fopen("test.txt", "w"), file_deleter); // 第3步:正常使用 if (file_ptr) { std::fputs("Hello RAII\n", file_ptr.get()); } // 第4步:无需手动 fclose,析构时自动调用 file_deleterunique_ptr 删除器的零开销:unique_ptr 使用 EBO(空基类优化),当删除器是无状态的(如无捕获 lambda),不占用额外空间,与原始指针大小完全相同。
第四步:shared_ptr — 共享资源的"会计"
4.1 引用计数的本质
shared_ptr 内部维护一个控制块(Control Block),像"合租记账本":
共享资源 (int: 42) 控制块 ┌──────────────┐ ┌─────────────────┐ │ 42 │ │ 强引用计数: 3 │ ← 几个 shared_ptr 指向它 │ │ │ 弱引用计数: 1 │ ← 几个 weak_ptr 在观察 └──────────────┘ │ 删除器 │ ← 怎么释放 ▲ ▲ ▲ │ 分配器 │ │ │ │ └─────────────────┘ sp1 sp2 sp3 ▲ │ (指针指向)
#include <memory> #include <iostream> // 第1步:创建第一个 shared_ptr,引用计数 = 1 auto sp1 = std::make_shared<int>(42); std::cout << "sp1 引用计数: " << sp1.use_count() << "\n"; // 输出: 1 // 第2步:拷贝构造,引用计数 +1 auto sp2 = sp1; // 不涉及原子操作?错了,实际上引用计数的增减在多数实现中是原子的 std::cout << "拷贝后引用计数: " << sp1.use_count() << "\n"; // 输出: 2 // 第3步:创建新的作用域 { auto sp3 = sp1; // 引用计数 → 3 std::cout << "进入作用域后: " << sp1.use_count() << "\n"; // 输出: 3 } // sp3 离开作用域被销毁,引用计数 → 2 // 第4步:重置 sp1 sp1.reset(); // 引用计数 → 1,sp1 不再指向资源 // 资源仍然存活,因为 sp2 还持有它! std::cout << "reset 后 sp2 引用计数: " << sp2.use_count() << "\n"; // 输出: 14.2 控制块的创建时机
⚠️关键陷阱:控制块只在以下两种情况下创建:
| 创建方式 | 控制块来源 | 是否安全 |
| make_shared<T>(...) | 随对象一起分配(单次内存分配) | ✅ 推荐 |
| shared_ptr<T>(new T(...)) | 构造时独立创建 | ⚠️ 注意下面陷阱 |
| 从已有 shared_ptr 拷贝 | 共享已有控制块 | ✅ 安全 |
| 从 weak_ptr::lock() | 共享已有控制块 | ✅ 安全 |
// ⚠️ 经典错误:同一原始指针创建两个 shared_ptr → 双重释放! int* raw = new int(42); std::shared_ptr<int> sp1(raw); // 创建控制块 A,引用计数 = 1 std::shared_ptr<int> sp2(raw); // ❌ 创建控制块 B,引用计数 = 1 // sp1 析构 → 控制块 A 引用计数归零 → delete raw ✅ // sp2 析构 → 控制块 B 引用计数归零 → delete raw 💥 双重释放!未定义行为 // ✅ 正确做法:始终从第一个 shared_ptr 拷贝 std::shared_ptr<int> sp1(new int(42)); // 控制块 A,引用计数 = 1 std::shared_ptr<int> sp2(sp1); // 共享控制块 A,引用计数 = 24.3 shared_ptr 的线程安全
| 操作 | 线程安全性 | 说明 |
| 多个线程同时读取同一个 shared_ptr | ✅ 安全 | 只读不修改引用计数 |
| 多个线程同时拷贝同一个 shared_ptr | ✅ 安全 | 引用计数操作是原子的 |
| 多个线程同时写入同一个 shared_ptr 指向的对象 | ❌ 不安全 | 你需要在对象上加锁 |
| 一个线程读、一个线程写同一 shared_ptr 对象本身 | ❌ 不安全 | shared_ptr 本身不是线程安全的 |
// ✅ 引用计数的增减是原子的,多线程拷贝 shared_ptr 安全 std::shared_ptr<int> global_sp = std::make_shared<int>(0); void reader_thread() { auto local_copy = global_sp; // 原子递增引用计数,安全 // 使用 local_copy... } // local_copy 析构,原子递减引用计数,安全 // ❌ 但这不保护 shared_ptr 管理的对象本身 void writer_thread_bad() { *global_sp = 42; // ❌ 如果 reader 同时在读,这是数据竞争! } // ✅ 正确做法:在对象上加锁 #include <mutex> std::mutex global_mutex; // 保护对象数据的锁 void writer_thread_good() { std::lock_guard<std::mutex> lock(global_mutex); *global_sp = 42; // 互斥写 }第五步:make_shared 与 make_unique — 工厂函数的优势与陷阱
5.1 为什么推荐 make_shared?
对比两种写法:
// 方式 A:new + shared_ptr 构造(两次内存分配) std::shared_ptr<int> sp1(new int(42)); // 内存布局: // 堆区 1:int 对象 (42) // 堆区 2:控制块 (引用计数、删除器等) // → 两次分配 + 两次释放,缓存不友好 // 方式 B:make_shared(一次内存分配) auto sp2 = std::make_shared<int>(42); // 内存布局: // 堆区:{ 控制块 | int 对象 (42) } 连续存储 // → 单次分配,缓存友好,性能更优5.2 make_shared 的三个优势
优势 1:性能 —— 单次内存分配,减少 malloc 开销
优势 2:异常安全 —— 函数参数求值顺序不确定时,避免裸指针泄漏
优势 3:代码简洁 —— 避免写两次类型名
// 优势 2 的经典场景:异常安全 void foo(std::shared_ptr<int> a, std::shared_ptr<int> b); // ❌ 危险:参数求值顺序未定义 foo(std::shared_ptr<int>(new int(1)), // 先 new,再构造 shared_ptr std::shared_ptr<int>(new int(2))); // 如果这里抛异常,第一个 new 泄漏! // ✅ 安全 foo(std::make_shared<int>(1), std::make_shared<int>(2));5.3 ⚠️ make_shared 的陷阱
// 陷阱 1:make_shared 不支持自定义删除器 auto sp = std::make_shared<FILE>(std::fopen("a.txt", "r"), fclose); // ❌ // 需要自定义删除器时只能用构造函数 // 陷阱 2:make_shared 的内存"延迟释放"问题 auto sp = std::make_shared<BigObject>(); // BigObject 和控制块合在一起分配 std::weak_ptr<BigObject> wp = sp; // weak_ptr 指向控制块 sp.reset(); // 强引用计数归零 → BigObject 析构,但内存未释放! // 因为 weak_ptr 仍指向控制块,而控制块和 BigObject 共享内存块 // 必须等 wp 也销毁,整块内存才释放 → 对于大对象可能浪费内存 // 陷阱 3:make_unique 在 C++11 不存在,C++14 才引入 // C++11 中手动实现: // template<typename T, typename... Args> // std::unique_ptr<T> make_unique(Args&&... args) { // return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); // }第六步:weak_ptr — 打破循环引用的"观察者"
6.1 问题:循环引用如何造成内存泄漏
#include <memory> struct Node { std::shared_ptr<Node> next; // 指向下一个节点 std::shared_ptr<Node> prev; // 指向前一个节点 ~Node() { std::puts("节点销毁"); } }; void create_cycle() { auto node1 = std::make_shared<Node>(); // node1 引用计数 = 1 auto node2 = std::make_shared<Node>(); // node2 引用计数 = 1 node1->next = node2; // node2 引用计数 → 2 (node2 + node1->next) node2->prev = node1; // node1 引用计数 → 2 (node1 + node2->prev) // 函数结束: // node2 析构 → node2 引用计数从 2 → 1 (只剩 node1->next) // node1 析构 → node1 引用计数从 2 → 1 (只剩 node2->prev) // 💥 两个节点仍然互相引用,引用计数永不归零,内存泄漏! } create_cycle(); // 永远不会输出"节点销毁"图形化理解:
node1 ──next──→ node2 ↑ │ └──prev─────────┘ (互相持有,形成环,谁也无法释放)
6.2 解决方案:weak_ptr 不参与计数
struct SafeNode { std::shared_ptr<SafeNode> next; // 强引用:我持有下一个节点 std::weak_ptr<SafeNode> prev; // 弱引用:我只是"观察"上一个节点 ~SafeNode() { std::puts("安全节点销毁"); } }; void no_cycle() { auto node1 = std::make_shared<SafeNode>(); // node1 引用计数 = 1 auto node2 = std::make_shared<SafeNode>(); // node2 引用计数 = 1 node1->next = node2; // node2 引用计数 → 2 node2->prev = node1; // ⭐ weak_ptr 不增加引用计数,node1 仍为 1 // 函数结束: // node2 析构 → node2 引用计数 1 → 0 → 资源释放 ✅ // node1 析构 → node1 引用计数 1 → 0 → 资源释放 ✅ // 输出两次"安全节点销毁" } no_cycle();6.3 weak_ptr 的正确使用姿势
std::shared_ptr<int> sp = std::make_shared<int>(42); std::weak_ptr<int> wp = sp; // 不增加引用计数 // 第1步:使用前必须"升级"——尝试获得 shared_ptr if (auto locked = wp.lock()) { // lock() 返回 shared_ptr(如果资源还活着) std::cout << "资源存在,值为: " << *locked << "\n"; // 安全使用 } else { std::cout << "资源已释放\n"; // 资源已被销毁 } // 第2步:提前释放原始资源 sp.reset(); // 第3步:再次尝试升级 if (auto locked = wp.lock()) { // 返回空 shared_ptr // 不会执行到这里 } else { std::cout << "确实已释放\n"; // ✅ 正确检测到资源消失 } // 第4步:检查 weak_ptr 是否过期(不用来访问,仅做状态检查) if (wp.expired()) { // expired() = true 表示资源已不存在 std::cout << "weak_ptr 已过期\n"; }⚠️重要:wp.expired() 和 wp.lock() 之间存在 TOCTOU(Time-of-Check-Time-of-Use)竞争条件。多线程中应直接使用 lock(),不要分开检查。
第七步:自定义删除器实战
7.1 四种传入删除器的方式
#include <memory> #include <cstdio> #include <functional> // 方式1:函数指针(unique_ptr 的删除器是类型的一部分) void my_deleter(int* p) { std::printf("删除器被调用,值=%d\n", *p); delete p; } std::unique_ptr<int, void(*)(int*)> up1(new int(42), my_deleter); // 方式2:lambda 表达式(最常用,零开销) auto up2 = std::unique_ptr<int, decltype([](int* p) { delete p; })>( new int(42), [](int* p) { delete p; } ); // C++20 起更简洁(lambda 在未求值语境中可用): // auto up2 = std::unique_ptr<int, decltype([](int* p){ delete p; })>( // new int(42), [](int* p){ delete p; } // ); // 方式3:std::function(shared_ptr 专用,有开销) std::shared_ptr<int> sp1(new int(42), [](int* p) { delete p; }); // shared_ptr 的删除器不是类型的一部分 // 方式4:函数对象(可携带状态) struct FileCloser { void operator()(std::FILE* f) const { if (f) std::fclose(f); } }; std::unique_ptr<std::FILE, FileCloser> file_ptr(std::fopen("a.txt", "r"));7.2 实战:用自定义删除器管理非内存资源
#include <memory> #include <iostream> // 场景1:管理 Socket(以 Windows 为例,跨平台概念通用) #ifdef _WIN32 #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") struct SocketDeleter { void operator()(SOCKET* s) const { if (s && *s != INVALID_SOCKET) { closesocket(*s); // 关闭 socket std::puts("Socket 已关闭"); } delete s; // 释放指针本身 } }; // 使用: // std::unique_ptr<SOCKET, SocketDeleter> sock(new SOCKET(/* ... */)); #endif // 场景2:管理 OpenGL 纹理(跨平台示例) struct GLTextureDeleter { void operator()(unsigned int* tex) const { // glDeleteTextures(1, tex); // 伪代码:释放 GPU 纹理资源 std::printf("OpenGL 纹理 %u 已释放\n", *tex); } }; // std::unique_ptr<unsigned int, GLTextureDeleter> texture(new unsigned int(0)); // 场景3:malloc/free 配对(来自 C 库的内存) auto malloc_deleter = [](void* p) { std::free(p); }; std::unique_ptr<char, decltype(malloc_deleter)> buffer(static_cast<char*>(std::malloc(1024)), malloc_deleter);第八步:异常安全保证深度解析
8.1 C++ 异常安全的三个等级
| 保证等级 | 含义 | RAII 能提供吗? |
| 基本保证 | 抛异常后,程序状态仍然有效,无资源泄漏 | ✅ 默认提供 |
| 强保证 | 抛异常后,程序状态回滚到操作前(commit-or-rollback) | ✅ 配合 copy-and-swap |
| 不抛出保证(noexcept) | 该操作绝对不会抛异常 | ✅ 析构函数天然满足 |
8.2 RAII 天然提供基本保证
#include <fstream> #include <memory> #include <stdexcept> void write_to_file() { // 第1步:打开文件(可能失败) std::ofstream file("output.txt"); if (!file) { throw std::runtime_error("无法打开文件"); } // 第2步:申请缓冲区(可能失败 → 抛 bad_alloc) auto buffer = std::make_unique<char[]>(1024 * 1024); // 1MB // 第3步:处理数据(可能失败) process(buffer.get()); // 可能抛任意异常 // 如果任何一步抛异常: // - file 的析构函数关闭文件句柄 ✅ // - buffer 的析构函数释放内存 ✅ // → 无需手动 try-catch-finally! }8.3 用 copy-and-swap 实现强保证
#include <memory> #include <algorithm> class StringBuffer { private: std::unique_ptr<char[]> data_; size_t size_; public: // 追加字符串——提供强异常保证 void append(const char* str, size_t len) { // 第1步:在"副本"上操作(可能失败,但不影响原对象) auto new_data = std::make_unique<char[]>(size_ + len + 1); std::copy(data_.get(), data_.get() + size_, new_data.get()); std::copy(str, str + len, new_data.get() + size_); // 第2步:如果上面都成功了,原子地"交换"新旧状态 // swap 不抛异常(对 unique_ptr 是 noexcept) data_.swap(new_data); // ⚠️ 注意是 swap 不是 std::swap size_ += len; // 第3步:new_data 离开作用域,旧数据自动释放 // 如果第1步抛异常 → data_ 和 size_ 不变,状态回滚 ✅ } // 用 std::swap 也行: // void append(...) { // auto new_data = std::make_unique<char[]>(size_ + len + 1); // // ... 拷贝 ... // std::swap(data_, new_data); // 同样 noexcept // size_ += len; // } };8.4 析构函数必须 noexcept
class Resource { public: // ✅ 正确:析构函数默认 noexcept ~Resource() { // 这里的代码绝不能抛异常 release(); // 假设 release() 是 noexcept } // ❌ 危险:如果 release() 可能抛异常 ~Resource() noexcept(false) { // 显式声明可能抛异常(不推荐) release(); // 如果抛异常且已有未处理异常 → std::terminate! } // ✅ 正确防御:吞掉异常 ~Resource() { try { release(); } catch (...) { // 记录日志,但不传播异常 // std::cerr << "析构时释放资源失败\n"; } } };⚠️铁律:析构函数必须设计为 noexcept。如果在已经有一个异常传播时析构函数又抛异常,C++ 运行时将直接调用 std::terminate(),程序立刻终止。
第九步:shared_ptr 与 this 指针的正确传递
9.1 问题场景:从成员函数返回自身的 shared_ptr
#include <memory> class Widget { public: // ❌ 错误方式:从 this 构造新的 shared_ptr std::shared_ptr<Widget> get_shared_bad() { return std::shared_ptr<Widget>(this); // 💥 创建第二个控制块! } }; void test_bad() { auto sp1 = std::make_shared<Widget>(); auto sp2 = sp1->get_shared_bad(); // sp1 和 sp2 各自有独立控制块 → 双重释放! }9.2 解决方案:enable_shared_from_this
#include <memory> class Widget : public std::enable_shared_from_this<Widget> { // ↑ 必须 public 继承 public: // ✅ 正确方式:使用 shared_from_this() std::shared_ptr<Widget> get_shared_good() { return shared_from_this(); // 共享已有控制块,安全! } // 工厂函数:强制通过 shared_ptr 创建 static std::shared_ptr<Widget> create() { return std::shared_ptr<Widget>(new Widget()); } private: Widget() = default; // 构造函数私有化,防止栈上创建 }; void test_good() { auto sp1 = Widget::create(); // 引用计数 = 1 auto sp2 = sp1->get_shared_good(); // 引用计数 = 2,共享同一控制块 ✅ // 都析构后引用计数归零,正确释放 }9.3 ⚠️ enable_shared_from_this 的三个注意事项
// 陷阱1:使用 shared_from_this() 前必须已有 shared_ptr 管理该对象 class Bad : public std::enable_shared_from_this<Bad> { public: void dangerous() { auto sp = shared_from_this(); // ❌ 如果没有 shared_ptr 管理,抛 bad_weak_ptr } }; // Bad b; // 栈对象 // b.dangerous(); // 💥 抛出 std::bad_weak_ptr 异常! // 陷阱2:构造函数中不能用 shared_from_this() class AlsoBad : public std::enable_shared_from_this<AlsoBad> { public: AlsoBad() { // auto sp = shared_from_this(); // ❌ 此时 weak_ptr 尚未初始化! } }; // 陷阱3:必须 public 继承 // class Wrong : private std::enable_shared_from_this<Wrong> { }; // ❌ 私有继承不行第十步:C++14 / 17 / 20 演进关键点
10.1 C++14:make_unique 正式引入
// C++11:没有 make_unique,只能这样写 std::unique_ptr<int> p11(new int(42)); // C++14:make_unique 标准化,推荐使用 auto p14 = std::make_unique<int>(42); // 单个对象 auto arr14 = std::make_unique<int[]>(100); // 数组10.2 C++17:shared_ptr 支持数组
// C++17 起,shared_ptr 可以直接管理数组 auto sp_arr = std::shared_ptr<int[]>(new int[10]); // ✅ C++17+ // 析构时自动调用 delete[](而非 delete) // 使用下标访问 sp_arr[3] = 42; // ⚠️ 但 make_shared 对数组的支持到 C++20 才完善 // C++17 中 make_shared<int[]> 的行为是实现定义的,不推荐使用10.3 C++17:weak_ptr 对数组的支持
// C++17 起,weak_ptr 也能指向数组 std::shared_ptr<int[]> sp(new int[5]); std::weak_ptr<int[]> wp = sp; if (auto locked = wp.lock()) { locked[0] = 10; // 通过 shared_ptr<int[]> 使用下标 }10.4 C++20:atomic<shared_ptr>
#include <memory> #include <atomic> // C++20 之前:用 std::atomic 的特化 free 函数 std::shared_ptr<int> global_sp = std::make_shared<int>(0); void legacy_thread_safe_update() { auto local = std::make_shared<int>(42); // 原子地交换 global_sp(C++11~17 方式) std::atomic_store(&global_sp, local); } void legacy_thread_safe_read() { // 原子地读取 global_sp auto local = std::atomic_load(&global_sp); } // C++20:真正的 atomic<shared_ptr<T>>(部分编译器支持) // #if __cplusplus >= 202002L // std::atomic<std::shared_ptr<int>> atomic_sp = std::make_shared<int>(0); // // void modern_thread_safe_update() { // auto local = std::make_shared<int>(42); // atomic_sp.store(local); // 原子存储 // } // // void modern_thread_safe_read() { // auto local = atomic_sp.load(); // 原子加载 // } // #endif⚠️ atomic<shared_ptr<T>> 在 C++20 标准中已定义,但实际编译器支持进度不一。截止 2026 年,GCC 12+ / Clang 16+ / MSVC 2022 17.8+ 已支持。
10.5 演进时间线总览
C++11 (2011) ├── unique_ptr / shared_ptr / weak_ptr 三件套诞生 ├── make_shared 引入 └── enable_shared_from_this 引入 C++14 (2014) └── make_unique 标准化(补上了 C++11 的遗漏) C++17 (2017) ├── shared_ptr<T[]> 数组支持 ├── weak_ptr<T[]> 数组支持 └── shared_ptr::weak_type 类型别名 C++20 (2020) ├── atomic<shared_ptr<T>> 标准化 ├── make_shared<T[]> 完善 └── make_unique_for_overwrite(不初始化,性能优化)
第十一步:性能对比与选型决策树
11.1 三种智能指针的开销对比
| 指针类型 | 内存占用 | 拷贝开销 | 析构开销 | 适合场景 |
| unique_ptr | 1 个指针(无自定义删除器时) | 移动 = 指针赋值(几乎零开销) | 直接 delete | 独占所有权(90%+) |
| shared_ptr | 2 个指针(对象 + 控制块) | 拷贝 = 原子递增引用计数 | 原子递减 + 可能 delete | 共享所有权 |
| weak_ptr | 2 个指针 | 拷贝 = 原子递增弱计数 | 原子递减弱计数 | 打破循环 / 缓存 |
11.2 选型决策树
你需要的所有权模式是? │ ├── 独占所有权(只有一个所有者) │ └── 使用 unique_ptr ✅ │ ├── 共享所有权(多个所有者,最后一个负责释放) │ ├── 可能有循环引用? │ │ ├── 是 → shared_ptr + weak_ptr 组合 │ │ └── 否 → shared_ptr │ └── 需要观察但不拥有? │ └── weak_ptr │ └── 不需要所有权(只是临时访问) └── 使用原始指针 T* 或引用 T&(不拥有资源)
常见问题速查表
| 问题 | 答案 | 关键点 |
| unique_ptr 能拷贝吗? | 不能,只能移动 | 拷贝构造和拷贝赋值被 = delete |
| make_shared 比 new 好吗? | 大部分情况是 | 单次分配、异常安全,但不支持自定义删除器 |
| 什么时候用 weak_ptr? | 打破循环引用 / 缓存观察 | 使用前必须 lock() |
| 智能指针能指向栈对象吗? | 不能,除非用空删除器 | 智能指针默认调用 delete,栈对象不需要 |
| shared_ptr 线程安全吗? | 引用计数操作安全,对象访问不安全 | 需要额外加锁保护对象 |
| 析构函数能抛异常吗? | 绝对不能 | 会导致 std::terminate |
| enable_shared_from_this 怎么用? | public 继承,必须已有 shared_ptr 管理 | 构造函数中不能用 |
| C++14 有什么更新? | make_unique 标准化 | 补上 C++11 的遗漏 |
| C++17 有什么更新? | shared_ptr<T[]> 数组支持 | 自动调用 delete[] |
| C++20 有什么更新? | atomic<shared_ptr> | 无锁的线程安全 shared_ptr |
| 怎么用智能指针管理 FILE*? | 自定义删除器 | lambda:[](FILE* f){ fclose(f); } |
| unique_ptr 比 shared_ptr 快多少? | 大约 3~5 倍 | 无原子操作、无控制块 |
| 智能指针和原始指针性能一样吗? | unique_ptr 几乎一样 | shared_ptr 有原子操作开销 |
延伸阅读推荐
- 《Effective Modern C++》第 4 章— Scott Meyers 关于智能指针的经典论述
- cppreference.com— std::unique_ptr / std::shared_ptr / std::weak_ptr 官方文档
- C++ Core Guidelines: R.规则* — 资源管理与 RAII 的最佳实践
- Boost 库文档— 智能指针的原始实现(C++11 标准库的前身)