%%
# 纲要
> 主干纲要、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>
#### 自定义删除器注意事项

%%
# 智能指针使用注意事项
注意避免一些常见的陷阱,例如**循环引用**(尤其是使用 `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)