C++标准库之智能指针

参考:C++ 内存管理库 <memory> | 菜鸟教程 (runoob.com)

<memory> 是 C++ 标准库中的一个头文件,它包含了用于动态内存管理的模板和函数。

在 C++ 中,内存管理是一个重要的概念。动态内存管理允许程序在运行时分配和释放内存,这在处理不确定大小的数据结构时非常有用。然而,不正确的内存管理可能导致内存泄漏、野指针等问题。

<memory> 头文件提供了智能指针等工具,帮助开发者更安全地管理动态内存。

什么是智能指针

智能指针是 C++ 标准库 <memory> 提供的、基于 RAII 实现、用来管理堆资源生命周期的包装类 ,本质是对裸指针的封装。C++ 没有垃圾回收 GC,需要程序员自己管理new/delete;裸指针很容易出现内存泄漏、重复释放、野指针等问题,智能指针就是用来降低这类内存错误的工具。

核心思想:资源生命周期绑定智能指针对象的生命周期。智能指针一般是栈上局部对象,离开作用域时自动执行析构,在析构里自动释放托管的堆内存(或者其他资源),不用你手动写delete。

标准库一共提供三种智能指针:std::unique_ptr、std::shared_ptr、std::weak_ptr,它们用不同的所有权模型来管理资源,适用场景不一样。

一. std::unique_ptr 独占智能指针(首选、默认推荐)

  • 所有权规则:独占所有权,同一份资源同一时间只能由一个 unique_ptr 管理
  • 特性:禁止拷贝 ,只支持std::move移动所有权;移动之后原来的 unique_ptr 变为空,资源交给新的 unique_ptr
  • 开销:极低,内部只存裸指针(加可选删除器),性能几乎等价裸指针
  • 适合场景:资源只需要一个管理者,不需要多方共享(绝大多数业务场景)
  • 支持数组形式 unique_ptr<T[]>,可以管理new T[]数组

二. std::shared_ptr 共享智能指针(谨慎使用)

  • 所有权规则:共享所有权,多个 shared_ptr 可以同时指向同一份资源
  • 原理:附带引用计数,每多一个 shared_ptr 拷贝,计数 + 1;shared_ptr 析构时计数 - 1;计数降到 0 的时候,释放底层资源
  • 代价:需要在堆上分配一块控制块存放引用计数,增减计数是原子操作,相比 unique_ptr 有额外性能开销
  • 适合场景:确实需要多个地方同时持有同一份资源,并且谁都不确定谁是最后使用者
  • 坑:互相持有 shared_ptr 会造成循环引用,引用计数永远无法归零,内存泄漏

三. std::weak_ptr 弱指针(配套 shared_ptr,不能单独使用)

  • 所有权规则:没有所有权,只是观察者,不会增加 shared_ptr 的引用计数
  • 特性:不能直接解引用访问资源;想要使用资源,必须调用.lock(),尝试获取一个临时的 shared_ptr
    • lock 成功:返回有效的 shared_ptr;
    • 资源已经释放:返回空 shared_ptr
  • 主要用途:专门解决 shared_ptr 的循环引用问题

通用基础能力

  • 支持 * 解引用、-> 访问成员,语法上和裸指针很像
  • .get():取出底层原始裸指针(只适合临时使用,不要长期保存)
  • .reset():主动放弃资源,或者接管新资源
  • 可以直接 if (ptr) 判断智能指针是否持有有效资源

重要边界(它不是万能的)

  • 智能指针只管托管资源的释放 ,不保证多线程安全:shared_ptr 的引用计数是原子的(安全),但是多个线程同时读写它指向的对象,依然需要加锁;
  • 智能指针只适合管理堆上由new分配的对象(也可以配合自定义删除器管理文件句柄、fd 等外部资源);
  • 不要滥用 shared_ptr,优先 unique_ptr;weak_ptr 只是配套补救方案,不是常规选择;
  • 智能指针本身是普通对象,如果把智能指针自己 new 到堆上,RAII 就失效了。

基本语法

我们先以以下std::unique_ptr<int> p;为例来说明这里面的语法

复制代码
std::unique_ptr<int> p;

一.先拆分模板语法

std::unique_ptr 是一个类模板

复制代码
template< class T, class Deleter = std::default_delete<T> >
class unique_ptr;
  • 第一个模板参数 int(这里就是 T=int):代表它要托管的是什么类型的对象 也就是:这个 unique_ptr 负责管理一块堆上的 int。
  • 第二个模板参数是删除器,默认是 std::default_delete<int>,可以省略不写。

