DongGu
智能指针详解

智能指针详解

裸指针

指针是一种强大的工具,但裸指针的使用通常伴随着一些问题:它的声明并未指明指向的是单个对象还是一个数组;通常无法判断指针是否悬垂;并且需要手动管理内存的生命周期。

在传统 C++ 中,new 触发堆内存分配后必须手动 delete 释放:未释放会造成内存泄漏,重复释放可能引发程序崩溃或未定义行为。

智能指针

C++11 引入了智能指针(smart pointer)。智能指针利用 RAII(Resource Acquisition Is Initialization,资源获取即初始化)技术,将动态分配的裸指针封装在栈上的对象中。当智能指针对象超出作用域析构时,会自动调用 delete 释放内存,从而彻底规避了手动内存管理的风险。

C++11 共有四种智能指针,全部位于 <memory> 头文件中,分别是:std::auto_ptrstd::unique_ptrstd::shared_ptrstd::weak_ptr。其中 std::auto_ptr 是 C++98 遗留的失败设计,被 std::unique_ptr 全面替代,并在 C++17 中被移除。

std::unique_ptr

std::unique_ptr 是独占所有权的智能指针。默认条件下(使用默认删除器)std::unique_ptr 具有和裸指针相同的大小;使用空类型(无状态仿函数或 lambda)的自定义删除器时,通过 EBO(Empty Base Optimization,空基类优化)与内联优化,同样可以做到零额外成本;但传入函数指针或有状态删除器时,std::unique_ptr 的尺寸会相应增大。

由于 std::unique_ptr 独占所有权的特性,其拷贝构造和拷贝赋值被禁用,只允许移动语义。

下面给出一个简易的 std::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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <utility>  // std::move、std::exchange

// 默认删除器(空类)
template <typename T>
struct DefaultDelete {
void operator()(T* p) const {
delete p;
}
};

// 简易版 unique_ptr
template <typename T, typename Deleter = DefaultDelete<T>>
class MyUniquePtr : private Deleter { // 私有继承 Deleter,触发空类优化(EBO)
private:
T* ptr_ = nullptr; // 唯一的成员变量:原始指针

public:
// 1. 构造函数
explicit MyUniquePtr(T* p = nullptr, Deleter d = Deleter())
: Deleter(std::move(d)), ptr_(p) {}

// 2. 析构函数:RAII 自动释放资源
~MyUniquePtr() {
if (ptr_) {
Deleter::operator()(ptr_); // 调用继承下来的 Deleter 成员函数
}
}

// 3. 彻底禁用拷贝(保证独占性)
MyUniquePtr(const MyUniquePtr&) = delete;
MyUniquePtr& operator=(const MyUniquePtr&) = delete;

// 4. 允许移动(转移所有权)
MyUniquePtr(MyUniquePtr&& other) noexcept
: Deleter(std::move(other)), ptr_(other.ptr_) {
other.ptr_ = nullptr; // 原指针置空!
}

MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
if (this != &other) {
// 先释放当前持有的资源
if (ptr_) Deleter::operator()(ptr_);

// 转移删除器与所有权
static_cast<Deleter&>(*this) = std::move(other);
ptr_ = std::exchange(other.ptr_, nullptr);
}
return *this;
}

// 5. 解引用与指针操作
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
T* get() const { return ptr_; }
};

std::unique_ptr 的工作原理很简单:内部保存一份原始指针供外部操作,并禁用拷贝、只允许移动以保证资源的独占性;生命周期结束时编译器会自动调用其析构函数释放资源,实现资源的自动回收。

std::unique_ptr 默认使用一个简单的删除器,其作用就是调用 delete 释放内存。

std::unique_ptr 支持自定义删除器。自定义删除器可以是普通函数,也可以是 lambda 匿名函数或仿函数。例如:

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

struct MyResource {};

// 1. 普通函数
void freeResourceFunc(MyResource* p) { delete p; }

// 2. 仿函数
struct ResourceDeleterFunctor {
void operator()(MyResource* p) const { delete p; }
};

// ------------------------------------------------------------
// 1. 函数指针作为删除器
// ------------------------------------------------------------
using FuncPtr = void(*)(MyResource*);
std::unique_ptr<MyResource, FuncPtr> ptr1(new MyResource(), freeResourceFunc);

