智能指针,从其本质上说,就是要控制对象的销毁时机。换句话讲就是何时调用对象的析构函数。从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
参考&鸣谢