所以:

复制代码
std::unique_ptr<int> p;
// 等价完整写法
std::unique_ptr<int, std::default_delete<int>> p;

p 是变量名。 p 的类型是:std::unique_ptr<int>,也就是说,p其实就是个类对象

简单拆开:

  • p 本身是一个局部变量,放在栈上;
  • p 这个变量里面,存了一个 int*(裸指针);
  • 这个内部裸指针,指向堆上面 new 出来的那个 int(值是 10)。

画图理解:

复制代码
【栈内存】
p (std::unique_ptr<int>对象)
   ↓ 里面保存一个int*指针
【堆内存】
那个int,值=10

p 能干什么

p 是智能指针对象,不是那个 int 本身!

  • *p:取出 p 内部指针指向的堆上的 int
  • p.get():拿到 p 里面存的那个裸指针

重点一句话区分

  • 堆上那个 int:是被管理的数据;
  • p:是管理者(unique_ptr 对象),负责照看这个 int,出作用域自动 delete 它。

再看移动的例子

复制代码
std::unique_ptr<int> a = std::make_unique<int>(10);
std::unique_ptr<int> b = std::move(a);

这里:

  • a 和 b 是两个独立的 unique_ptr<int> 变量;
  • std::move(a) 把 a 里面存的裸指针,转移给 b;
  • 转移之后:
    • b 里面的指针指向堆上 int(b 现在是管理者)
    • a 里面的指针变成 nullptr,a 不再管理任何东西

容易踩坑的点

❌ 不要理解成:p 就是堆上的 int✅ 正确理解:p 是一个包装对象,它里面装着指向堆 int 的指针,负责生命周期管理。

二. 几种初始化写法(std::unique_ptr<int>)

方式 1:空的 unique_ptr,什么都不管理

复制代码
std::unique_ptr<int> p;

此时 p 里面存的裸指针是 nullptr,没有托管任何堆内存。if (p) 判断会是 false。

方式 2:传入 new 出来的 int(老式写法,可以看懂,但不推荐优先用)

复制代码
std::unique_ptr<int> p(new int(100));
  • new int(100):在堆上分配一个 int,值初始化为 100,返回 int*
  • unique_ptr 拿到这个裸指针,接管管理权
  • 离开作用域的时候,unique_ptr 析构,自动执行 delete

⚠️ 不要这么写:

复制代码
int* raw = new int(100);
std::unique_ptr<int> p(raw);
std::unique_ptr<int> p2(raw); // 严重错误!两个unique_ptr接管同一块内存,double free崩溃

方式 3:std::make_unique(C++14 起,推荐写法)

复制代码
std::unique_ptr<int> p = std::make_unique<int>(200);

std::make_unique<int>(200) 内部帮你做 new int(200),返回一个 std::unique_ptr<int>。


三. 访问托管的 int

复制代码
std::unique_ptr<int> p = std::make_unique<int>(10);

*p = 66;        // 解引用:*p 就是那个堆上的int
int val = *p;   // 读取里面的值

如果托管的是类,就可以用 ->:

复制代码
std::unique_ptr<std::string> str_p = std::make_unique<std::string>("hi");
str_p->size();

四. unique_ptr<int> 不能拷贝,只能移动

复制代码
std::unique_ptr<int> a = std::make_unique<int>(100);

// ❌ 编译报错!unique_ptr<int> 禁止拷贝
std::unique_ptr<int> b = a;

✅ 只能用 std::move 转移所有权:

复制代码
std::unique_ptr<int> a = std::make_unique<int>(100);
std::unique_ptr<int> b = std::move(a);

执行之后:

  • b 接管堆上那个 int
  • a 变成空(内部指针 = nullptr),不再管理资源

这就是 "独占" 的含义:同一时间,只能有一个 unique_ptr<int> 负责这块内存。


五. 它的成员函数(针对 std::unique_ptr<int>)

复制代码
std::unique_ptr<int> p = std::make_unique<int>(10);

int* raw = p.get();
// get():取出内部保存的 int*,只是拿指针看一眼,所有权还在p身上
// 警告:不要 delete 这个 raw!p还会自动delete,造成double free

p.reset();
// reset():释放当前托管的int,p变空,等价于销毁资源

p.reset(new int(999));
// 先释放旧的int,再接管新new出来的int

