一、引言

引入智能指针(Smart Pointer)是 C++ 为解决动态内存管理问题而设计的工具,其核心目的是自动管理动态分配的内存(堆内存),避免手动管理内存时常见的错误(如内存泄漏、野指针、二次释放等),提高代码的安全性和健壮性。

二、智能指针基础

2.1 具体解决的问题

手动使用new分配内存和delete释放内存时,容易出现以下问题,而智能指针通过自动化机制解决了这些问题:

内存泄漏

  • 手动管理时,若忘记调用delete释放已分配的内存,或因异常、分支跳转等导致delete未执行,会造成内存泄漏(内存被占用且无法回收)。

  • 智能指针通过RAII(资源获取即初始化) 机制,在其生命周期结束(如离开作用域)时自动调用析构函数释放所管理的内存,无需手动操作。

野指针

  • 若对已释放的内存再次访问(即野指针),会导致未定义行为(如程序崩溃、数据损坏)。

  • 智能指针在内存释放后,会自动将内部指针置空(或标记为无效),避免野指针访问。

二次释放

  • 若对同一块内存多次调用delete,会导致二次释放,引发程序崩溃。

  • 智能指针通过内部引用计数或所有权管理,确保内存仅被释放一次。

2.2 RAII 模式

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 中管理资源的重要模式。智能指针是 RAII 模式的典型应用:

  • 当智能指针被创建时,获取资源(指向动态分配的对象)

  • 当智能指针被销毁时,自动释放资源

这种模式确保了资源的正确释放,即使在发生异常的情况下也不例外。

三、unique_ptr

3.1 定义与特性

实现独占所有权,确保同一时间只有一个智能指针管理内存,适用于单一所有者的场景,避免资源竞争。当unique_ptr被销毁时,它所指向的内存也会被自动释放。

unique_ptr的主要特性:

  • 独占性:不允许复制操作,只能进行移动操作

  • 轻量级:内存开销小,性能接近原始指针

  • 可自定义删除器

3.2 实现原理

unique_ptr的实现核心在于禁用复制构造函数和复制赋值运算符,只允许移动构造和移动赋值。这确保了所有权的唯一归属。

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
template<typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
private:
T* ptr;
Deleter del;

// 禁用复制构造
unique_ptr(const unique_ptr&) = delete;
// 禁用复制赋值
unique_ptr& operator=(const unique_ptr&) = delete;

public:
// 构造函数
explicit unique_ptr(T* p = nullptr) : ptr(p) {}

// 移动构造函数
unique_ptr(unique_ptr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}

// 移动赋值运算符
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
del(ptr); // 释放当前资源
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}

// 析构函数
~unique_ptr() {
del(ptr);
}

// 其他成员函数...
};

3.3 使用示例

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
#include <memory>
#include <iostream>

class MyClass {
public:
MyClass() { std::cout << "MyClass constructed\n"; }
~MyClass() { std::cout << "MyClass destructed\n"; }
void doSomething() { std::cout << "Doing something...\n"; }
};

int main() {
// 创建unique_ptr
std::unique_ptr<MyClass> up1(new MyClass());

// 使用箭头运算符访问成员
up1->doSomething();

// 使用解引用运算符
(*up1).doSomething();

// 所有权转移(移动语义)
std::unique_ptr<MyClass> up2 = std::move(up1);

// up1现在为空
if (!up1) {
std::cout << "up1 is null\n";
}

// 自定义删除器示例
auto deleter = [](MyClass* p) {
std::cout << "Custom deleter called\n";
delete p;
};

std::unique_ptr<MyClass, decltype(deleter)> up3(new MyClass(), deleter);

// 数组版本
std::unique_ptr<MyClass[]> up4(new MyClass[3]);

return 0;
}

3.4 使用场景

  1. 动态分配的单个对象或数组:作为局部变量管理动态内存

  2. 工厂函数返回值:工厂函数返回unique_ptr,明确表示调用者获得对象的唯一所有权

  3. 容器元素:作为容器元素存储动态分配的对象

  4. Pimpl 惯用法:实现接口与实现的分离,减少编译依赖

3.5 注意事项

  1. 不能将unique_ptr直接赋值给另一个unique_ptr,必须使用std::move()

  2. 不要将原始指针交给多个unique_ptr管理,这会导致重复释放

  3. 避免将unique_ptr管理的原始指针暴露出去,以免造成悬空指针

  4. 当需要传递unique_ptr到函数时,可以通过移动语义转移所有权,或传递引用(如果不需要转移所有权)

四、shared_ptr

4.1 定义与特性