// ------------------------------------------------------------
// 2. 仿函数(functor)作为删除器
// ------------------------------------------------------------
// 模板参数传入类名即可,无需传入对象实例(会自动默认构造)
std::unique_ptr<MyResource, ResourceDeleterFunctor> ptr2(new MyResource());

// ------------------------------------------------------------
// 3. 无状态 lambda 作为删除器
// ------------------------------------------------------------
auto statelessLambda = [](MyResource* p) { delete p; };
std::unique_ptr<MyResource, decltype(statelessLambda)> ptr3(new MyResource(), statelessLambda);

// ------------------------------------------------------------
// 4. 有状态 lambda 作为删除器
// ------------------------------------------------------------
int contextId = 42;
auto statefulLambda = [contextId](MyResource* p) { delete p; };
std::unique_ptr<MyResource, decltype(statefulLambda)> ptr4(new MyResource(), statefulLambda);

自定义删除器的参数为所管理类型的指针,其内部除了释放指针外,还可以执行其他操作,比如日志打印等。

如果传入普通的函数指针,std::unique_ptr 内部会额外保存一份函数指针,产生额外的内存占用。

如果传入的是仿函数或 lambda 匿名函数,实际上是传入了一个对象实体:无状态条件下(对象没有捕获变量),编译器会进行 EBO 空基类优化,不产生额外的内存占用;有状态情况下,则会产生额外的内存占用。

std::shared_ptr

std::shared_ptr 是共享所有权的指针。与 std::unique_ptr 不同,std::shared_ptr 确实带来了运行时的额外开销。

std::shared_ptr 通过引用计数来实现资源的共享所有权管理:当最后一个指向资源的 std::shared_ptr 被销毁或重置时,该资源就会被自动释放。

std::shared_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
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#include <utility>  // std::move、std::exchange
#include <atomic> // std::atomic

// 1. 默认删除器(空类)
template <typename T>
struct DefaultDelete {
void operator()(T* p) const {
delete p;
}
};

// 2. 控制块基类:用于类型擦除,方便统一管理引用计数
struct ControlBlockBase {
std::atomic<size_t> ref_count_{1}; // 强引用计数(线程安全)
std::atomic<size_t> weak_count_{1}; // 弱引用计数(线程安全)

virtual void dispose(void* p) = 0; // 纯虚函数:抽象资源清理行为

// 强引用归零后调用:当弱引用也归零时销毁控制块自身
void release_block() noexcept {
if (weak_count_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete this;
}
}

virtual ~ControlBlockBase() = default;
};

// 3. 具体控制块:继承 Deleter 触发 EBO(空类优化)
template <typename T, typename Deleter = DefaultDelete<T>>
class ControlBlock : public ControlBlockBase, private Deleter {
public:
explicit ControlBlock(Deleter d) : Deleter(std::move(d)) {}

void dispose(void* p) override {
Deleter::operator()(static_cast<T*>(p)); // 调用继承自 Deleter 的释放逻辑
}
};