int* old = p.release();
// release():把内部int*拿出来交给你,p直接变空
// ⚠️ 现在你必须手动 delete old,不然内存泄漏!

if (p) {
    // 判断p是否持有有效的int资源
}

六. 数组版本:std::unique_ptr<int[]>,区分开 std::unique_ptr<int>

复制代码
// 单个int
std::unique_ptr<int> p_single = std::make_unique<int>(10);

// int数组,new int[5]
std::unique_ptr<int[]> p_arr = std::make_unique<int[]>(5);
p_arr[0] = 1;
p_arr[1] = 2;
  • std::unique_ptr<int>:管理单个 int 对象 ,没有下标 [] 用法
  • std::unique_ptr<int[]>:管理int 数组 ,支持 p_arr[i] 访问数组元素

这个模板参数差别很大,不要写混。


七. 一句话总结 std::unique_ptr<int>

std::unique_ptr<int> 是一个模板实例化出来的类;它是一个栈上的小对象 ,内部包着一个 int*;它的职责:独占管理堆上的一个 int,出作用域自动 delete;语法上:禁止拷贝,只能 move 转移所有权;支持 *、.get()、.reset()、.release()。


再讲讲另外的两个智能指针↓↓↓↓↓↓↓↓↓↓


八、std::shared_ptr 共享智能指针

类型全名:std::shared_ptr<T>特点:可以拷贝,引用计数管理生命周期

1. 定义初始化

写法 1:包裹 new(不推荐)

复制代码
std::shared_ptr<int> sp1(new int(10));

写法 2:std::make_shared(推荐)

复制代码
std::shared_ptr<int> sp2 = std::make_shared<int>(20);

写法 3:空 shared_ptr

复制代码
std::shared_ptr<int> sp3;

2. 拷贝(和 unique_ptr 最大区别)

复制代码
std::shared_ptr<int> a = std::make_shared<int>(100);
std::shared_ptr<int> b = a;
// a 和 b 指向同一块内存,引用计数 +1

3. 访问对象,语法和 unique_ptr 一样

复制代码
std::shared_ptr<std::string> s_ptr = std::make_shared<std::string>("test");
*s_ptr;
s_ptr->empty();

4. 成员函数

复制代码
std::shared_ptr<int> sp = std::make_shared<int>(10);

long cnt = sp.use_count();  // use_count():拿到当前引用计数,仅调试用
int* raw = sp.get();        // 获取底层裸指针
sp.reset();                 // 放弃引用,计数减一
sp.reset(new int(999));     // 放弃旧资源,接管新资源

if (sp) {
    // 是否有效
}

注意:C++17 之前 shared_ptr<T[]> 支持很差,数组优先 unique_ptr


九、std::weak_ptr 弱指针,只能配合 shared_ptr

类型全名:std::weak_ptr<T>重点:weak_ptr 不能单独创建、不能直接 * 或者 ->,只能从 shared_ptr 构造

1. 初始化

复制代码
std::shared_ptr<int> master = std::make_shared<int>(100);
std::weak_ptr<int> wp = master;

2. 核心函数 lock (),获取 shared_ptr

复制代码
// lock() 返回 shared_ptr<int>
std::shared_ptr<int> temp = wp.lock();
if (temp) {
    // 资源还活着,可以使用
    *temp = 200;
}

3. 成员函数

复制代码
bool is_expire = wp.expired(); // expired():判断资源是否已经释放

❌ 错误写法:

复制代码
*wp;        // 编译报错!weak_ptr不支持解引用
wp->xxx();  // 编译报错!

十、统一整理【支持的语法清单】

对于 unique_ptr /shared_ptr(两者都支持)

  • *ptr:解引用,拿到托管对象
  • ptr->成员:访问托管对象的成员、成员函数
  • ptr.get():获取底层 const / 非 const 裸指针
  • ptr.reset():释放资源,置空
  • ptr.reset(裸指针):更换托管资源
  • if(ptr)、!ptr:布尔判断是否持有有效资源

unique_ptr 额外

  • std::move 转移所有权
  • .release() 交出资源返回裸指针
  • 支持数组版本 unique_ptr<T[]>

shared_ptr 额外

  • 支持拷贝赋值 sp2 = sp1
  • .use_count() 获取引用计数

weak_ptr 仅有的语法

  • 从 shared_ptr 赋值构造
  • .lock() → shared_ptr
  • .expired() 判断是否失效