实现共享所有权,通过引用计数跟踪内存被多少个指针共享,当计数为 0 时自动释放内存,适用于多对象共享资源的场景。shared_ptr通过引用计数来管理内存:当最后一个指向该内存的shared_ptr被销毁时,内存才会被释放。

shared_ptr的主要特性:

  • 共享性:多个shared_ptr可以指向同一对象

  • 引用计数:内部维护引用计数,跟踪对象被引用的次数

  • 可自定义删除器

  • 线程安全:引用计数的操作是线程安全的

4.2 实现原理

shared_ptr的实现包含两个部分:

  1. 指向实际对象的指针

  2. 指向控制块的指针(包含引用计数、弱引用计数和删除器等)

当复制shared_ptr时,控制块中的引用计数会增加;当shared_ptr被销毁或重置时,引用计数会减少。当引用计数变为 0 时,控制块会调用删除器释放对象内存。

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
template<typename T>
class shared_ptr {
private:
T* ptr; // 指向对象的指针
control_block* control; // 指向控制块的指针

struct control_block {
std::size_t ref_count; // 引用计数
std::size_t weak_count; // 弱引用计数
// 其他信息:删除器、分配器等
};

public:
// 构造函数
explicit shared_ptr(T* p = nullptr) : ptr(p) {
if (p) {
control = new control_block{1, 0};
} else {
control = nullptr;
}
}

// 复制构造函数
shared_ptr(const shared_ptr& other) : ptr(other.ptr), control(other.control) {
if (control) {
++control->ref_count;
}
}

// 析构函数
~shared_ptr() {
if (control) {
--control->ref_count;
if (control->ref_count == 0) {
delete ptr; // 释放对象
if (control->weak_count == 0) {
delete control; // 当没有弱引用时,释放控制块
}
}
}
}

// 其他成员函数...
};

4.3 使用示例

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
#include <memory>
#include <iostream>
#include <vector>

class MyClass {
public:
MyClass(int id) : id_(id) {
std::cout << "MyClass " << id_ << " constructed\n";
}
~MyClass() {
std::cout << "MyClass " << id_ << " destructed\n";
}
void doSomething() {
std::cout << "MyClass " << id_ << " doing something\n";
}
private:
int id_;
};

int main() {
// 创建shared_ptr
std::shared_ptr<MyClass> sp1(new MyClass(1));
std::cout << "Reference count: " << sp1.use_count() << "\n";

// 复制shared_ptr,引用计数增加
std::shared_ptr<MyClass> sp2 = sp1;
std::cout << "Reference count after copy: " << sp1.use_count() << "\n";

// 通过make_shared创建(更高效)
auto sp3 = std::make_shared<MyClass>(2);

// 存储在容器中
std::vector<std::shared_ptr<MyClass>> vec;
vec.push_back(sp1);
vec.push_back(sp2);
vec.push_back(sp3);
std::cout << "sp1 reference count: " << sp1.use_count() << "\n";

// 函数参数传递
auto func = [](std::shared_ptr<MyClass> sp) {
std::cout << "In function, ref count: " << sp.use_count() << "\n";
};
func(sp1);

// 自定义删除器
auto deleter = [](MyClass* p) {
std::cout << "Custom deleter for MyClass " << p->id_ << "\n";
delete p;
};
std::shared_ptr<MyClass> sp4(new MyClass(3), deleter);

return 0;
}

4.4 使用场景

  1. 共享资源:当多个对象需要共享同一个动态分配的对象时

  2. 长期存在的对象:当对象的生命周期不明确,需要多个所有者共同管理时

  3. 循环数据结构:与weak_ptr配合使用,处理可能的循环引用

  4. 跨模块资源共享:在不同模块间传递对象所有权时

4.5 注意事项

  1. 优先使用std::make_shared创建shared_ptr,而非直接使用构造函数,这样更高效且更安全

  2. 避免通过原始指针创建多个shared_ptr,这会导致引用计数不共享,造成重复释放

  3. 注意循环引用问题,这会导致内存泄漏(解决方案见weak_ptr部分)

  4. 不要用this指针初始化shared_ptr,应使用enable_shared_from_this

  5. shared_ptr的引用计数操作是线程安全的,但对所指向对象的访问需要额外同步

五、weak_ptr

5.1 定义与特性

配合shared_ptr使用,解决循环引用问题(两个shared_ptr相互引用导致引用计数无法归零的内存泄漏),它不增加引用计数,仅作为 “弱引用” 观察资源。