// 4. 简易版 shared_ptr
template <typename T>
class MySharedPtr {
private:
T* ptr_ = nullptr; // 指向托管资源的裸指针
ControlBlockBase* cb_ = nullptr; // 指向堆上控制块的指针

// 私有辅助方法:减少当前引用计数,归零时释放资源
void release() noexcept {
if (cb_) {
// fetch_sub 返回减 1 前的值;若原值为 1,说明当前是最后一个持有人
if (cb_->ref_count_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
cb_->dispose(ptr_); // 析构托管资源
cb_->release_block(); // 弱引用也归零时释放控制块
}
ptr_ = nullptr;
cb_ = nullptr;
}
}

public:
// 构造函数 1:默认无参构造
constexpr MySharedPtr() noexcept = default;

// 构造函数 2:接管裸指针,支持自定义删除器
template <typename Deleter = DefaultDelete<T>>
explicit MySharedPtr(T* p, Deleter d = Deleter()) : ptr_(p) {
if (ptr_) {
cb_ = new ControlBlock<T, Deleter>(std::move(d)); // 在堆上创建控制块
}
}

// 析构函数:RAII 核心,引用计数归零时销毁内存
~MySharedPtr() {
release();
}

// 拷贝构造:共享所有权,强引用计数 +1
MySharedPtr(const MySharedPtr& other) noexcept
: ptr_(other.ptr_), cb_(other.cb_) {
if (cb_) {
cb_->ref_count_.fetch_add(1, std::memory_order_relaxed);
}
}

// 拷贝赋值:解绑旧资源(计数 -1),接管新资源(计数 +1)
MySharedPtr& operator=(const MySharedPtr& other) noexcept {
if (this != &other) {
release(); // 释放自己当前持有的资源

ptr_ = other.ptr_;
cb_ = other.cb_;
if (cb_) {
cb_->ref_count_.fetch_add(1, std::memory_order_relaxed);
}
}
return *this;
}

// 移动构造:直接窃取所有权,无需增加引用计数
MySharedPtr(MySharedPtr&& other) noexcept
: ptr_(std::exchange(other.ptr_, nullptr)),
cb_(std::exchange(other.cb_, nullptr)) {}

// 移动赋值:释放自身并窃取 other 的资源
MySharedPtr& operator=(MySharedPtr&& other) noexcept {
if (this != &other) {
release();

ptr_ = std::exchange(other.ptr_, nullptr);
cb_ = std::exchange(other.cb_, nullptr);
}
return *this;
}

// 指针常规操作
T& operator*() const noexcept { return *ptr_; }
T* operator->() const noexcept { return ptr_; }
T* get() const noexcept { return ptr_; }

// 获取当前强引用计数
size_t use_count() const noexcept {
return cb_ ? cb_->ref_count_.load(std::memory_order_relaxed) : 0;
}

explicit operator bool() const noexcept {
return ptr_ != nullptr;
}

private:
// 供 MyWeakPtr::lock() 使用:共享已有控制块,强引用计数 +1
MySharedPtr(T* p, ControlBlockBase* cb) noexcept
: ptr_(p), cb_(cb) {
if (cb_) {
cb_->ref_count_.fetch_add(1, std::memory_order_relaxed);
}
}

friend class MyWeakPtr<T>;
};

std::shared_ptr 的原理是其内部存储了两份指针:一份指向资源地址,另一份指向一个控制块。控制块位于堆内存中,其内部包括强引用计数、弱引用计数以及自定义删除器等。

当发生拷贝赋值或拷贝构造时,对应控制块的强引用计数会增加;当 std::shared_ptr 的生命周期结束时,对应控制块的强引用计数会减少。强引用计数归零时资源会被释放;当强引用和弱引用全部归零时,控制块内存也会被释放。

std::unique_ptr 相比,std::shared_ptr 显然更加灵活,但是也付出了相应的代价:

  • 尺寸是裸指针的两倍:因为必须同时存储”资源指针”和”控制块指针”。

  • 控制块需要动态内存分配:创建 std::shared_ptr 时,如果没有现成的控制块,就必须在堆上额外分配一次内存(除非使用 std::make_shared 将对象和控制块合并分配)。

  • 引用计数的操作必须是原子性的:递增/递减引用计数需要原子操作,在多线程环境下存在 CPU 缓存同步的开销。

  • 拷贝操作比 std::unique_ptr 昂贵:拷贝 std::shared_ptr 时需要对引用计数执行原子递增;移动操作则无需修改原子计数,只涉及指针赋值。

std::unique_ptr 相比,std::shared_ptr 的构造也更加灵活:自定义删除器不再作为模板参数传递,删除器类型不再是指针类型的一部分,使得属于同一类型的 std::shared_ptr 可以放入同一个容器内。

需要注意的是,用同一份资源初始化不同的 std::shared_ptr 时,会多次分配控制块内存,每个 std::shared_ptr 独立计数。

1
2
3
Widget* rawPtr = new Widget();
std::shared_ptr<Widget> sp1(rawPtr); // 创建控制块 A
std::shared_ptr<Widget> sp2(rawPtr); // 创建控制块 B

此时两者析构时,裸指针会被重复释放,导致未定义行为。正确做法是直接使用 std::make_shared<Widget>();若必须手动 new,则应在 new 之后立即用同一个表达式初始化唯一的 std::shared_ptrstd::shared_ptr<Widget> sp1(new Widget());

还有一点需要注意,当你在类内用 this 指针初始化 std::shared_ptr 时,也会创建多个控制块。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
std::vector<std::shared_ptr<Widget>> processedWidgets;

class Widget {
public:
...
void process();
...
};

