1 Preface
软件工程遇到的问题都可以通过增加一个中间层
来解决, 智能指针也是基于这样的思想;
C++ 11 包含了以下 3 种常用的智能指针:
- std::unique_ptr
- std::shared_ptr
- std::weak_ptr
包含头文件 #include <memory>
即可
2 原理分析
2.1 shared_ptr
直接用 shared_ptr 管理一个堆上的裸指针对象:
1
|
std::shared_ptr<Good> gp1(new Good());
|
2.2 weak_ptr
weak_ptr类 和 shared_ptr类 的成员变量相同。
为什么需要 weak_ptr?
因为 shared_ptr 是对象的强引用, 一旦发生循环引用,对象就无法析构, 所以 weak_ptr 出现就是为了解决循环引用的问题。
打个不恰当的比喻:weak_ptr就像寄生虫,shared_ptr就是宿主。
weak_ptr 和 shared_ptr 内存结构相同(1 个原始对象指针 + ctrl_block_t 指针);
不同之处在于:
(1) 不直接使用 weak_ptr 进行对象的各种操作
每次使用 weak_ptr 时,提升一个shared_ptr, 然后使用shared_ptr; 但是如果提升失败,表示对象已经析构。
(2) 负责管理不同对象的生命周期
shared_ptr 管理 T*对象
的生命周期,而 weak_ptr 管理 ctrl_block_t*
的生命周期。
(3) 不对称
shared_ptr 可以构造对象产生(make_shared() 或 shared_ptr(T*)),或者通过 weak_ptr 提升。
1
2
|
weak_ptr<T> wp;
shared_ptr<T> sp = wp.lock(); //提升
|
weak_ptr 通过拷贝 shared_ptr 构造得到,或者直接拷贝(或移动构造)其他的 weak_ptr。
也就是说, 第一个有效的
weak_ptr 一定从 shared_ptr 拷贝得到的。
1
2
3
4
|
shared_ptr<T> sp;
weak_ptr<T> wp = sp; // 拷贝 shared_ptr 构造
weak_ptr<T> wp1 = wp; // 拷贝 weak_ptr 构造
weak_ptr<T> wp2 = std::move(wp1); //weak_ptr move构造
|
2.3 make_shared<T>()
为什么要引入make_shared?
好处1: 把指针计数器和对象一次性一起分配
;提高内存分配效率。
好处2: 提高数据的局部性,引用计数和对象可能在同一内存页中(甚至是 same cache line)。
所以, 一般来说, 我们推荐使用 make_shared 来创建shared_ptr对象, 能够有效的减少一次堆的小对象分配。
1
|
std::shared_ptr<Good> gp1 = std::make_shared<Good>();
|
2.4 enable_shared_from_this
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Good : public std::enable_shared_from_this<Good> // public 继承
{
public:
std::shared_ptr<Good> getInstance() {
return shared_from_this();
}
~Good() { std::cout << "Good::~Good() called" << std::endl; }
};
// Good: the two shared_ptr's share the same object
std::shared_ptr<Good> gp1 = std::make_shared<Good>();
std::shared_ptr<Good> gp2 = gp1->getInstance();
std::cout << "gp2.use_count() = " << gp2.use_count() << '\n'; // output 2
|
c++ 为了解决class内部, 使用自身class的智能指针的问题,引入了enable_shared_from_this
,
enable_shared_from_this 这个类只包含一个 weak_ptr<T> 成员对象(16 bytes 2个指针),
当每次调用shared_from_this()
, 就通过 weak_ptr
提升创建一个 shared_ptr
对象。
其内存模型如下:
2.5 unique_ptr
unique_ptr<> 管理一个独占的指针对象;
2.6 make_unique<T>()
make_unique<> 只是 unique_ptr<T>(new T()) 语法糖的包装,
作用只是为了智能指都有make_xxx
一个统一的写法而已。
3 对象字节数(64 bit system)
a> shared_ptr, weak_ptr 由于内部都只包含两个pointer,所以都是 16 bytes
;
b> control_block<T*> 包含两个 atomic_llong 和一个指针,所以是 24 bytes
;
c> 特别地,如果采用 make_shared 来分配智能指针,不考虑内存对其的前提下,
control_block<T*> 占用空间为 16 + sizeof(T)
d> unique_ptr 内部有一个 tuple<T*,functor_class> 成员, 这个tuple成员对象只占用 8 bytes
;
4 shared_ptr 和 weak_ptr 实现的细节
-
当每次有新的 shared_ptr 生成时,会增加 _Sp_counted_base 的 _M_use_count (+1);
-
当每次有新的 weak_ptr 生成时,会增加 _Sp_counted_base 的 _M_weak_count (+1);
-
通过weak_ptr<x>::lock()函数可以获取x对象的强引用(shared_ptr)。
通过判断 !expired(){return _M_use_count == 0;}, 那么可以提升成功。
-
对应的,shared_ptr 析构时,会调用 _Sp_counted_base 的 _M_release 来使 _M_use_count (-1);
-
weak_ptr 析构时,会调用 _Sp_counted_base 的 _M_release 来使 _M_weak_count (-1);
-
当 _M_use_count 的数量为 0 的时候 就会释放,原始的对象(用户自定义对象) 的内存,但是不会释放
_Sp_counted_base 的内存。
-
当 _M_use_count 和 _M_weak_count 都为 0 的时候, 才会释放 _Sp_counted_base 的内存。
5 最后
最后,我们来看下智能指针内存空间分布:
- 智能指针对象自身是在当前的栈空间上,而智能指针管理的对象是在堆空间!
- 特别的,对于 shared_ptr 还多一个控制块对象,这个控制块也在堆空间上。
6 答疑
❓* 深挖 unique_ptr 的 tuple 成员对象为啥只占用8个字节?*
因为 gcc 内部实现tuple采用的是递归继承的方式,对应的继承树为:
tuple<T*,class functor()> -> tuple<class functor()> -> tuple<>
其中仿函数的缺省实现(简化版):
void operator()(Tp* ptr) const {
delete ptr;
}
所以问题的本质是:继承一个没有任何成员的 class, 并不会增加类的大小!
❓make_shared 对象的内存可能无法及时回收?
make_shared 只分配一次内存, 这看起来很好. 减少了内存分配的开销。
问题来了, weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存, 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 弱引用都减为 0 时才能释放, 意外的延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题. 关于这个问题可以看这里 make_shared, almost a silver bullet
附录
测试代码:
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
|
#pragma pack(1)
#include <memory>
#include <iostream>
int main()
{
std::shared_ptr<std::string> sp = std::make_shared<std::string>("123");
std::cout << "\n sizeof std::shared_ptr<string> " << sizeof(sp);
std::cout << "\n sizeof(int) " << sizeof(int) << ", sizeof(string) " << sizeof(std::string);
std::cout << "\n sizeof(_Atomic_word) " << sizeof(_Atomic_word);
sp.get();
std::cout << "\n size std::__shared_count(8) = "
<< sizeof(std::__shared_count<std::__default_lock_policy>(sp.get()));
typedef std::_Sp_counted_ptr<std::string*,std::__default_lock_policy> counter_ptr;
std::cout << "\n counter_ptr size :" << sizeof (counter_ptr);
typedef std::_Sp_counted_ptr_inplace<int, std::allocator<int>, std::__default_lock_policy> counter_in_place;
std::cout << "\n counter_in_place :" << sizeof (counter_in_place);
typedef std::_Sp_counted_ptr_inplace<char, std::allocator<char>, std::__default_lock_policy> counter_in_place_char;
std::cout << "\n counter_in_place_char :" << sizeof (counter_in_place_char);
typedef std::enable_shared_from_this<std::string> from_this;
std::cout << "\n from_this (equals weak_ptr<T>):" << sizeof (from_this);
typedef std::weak_ptr<std::string> weak_ptr_tp;
std::cout << "\n weak_ptr_tp :" << sizeof (weak_ptr_tp);
std::unique_ptr<std::string> up = std::make_unique<std::string>("hello");
std::cout << "\n sizeof(up) :" << sizeof (up); // 8 bytes
std::__uniq_ptr_impl<std::string, std::default_delete<std::string>> unique_imp;
std::cout << "\n sizeof(unique_imp) :" << sizeof (unique_imp); // 8 bytes
std::tuple<std::string*, std::default_delete<std::string>> tup;
std::cout << "\n sizeof(tuple<pointer, deleter()>) :" << sizeof (tup); // 8 bytes
std::cout << "\n";
return 0;
}
#pragma pack()
|
输出结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/**
* # g++ -g -Wall main.cpp -o a.out
* # ./a.out
sizeof std::shared_ptr<string> 16
sizeof(int) 4, sizeof(string) 8
sizeof(_Atomic_word) 4
size std::__shared_count(8) = 8
counter_ptr size :24
counter_in_place :20
counter_in_place_char :17
from_this (equals weak_ptr<T>):16
weak_ptr_tp :16
sizeof(up) :8
sizeof(unique_imp) :8
sizeof(tuple<pointer, deleter()>) :8
*/
|
last update: 2020-08-04 17:30:04 by sunquan