1. 引言
C++ 引入智能指针的主要目的是为了自动化内存管理,减少手动 new 和 delete 带来的复杂性和错误。智能指针通过 RAII(资源获取即初始化)机制,在对象生命周期结束时自动释放资源,从而有效防止内存泄漏和资源管理错误。
2. 原生指针 vs 智能指针
原生指针
原生指针是 C++ 最基本的指针类型,允许程序员直接管理内存。然而,原生指针存在以下问题:
- 内存泄漏:未释放动态分配的内存。
 - 悬挂指针:指针指向已释放或未初始化的内存。
 - 双重释放:多次释放同一内存区域。
 
智能指针的优势
智能指针通过封装原生指针,自动管理内存,解决上述问题。主要优势包括:
- 自动销毁:在智能指针生命周期结束时自动释放资源。
 - 引用计数:共享智能指针能够跟踪引用数量,确保资源在最后一个引用结束时释放。
 - 避免内存泄漏:通过 RAII 机制自动管理资源生命周期。
 - 类型安全:提供更严格的类型检查,减少错误。
 
3. std::unique_ptr
3.1 定义与用法
std::unique_ptr 是一种独占所有权的智能指针,任何时刻只能有一个 unique_ptr 实例拥有对某个对象的所有权。不能被拷贝,只能被移动。
主要特性:
- 独占所有权:确保资源在一个所有者下。
 - 轻量级:没有引用计数,开销小。
 - 自动释放:在指针销毁时自动释放资源。
 
3.2 构造函数与赋值
unique_ptr 提供多种构造函数和赋值操作,以支持不同的使用场景。
- 默认构造函数:创建一个空的 
unique_ptr。 - 指针构造函数:接受一个裸指针,拥有其所有权。
 - 移动构造函数:将一个 
unique_ptr的所有权转移到另一个unique_ptr。 - 移动赋值操作符:将一个 
unique_ptr的所有权转移到另一个unique_ptr。 
3.3 移动语义
由于 unique_ptr 不能被拷贝,必须通过移动语义转移所有权。这保证了资源的独占性。
3.4 代码案例
1  | 
  | 
输出:
1  | Test Constructor: 100  | 
解析:
ptr1拥有Test(100),ptr2拥有Test(200)。- 通过 
std::move将ptr1的所有权转移到ptr3,ptr1变为空。 ptr2.reset(new Test(300))释放了原有的Test(200),并拥有新的Test(300)。- 程序结束时,
ptr3和ptr2自动释放各自拥有的资源。 
4. std::shared_ptr
4.1 定义与用法
std::shared_ptr 是一种共享所有权的智能指针,允许多个 shared_ptr 实例共享对同一个对象的所有权。通过引用计数机制,管理资源的生命周期。
主要特性:
- 共享所有权:多个 
shared_ptr可以指向同一个对象。 - 引用计数:跟踪有多少 
shared_ptr实例指向同一对象。 - 自动释放:当引用计数为0时,自动释放资源。
 
4.2 引用计数与控制块
shared_ptr 背后依赖一个控制块(Control Block),用于存储引用计数和指向实际对象的指针。控制块的主要内容包括:
- 强引用计数(
use_count):表示有多少个shared_ptr指向对象。 - 弱引用计数(
weak_count):表示有多少个weak_ptr指向对象(不增加强引用计数)。 
4.3 构造函数与赋值
shared_ptr 提供多种构造函数和赋值操作,以支持不同的使用场景。
- 默认构造函数:创建一个空的 
shared_ptr。 - 指针构造函数:接受一个裸指针,拥有其所有权。
 - 拷贝构造函数:增加引用计数,共享对象所有权。
 - 移动构造函数:转移所有权,源 
shared_ptr变为空。 - 拷贝赋值操作符:释放当前资源,增加引用计数,指向新对象。
 - 移动赋值操作符:释放当前资源,转移所有权,源 
shared_ptr变为空。 
4.4 代码案例
1  | 
  | 