完整可直接编译示例

复制代码
#include <memory>
#include <string>
#include <cstdio>

int main()
{
    // unique_ptr
    std::unique_ptr<int> up = std::make_unique<int>(10);
    *up = 20;

    std::unique_ptr<int> up_move = std::move(up);

    // shared_ptr
    std::shared_ptr<std::string> sp = std::make_shared<std::string>("hello");
    std::shared_ptr<std::string> sp_copy = sp;

    // weak_ptr
    std::weak_ptr<std::string> wp = sp;
    std::shared_ptr<std::string> temp = wp.lock();
    if (temp)
    {
        temp->size();
    }

    return 0;
}

十一、禁止的语法(一定要记住)

  • unique_ptr 不能拷贝赋值
  • weak_ptr 不能 *、->
  • 不要长期保存 .get() 返回的裸指针
  • 不要把同一个 new 出来的裸指针交给两个独立 shared_ptr,会 double free

make_unique和make_shared

前置头文件

复制代码
#include <memory>

两个都是标准库里面的函数模板 ,作用:帮你分配堆内存,并且返回智能指针。我们不用 auto,全部写完整类型。

1. std::make_unique(C++14 才有的)

原型理解(简化):

复制代码
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args);

作用:在堆上分配一个 T 对象,构造这个对象,返回一个 std::unique_ptr<T>。

示例:

复制代码
// 在堆上 new int(100),返回 std::unique_ptr<int>
std::unique_ptr<int> p = std::make_unique<int>(100);

等价效果(但是优先推荐上面 make_unique 写法):

复制代码
std::unique_ptr<int> p(new int(100));

数组版本 make_unique:

复制代码
// 分配 int[5],返回 std::unique_ptr<int[]>
std::unique_ptr<int[]> arr = std::make_unique<int[]>(5);

2. std::make_shared(C++11 就有)

原型简化:

复制代码
template<typename T, typename... Args>
std::shared_ptr<T> make_shared(Args&&... args);

作用:堆上分配 T 对象 + 配套的引用计数控制块,一次性一块内存分配 ,返回 std::shared_ptr<T>。

示例:

复制代码
std::shared_ptr<int> sp = std::make_shared<int>(200);

等价效果(不推荐这种直接 new 写法)

复制代码
std::shared_ptr<int> sp(new int(200));

核心区别(重点)

返回类型不一样

  • make_unique<T> → 返回 std::unique_ptr<T>(独占指针)
  • make_shared<T> → 返回 std::shared_ptr<T>(共享指针,带引用计数)

内存分配方式不一样

  • make_unique:只分配一份 T 对象;控制信息在 unique_ptr 本身里面,没有额外单独堆上的控制块
  • make_shared:一次堆分配,一块连续内存同时存放 T 对象 + shared_ptr 的引用计数控制块✅ 好处:减少一次堆分配,效率更高,内存碎片更少

数组支持

  • make_unique 原生支持数组:make_unique<int[]>(5)
  • make_shared 数组支持很晚、日常很少用,数组优先用 unique_ptr + make_unique

开销

  • make_unique 开销很小,几乎等于裸指针
  • make_shared 有引用计数,拷贝的时候会做原子加减,有额外开销

生命周期坑(非常重要)make_shared 把对象和计数放同一块内存。哪怕你所有 shared_ptr 都 reset 掉、计数变成 0,这块整块内存不会立刻还给操作系统,直到所有弱引用 wp 全部消失(weak_ptr 会引用这块控制块)。


什么时候选谁

  • 不需要多个地方共享这个资源 → 优先 make_unique(默认首选)
  • 需要多个 shared_ptr 一起共享同一个资源,不知道谁最后释放 → 用 make_shared

为什么推荐用 make_unique /make_shared,而不是直接 new

  • 异常安全更好
  • 代码简洁,把 new 封装起来,不要裸 new 暴露在外
  • make_shared 一次分配,性能更好

两行对照放一起

复制代码
// unique,独占
std::unique_ptr<int> up = std::make_unique<int>(10);

// shared,共享,可以拷贝
std::shared_ptr<int> sp = std::make_shared<int>(20);
std::shared_ptr<int> sp2 = sp;

补充:它们也是函数模板

make_unique<int> 就是函数模板实例化:模板参数 T=int,编译器生成一个版本的 make_unique 函数,专门用来创建管理 int 的 unique_ptr

智能指针分配的内存是在堆上吗

先把一句话结论说清楚:

智能指针这个对象本身,一般在栈上;它托管的、make_unique/make_shared 分配出来的 T 对象,是在堆上。

拆开两层,一定要分开看:

复制代码
auto p = std::make_unique<int>(10);
  1. p:std::unique_ptr<int> 这个对象本身 这里 p 是局部变量,放在栈内存 。它很小,内部就存一个裸指针(再加删除器)。出 {} 作用域,p(栈上对象)析构,析构函数里去释放堆内存。

  2. make_unique<int> 里面 new 出来的 int(10) 这块 int 数据,分配在 堆(heap),由 p 托管。


再看 make_shared(有点不一样)

复制代码
auto sp = std::make_shared<int>(20);

make_shared 会一次性在堆上分配一块连续内存:同时存放用户对象 + shared_ptr 的引用计数控制块。

  • sp 这个 shared_ptr 对象本身:栈上
  • 目标 int + 控制块:都在同一块堆内存

这也是 make_shared 的优势:一次堆分配,开销更小。

容易混淆的例子

复制代码
std::unique_ptr<int> func()
{
    auto p = std::make_unique<int>(100);
    return p; // p本身在栈,通过move把里面堆资源的所有权转移出去
}

返回的是智能指针对象(移动),堆上的 int 不会被拷贝,只是所有权转移。

特殊情况:智能指针也可以托管不在堆上的资源

unique_ptr 支持自定义删除器,你可以让它托管栈上对象、fd、FILE 句柄,只是业务几乎不会这么干,属于高级用法:

复制代码
int x = 10; // x 在栈
std::unique_ptr<int, void(*)(int*)> ptr(&x, [](int*){});
// 托管栈变量,删除器什么都不做;只是语法允许,不推荐日常写

总结记忆

  • ✅ 智能指针变量本身:局部变量大多在栈
  • ✅ make_unique /make_shared 分配出来的被管理对象:在堆
  • ❌ 智能指针 ≠ 堆内存,它只是一个管理堆资源的包装器

为什么要设计智能指针

根源:裸指针 + new/delete 这套方案,天生容易出错,而且错误很难排查,C++ 没有 GC(垃圾回收),需要程序员自己管堆内存。

我们先看裸指针的痛点,智能指针就是为解决这些痛点而生的:

1. 裸指针最大痛点:很难保证 delete 一定会被执行

复制代码
void test() {
    int* p = new int(100);

    if (some_condition) {
        return; // 直接return了!下面delete跑不到 → 内存泄漏
    }

    do_something(); // 如果这里抛异常,程序直接跳出函数,delete也执行不到

    delete p;
}

人很难把所有分支(提前 return、异常、多重 if)全部照顾到,一不小心就漏 delete,造成内存泄漏。

智能指针依靠 RAII :智能指针本身是栈对象,只要离开作用域(无论正常结束、return、抛异常),析构函数一定会调用,自动释放资源。编译器帮你保证释放逻辑一定执行,不靠人记着写 delete。

2. 防止 double free(重复释放)

裸指针很容易出现:

复制代码
int* p = new int;
delete p;
delete p; // 灾难,double free,程序崩溃、堆损坏

还有一种场景:一份堆内存交给两个裸指针管理,两个人都去 delete。

智能指针用所有权模型在类型层面约束:

  • unique_ptr:独占,不能拷贝,同一时刻只有一个管理者;所有权转移后原来的指针变空,不能再释放
  • shared_ptr:引用计数,计数归零才释放,天然不会重复释放

编译器直接禁止很多危险操作,而不是等到运行时炸了才发现。

3. 减少野指针

裸指针 delete 之后,指针变量还保留原来的地址,变成野指针,后续再使用就是未定义行为:

复制代码
int* p = new int;
delete p;
*p = 10; // 野指针,UB,有时候跑起来有时候崩溃,极难复现

unique_ptr 释放之后内部置空;shared_ptr 在计数归 0 后资源销毁,指针本身不再指向有效对象,减少误用机会。

4. 方便资源生命周期传递(所有权转移 / 共享)

很多业务场景需要在多个函数、多个对象之间传递资源:

  • 场景 A:资源交给另一个对象接管(转移,谁拿到谁负责释放) → unique_ptr 移动语义
  • 场景 B:多个地方同时在用同一份资源,谁都不确定谁最后一个用完 → shared_ptr 引用计数