void Widget::process() {
...
processedWidgets.emplace_back(this); // 每次调用都会用 this 创建新的控制块
}

此时每次调用 process 函数,都会利用 this 裸指针创建一份新的控制块;当这些 std::shared_ptr 析构时会重复释放同一资源,导致未定义行为。

正确做法是通过继承 std::enable_shared_from_this<T> 并调用 shared_from_this(),让对象共享已有的控制块,使引用计数正确地 +1。

1
2
3
4
5
6
7
8
9
10
11
12
13
std::vector<std::shared_ptr<Widget>> processedWidgets;

class Widget : public std::enable_shared_from_this<Widget> {
public:
...
void process();
...
};

void Widget::process() {
...
processedWidgets.emplace_back(shared_from_this()); // 共享已有控制块
}

当外部的 std::shared_ptr 构造函数初始化该对象时,会发现对象继承了 std::enable_shared_from_this,从而把控制块的地址写入对象内部的 weak_this 成员。之后类内调用 shared_from_this() 时,会基于已有控制块构造一个新的 std::shared_ptr 并增加强引用计数。由此也可以理解:为什么在构造函数中调用 shared_from_this() 会出错(此时外部的 std::shared_ptr 尚未接管对象,weak_this 还是空的),以及为什么类外从未用 std::shared_ptr 管理过对象时不能调用 shared_from_this()

std::weak_ptr

std::weak_ptr 是一种区别于 std::unique_ptrstd::shared_ptr 的指针,它不具有对象的所有权。std::weak_ptr 通常伴随着 std::shared_ptr 使用:std::weak_ptr 不增加强引用计数,而是增加弱引用计数。

简单来说,std::weak_ptr 是观察者视角:它可以观察对象的存活情况但不能直接操作,当需要操作对象时可以提升为 std::shared_ptr

std::weak_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
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
// 简易版 weak_ptr:与 MySharedPtr 共享控制块,只增加弱引用计数
template <typename T>
class MyWeakPtr {
private:
T* ptr_ = nullptr; // 缓存的资源指针(仅观察,不拥有)
ControlBlockBase* cb_ = nullptr; // 指向共享的控制块

// 释放当前持有的控制块:弱引用归零时销毁控制块
void release() noexcept {
if (cb_) {
cb_->release_block();
ptr_ = nullptr;
cb_ = nullptr;
}
}

public:
MyWeakPtr() noexcept = default;

// 从 shared_ptr 构造:只增加弱引用计数,不影响对象生命周期
MyWeakPtr(const MySharedPtr<T>& sp) noexcept
: ptr_(sp.ptr_), cb_(sp.cb_) {
if (cb_) {
cb_->weak_count_.fetch_add(1, std::memory_order_relaxed);
}
}

// 拷贝构造:弱引用计数 +1
MyWeakPtr(const MyWeakPtr& other) noexcept
: ptr_(other.ptr_), cb_(other.cb_) {
if (cb_) {
cb_->weak_count_.fetch_add(1, std::memory_order_relaxed);
}
}

// 移动构造:直接窃取
MyWeakPtr(MyWeakPtr&& other) noexcept
: ptr_(std::exchange(other.ptr_, nullptr)),
cb_(std::exchange(other.cb_, nullptr)) {}

// 拷贝赋值:先释放旧控制块,再接管新控制块
MyWeakPtr& operator=(const MyWeakPtr& other) noexcept {
if (this != &other) {
release();
ptr_ = other.ptr_;
cb_ = other.cb_;
if (cb_) {
cb_->weak_count_.fetch_add(1, std::memory_order_relaxed);
}
}
return *this;
}

// 移动赋值:释放自身并窃取 other 的资源
MyWeakPtr& operator=(MyWeakPtr&& other) noexcept {
if (this != &other) {
release();
ptr_ = std::exchange(other.ptr_, nullptr);
cb_ = std::exchange(other.cb_, nullptr);
}
return *this;
}

~MyWeakPtr() { release(); }

// 观察对象是否存活:强引用计数是否为 0
bool expired() const noexcept {
return !cb_ || cb_->ref_count_.load(std::memory_order_relaxed) == 0;
}

// 提升为 shared_ptr:对象存活则成功,否则返回空指针。
// 说明:真实实现会原子地"检查强计数并 +1",避免与最后一个
// shared_ptr 的析构发生竞争;简化版仅演示概念
MySharedPtr<T> lock() const noexcept {
if (expired()) return MySharedPtr<T>();
return MySharedPtr<T>(ptr_, cb_); // 私有构造函数:共享控制块,强计数 +1
}
};

