智能指针,从其本质上说,就是要控制对象的销毁时机。换句话讲就是何时调用对象的析构函数。从C++11开始引入三个智能指针(unique_ptr
,shared_ptr
,weak_ptr
),准确的说是四种(还有auto_ptr
),但从C++17开始auto_ptr
被移除了。所以就剩下上述三种了。所有的智能指针都包含在memory
头文件中。
首先,很久以前的C++是没有智能指针的,用户创建在堆上的内存,智能自己显示的释放,如果没有释放就会造成内存泄漏。这种特点导致C++的使用成本很高,为了降低成本,引入了智能指针unique_ptr
和shared_ptr
。
unique_ptr
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
| class Simple { private: int m_a; public: void Show() { printf("Hello Simple %d\n", m_a); } void showfunc() { printf("Hello Simple func \n"); } Simple(int n) { m_a = n; printf("Simple Construct\n"); } Simple(const Simple& p) { printf("Simple Copy Construct\n"); } ~Simple() { printf("Simple Destroy\n"); } };
void unique_test() { auto s_ptr = std::make_unique<Simple>(1); s_ptr->Show(); std::unique_ptr<Simple> s_copy_ptr = std::move(s_ptr); s_copy_ptr->Show(); s_ptr->showfunc(); }
|
unique_ptr
禁用拷贝构造
由于unique_ptr
禁用了拷贝构造函数unique_ptr(const unique_ptr&) = delete;
,所以一切试图触发拷贝构造函数的操作都会引发编译错误。
unique_ptr
所有权一旦变更就不能使用原指针访问对象资源
上述代码中有两处使用了原指针访问对象资源,第一处s_ptr->showfunc();
没有报错,可以正常打印;s_ptr->Show();
中使用到了成员变量m_a
所以会导致报错“this
空指针”。这是由于std::move
的赋值操作触发了unique_ptr
中的Move构造函数(unique_ptr(unique_ptr&&) = default;
),从而将s_ptr
中的成员清空。所以再次访问原对象指针,就会出错。
shared_ptr
shared_ptr
采用的是引用计数的机制来控制对象的销毁时机。如其名字所示,其对象是共享的。当计数器等于0是,调用对象的析构函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void shared_test_inner(std::shared_ptr<Simple> *steal_ptr) { auto s_ptr = std::make_shared<Simple>(1); s_ptr->Show(); std::shared_ptr<Simple> s_copy_ptr(s_ptr); s_copy_ptr->Show(); std::shared_ptr<Simple> s_ptr_2 = s_ptr; s_ptr_2->Show(); *steal_ptr = s_ptr; }
void shared_test() { shared_test_leak(); std::shared_ptr<Simple> s_ptr; shared_test_inner(&s_ptr); s_ptr->Show(); }
|
尽可能使用make_shared
创建shared_ptr
,如果使用std::shared_ptr<Simple> s_ptr(new Simple(1))
创建shared_ptr
,需要分配两次内存,一次是new Simple(1)
;另一次是shared_ptr
的引用计数。make_shared
只分配一次。
现在创建一个SimpleBack
类,然后再让Simple
和SimpleBack
循环引用。
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
| class SimpleBack;
class Simple { private: int m_a; public: void Show() { printf("Hello Simple %d\n", m_a); } void showfunc() { printf("Hello Simple func \n"); } Simple(int n) { m_a = n; printf("Simple Construct\n"); } Simple(const Simple& p) { printf("Simple Copy Construct\n"); } ~Simple() { printf("Simple Destroy\n"); } std::shared_ptr<SimpleBack> m_sb; };
class SimpleBack { public: std::shared_ptr<Simple> m_s; ~SimpleBack() { printf("SimpleBack Destroy\n"); } };
void shared_test_leak() { auto s = std::make_shared<Simple>(2); auto sb = std::make_shared<SimpleBack>();
s->m_sb = sb; sb->m_s = s; }
|
通过析构函数函数的打印语句可以看出s
和sb
并没有被析构,这说明s
和sb
泄漏了。
为了解决shared_ptr
在循环依赖中内存泄漏的问题,推出了weak_ptr
。
weak_ptr
weak_ptr
不会增加引用计数,不能直接操作对象的内存(需要先调用lock
接口),需要和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
| class SimpleBack;
class Simple { private: int m_a; public: void Show() { printf("Hello Simple %d\n", m_a); } void showfunc() { printf("Hello Simple func \n"); } Simple(int n) { m_a = n; printf("Simple Construct\n"); } Simple(const Simple& p) { printf("Simple Copy Construct\n"); } ~Simple() { printf("Simple Destroy\n"); } std::shared_ptr<SimpleBack> m_sb; };
class SimpleBack { public: std::weak_ptr<Simple> m_s; ~SimpleBack() { printf("SimpleBack Destroy\n"); } };
void shared_test_leak() { auto s = std::make_shared<Simple>(2); auto sb = std::make_shared<SimpleBack>();
s->m_sb = sb; sb->m_s = s; }
|
通过析构函数的打印语句可以看出,s
和sb
的析构函数在shared_test_lead()
调用结束后被调用。
那么,weak_ptr
的使用是不是也像shared_ptr
一样呢?不是的。weak_ptr
需要与shared_ptr
配合使用,看一个例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void weak_test() { std::weak_ptr<Simple> w; { auto s = std::make_shared<Simple>(3); w = s; auto s2 = w.lock(); if(s2 != nullptr) { s2->Show(); } } if(w.expired()) { printf("object 's' is destroied. \n"); } }
|
lock
若对象已被析构,则返回一个空的shared_ptr
;否则返回实际的shared_ptr
。
expired
若对象已被析构,则返回true
;否则返回false
参考&鸣谢