如果只用裸指针,你必须自己写一套引用计数、自己约定规则、自己维护,很容易写 bug;智能指针把这套成熟逻辑内置在标准库里。

5. 不只是内存,统一管理各种资源

RAII 是通用思想,不局限于 new 出来的内存。unique_ptr 支持自定义删除器,可以管理:文件句柄 FILE、socket fd、锁、数据库句柄等。一套模式:进入作用域拿到资源,出作用域自动释放,统一编程范式。

6. 对比别的语言,理解定位

Java、Go 有 GC,自动回收堆内存;C++不想要 GC 带来的停顿、开销、不可控 ,所以不引入垃圾回收。智能指针是手动可控、编译期辅助的资源管理方案,兼顾性能和安全。


一句话浓缩设计目的

C++ 没有 GC,裸指针手动管理资源极易泄漏、重复释放、产生野指针;于是基于 RAII 设计智能指针:用栈对象的析构自动释放资源,并用所有权(独占 / 共享)规范资源归属,把容易写错的内存管理逻辑交给编译器和标准库,在几乎不损失性能的前提下减少内存 bug。

补充:它不是万能的,设计上的取舍

它没有解决所有问题,所以不是强制替换一切裸指针:

  • shared_ptr 有原子引用计数的开销,所以优先 unique_ptr;
  • shared_ptr 存在循环引用问题,配套 weak_ptr 补救;
  • 它不会帮你解决多线程并发访问对象本身的安全问题;
  • 它依然要求程序员理解生命周期,比如 string_view、裸指针长期持有 get () 返回值依然会踩坑。

如果你想,我们可以单独讲 RAII 到底是什么,这个是智能指针的底层核心思想。

RAII是啥

Resource Acquisition Is Initialization 直译:资源获取即初始化。它是 C++ 最重要的编程范式,智能指针、锁、文件流全都是靠 RAII 实现的。

一句话通俗理解:在构造函数里拿到资源,在析构函数里释放资源;资源的生命周期,绑定到这个对象的生命周期。

拆解两句话

  1. 获取资源 = 对象构造的时候(构造函数)
  2. 释放资源 = 对象销毁的时候(析构函数)

这个对象一般是栈上局部对象 。栈对象有个特点:只要离开作用域(}、return、抛出异常),一定会自动调用析构函数,由 C++ 语言保证。

举最简单例子(不用智能指针,自己手写一个 RAII 类)

复制代码
#include <iostream>

class IntGuard {
public:
    // 构造:获取资源(分配内存)
    IntGuard() {
        ptr = new int(100);
        std::cout << "获取内存\n";
    }

    // 析构:释放资源(释放内存)
    ~IntGuard() {
        delete ptr;
        std::cout << "释放内存\n";
    }

    int* get() { return ptr; }
private:
    int* ptr;
};

void func()
{
    IntGuard g; // 构造,拿到资源
    // 不管这里提前 return
    // 或者中间代码抛异常
    // 只要离开这个大括号,g 就会析构,自动 delete
    *g.get() = 200;
} // 出作用域,g析构,释放内存

对比裸指针:裸指针要你手动写 delete,很容易漏掉。RAII:释放动作写在析构里,编译器帮你保证一定执行。

智能指针 unique_ptr / shared_ptr,本质就是标准库已经写好的 RAII 包装类!

RAII 管理的不只是堆内存

凡是需要「获取 + 手动释放」的东西,都能用 RAII:

  • 堆内存(智能指针)
  • 互斥锁 std::lock_guard(构造 lock,析构 unlock)
  • 文件句柄、socket、数据库连接
  • 临时状态回滚等

锁的经典例子:

复制代码
std::mutex mtx;

void safe()
{
    std::lock_guard<std::mutex> lock(mtx); 
    // lock_guard 是RAII:构造上锁,析构自动解锁
    // 哪怕中间return、抛异常,锁一定会解开
}

重点:为什么 RAII 能解决异常、提前 return 泄漏?

C++ 语言规则:栈上局部对象,在栈展开(stack unwinding)的时候一定会析构。只要对象是栈变量,不管函数是正常走完、提前 return、异常跳出,析构都跑。

裸指针方案的痛点:

复制代码
int* p = new int;
throw 1;  // 直接跳走,后面delete永远执行不到,泄漏
delete p;

RAII 方案:只要对象 g 在栈上,抛异常时 g 依然会析构,资源释放。

