本站已经几年没有更新 C++ 相关的问题了,这就来补一个~

起因是最近秋招面试被问到了一个 C++ 智能指针相关的问题,没能答上来,下来仔细研究了一下,发现还挺有意思,将 C++ 对象布局、内存分配、智能指针生命周期等很多知识点都串联起来,有一定的学习价值,特此分享出来。

问题描述

在 C++ 中,通过 make_shared<T>(Args, ...) 来创建一个对象并拿到接管它的智能指针,和通过 shared_ptr<T>(new T(Args, ...)) 来先创建一个对象,然后用智能指针接管该对象,二者的区别是什么?

例如,分别采用下列两种写法:

1
2
3
auto p1 = std::make_shared<T>(arg1, arg2);

std::shared_ptr<T> p2(new T(arg1, arg2));

分析解答

最核心的区别:内存分配次数不同

在与 AI 问答和阅读相关材料的过程中,发现似乎 C++ 标准并没有对 make_shared 的“内存连续”和“单次分配”做严格限定,因此本文讨论的主要是主流的工程实现。

make_shared 通常来说会将 T 对象和智能指针相应的控制块放在连续的内存区域中,布局上类似于:

1
2
3
4
5
6
7
8
┌──────────────────────────────┐
│ control block │
│ - reference count │
│ - weak reference count │
│ - deleter / allocator ... │
├──────────────────────────────┤
│ T object │
└──────────────────────────────┘

因此 make_shared 通常只需要一次动态内存分配,而 shared_ptr<T>(new T(...)) 则是分别的两次:

1
2
3
4
5
6
7
8
9
10
11
12
new T(...)

┌─────────────┐
│ T object │
└─────────────┘

shared_ptr create control block

┌────────────────────┐
│ control block │
│ - ref count ... │
└────────────────────┘

由于 make_shared 产生的智能指针对象位于连续的内存区域中,因此程序的局部性也会更好。整体来看 make_shared 是一个性能更为优越的选择。

另外一个值得关注的区别:弱引用导致的对象存活时间不同(同样是由于内存分配所导致)

假设:

1
2
3
4
auto p = std::make_shared<T>();
std::weak_ptr<T> w = p;

p.reset(); // 放弃当前所管理的对象

此时 T 对象已经结束了生命周期,理论上其内存空间可以被释放;但是由于弱引用 w 的存在,智能指针 p 的控制块还不能被释放;又由于控制块和 T 对象的内存空间源自一次内存分配,它们的内存回收也必须保持同步,因此 T 对象内存空间也就暂时无法被释放,直到所有的弱引用结束生命周期,控制块中的弱引用计数归零,此时控制块和 T 对象的内存空间通过单次的 free() 进行释放。

“控制块和 T 对象内存空间源自一次内存分配,它们的内存回收也必须保持同步”

上面的原因来自 POSIX 标准,其中对 free() 的描述如下:

The free() function shall cause the space pointed to by ptr to be deallocated… Otherwise, if the argument does not match a pointer earlier returned by a function in POSIX.1-2017 that allocates memory as if by malloc(), or if the space has been deallocated by a call to free() or realloc(), the behavior is undefined.

简单来说,就是 free() 所传入的指针必须 malloc() 所得到的指针相对应,否则将是未定义行为。