%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # 智能指针 Smart Pointer > 智能指针定义在 `<memory>` 头文件中,自 C++11 引入。 智能指针是 C++ 标准库提供的 "**==动态内存==管理**" 工具类,旨在**替代 `new` 和 `delete`**,提供安全的动态内存管理机制。 智能指针**封装了原生指针**,基于 **==RAII== 机制**来**自动管理动态内存资源**,确保了会正确对动态内存进行释放。 > [!tip] tip:当需要使用动态内存时,应当通过 **==智能指针==** 来**创建==基于动态内存的对象==**,由智能指针来**自动实现内存管理**。 <br><br> # 智能指针的使用 C++标准库提供了两种 **智能指针** 及一个**伴随类**: - `std::unique_ptr<T>` "**独占**" 所指向的对象; - `std::shared_ptr<T>` 允许**多个指针指向同一个对象**; - `std::weak_ptr<T>` **伴随类**,一种弱引用,指向`shared_ptr` 所管理的对象,但**不增加引用计数**。 上述智能指针都是**类模版**,**可指向任何类型的内存**,需提供 **==所指类型==** 作为模版参数,来得到**指向具体类型实例的智能指针**。 > [!NOTE] **"智能指针" 实例**本身只是对 "原始指针" 的封装,不包括 "==申请动态内存并初始化对象==" 的过程。 > C++提供了两个函数模版,用于 "**==动态分配一个对象并初始化==,并返回指向该对象的==智能指针==**": - `std::make_shared<T>()`:动态分配一个对象 `T`,返回指向该对象的 `std::shared_ptr<T>` 智能指针实例; - `std::make_unique<T>()`:动态分配一个对象 `T`,返回指向该对象的 `std::unique_ptr<T>` 智能指针实例(since **C++14**) ## 🚨 注意事项 ##### <font color="#c00000">(1)尽可能使用 make_uniuqe 和 make_shared 动态分配对象并绑定到智能指针</font> 两个函数模版基于 "**完美转发**" 实现,其**接受类型 `T` 的构造函数的参数列表**,在内部**通过 `new` 动态分配内存并将参数转发给 `T` 的相应构造函数**,完成动态对象的构建,**返回指向该动态对象的智能指针**。 **通常应当尽可能使用这两个函数**,不是手动将 `new` 返回的指针赋给 `shared_ptr` 或 `unique_ptr`。 一个可能导致 "**==潜在内存泄露==**" 的情况: ```cpp // 下述函数调用涉及三个步骤, 执行顺序取决于编译器, 例如可能是 // 1) `new MyClass` // 2)`someOtherFunc()` // 3) `std::make_shared<MyClass>()`, 其中调用MyClass构造函数. // 如果第二步的函数 "抛出异常", 则第一步new的MyClass就发生了内存泄露, 因为无法走到第三步交由`make_shared`管理. func(std::make_shared<MyClass>(new MyClass), someOtherFunc()); ``` 不适合使用`make_unique` 或 `make_shared` 的情况包括[^6]: - (1)**需要指定自定义删除器**; - (2)**希望用花括号初始化时**(原因:不支持 `make_unique<T>({x1, x2, ...})`,其中 `T` 为 - (3)有**自定义内存管理**的类(重载了 `new` 与 `delete` 的类); ##### <font color="#c00000">(2) 禁止用 "同一个原生指针" 初始化(或 reset)多个智能指针</font> **防止将同一块动态内存绑定到多个 "==独立==" 创建的智能指针上,导致重复释放!** 例如,用一个原生指针初始化多个 `shared_ptr` 对象时,会**导致各对象==维护独立的引用计数==**,进而触发 **==双重释放== 问题**(默认删除器下)。 一个原生指针**只应当用于初始化一个 `shared_ptr`对象**,再 **由该 `shared_ptr` 对象来初始化、赋值其他 `shard_ptr` 对象**。 由此,**保证==所有 `shared_ptr` 共享/维护同一份引用计数==**。 <br> ## 使用说明 #### 智能指针的初始化 声明 & 初始化一个智能指针对象的**几种方式**: ```cpp // 智能指针的声明/初始化的几种方式 // 1) 无参数, 空指针 unique_ptr<string> p0; // 空指针nullptr. 执行默认初始化 // 2) 以原生指针为参数的构造函数(explicit的, 禁止拷贝初始化语法) unique_ptr<string> p1 (new string("Tes.")); // 指向一个string对象的智能指针. unique_ptr<string> p11 {new string("Tes.")}; // 指向一个string对象的智能指针. // 3) 以同类型智能指针的"右值"为参数的移动构造函数. unique_ptr<string> p2 = std::make_unique<string>("Test"); // 推荐; 指向一个string对象的智能指针. 调用string的构造函数 // shared_ptr 智能指针声明/初始化方式同上 shared_ptr<string> p1 = std::make_shared<string>("Test"); // 推荐 // 下列方式等价 shared_ptr<T> ptr {new T(param)}; shared_ptr<T> ptr {make_unique<T>(param)}; // 推荐 ``` ###### 一个自定义的 `make_unique`(适用于 C++11 中,标准库未提供) ```cpp template<typename T, typename... Ts> std::unique_ptr<T> make_unique(Ts&& args...) { return std::unique_ptr<T>(new T(std::forward<Ts>(args)...)); } ``` #### 智能指针的使用 智能指针与`new,delete` 的等价使用示例: ```cpp // 通过原生指针来使用动态内存 void UseRawPointer() { // Using a raw pointer -- not recommended. Song* pSong = new Song(L"Nothing on You", L"Bruno Mars"); // Use pSong... delete pSong; // 必须记得手动delete } // 通过智能指针来使用动态内存(与new等价的方式) void UseSmartPointer() { // 声明一个指向Song类型对象的智能指针对象 song. // 该智能指针对象自动为一个Song类型实例申请了动态内存, 并在内部持有一个指向该动态内存的原生指针 unique_ptr<Song> song = make_unique<Song>(L"Nothing on You", L"Bruno Mars"); // Use song2... } // song2 is deleted automatically here. ``` <br><br> # `std::unique_ptr` 指针 > 参见[^1] [^2] `unique_ptr` "**独占**" 所指向对象的资源。**当 `unique_ptr` 被析构或调用其 `.reset()` 时,资源将自动释放**。 > [!NOTE] 应当将 `unique_ptr` 用作默认的智能指针,仅当真正需要资源共享时,才使用 `shared_ptr`。 > > `unique_ptr` 可用于构造 or 赋值给 `shared_ptr` > [!info] 默认情况下,`std::unique_ptr` 大小等同于 "原生指针"(使用默认删除器时) > [!caution] `std::unique_ptr` 指向的类型,**在编译器生成的特殊成员函数(如析构函数,移动操作)被 "定义" 时(该位置处),必须==已经是一个完整类型==**。参见[^7] <br> ## 使用说明 ![[_attachment/02-开发笔记/01-cpp/内存管理/cpp-智能指针.assets/IMG-cpp-智能指针-9149EC6263DBFAE2571F711882DA6063.png|752]] > [!caution] `.release()` **不会释放所指内存**,而 `.reset()` 会释放内部指针所指的内存! > [!danger] 区分 `u.reset()` 与 `u->reset()`! > > 前者是 `unique_ptr` 的`reset()` 方法,后者是所指对象的 `reset()` 方法。 <br> #### 构造与赋值 - 构造函数:参数可以是**指向相同类型的==原生指针==、==`unique_ptr`指针==** - (1)以 **原生指针为参数**的构造函数( `explicit` ,故不能进行拷贝初始化) - (2)以 **`unique_ptr` 右值引用为参数**的 **==移动构造函数==** - 赋值: - `unique_ptr` 仅支持同类型 `unique_ptr` 指针间的 **==移动赋值==**; > [!caution] `unique_ptr` 只支持移动语义,不支持拷贝。移动一个` unique_ptr`,会将其内部原生指针设为 `nullptr` ```cpp // 声明 & 初始化(建议始终使用make_unique创建unique_ptr指针) // 1) 无参, 空指针 unique_ptr<string> p0; // 空指针nullptr, 未指向任何对象. cout << "p0 == nullptr: " << (p0 == nullptr) << endl; // true // 2) 以原生指针为参数的构造函数(声明为explicit的, 因此不能进行拷贝初始化) unique_ptr<string> p1(new string("Test1")); // 指向一个Entity对象, 调用了Entity的构造函数. unique_ptr<string> p11{new string("Test11")}; // 列表初始化 string* sp = new string("Test12"); unique_ptr<string> p12(sp); // 通过原生指针初始化智能指针 // unique_ptr<string> p19 = {new string()}; // error // unique_ptr<string> p19 = new string(); // error // 3) 以右值unique_ptr为参数的移动构造函数. unique_ptr<string> p21(make_unique<string>("Test21")); auto p22 = make_unique<string>("Test22"); // 指向一个Entity对象, 调用了Entity的构造函数. auto p23 = make_unique<int>(35); // 指向一个int型, 初始化值为35. auto p24 = make_unique<int>(); // 指向一个"值初始化"的int, 初始化值为0. // 赋值 // unique_ptr支持同类型智能指针间的移动赋值 p24 = std::move(p23); // 移动赋值, p23变为空指针, p3指向p23所指对象. p0 = make_unique<string>("Test0"); ``` <br> #### 在两个 unique_ptr 指针间转移资源所有权 两种方式: - (1)通过 **移动构造 or 移动赋值** - (2)通过 成员函数 `.release()` 与 `.reset()` ```cpp // 场景一: 用p1构造p2, 转移p1资源到p2 unique_ptr<string> p1 = make_unique<string>("Hello"); unique_ptr<string> p2(p1.release()); // p1释放对内部指针的控制权, 返回内部指针 unique_ptr<string> p2(std::move(p1)); // 通过p1移动构造p2. // 场景二: p2 = std::move(p1); p2.reset(p1.release()); // p1释放对内部指针的控制权, 返回内部指针; p2释放原先资源, 重置指针为p1传递的内部指针. ``` <br> #### 传递 unique_ptr 指针 > 参见[^3]、[^1] > [!important] 当需要在某些函数内 **"==使用 `unique_ptr` 所指资源=="**(不涉及生存期语义)时,可采用以下两种方式: > > 1. 将 "**==引用类型==** `unique_ptr<T>&`" 或 `const unique_ptr<T>&` 作为函数形参; > 2. 将 "**==原生指针类型==**" 作为函数形参,以 "**`unique_ptr` 内的==原生指针==**" 作为实参传递(通过 `.get()` 获取原生指针) > > 仅当需要 "**转移资源所有权**" 时,才应当以 "`unique_ptr` **值类型**" 作为函数形参,利用**移动语义** `std::move(unique_ptr)` 作为实参。 以 `unique_ptr` 作为参数时,通过 "**移动语义**" 进行传递: > [!example] 在线程间通过**移动语义**传递 `std::unique_ptr` 指针 > > ```cpp > // 在线程间通过移动语义传递 std::unique_ptr 指针 > void process_big_object(std::unique_ptr<big_object>); // 线程函数 > > std::unique_ptr<big_object> p = std::make_unique<big_object>(); > p->prepare_data(42); > // 通过移动语义, 传递 std::unique_ptr 指针给新线程. > std::thread t(process_big_object, std::move(p)); > ``` > > 从函数返回一个 `unique_ptr`: ```cpp // unique_ptr禁止拷贝语义, 但下列场景是允许的, 因为实际上执行RVO优化或"移动语义". unique_ptr<int> clone(int p) { return make_unique<int>(p); // 正确的, 在可触发RVO的场景, 即使编译器不执行RVO优化, 也会隐式将返回值当作右值, 等价于使用`std::move()`; } unique_ptr<int> clone(int p) { unique_ptr<int> ret = make_unique<int>(p); ... return ret; } ``` #### 其他使用 ```cpp // unique_ptr使用示例 void exam_unique_ptr() { // 智能指针具有原生指针特性, 支持`*`和`->`操作符. cout << p1->substr(2) << endl; cout << *p1 << endl; // 解引用 // `.get()`返回智能指针内持有的原生指针 // 注: unique_ptr没有重载`<<`, 因此不能直接打印其所持有的指针 cout << p1.get() << endl; // 输出智能指针对象中的指针值(指针所指地址) // 交换: 交换同类型智能指针对象中的指针 swap(p21, p22); cout << "*p21: " << *p21 << endl; cout << "*p22: " << *p22 << endl; // 重置智能指针(释放底层指针所指资源, 此后为空) auto p3 = make_unique<int>(99); // 指向一个值初始化的int, 默认初始化值为0. p3.reset(); cout << "p6 == nullptr: " << (p3 == nullptr) << endl; p3.reset(new int(37)); // 不推荐, 应当使用make_unique重新创建智能指针对象; cout << "*p6: " << *p3 << endl; // 断开unique_ptr与原生指针的关联并返回原生指针. unique_ptr失去对资源的控制权 string* sptr = p21.release(); delete sptr; sptr = nullptr; } ``` <br><br><br> ## 自定义删除器 当使用 "**自定义删除器**" 时,删除器类型将作为 `unique_ptr` 指针类型的一部分, 故必须将其类型 `D` 作为 "**第二个模版参数**" 来声明智能指针类型: `unique_ptr<T,D>`。 在**构造或 reset** 这样的 unique_ptr 指针实例时,需要将 "**自定义删除器**" 类的一个实例作为 "**最后一项参数**" 传入。 示例:`std::unique_ptr` 常用于 "**工厂函数**" 中 ```cpp class Investment { public: ... virtual ~Investment(); ... }; class Stock : public Investment { ... }; class Bond : public Investment { ... }; class RealEstate : public Investment { ... }; // 自定义删除器 auto delInvmt = [](Investment* pInvestment) { makeLogEntry(pInvestment); // 记录日志; delete pInvestment; // 释放内存 }; // 工厂函数: template<typename... Args> auto makeInvestment(Args&&... args) { // 创建一个智能指针, 提供自定义删除器delInvmt std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt); if (/*一个Stock对象需要被创建*/) { pInv.reset(new Stock(std::forward<Ts>(args)...)); } else if (/*一个Bond对象应被创建*/) { pInv.reset(new Bond(std::forward<Ts>(args)...)); } else if (/*一个RealEstate对象应被创建*/) { pInv.reset(new RealEstate(std::forward<Ts>(args)...)); } return pInv; } // 工厂函数统一返回unique_ptr, 其可被转换为shared_ptr. std::shard_ptr<Investment> sp = makeInvestment(arguments); ``` <br><br><br> # `std::shared_ptr` 指针 > 参见 [^4] `std::shared_ptr` 支持**共享资源的所有权**,其内部维护着一个 "**==引用计数==**",**允许多个 `shared_ptr` 指向同一个动态内存对象**, 仅当**指向该对象的 ==最后一个 `shared_ptr` 析构== 而令引用计数减少为 0 时**,才自动释放所指对象占用的动态内存。 > [!info] `std::shared_ptr` 的大小通常是 "原始指针" 的两倍(标准库的实现几乎均如此) > > 其内部包含两个指针: > > 1. 一个指向 "**动态内存对象**" 的原生指针 > 2. 一个指向 "**==引用计数控制块==**" 的原生指针 > > ![[_attachment/02-开发笔记/01-cpp/内存管理/cpp-智能指针.assets/IMG-cpp-智能指针-81FF9F73E736CC5A8451C4C4E8352501.png|591]] > 由一个 `shared_ptr` 初始化/赋值的 `weak_ptr` 会与其 "**==指向同一控制块==**"(增加 `weak count` 计数) > > 故 `shard_ptr` 内部两个指针所指内存的释放时机为: > > - "**对象占有的动态内存**" 通常在**最后一个 `shared_ptr` 销毁时**就会释放; > - "**==控制块==占有的动态内存**" 直到 **最后一个 `shared_ptr`** 以及 **最后一个 `weak_ptr`** 均被析构时,才会释放。 > - 即 `weak count`,`reference count` 均减为 0 时。 > [!NOTE] > 为了在结构较复杂的情境中为`shared_ptr` 实现内存管理,标准库提供了 `weak_ptr`, `bad_weak_ptr` 和`enable_shared_from_this` 等辅助类 <br><br> ## share_ptr 的引用计数机制 > 引用计数(reference counting) **`share_ptr` 维护 ==引用计数== 来记录有多少个 `share_ptr` 共同指向同一个对象**。 记最初 sptrA 指向对象 A,sptrB 指向对象 B,sptrC 为空,则相关操作下: | | 对 A 的引用计数 | 对 B 的引用计数 | 说明 | | --------------------- | --------- | --------- | ----------------------------------------------------------------------------------------- | | sptrA 拷贝初始化 sptrC | +1 | | sptrC 此时也指向 A; | | sptrA **移动**初始化 sptrC | **不变** | | 变为仅 sptrC 指向 A,而 sptr 被置空 | | sptrA 拷贝赋值给 sptrB | +1 | -1 | - 对 A:两个指针都指向 A,故+1; <br>- 对 B:sptrB 不再指向 B,故-1; | | sptrA **移动**赋值给 sptrC | **不变** | **-1** | - 对 A : 被移动的 shared_ptr 被置为空,而被赋值/初始化的 shared_ptr 又指向向该资源,故不变;<br>- 对 B:sptrB 不再指向 B,故-1; | > [!caution] `shared_ptr` 的析构函数会递减其所指对象的引用计数,==仅当引用计数变为 0== 时,析构函数才会销毁该对象并释放其占用的动态内存 > [!info] C++标准保证对 `shared_ptr` 指针内部的 "引用计数" 是==线程安全==的原子操作! ```cpp // shared_ptr指针引用计数示例 void refer_counting_exam() { shared_ptr<string> ptr1 = make_shared<string>("hello"); cout << "ptr1: " << *ptr1 << ", use_count: " << ptr1.use_count() << endl; { shared_ptr<string> ptr2 = ptr1; // 共享所有权, ptr1对象的引用计数加1 shared_ptr<string> ptr3; ptr3 = ptr1; // 共享所有权, ptr1对象的引用计数加1 cout << "ptr1: " << *ptr1 << ", use_count: " << ptr1.use_count() << endl; ptr3 = nullptr; // ptr3被赋予其他值, ptr1对象的引用计数减1 } // ptr2, 离开作用域, ptr1对象的引用计数减1 cout << "ptr1: " << *ptr1 << ", use_count: " << ptr1.use_count() << endl; cout << "ptr1.unique(): " << ptr1.unique() << endl; // true } // ptr1 在离开作用域时, 所指对象引用计数为0, 自动释放内存 ``` ## 使用说明 ![[_attachment/02-开发笔记/01-cpp/内存管理/cpp-智能指针.assets/IMG-cpp-智能指针-9CAE35B37FBEC6C52ECAEC8D249A968A.png|768]] ### 构造与赋值 - 构造函数:参数可以是**指向相同类型的==原生指针==、==`unique_ptr`指针==、==`shared_ptr`指针==、==`weak_ptr`指针==** - 以 **原生指针为参数**的构造函数(`explicit`) - 以 `shared_ptr` 为参的 **拷贝构造函数**、**移动构造函数**(移动构造不会导致引用计数增加!) - 以 **`unique_ptr` 右值引用**为参的 **移动构造函数**(转移`unique` 独占所有权为共享所有权,此后 `unique_ptr` 为空指针) - 以 `weak_ptr` 为参的**构造函数** (`explicit`) - 赋值: - 支持 `shared_ptr`、`unique_ptr` 的 **==移动赋值==**; - 支持 `shared_ptr` 、`weak_ptr` 的 **==拷贝赋值==**,会使得所指**新对象引用计数+1**,**指针所指原对象计数-1**。 使用示例: ```cpp // shared_ptr使用示例 void exam_shared_ptr() { // 声明 & 初始化(应当始终使用make_shared创建shared_ptr指针) // 1) 无参, 空指针 shared_ptr<string> p0; // 空指针nullptr, 未指向任何对象. cout << "p0 == nullptr: " << (p0 == nullptr) << endl; // true // 2) 以原生指针为参数的构造函数(声明为explicit的, 因此不能进行拷贝初始化) shared_ptr<string> p1(new string("Test1")); // 指向一个string对象, 调用了string的构造函数. shared_ptr<string> p11{new string("Test11")}; // 列表初始化 // shared_ptr<string> p19 = {new string()}; // error // shared_ptr<string> p19 = new string(); // error // 3) 以shared_ptr右值引用为参数的移动构造函数 shared_ptr<string> p21(make_shared<string>("Test21")); auto p22 = make_shared<string>("Test22"); // 指向一个string对象, 调用了string的构造函数. auto p23 = std::move(p22); // p22变为空指针, p23指向p22所指对象. // 4) 以shared_ptr为参数的拷贝构造函数(引用计数+1) shared_ptr<string> p31 = p1; shared_ptr<string> p32(p1); // 5) 以weak_ptr为参数的构造函数(explicit) weak_ptr<string> w_ptr(p11); shared_ptr<string> p4(w_ptr); // p11的引用计数+1. // 6) 以unique_ptr右值引用为参数的移动构造函数(转移unique_ptr所有权, 其指向空指针) unique_ptr<string> u0(new string("Test5")); shared_ptr<string> p5(std::move(u0)); cout << "u0 == nullptr: " << (u0 == nullptr) << endl; // true shared_ptr<string> p51(make_unique<string>("Test")); // 赋值 & 移动赋值 // p4原本指向的对象的引用计数减少, 而p5指向的对象的引用计数增加(p4与p5现在指向同一对象) // p4原本指向的对象由于不再有任何引用(引用计数为0), 而被自动释放资源 p4 = p5; // 将shared_ptr "移动赋值" 给另一个shared_ptr, 对象的引用计数不变; 被移动的shared_ptr中将变成空指针. shared_ptr<string> p6; p6 = std::move(p51); // p51中指针变为nullptr, 而p51所指对象的引用计数不变, p6替代p51指向了该对象. if (!p51) { cout << "shared_ptr has been nullptr after move assignment to a shared_ptr" << endl; } // 将unique_ptr "移动赋值" 给另一个shared_ptr, unique_ptr变为空指针, 其原先指向的对象现在被shared_ptr指向. unique_ptr<string> u1(new string("TestU")); p6 = std::move(u1); // if (!u1) { cout << "unique_ptr has been nullptr after move assignment to a shared_ptr" << endl; } // 重置 p6.reset(); // shared_ptr重置为空指针 cout << p6 << endl; // 0 p6.reset(new string("Testing6")); // shard_ptr重置初始化为指向其它对象 cout << *p6 << endl; // 交换: 交换智能指针对象中的pointer 以及 deleter. swap(p4, p5); cout << "*p4: " << *p4 << endl; cout << "*p5: " << *p5 << endl; // 取得智能指针中保存的指针 // 注: shared_ptr 有重载`<<`, 因此可以直接打印其所持有的指针 cout << "p6: " << p6 << endl; // 输出智能指针对象中的指针值(指针所指地址) cout << "p6.get(): " << p6.get() << endl; // 自定义deleter, 在其所指"对象"引用计数归0, 将被销毁时会将调用. // deleter // 接受的参数应当是该智能指针所对应的指针类型 { shared_ptr<string> s_ptr(new string("nico"), [](string *p) { // 接受string指针 cout << "delete: " << *p << "(" << p << ")" << endl; delete p; // 这里必须delete p, }); cout << "s_ptr: " << s_ptr << endl; } } ``` <br> ## 循环引用问题 如果**两个类类型对象** 互相 **持有对方的 `shared_ptr`**,就会形成循环引用,导致内存泄漏,因为**引用计数永远不会达到零**,**对象也就不会被删除**。 产生情景: - **两个 `shared_ptr` 指针`ptrA`, `ptrB`** 分别指向两个动态创建的**类类型对象 A 和 B**; - **对象 A** 持有一个指向 B 类型的 `shared_ptr`指针,同时对象 B 持有一个指向 A 类型的`shared_ptr` 指针,二者**互相形成引用**; - 离开作用域时,**两个 `shared_ptr` 指针 `ptrA` 和 `ptrB` 的==析构函数均会被调用==**, 但是**二者所指的对象 A 和对象 B 的==析构函数不会被调用==**,因为此时**对 A 和 B 的引用计数都非 0**。 循环引用导致内存泄漏示例: ```cpp class B; // 前置声明 class A { public: shared_ptr<B> p_to_B; ~A() { cout << "~A() called" << endl; } }; class B{ public: shared_ptr<A> p_to_A; ~B() { cout << "~B() called" << endl; } }; void exam1() { // 对象a和b 均通过 `shared_ptr` 创建. // 对象a的数据成员中包含一个指向B类型的 `shared_ptr`指针,同时对象b的数据成员中包含一个指向A类型的`shared_ptr` 指针,二者互相形成引用。 shared_ptr<A> ptrA = make_shared<A>(); // ptrA所指对象的计数为1 shared_ptr<B> ptrB = make_shared<B>(); // ptrB所指对象的计数为1 // 形成循环引用 ptrA->p_to_B = ptrB; // ptrB所指对象的引用计数为2 ptrB->p_to_A = ptrA; // ptrA所指对象的引用计数为2 // 记share_ptr<A>对象ptrA所指的类A对象的为a, share_ptr<B>对象ptrB所指的类B对象的为b. // 离开作用域时, ptrA的析构函数会被调用, 递减ptrA所指的对象a的引用计数, 但a的析构函数不会被调用, 因为此时对a的引用计数不为0. // 同理, ptrB的析构函数会被调用, 递减ptrB所指的对象b的引用计数, 但b的析构函数不会被调用, 因为此时对b的引用计数不为0. // 由此, 导致内存泄露 } ``` ##### 解决循环引用问题 将类成员的中的 `shared_ptr` 改为 `weak_ptr`,**避免循环引用问题**: ```cpp class B2; class A2 { public: weak_ptr<B2> pointer_to_B2; // 使用weak_ptr替代shard_ptr, 不增加引用计数 ~A2() { cout << "~A2() called" << endl; } }; class B2 { public: weak_ptr<A2> pointer_to_A2; // 使用weak_ptr替代shard_ptr, 不增加引用计数. ~B2() { cout << "~B2() called" << endl; } }; void exam2() { shared_ptr<A2> a = make_shared<A2>(); shared_ptr<B2> b = make_shared<B2>(); // 对象a中的pointer_to_B2是weak_ptr, 可被赋予shared_ptr而不增加引用计数 // 由此, 避免了循环引用而导致的内存泄漏问题 a->pointer_to_B2 = b; b->pointer_to_A2 = a; } int main() { exam2(); // 对象a和b的析构函数被调用. } ``` <br> ## enable_shared_from_this 基类模版 `std::enable_shared_from_this<T>` 是**由标准库提供的类模版**,供类 **==继承==**,进而支持 **在==类的成员函数内部==安全地创建==指向自身==的 `std::shared_ptr`**。 使用场景:在一个类 A 的成员函数中,**用 `this` 直接构造 `shared_ptr` 时,防止 `this` 被两次析构**。 用法说明: - (1)**令目标类 A 继承自 `std::enable_shared_from_this<A>` 基类**; - (2)在类 A 的成员函数中,**调用该基类提供的 `shared_from_this();`** 来**安全获取指向 `this` 的 `shared_ptr` 对象**。 > [!caution] 仅当类对象本身 **已经** 被 `std::shared_ptr` 管理时,才能使用 `shared_from_this()`。 > > 注意以下错误用法,会触发 `std::bad_weak_ptr` 异常,或是表现为更莫名其妙的 bug。 > > - 在 **栈对象** 或 **未由 `shared_ptr` 管理的对象** 上调用 `shared_from_this()`; > - 在 **构造函数内** 使用 `shared_from_this()`。 > - 即使该对象是由 `make_shared<T>()` 触发构造的,**在构造函数内尚未由 `shared_ptr` 管理**。 > > ![[_attachment/02-开发笔记/01-cpp/内存管理/cpp-智能指针.assets/IMG-cpp-智能指针-B41EB345326111D41AFFD161CB626B9E.png|444]] > #### 使用示例 错误使用:用 this 直接构造 `shared_ptr` ```cpp class BadExample { public: void dangerous() { shared_ptr<BadExample> sp_this(this); // 原生指针直接初始化shared_ptr, 是"独立的引用计数", 进而导致两次delete. cout << "Use count: " << sp_this.use_count() << endl; } }; int main() { shared_ptr<BadExample> obj = make_shared<BadExample>(); obj->dangerous(); } ``` 错误使用:在构造函数内使用 `shared_from_this()` 正确用法: ```cpp class GoodExample : public std::enable_shared_from_this<GoodExample> { public: void safe() { shared_ptr<GoodExample> sp_this = shared_from_this(); // 通过 `shared_from_this()`安全获取指向this的shared_ptr对象. cout << "Use count: " << sp_this.use_count() << endl; } }; int main() { shared_ptr<GoodExample> obj = make_shared<GoodExample>(); obj->safe(); } ``` <br> # `std::weak_ptr` 指针 > `weak_ptr` 为解决 `shared_ptr` 可能产生的循环引用问题而设计,用于和 `shared_ptr` 协同工作。 `std::weak_ptr` 用以**指向一个由`std::shared_ptr` 管理的对象**,而 **==不增加引用计数==。** 作用: 1. **防止==循环引用==**:使用 `weak_ptr` 作为类成员(而不是 `shared_ptr`),避免循环引用问题。 2. **临时获取所有权**:通过 `weak_ptr`,可以检查**所观察的对象是否还存活**,并在需要时**通过 `.lock()` 创建一个 `shared_ptr` 来安全地使用它所指向的对象**。 使用场景[^5]: 1. **管理缓存**:**读取数据的 "函数" 返回指向该数据的 `shared_ptr` 指针**,而**缓存结构内部维护一个指向该数据的 `weak_ptr` 指针**。 - 当外部使用完数据后,`shared_ptr` 指针销毁; - 缓存结构内部通过 `weak_ptr` 检测到对象被销毁,则清除该 `weak_ptr` 指针; 2. **观察者模式**: - 主题持有观察者的 `weak_ptr` 列表,从而在使用前**检查是否已经悬空**。 - 观察者通过 `weak_ptr` 持有对主题的引用,从而**避免循环引用**问题。 - 注意,双方持有的均是**指向对方的 ==`weak_ptr`指针==**,从而使**双方生命周期独立**,不受对方影响。 <br> ## 使用说明 ![[_attachment/02-开发笔记/01-cpp/内存管理/cpp-智能指针.assets/IMG-cpp-智能指针-699B1A3079EF2E1C782C88C88346B9E9.png|812]] - **初始化与赋值**: `weak_ptr` 只能**由一个 `shard_ptr` 或另一个 `weak_ptr` 来==创建&初始化==、==赋值==**。 - **访问所指对象**:`weak_ptr` 不能直接访问其所指对象(不支持 `*` 和 `->` 操作符),**只能通过 `.lock()` 建立一个 `shared_ptr` ,由后者来访问其所指对象**。 两个主要成员函数: - `.expired()` :**检查 `weak_ptr` 所指的对象是否已经已被销毁**。 - `.lock()` :**返回一个`shared_ptr`**,如果 `w_ptr` 所指的**原对象已经被销毁**则其中指针为`nullptr`。 ### 使用示例 ```cpp auto p = make_shared<int>(42); weak_ptr<int> wp(p); // 使用shared_ptr初始化一个weak_ptr if (shared_ptr<int> ptr = wp.lock()) { // 如果所指对象仍然存在, 则返回一个指向该对象的shared_ptr指针 ... } ``` <br><br><br> # 使用智能指针管理动态数组 > 智能指针通常用于管理 "单个对象" 的生命周期。 C++ 标准库中,智能指针对 "**动态数组**" 的支持如下: - `unique_ptr` **提供了对数组类型的支持**,模版参数可声明为 `T[]`,其提供**下标运算符**、以及**针对数组的默认删除器**(调用`delete []`)。 - `shared_ptr` 不直接支持**数组类型** (自 C++17 起支持使用 `std::shared_ptr<T[]>`,同 `unique_ptr`) > [!caution] C++17 之前,使用 `shared_ptr<T>` 管理 "**元素类型为 `T` 的动态数组**"的方法 > > 需要将 `T` 声明为元素类型,并传入一个 "指向动态数组首元素" 的原生指针给该智能指针,同时提供 "**自定义删除器**"。 > > - 删除器可以是一个 lambda,使用 `delete[]` 释放指针;或者使用 "为 `unique_ptr` 而提供的辅助函数" 作为 deleter。 > - **`shared_ptr` 不支持下标访问**,要访问数组元素,只能先通过 `.get()` 获得原始指针,通过原生指针进行操作。 #### unique_ptr 管理动态数组示例 ```cpp // 使用unique_ptr管理数组, 模版参数为"Type[]". unique_ptr<int[]> up(new int[10]); // 通过unique_ptr直接访问数组元素,支持下标运算符. for(int i = 0; i < 10; ++i) { up[i] = i * 2; cout << up[i] << endl; } up.reset(); // 释放动态数组, unique_ptr内部默认删除器会使用`delete []`. ``` #### share_ptr 管理动态数组 ```cpp // 使用shared_ptr管理数组, 模版参数为数组中的元素类型 "Type", 需要自定义删除器。 // `shared_ptr` 在标准库中并没有直接支持数组,因此需要自定义一个删除器来调用 `delete []`。 shared_ptr<int> myArray(new int[10], [](int* ptr) { delete[] ptr; cout << "Array deleted!" << endl; }); // 也可以使用为unique_ptr而提供的辅助函数作为deleter, 其内调用delete[]. shared_ptr<int> myArray2(new int[10], std::default_delete<int[]>()); // 不能直接通过shared_str直接访问数组元素, 只能先获取指针, 通过指针访问. int *ptr = myArray.get(); for(int i = 0; i < 10; ++i) { ptr[i] = i * 2; cout << ptr[i] << endl; } ``` 注:智能指针都可以用于 STL 容器,例如 vector 和 array。 ```cpp // 使用 make_shared 创建一个容器 auto myArray5 = std::make_shared<std::array<int, 10>>(); // 直接像操作普通数组一样初始化和访问数组元素 for (int i = 0; i < 10; ++i) { (*myArray5)[i] = i * 2; } ``` <br><br><br> # Deleter 删除器 智能指针通过调用其关联的 "**删除器 deleter**"(可调用对象)来**释放所指资源**。 - **对于 `std::unique_ptr`**:**删除器的类型是 `unique_ptr` 类型的一部分**; - **对于 `std:shared_ptr`**:删除器的类型不属于其类型的一部分。 > [!info] 智能指针的 **"默认删除器"** 的实现是 **"调用 `delete` 来释放内存"** ```cpp // 删除器是unique_ptr类型的一部分 std::unique_ptr<Widget, decltype(widgetDeleter)> upw(new Widget, widgetDeleter); std::shared_ptr<Widget> spw(new Widget, widgetDeleter); ``` <br> ### 删除器调用时机 - **对于`std::unique_ptr`**:当`unique_ptr`指针**被销毁**时,或是**显式 `.reset()` 重置指针**时,deleter 就会被调用。 - **对于`std::shared_ptr`**:仅当**最后一个引用`shared_ptr` 被销毁或重置**时,其 deleter 才会被调用,释放所指对象的资源。 <br> ### 自定义删除器 智能指针默认删除器是调用 `delete` 释放内部指针所指内存, 在某些情况下,可能需要**以特殊方式销毁智能指针(例如不释放所指内存)** 或**可能需要执行一些额外的清理工作**,为此,需要自定义删除器。 示例:**使用哨兵 `shared_ptr` 时,不能 `delete` 所指内存**。 ```cpp // 如下, 引入了一个哨兵指针, 地址取为0xffff....., 为避免shared_ptr析构时delete该地址内存, 必须自定义一个空删除器. shared_ptr<Object> dummy_ptr(reinterpret_cast<Object*>(UINTPTR_MAX), [](Object*){}); ``` 示例:**自定义一个通用的泛型删除器** ```cpp class DebugDelete { public: DebugDelete(std::ostream &s = std::cerr): os(s) { } // 将`operator()`声明为一个成员模版, T的类型在调用时由编译器推断 template <typename T> void operator()(T *p) const { os << "deleting ptr" << std::endl; delete p; } private: std::ostream& os; }; int main() { // 实例化 DebugDelete::operator()<int>(int*); std::unique_ptr<int, DebugDelete> u_ptr(new int(25), DebugDelete()); // 实例化 DebugDelete::operator()<std::string>(std::string*); std::shared_ptr<std::string> s_ptr(new std::string, DebugDelete()); return 0; } ``` 示例二:以**函数对象** or **普通函数**作为删除器传递 ```cpp // 示例:声明自定义删除器 struct MyDeleter { void operator()(MyClass *p) const { delete p; } }; void MyDeleterFunc(MyClass *p) { delete p; } // (1) 为unique_ptr声明自定义删除器--------------------------------------- // 对于unique_ptr, 删除器的类型是指针类型的一部分. // 必须在模版参数中声明删除器的类型(函数类型, 或函数对象类); // 1.删除器定义为普通函数, 模版参数中指明"函数指针"类型, 智能指针的构造函数中传入"函数指针" unique_ptr<MyClass, void(*)(MyClass*p)> ptr1(new MyClass(), MyDeleteFunc); unique_ptr<MyClass, decltype(&MyDeleterFunc)> ptr11(new MyClass(), MyDeleterFunc); // 注: 上面普通函数的函数名可以作为函数指针使用, 因此第二项参数`&`取址运算符可选; // 2.删除器定义为函数对象的形式, 模版参数中指明删除器类型, 构造函数中传入"删除器实例对象" unique_ptr<MyClass, MyDeleter> ptr2(new MyClass()); unique_ptr<MyClass, MyDeleter> ptr22(new MyClass(), MyDeleter()); // 3.删除器定义为lambda表达式形式, 构造函数中传入lambda函数对象. auto MyDeleterLambda = [](MyClass* ptr) { delete ptr; }; unique_ptr<MyClass, decltype(MyDeleter)> ptr3(new MyClass(), MyDeleterLambda); // (2) 为shared_ptr声明自定义删除器--------------------------------------- // 对于shared_ptr, 删除器的类型不是指针类型的一部分. // 因此, 只需在指针的构造函数中传入 "函数指针" 或函数对象 // 1.删除器定义为普通函数, 智能指针的构造函数中传入"函数指针"; shared_ptr<MyClass> ptr1(new MyClass(), MyDeleteFunc); // 2.删除器定义为函数对象的形式, 构造函数中传入"删除器实例对象" shared_ptr<MyClass> ptr2(new MyClass(), MyDeleter()); // 3.删除器定义为lambda表达式形式, 构造函数中传入lambda函数对象. auto MyDeleterLambda = [](MyClass* ptr) { delete ptr; }; shared_ptr<MyClass> ptr3(new MyClass(), MyDeleterLambda); ``` 示例:为一个打开文件的`unique_ptr` 自定义删除器,调用`fclose`。 ```cpp #include <memory> #include <cstdio> using namespace std; struct FileCloser { void operator()(FILE* file) const { if (file) fclose(file); } }; int main() { // 使用自定义删除器创建智能指针, 打开文件 unique_ptr<FILE, FileCloser> filtPtr(fopen("exam.txt", "r")); // ...使用文件... return 0; } // 当filePtr所指对象的引用计数为0而被销毁时, FileCloser自动关闭文件 ``` <br> #### 自定义删除器注意事项 ![image-20231017155308371|630](_attachment/02-开发笔记/01-cpp/内存管理/cpp-智能指针.assets/IMG-cpp-智能指针-8152822B94A81414FDACB0AEA7BDB910.png) %% # 智能指针使用注意事项 注意避免一些常见的陷阱,例如**循环引用**(尤其是使用 `std::shared_ptr`)。 循环引用可能会防止智能指针正确释放资源,导致内存泄漏。为避免这种情况,可以结合使用 `std::weak_ptr` [^8] [^9]。 %% <br><br> # 参考资料 - [CppGuide/articles/C++必知必会的知识点/详解C++11中的智能指针.md at master · balloonwj/CppGuide · GitHub](https://github.com/balloonwj/CppGuide/blob/master/articles/C++%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E7%9A%84%E7%9F%A5%E8%AF%86%E7%82%B9/%E8%AF%A6%E8%A7%A3C++11%E4%B8%AD%E7%9A%84%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88.md) - [什么是智能指针?为什么要用智能指针? - 掘金](https://juejin.cn/post/6844903809425096712) - [C++ 智能指针 - 全部用法详解 - 知乎](https://zhuanlan.zhihu.com/p/526147194) # Footnotes [^1]: 《C++ Primer》P417 [^2]: 《Effective Mordern C++》Item18 [^3]: 《C++ Core GuideLines 核心指南》 P129 [^4]: 《Effective Mordern C++》Item19 [^5]: 《Effective Mordern C++》Item20 [^6]: 《Effective Mordern C++》Item21 [^7]: 《Effective Mordern C++》Item22 [^8]: [c++智能指针会引起错误的两种情况](https://blog.csdn.net/asdasd121312dasd/article/details/127944613) [^9]: [野(wild)指针与悬空(dangling)指针](https://www.cnblogs.com/idorax/p/6475941.html)