容易踩的误区

  1. RAII 对象一般放栈上 才有这个效果!如果你 IntGuard* g = new IntGuard();,那 g 本身是堆对象,你不 delete 它,它的析构根本不会跑,RAII 失效。
  2. RAII 不是 "垃圾回收 GC"GC 是运行时扫描,自动找没人用的内存回收;RAII 是编译期、作用域驱动,对象一出作用域立刻释放,时机完全确定,没有 GC 停顿。

一句话总结背诵

RAII = 把资源的获取放到构造函数、释放放到析构函数,利用栈对象自动析构的语言特性,保证资源一定会释放,是智能指针、lock_guard 的底层原理。

enable_shared_from_this

enable_shared_from_this<>啥意思

std::enable_shared_from_this<T> 是 C++ 标准库中的一个模板类,用于让类的对象能够安全地获取指向自身的 std::shared_ptr。这在需要将当前对象的引用传递给外部函数或存储在容器中时特别有用。

一、核心用途:安全获取自身的 shared_ptr

当一个对象被 std::shared_ptr 管理时,直接通过 this 指针返回自身会导致引用计数不一致,可能引发双重释放。enable_shared_from_this 提供了 shared_from_this() 方法,安全地返回一个新的 std::shared_ptr,与现有管理该对象的 shared_ptr 共享引用计数。

错误示例(直接返回 this):

复制代码
class Bad {
public:
    std::shared_ptr<Bad> getShared() {
        return std::shared_ptr<Bad>(this); // 危险!创建独立的 shared_ptr
    }
    ~Bad() { std::cout << "Bad destroyed" << std::endl; }
};

// 错误使用
std::shared_ptr<Bad> p1(new Bad());
std::shared_ptr<Bad> p2 = p1->getShared(); // p1 和 p2 独立管理同一对象
// 程序结束时,该对象会被双重释放(未定义行为)

二、正确用法:继承 enable_shared_from_this

复制代码
#include <memory>

class Good : public std::enable_shared_from_this<Good> {
public:
    std::shared_ptr<Good> getShared() {
        return shared_from_this(); // 安全返回共享的 shared_ptr
    }
};

// 正确使用
std::shared_ptr<Good> p1(new Good());
std::shared_ptr<Good> p2 = p1->getShared(); // p1 和 p2 共享同一引用计数
// 程序结束时,对象仅被释放一次

三、使用条件与注意事项

必须通过 shared_ptr 创建对象

错误示例:

复制代码
Good obj; // 栈上直接创建对象
std::shared_ptr<Good> p = obj.shared_from_this(); // 抛出异常!

对象必须已经被 std::shared_ptr 管理,否则调用 shared_from_this() 会抛出 std::bad_weak_ptr 异常。

模板参数必须是当前类

复制代码
class Derived : public std::enable_shared_from_this<Derived> {}; // 正确
class Derived : public std::enable_shared_from_this<Base> {};    // 错误!

线程安全

shared_from_this() 本身是线程安全的,但返回的 shared_ptr 在多线程环境下的使用需额外同步。

四、实现原理

enable_shared_from_this 内部维护一个 std::weak_ptr,当对象被 std::shared_ptr 首次创建时,该 weak_ptr 会被自动初始化为指向该对象。shared_from_this() 通过这个 weak_ptr 创建新的 shared_ptr,确保引用计数正确。

相关推荐
程序员大雄学编程1 小时前
Linux 磁盘满了怎么排查?df / du / find 三步定位大文件
linux·运维·php
RuoyiOffice1 小时前
Hyper-V+Kubernetes 搭建实战:保留 Windows,构建三节点 Linux 容器集群
linux·spring boot·kubernetes·containerd·hyper-v·微服务部署·ruoyi office
hetao17338371 小时前
2026-11-02 hetao1733837 的刷题记录
c++·算法
承渊政道1 小时前
Linux网络学习【五种IO模型与⾮阻塞IO】
linux·网络·学习·ubuntu·io模型·非阻塞io
衍生星球1 小时前
【Java 语言】— 面向对象:构造方法(无参、有参构造)
java·开发语言
Tairitsu_H1 小时前
[C++高阶数据结构·图论下] 最短路径:为什么“挑最小的”一定正确?
数据结构·c++·图论
yuniko-n1 小时前
【JVM】对象内存布局
java·开发语言·jvm
何中应2 小时前
【Go-1】变量&常量定义
开发语言·后端·golang
CarIise2 小时前
Spring 框架AOP与 XML 基础
java·开发语言