从实现可以看出:std::weak_ptr 的拷贝只影响弱引用计数,不会阻止资源被释放;而 lock() 是进入”操作模式”的唯一入口,且必须先检查对象是否存活。

std::weak_ptr 主要有两大核心应用场景:

1. 缓存 / 观察者模式

当我们缓存了一个指针并需要使用时,我们需要知道指针指向对象的存活情况:如果使用裸指针,我们无法得知指针指向的对象内存是否已经被释放;如果使用 std::shared_ptr,则会导致对象内存永远不被释放。

std::weak_ptr 可以观察对象存活情况而不干扰对象生命周期;需要操作对象时,先通过 lock() 判断对象是否存活。

1
2
3
4
5
6
7
8
9
10
11
12
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
static std::unordered_map<WidgetID, std::weak_ptr<const Widget>> cache;

auto objPtr = cache[id].lock();

if (!objPtr) {
objPtr = loadWidget(id);
cache[id] = objPtr;
}
return objPtr;
}

利用 std::weak_ptr 缓存指针,使用时通过 lock()std::weak_ptr 提升为 std::shared_ptr:提升成功则直接返回,失败则重新加载,避免了使用悬空指针的风险。

2. 避免循环引用

std::shared_ptr 的使用有时会导致循环引用,使内存无法释放。严格来说这是内存泄漏,并非并发编程意义上的死锁。

我们设计这样一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct A {
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A 析构了\n"; }
};

struct B {
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B 析构了\n"; }
};

int main() {
{
auto a = std::make_shared<A>(); // 1. 创建节点 A,引用计数 = 1
auto b = std::make_shared<B>(); // 2. 创建节点 B,引用计数 = 1

a->b_ptr = b; // 3. 互相引用:产生闭环!
b->a_ptr = a; // B 的引用计数变为 2,A 的引用计数变为 2
}
}

存在 A 和 B 两种结构体,其内部分别持有指向对方的 std::shared_ptr 指针 b_ptra_ptr

首先,我们创建 A 和 B 的实例,并且使用 std::shared_ptr 变量 ab 指向它们。此时 A 和 B 的强引用计数都为 1。

随后,我们让 A 内部的 b_ptr 拷贝 b,B 内部的 a_ptr 拷贝 a,此时由于 std::shared_ptr 发生拷贝,A 和 B 的强引用计数都变为 2。

当离开作用域时:

  • 栈上的局部变量 b 析构,对象 B 的引用计数从 2 降为 1(a->b_ptr 还在持有 B)。
  • 栈上的局部变量 a 析构,对象 A 的引用计数从 2 降为 1(b->a_ptr 还在持有 A)。

此时陷入了一个僵局:

  • 想要释放对象 A → 必须让 A 的强引用计数归 0 → 必须先销毁持有 A 的对象 B。
  • 想要释放对象 B → 必须让 B 的强引用计数归 0 → 必须先销毁持有 B 的对象 A。

结果:两者互相僵持,引用计数永远卡在 1,任何一方的析构函数都无法被调用,分配在堆上的 A 和 B 内存彻底泄漏。

解决方法是使 A 或 B 一方的反向指针改为 std::weak_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct A {
std::shared_ptr<B> b_ptr; // A 拥有 B(强引用)
~A() { std::cout << "A 析构了\n"; }
};

struct B {
std::weak_ptr<A> a_ptr; // B 只是观察 A(弱引用)
~B() { std::cout << "B 析构了\n"; }
};

int main() {
{
auto a = std::make_shared<A>(); // A 计数 = 1
auto b = std::make_shared<B>(); // B 计数 = 1

a->b_ptr = b; // B 计数 = 2
b->a_ptr = a; // A 计数仍然为 1(弱引用不增加强计数)
}
}

此时,离开作用域时栈上的局部变量 b 先析构,B 的强引用计数从 2 降为 1(a->b_ptr 还在持有 B);随后 a 析构,A 的强引用计数从 1 降为 0,A 析构后其内部的 b_ptr 被销毁,B 的计数也降为 0,B 成功析构。