输出:
1  | Test Constructor: 100  | 
解析:
创建
sp1,引用计数为1。拷贝构造
sp2,引用计数增加到2。拷贝赋值
sp3,引用计数增加到3。```
sp2.reset(new Test(200))1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
:
- 原 `Test(100)` 的引用计数减少到2。
- 分配新的 `Test(200)`,`sp2` 拥有它,引用计数为1。
- 程序结束时:
- `sp1` 和 `sp3` 释放 `Test(100)`,引用计数降到0,资源被销毁。
- `sp2` 释放 `Test(200)`,引用计数为0,资源被销毁。
## 5. `std::weak_ptr`
### 5.1 定义与用法
`std::weak_ptr` 是一种不拥有对象所有权的智能指针,用于观察但不影响对象的生命周期。主要用于解决 `shared_ptr` 之间的循环引用问题。
**主要特性**:
- **非拥有所有权**:不增加引用计数。
- **可从 `shared_ptr` 生成**:通过 `std::weak_ptr` 可以访问 `shared_ptr` 管理的对象。
- **避免循环引用**:适用于双向关联或观察者模式。
### 5.2 避免循环引用
在存在双向关联(如父子关系)时,使用多个 `shared_ptr` 可能导致循环引用,导致内存泄漏。此时,可以使用 `weak_ptr` 来打破循环。
### 5.3 代码案例
#### 场景:双向关联导致循环引用
```cpp
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> ptrB;
A() { std::cout << "A Constructor" << std::endl; }
~A() { std::cout << "A Destructor" << std::endl; }
};
class B {
public:
std::shared_ptr<A> ptrA;
B() { std::cout << "B Constructor" << std::endl; }
~B() { std::cout << "B Destructor" << std::endl; }
};
int main() {
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->ptrB = b;
b->ptrA = a;
}
std::cout << "Exiting main..." << std::endl;
return 0;
}
输出:
1  | A Constructor  | 
问题:
虽然 a 和 b 离开作用域,但 A Destructor 和 B Destructor 并未被调用,因为 a 和 b 相互引用,引用计数无法降到0,导致内存泄漏。
解决方案:使用 weak_ptr
改用 weak_ptr 其中一方(如 B 的 ptrA),打破循环引用。
1  | 
  | 
输出:
1  | A Constructor  | 
解析:
B使用weak_ptr指向A,不增加引用计数。a和b离开作用域,引用计数降为0,资源被正确释放。- 防止了循环引用,避免了内存泄漏。
 
5.4 访问 weak_ptr 指向的对象
weak_ptr 不能直接访问对象,需要通过 lock() 方法转换为 shared_ptr,并检查对象是否仍然存在。
1  | 
  | 
输出:
1  | Value: 42  | 
解析:
wp.lock()返回一个shared_ptr,如果对象依然存在,则有效。sp.reset()释放资源后,wp.lock()无法获取有效的shared_ptr。
6. 自定义删除器
6.1 用例与实现
有时,默认的 delete 操作不适用于所有资源管理场景。此时,可以使用自定义删除器来指定资源释放的方式。例如,管理文件句柄、网络资源或自定义清理逻辑。
6.2 代码案例
用例:管理 FILE* 资源
1  | 
  | 
输出:
1  | File opened successfully.  | 
解析:
- 自定义删除器 
FileDeleter用于在shared_ptr被销毁时关闭文件。 - 使用 
filePtr.get()获取原生FILE*指针进行文件操作。 - 离开作用域时,自动调用 
FileDeleter关闭文件。 
6.3 使用 Lambda 表达式作为删除器
C++11 允许使用 lambda 表达式作为删除器,简化代码。
1  | 
  | 
输出:
1  | File opened successfully.  | 
解析:
- 使用 
std::unique_ptr搭配 lambda 删除器管理FILE*。 - 提供了更灵活和简洁的删除器实现。
 
7. 最佳实践与常见陷阱
7.1 选择合适的智能指针
- **
std::unique_ptr**:- 用于明确的独占所有权场景。
 - 适用于资源的单一管理者或需要所有权转移的情况。
 - 更轻量,性能更优。
 
 - **
std::shared_ptr**:- 用于共享所有权的场景。
 - 需要多个指针共同管理同一资源时使用。
 - 引用计数带来一定的性能开销。
 
 - **
std::weak_ptr**:- 用于观察不拥有资源的场景。
 - 适用于需要避免循环引用或只需临时访问资源的情况。
 
 
7.2 避免循环引用
在使用 shared_ptr 时,特别是在对象间存在双向引用时,容易导致循环引用,内存泄漏。使用 weak_ptr 打破循环引用。
7.3 使用 make_shared 与 make_unique
优先使用 make_shared 和 make_unique 来创建智能指针,避免直接使用 new,提高效率和异常安全性。
1  | auto sp = std::make_shared<Test>(100);  | 
7.4 不要混用原生指针与智能指针
避免在智能指针管理的对象上同时使用原生指针进行管理,防止重复释放或不安全访问。
7.5 理解智能指针的所有权语义
深入理解不同智能指针的所有权规则,避免误用导致资源管理错误。
8. 总结
智能指针是 C++ 中强大的资源管理工具,通过封装原生指针,提供自动化的内存管理,极大地减少了内存泄漏和资源管理错误。std::unique_ptr、std::shared_ptr 和 std::weak_ptr 各有其应用场景,理解它们的差异和使用方法对于编写安全、高效的 C++ 代码至关重要。此外,通过实现自己的智能指针(如 SimpleSharedPtr),可以更深入地理解智能指针的工作原理,为高级 C++ 编程打下坚实基础。