C++ 智能指针与 RAII 深度解析:从资源管理到异常安全
2026/7/24 19:05:16 网站建设 项目流程

适用读者:零基础小白,有 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 三个字母的含义

字母全称含义生活类比
RResource资源你借的那本书
AAcquisition获取(即初始化)借书的动作(构造函数执行)
IInitialization初始化把书拿到手的同时登记(构造即获取)
IIs——(连接词,无特殊含义)
(隐含)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_deleter

unique_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"; // 输出: 1

4.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,引用计数 = 2

4.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_ptr1 个指针(无自定义删除器时)移动 = 指针赋值(几乎零开销)直接 delete独占所有权(90%+)
shared_ptr2 个指针(对象 + 控制块)拷贝 = 原子递增引用计数原子递减 + 可能 delete共享所有权
weak_ptr2 个指针拷贝 = 原子递增弱计数原子递减弱计数打破循环 / 缓存

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 标准库的前身)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询