weak_ptr的主要特性:

  • 非拥有性:不影响所指向对象的生命周期

  • 可观察性:可以观察shared_ptr管理的对象

  • 解决循环引用:打破shared_ptr可能形成的循环引用

5.2 实现原理

weak_ptr指向shared_ptr的控制块,它维护了一个弱引用计数。当shared_ptr的引用计数变为 0 时,对象会被销毁,但控制块会保留直到弱引用计数也变为 0。

weak_ptr不能直接访问对象,必须通过lock()方法获取一个shared_ptr,该方法在对象仍然存在时返回一个有效的shared_ptr,否则返回空shared_ptr。

5.3 使用示例

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
#include <memory>
#include <iostream>

class B; // 前向声明

class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destructed\n"; }
};

class B {
public:
// 使用weak_ptr打破循环引用
std::weak_ptr<A> a_ptr;
~B() { std::cout << "B destructed\n"; }
};

int main() {
// 创建循环引用场景
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();

a->b_ptr = b;
b->a_ptr = a;

// 此时a和b的引用计数都是1
std::cout << "a use count: " << a.use_count() << "\n";
std::cout << "b use count: " << b.use_count() << "\n";

// 通过weak_ptr访问对象
if (auto locked = b->a_ptr.lock()) {
std::cout << "Successfully locked a from b\n";
std::cout << "Locked a use count: " << locked.use_count() << "\n";
} else {
std::cout << "a has been destroyed\n";
}

// weak_ptr的过期检查
std::weak_ptr<A> weak_a = a;
a.reset(); // 重置a,引用计数减为0

if (weak_a.expired()) {
std::cout << "weak_a has expired\n";
} else {
std::cout << "weak_a is still valid\n";
}

return 0;
}

5.4 使用场景

  1. 打破循环引用:当两个或多个shared_ptr形成循环引用时,使用weak_ptr打破循环

  2. 缓存机制:临时引用对象,不希望影响对象的生命周期

  3. 观察者模式:观察者可以通过weak_ptr观察被观察者,而不影响被观察者的生命周期

  4. 避免悬空指针:需要检查对象是否仍然存在时

5.5 注意事项

  1. weak_ptr不能直接访问对象,必须通过lock()方法获取shared_ptr后才能访问

  2. 在使用lock()返回的shared_ptr之前,应检查其有效性

  3. weak_ptr的expired()方法可以检查对象是否已被销毁

  4. 不要存储lock()返回的shared_ptr,这会无意中延长对象的生命周期

六、auto_ptr(已弃用)

6.1 定义与问题

auto_ptr是 C++98 标准中引入的智能指针,旨在解决手动内存管理的问题。然而,由于其设计缺陷,在 C++11 标准中已被弃用,并在 C++17 中被正式移除。

auto_ptr的主要问题:

  • 复制语义不合理:复制auto_ptr会转移所有权,这与直觉不符

  • 不能用于容器:由于复制语义的问题,auto_ptr不能安全地存储在标准容器中

  • 不支持数组:auto_ptr不能管理动态分配的数组

  • 缺乏自定义删除器支持

6.2 使用示例(不推荐)

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
#include <memory>
#include <iostream>

class MyClass {
public:
MyClass(int id) : id_(id) {
std::cout << "MyClass " << id_ << " constructed\n";
}
~MyClass() {
std::cout << "MyClass " << id_ << " destructed\n";
}
private:
int id_;
};

int main() {
std::auto_ptr<MyClass> ap1(new MyClass(1));

// 复制auto_ptr会转移所有权
std::auto_ptr<MyClass> ap2 = ap1;

// ap1现在变为空,访问会导致未定义行为
// ap1->someMethod(); // 危险!

return 0;
}

6.3 替代方案

auto_ptr的所有使用场景都可以被其他智能指针替代:

  • 独占所有权场景:使用unique_ptr替代

  • 共享所有权场景:使用shared_ptr替代

  • 数组管理:使用unique_ptr<T[]>替代

七、智能指针对比分析

特性 unique_ptr shared_ptr weak_ptr auto_ptr(已弃用)
所有权 独占 共享 独占(复制转移)
复制操作 禁止 允许(引用计数 + 1) 允许 允许(转移所有权)
移动操作 允许 允许 允许 不支持(C++11 前)
内存开销 小(与原始指针相当) 中(额外控制块) 中(指向控制块)
引用计数 无(观察引用计数)
线程安全 无(指针本身) 引用计数线程安全
数组支持 有(unique_ptr<T []>) 无(需自定义删除器)
循环引用 不涉及 可能产生 解决循环引用 不涉及
自定义删除器 支持 支持