C++从C++11开始引入了智能指针(std::unique_ptr、std::shared_ptr、std::weak_ptr),并后面的各个版本中对智能指针进行了改进。

unique_ptr

unique_ptr为独占所有权的智能指针,不允许拷贝构造和赋值构造,仅支持std::move移动。

从C++14 起支持使用 std::make_unique 方式构造 unique_ptr,推荐使用std::make_unique 方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <memory>
using namespace std;

unique_ptr<Foo> up1(new Foo());
unique_ptr<Foo> up2 = make_unique<Foo>(); // C++14 起支持 make_unique
unique_ptr<Foo> up3 = std::move(up1); // 移动后,up1为nullptr

unique_ptr<Foo> up4 = up3; // 不允许拷贝,编译错误
unique_ptr<Foo> up5(up1); // 不允许拷贝,编译错误

// 使用 get 方法获取原始指针
Foo* f1 = up1.get(); // 因为up1为空,所以获取到的原始指针也为空
Foo* f2 = up2.get();

删除器

std::unique_ptr的默认行为是使用 delete 或 delete[] 来释放其管理的资源。但当你需要管理非传统资源​(如文件句柄、数据库连接)或需要特殊的释放逻辑时,自定义删除器就变得至关重要。

1
2
3
4
_EXPORT_STD template <class _Ty, class _Dx /* = default_delete<_Ty> */>
class unique_ptr { // non-copyable pointer to an object
...
};

从上面的unique_ptr模板类的定义可以知道,需要通过模板的第二个参数 _Dx 来指定自定义的删除器。unique_ptr的删除器通过模板参数指定,在编译期确定,这样做的好处是通常没有额外的运行时开销,但灵活性较差。

unique_ptr的删除器可以有多种指定方式,如函数对象、函数指针、std::function、lamada 表达式。

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

using namespace std;

struct FileCloser {
void operator()(std::FILE* fp) const {
if (fp) {
std::fclose(fp);
std::cout << "File closed." << std::endl;
}
}
};

void customFileCloser(std::FILE* fp) {
if (fp) {
std::fclose(fp);
std::cout << "File closed." << std::endl;
}
}

int main() {
std::FILE* fp = std::fopen("test.txt", "w");

// 函数对象方式,函数对象就是一个重载了 operator()的类或结构体
unique_ptr<std::FILE, FileCloser> up1(fp);

// Lamada方式,需要结合 decltype来获取 Lambda 的类型
auto lamadaCloser = [](std::FILE* fp) {
if (fp) {
std::fclose(fp);
std::cout << "File closed." << std::endl;
}
};
unique_ptr<std::FILE, decltype(lamadaCloser)> up2(fp, lamadaCloser);

// std::function 方式
unique_ptr<std::FILE, std::function<void(std::FILE*)>> up3(fp, customFileCloser);

// 函数指针方式
unique_ptr<std::FILE, void (*)(std::FILE*)> up4(fp, customFileCloser);
}

shared_ptr

std::shared_ptr 基于引用计数,允许多个 shared_ptr 共享同一对象的所有权。每当一个 shared_ptr 被拷贝时,引用计数加 1(可以使用use_count成员函数获取引用计数)。每当一个 shared_ptr 被销毁或重置时,引用计数就会减 1。当引用计数降为 0 时,所管理的对象会被自动删除。

1
2
3
4
5
6
7
8
9
 // 若在 new 和 shared_ptr构造间发生异常,可能出现内存泄漏
shared_ptr<Foo> sp1(new Foo());

// 推荐使用 make_shared,在一行代码中分配内存,效率更高,更安全
shared_ptr<Foo> sp2 = make_shared<Foo>();

Foo* p = sp2.get(); // 获取源指针
sp1.reset(); // 将sp1置为空,sp1.use_count为0,但资源的总引用计数只会减1,此时管理的Foo不一定会被释放,取决于总引用计数是否减到0了
sp2.reset(new Foo()); // 释放旧对象,并接管新对象的所有权

