智能指针(三):weak_ptr浅析
前言
weak_ptr
这个指针天生一副“小弟”的模样,也是在C++11的时候引入的标准库,它的出现完全是为了弥补它老大shared_ptr
天生有缺陷的问题,其实相比于上一代的智能指针auto_ptr
来说,新进老大shared_ptr
可以说近乎完美,但是通过引用计数实现的它,虽然解决了指针独占的问题,但也引来了引用成环的问题,这种问题靠它自己是没办法解决的,所以在C++11的时候将shared_ptr
和weak_ptr
一起引入了标准库,用来解决循环引用的问题。
weak_ptr
本身也是一个模板类,但是不能直接用它来定义一个智能指针的对象,只能配合shared_ptr
来使用,可以将shared_ptr
的对象赋值给weak_ptr
,并且这样并不会改变引用计数的值。查看weak_ptr
的代码时发现,它主要有lock
、swap
、reset
、expired
、operator=
几个函数,与shared_ptr
相比多了lock
、expired
函数,但是却少了get
、use_count
等函数,甚至连operator*
和 operator->
都没有,可用的函数数量少的可怜,下面通过一些例子来了解一下weak_ptr
的具体用法。
使用环境
- VS2015 + Windows7(应该是C++11标准)
- 头文件
#include <memory>
- 命名空间
using namespace std;
测试过程
-
weak_ptr
解决shared_ptr
循环引用的问题
定义两个类,每个类中又包含一个指向对方类型的智能指针作为成员变量,然后创建对象,设置完成后查看引用计数后退出,看一下测试结果:class CB; class CA { public: CA() { cout << "CA() called! " << endl; } ~CA() { cout << "~CA() called! " << endl; } void set_ptr(shared_ptr<CB>& ptr) { m_ptr_b = ptr; } void b_use_count() { cout << "b use count : " << m_ptr_b.use_count() << endl; } void show() { cout << "this is class CA!" << endl; } private: shared_ptr<CB> m_ptr_b; }; class CB { public: CB() { cout << "CB() called! " << endl; } ~CB() { cout << "~CB() called! " << endl; } void set_ptr(shared_ptr<CA>& ptr) { m_ptr_a = ptr; } void a_use_count() { cout << "a use count : " << m_ptr_a.use_count() << endl; } void show() { cout << "this is class CB!" << endl; } private: shared_ptr<CA> m_ptr_a; }; void test_refer_to_each_other() { shared_ptr<CA> ptr_a(new CA()); shared_ptr<CB> ptr_b(new CB()); cout << "a use count : " << ptr_a.use_count() << endl; cout << "b use count : " << ptr_b.use_count() << endl; ptr_a->set_ptr(ptr_b); ptr_b->set_ptr(ptr_a); cout << "a use count : " << ptr_a.use_count() << endl; cout << "b use count : " << ptr_b.use_count() << endl; }
测试结果如下:
CA() called! CB() called! a use count : 1 b use count : 1 a use count : 2 b use count : 2
通过结果可以看到,最后
CA
和CB
的对象并没有被析构,其中的引用效果如下图所示,起初定义完ptr_a
和ptr_b
时,只有①③两条引用,然后调用函数set_ptr
后又增加了②④两条引用,当test_refer_to_each_other
这个函数返回时,对象ptr_a
和ptr_b
被销毁,也就是①③两条引用会被断开,但是②④两条引用依然存在,每一个的引用计数都不为0,结果就导致其指向的内部对象无法析构,造成内存泄漏。
解决这种状况的办法就是将两个类中的一个成员变量改为weak_ptr
对象,因为weak_ptr
不会增加引用计数,使得引用形不成环,最后就可以正常的释放内部的对象,不会造成内存泄漏,比如将CB
中的成员变量改为weak_ptr
对象,代码如下:class CB { public: CB() { cout << "CB() called! " << endl; } ~CB() { cout << "~CB() called! " << endl; } void set_ptr(shared_ptr<CA>& ptr) { m_ptr_a = ptr; } void a_use_count() { cout << "a use count : " << m_ptr_a.use_count() << endl; } void show() { cout << "this is class CB!" << endl; } private: weak_ptr<CA> m_ptr_a; };
测试结果如下:
CA() called! CB() called! a use count : 1 b use count : 1 a use count : 1 b use count : 2 ~CA() called! ~CB() called!
通过这次结果可以看到,
CA
和CB
的对象都被正常的析构了,引用关系如下图所示,流程与上一例子相似,但是不同的是④这条引用是通过weak_ptr
建立的,并不会增加引用计数,也就是说CA
的对象只有一个引用计数,而CB
的对象只有2个引用计数,当test_refer_to_each_other
这个函数返回时,对象ptr_a
和ptr_b
被销毁,也就是①③两条引用会被断开,此时CA
对象的引用计数会减为0,对象被销毁,其内部的m_ptr_b
成员变量也会被析构,导致CB
对象的引用计数会减为0,对象被销毁,进而解决了引用成环的问题。
-
测试
weak_ptr
对引用计数的影响
其实weak_ptr
本身设计的很简单,就是为了辅助shared_ptr
的,它本身不能直接定义指向原始指针的对象,只能指向shared_ptr
对象,同时也不能将weak_ptr对象直接赋值给shared_ptr
类型的变量,最重要的一点是赋值给它不会增加引用计数:void test1() { // 编译错误 // error C2665: “std::weak_ptr<CA>::weak_ptr”: 3 个重载中没有一个可以转换所有参数类型 // weak_ptr<CA> ptr_1(new CA()); shared_ptr<CA> ptr_1(new CA()); cout << "ptr_1 use count : " << ptr_1.use_count() << endl; // 输出:ptr_1 use count : 1 shared_ptr<CA> ptr_2 = ptr_1; cout << "ptr_1 use count : " << ptr_1.use_count() << endl; // 输出:ptr_1 use count : 2 cout << "ptr_2 use count : " << ptr_2.use_count() << endl; // 输出:ptr_1 use count : 2 weak_ptr<CA> wk_ptr = ptr_1; cout << "ptr_1 use count : " << ptr_1.use_count() << endl; // 输出:ptr_1 use count : 2 cout << "ptr_2 use count : " << ptr_2.use_count() << endl; // 输出:ptr_1 use count : 2 // 编译错误 // error C2440 : “初始化”: 无法从“std::weak_ptr<CA>”转换为“std::shared_ptr<CA>” // shared_ptr<CA> ptr_3 = wk_ptr; }
-
测试
weak_ptr
常用函数的用法weak_ptr
中只有函数lock
和expired
两个函数比较重要,因为它本身不会增加引用计数,所以它指向的对象可能在它用的时候已经被释放了,所以在用之前需要使用expired
函数来检测是否过期,然后使用lock
函数来获取其对应的shared_ptr
对象,然后进行后续操作:void test2() { shared_ptr<CA> ptr_a(new CA()); // 输出:CA() called! shared_ptr<CB> ptr_b(new CB()); // 输出:CB() called! cout << "ptr_a use count : " << ptr_a.use_count() << endl; // 输出:ptr_a use count : 1 cout << "ptr_b use count : " << ptr_b.use_count() << endl; // 输出:ptr_b use count : 1 weak_ptr<CA> wk_ptr_a = ptr_a; weak_ptr<CB> wk_ptr_b = ptr_b; if (!wk_ptr_a.expired()) { wk_ptr_a.lock()->show(); // 输出:this is class CA! } if (!wk_ptr_b.expired()) { wk_ptr_b.lock()->show(); // 输出:this is class CB! } // 编译错误 // 编译必须作用于相同的指针类型之间 // wk_ptr_a.swap(wk_ptr_b); // 调用交换函数 wk_ptr_b.reset(); // 将wk_ptr_b的指向清空 if (wk_ptr_b.expired()) { cout << "wk_ptr_b is invalid" << endl; // 输出:wk_ptr_b is invalid 说明改指针已经无效 } wk_ptr_b = ptr_b; if (!wk_ptr_b.expired()) { wk_ptr_b.lock()->show(); // 输出:this is class CB! 调用赋值操作后,wk_ptr_b恢复有效 } // 编译错误 // 编译必须作用于相同的指针类型之间 // wk_ptr_b = wk_ptr_a; // 最后输出的引用计数还是1,说明之前使用weak_ptr类型赋值,不会影响引用计数 cout << "ptr_a use count : " << ptr_a.use_count() << endl; // 输出:ptr_a use count : 1 cout << "ptr_b use count : " << ptr_b.use_count() << endl; // 输出:ptr_b use count : 1 }
现象分析
引用计数的出现,解决了对象独占的问题,但是也带来了循环引用的困扰,使用weak_ptr
可以打破这种循环,当你理不清引用关系的时候,不妨采用文中画图的方式来理一理头绪,或许就会有眼前一亮的感觉。
总结
-
weak_ptr
虽然是一个模板类,但是不能用来直接定义指向原始指针的对象。 -
weak_ptr
接受shared_ptr
类型的变量赋值,但是反过来是行不通的,需要使用lock
函数。 -
weak_ptr
设计之初就是为了服务于shared_ptr
的,所以不增加引用计数就是它的核心功能。 - 由于不知道什么之后
weak_ptr
所指向的对象就会被析构掉,所以使用之前请先使用expired
函数检测一下。
测试源码
上一篇: HTTPS抓包