智能指针的构造

与用 new 初始化对象作为构造智能指针的参数相比,使用 std::make_uniquestd::make_shared 往往更好。

make 系列函数构造智能指针主要具有三大优势:

1. 代码更简洁,避免冗余重复类型

手写 new 需要重复书写目标类型,而 make 函数配合 auto 能让代码极其简洁:

1
2
3
4
5
// 冗余
std::unique_ptr<Widget> w1(new Widget());

// 简洁
auto w2 = std::make_unique<Widget>();

2. 保证异常安全性

在 C++17 以前,函数参数的求值顺序是未定义的,使用 new 可能会导致内存泄漏:

1
2
3
4
void processWidget(std::shared_ptr<Widget> sp, int priority);

// 危险调用
processWidget(std::shared_ptr<Widget>(new Widget), computePriority());

像这段代码的执行顺序可能是:

  1. 执行 new Widget 在堆上分配内存。
  2. 执行 computePriority()(如果此时该函数抛出异常!)。
  3. 调用 std::shared_ptr 的构造函数。

当第二步抛出异常后,std::shared_ptr 的构造函数不会再执行,此时 Widget 便无法执行析构,发生了内存泄漏。

如果使用 std::make_shared

1
processWidget(std::make_shared<Widget>(), computePriority());

std::make_shared 将内存分配与智能指针初始化结合在一起,彻底规避了内存泄漏风险。(C++17 起规定了函数参数的求值顺序,即使手写 new 也不会泄漏,但 make_shared 的写法依然更优。)

3. 减少内存分配次数,提升性能

由于 std::shared_ptr 需要控制块,如果使用 new 作为构造参数,对象和控制块会进行两次内存分配,开销较大;而使用 std::make_shared 只会分配一次,将对象和控制块合并分配,开销更小。

但是有时也必须直接使用 new,make 函数不再适用:

1. 需要指定自定义删除器

std::make_sharedstd::make_unique 的 API 不支持传入自定义删除器。如果你需要自定义释放逻辑,必须直接使用 new

1
2
3
4
auto customDeleter = [](Widget* pw) { releaseWidget(pw); };

// 直接 new 才能传删除器
std::shared_ptr<Widget> sp(new Widget(), customDeleter);

2. 构造函数需要初始化列表

make 函数内部通过模板完美转发(perfect forwarding)参数,但完美转发无法推导 {1, 2, 3} 这种花括号初始化列表——花括号列表本身没有类型,它向 std::initializer_list<int> 的隐式转换只在非模板函数或明确声明为 std::initializer_list<T> 参数时才会发生。

1
2
3
4
5
// 编译报错!无法推导 {1, 2, 3}
auto sp = std::make_shared<std::vector<int>>{1, 2, 3};

// 变通方案:显式构造 initializer_list
auto sp = std::make_shared<std::vector<int>>(std::initializer_list<int>{1, 2, 3});

3. 类的 new / delete 运算符被重载

std::make_shared 内部分配的尺寸是 sizeof(Widget) 加上控制块的大小,这会导致类自定义的 operator new 无法正常匹配(分配尺寸与自定义重载期望的不同)。

4. 内存敏感场景下的延迟释放

std::make_shared 把对象和控制块分配在同一块连续内存中。对象占用的内存必须等到强引用计数和弱引用计数全部降为 0 时,整块内存才能返还给操作系统。当对象的强引用计数归零时,对象会被析构;但如果还存在 std::weak_ptr 持有该控制块(弱引用未归零),对象占用的内存将不会被释放,直到最后一个 std::weak_ptr 销毁。

所以,如果管理大对象且存在长生命周期的 std::weak_ptr,应退回手写 new(即 std::shared_ptr<Widget>(new Widget()))。这样当强引用归零时,Widget 占用的内存会立刻释放,仅保留几十字节的控制块供 std::weak_ptr 检查。


这次的插图来自画师 TheProtagonists

图片地址:https://www.pixiv.net/artworks/147800615

文章内容参考:Effective Modern C++ 42 Specific Ways to Improve Your Use of C++11 and C++14 (Scott Meyers) Item 18、Item 19、Item 20、Item 21

本文作者:DongGu
本文链接:https://donggu.xyz/2026/09/02/现代C++学习/智能指针详解/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可