只有在需要自定义删除器、对内存释放时机有极其苛刻的要求、处理私有构造函数或必须使用花括号初始化列表这些少数特定场景下,才考虑直接使用 new来构造 std::shared_ptr。

拷贝

支持拷贝和移动,每拷贝一次,引用计数加 1:

1
shared_ptr<Foo> sp3 = sp1;  // sp1和sp3共享所有权,引用计数加1

移动

支持使用 std::move 进行所有权移动,移动后,源 shared_ptr 变为 nullptr,但新shared_ptr的​引用计数与之前一样:

1
2
3
4
cout << "sp3 use_count: " << sp3.use_count() << endl;    // 2
auto sp4 = std::move(sp3);// 转移所有权,sp3 变为空,引用计数不变
cout << std::boolalpha << !!sp3 << "," << !!sp4 << endl; // false, true
cout << "sp4 use_count: " << sp4.use_count() << endl; // 2

删除器

当需要自定义删除器时,只能使用 new 来构造shared_ptr,不能使用 make_shared。

shared_ptr 的删除器与unique_ptr不同,其不是通过模板参数指定的,而是通过函数参数指定的,因此是运行时动态存储的,虽然灵活性较高,但性能会有轻微影响。

shared_ptr 的删除器支持函数对象、lamada表达式、函数指针等形式,通过第2个参数指定。

1
2
3
4
5
6
7
8
9
10
std::FILE* fp = std::fopen("test.txt", "w");

auto lamadaCloser = [](std::FILE* fp) {
if (fp) {
std::fclose(fp);
std::cout << "File closed." << std::endl;
}
};

auto sp1 = shared_ptr<std::FILE>(fp, lamadaCloser);

循环引用

当两个或多个对象互相持有对方的 shared_ptr,会导致引用计数永远无法归零,从而内存泄漏。如下面场景:

1
2
3
4
5
6
7
struct Node {
std::shared_ptr<Node> next; // 使用 shared_ptr
};
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2;
node2->next = node1; // 循环引用!引用计数永不归零。

解决方案:将其中一个指针改为 std::weak_ptr。

1
2
3
struct Node {
weak_ptr<Node> next; // 使用 weak_ptr 打破循环引用
};

weak_ptr

std::weak_ptr 是 C++ 智能指针体系中一个非常独特的角色,其​必须从 std::shared_ptr 或另一个 std::weak_ptr创建,不能独立存在,主要用于解决共享所有权带来的循环引用问题。

1
2
3
4
5
#include <memory>

auto sharedPtr = std::make_shared<int>(42); // 引用计数=1
std::weak_ptr<int> weakPtr(sharedPtr); // 从 shared_ptr 创建,引用计数仍为1
std::weak_ptr<int> anotherWeakPtr = weakPtr; // 从另一个 weak_ptr 拷贝

std::weak_ptr的核心价值在于它不拥有对象的所有权,不会增加引用计数。

使用weak_ptr可以解决shared_ptr带来的两个关键问题:

  • ​循环引用 (Circular Reference)​​:当两个或多个对象相互持有对方的 std::shared_ptr时,它们的引用计数永远无法降为零,导致内存泄漏。使用 std::weak_ptr替代其中一个引用可以打破循环。

  • ​观察者模式 (Observer Pattern)​​:观察者不应控制被观察对象的生命周期。使用 std::weak_ptr允许观察者检查对象是否还存在,而不会阻止其被销毁。

必须使用 lock()方法来尝试获取一个有效的 std::shared_ptr,然后判断并使用。

1
2
3
4
5
6
7
if (auto tempSharedPtr = weakPtr.lock()) { // lock() 返回一个临时的 shared_ptr
// 对象存在,可以安全使用
std::cout << "Value: " << *tempSharedPtr << std::endl;
} else {
// 对象已被释放
std::cout << "Resource has been released." << std::endl;
}