好长一段时间没明白共享指针的理解和使用,今天认认真真查了一些资料,搞懂了很多。在这里整理了一下两个链接的内容。
主要参考链接:
https://blog.csdn.net/u011866460/article/details/42027457
https://blog.csdn.net/shaosunrise/article/details/85228823
共享指针 (shared_ptr) 是现在的 Boost 库中提供的,并且应该是将来 C++1x 的标准库中提供的一个模板类。在此之前,ISO/IEC 14882:2003 标准库
shared_ptr 主要的功能是,管理动态创建的对象的销毁。它的基本原理就是记录对象被引用的次数,当引用次数为 0 的时候,也就是最后一个指向某对象的共享指针析构的时候,共享指针的析构函数就把指向的内存区域释放掉。
与普通指针相比,共享指针对象重载了 * 、-> 和==运算符, 所以你可以像通常的指针一样使用它。没有重载+、-、++、--、[ ]等运算法。
它遵循共享所有权的概念,即不同的 shared_ptr 对象可以与相同的指针相关联,并在内部使用引用计数机制来实现这一点。
每个 shared_ptr 对象在内部指向两个内存位置:
1、指向对象的指针。
2、用于控制引用计数数据的指针。(这里的引用计数为指向该内存块的共享指针个数)
共享所有权如何在参考计数的帮助下工作:
1、当新的 shared_ptr 对象与原共享指针关联时,则在其构造函数中,将与此指针关联的引用计数增加1。
2、当任何 与之相关的shared_ptr 对象被析构时,例如局部函数调用结束,则在其析构函数中,它将关联指针的引用计数减1。如果引用计数变为0,则表示没有其他 shared_ptr 对象与此内存关联,在这种情况下,它使用delete函数删除堆中对应内存。
3.1. 对于 Visual C++ 2010
目前,Visual C++ 2010 的
#include
3.2. 对于其它支持 ISO/IEC 14882:2003 标准的编译器
而 GNU G++ 的标准库中还没有支持(毕竟是将来的标准),如果在 G++ 中想使用 shared_ptr, 还是得用到 Boost 库,就是说,在 G++ 里,你得这样写:
#include
保险起见,你应该仅从以下几种途径构造一个共享指针(以下例子中若没特殊说明,T 就代表共享指针所指向的对象的类型):
4.1. 使用空参数构造函数构造
也就是说,你可以直接定义一个 shared_ptr 而不指定构造函数的内容:
1 |
std::shared_ptr |
这样做的话,ptr 的意义就相当于一个 NULL 指针。当你试图在一个空指针上做类似于 *ptr 或者 ptr->xx 之类的东西的时候,应该会收到异常的。
4.2. 直接从 new 操作符的返回值构造
用代码来表示,就是可以这样使用:
1 |
std::shared_ptr |
上面这行代码在堆里创建了两块内存:1.存储T。2.用于引用计数的内存,管理附加此内存的shared_ptr对象的计数,最初计数将为1.
因为带有参数的shared_ptr的复制构造函数(4.3)是explicit类型的,所以不能像这样 std::shared_ptr
但是链接里说构造新的shared_ptr对象的最佳方法是使用std::make_shared类模板。因为 1)它一次性为T对象和用于引用计数的数据都分配了内存,而new操作符只是为T分配了内存(没理解,求高人解答)。2)它可以避免一些由堆指针或者new分配指针导致的错误。
1 |
std::shared_ptr |
4.3. 使用复制构造函数(或等号重载),从其它 shared_ptr 的对象构造
一种显然的情况是这样的:
1 |
std::shared_ptr |
还有,shared_ptr 可以当作函数的参数传递,或者当作函数的返回值返回,这个时候其实也相当于使用复制构造函数。
过程如下:作函数实参时,将指针执行复制构造函数传入函数体内,因此该内存块的引用计数+1;当作为函数返回值时,复制构造函数将内存地址传递给新指针,引用计数+1,然后,局部指针执行析构,引用计数-1。
4.4. 从 shared_ptr 提供的类型转换 (cast) 函数的返回值构造
shared_ptr 也可以类型转换,有关类型转换的详情参见下面的 6. 此处假设 B 是 A 的子类,那么,根据C++继承与派生中的知识,B 的指针当然是可以转换成 A 的指针的。在共享指针里,应该这样做:
1 |
std::shared_ptr ptrb(new B()); |
shared_ptr 也可以直接赋值,但是必须是赋给相同类型的 shared_ptr 对象,而不能是普通的 C 指针或 new 运算符的返回值。当共享指针 a 被赋值成 b 的时候,如果 a 原来是 NULL, 那么直接让 a 等于 b 并且让它们指向的东西的引用计数加 1; 如果 a 原来也指向某些东西的时候,如果 a 被赋值成 b, 那么原来 a 指向的东西的引用计数被减 1, 而新指向的对象的引用计数加 1. 就是说以下代码是允许的:
1 |
std::shared_ptr |
shared_ptr 的对象在构造之后,可以被赋予空值,此时使用的应该是 reset() 函数或者nullptr,如:
1 3 |
std::shared_ptr a=nullptr; //同上。推荐多用 |
当然理论上也可以这样写:
1 |
std::shared_ptr |
shared_ptr 有两种类型转换的函数,一个是 static_pointer_cast, 一个是 dynamic_pointer_cast. 其实用法真的和 C++ 提供的 static_cast 和 dynamic_cast 很像,再结合 4.4. 的代码和以下类似的代码,几乎没什么好讲的:
1 |
std::shared_ptr ptra; |
很简单,可以这样用:
1 |
std::shared_ptr |
建议少用get()函数,因为如果在shared_ptr析构之前手动调用了delete函数,同样会导致类似的错误。
8.1 共享指针的重置
比如,“我想让一个已经构造好的共享指针,丢弃掉原来所指的对象(或者让其引用计数减 1),然后指向一个新的 new 出来的对象,该怎么办?”参考如下代码:
1 |
std::shared_ptr |
8.2 NULL检测
当我们创建 shared_ptr 对象而不分配任何值时,它就是空的,即地址为000000000;普通指针不分配空间的时候相当于一个野指针,指向垃圾空间,且无法判断指向的是否是有用数据。
shared_ptr 检测空值方法
1 3 4 5 6 7 8 9 |
std::shared_ptr if(ptr3==0) std::cout<<"ptr3 is empty" << std::endl; |
一定要注意,本节所述所有方法,都是错误的!
9.1. 在“中途”使用传统 C 指针构造共享指针
所谓在中途,指的就是不从 new 的返回值直接构造共享指针,比如从 this 指针构造自己的共享指针等。
9.2. 从一个对象的传统 C 指针,构造出两个或以上的共享指针
其实这种和 9.1. 也是类似的,或者说,这种情况是 9.1. 的一种具体情况,比如,下面的代码是错误的:
1 |
T *a = new T(); |
这样的话,ptr1 和 ptr2 的引用计数是单独算的,它们任意一个对象在析构的时候,都会销毁 a 所指的对象,a就为悬空指针,所以,这个对象会被“销毁两次”。因此报错。(make_shared类模板可以避免)
9.3 不要用栈中的指针构造shared_ptr对象
shared_ptr默认的构造函数中使用的是delete来删除关联的内存,所以构造的时候也必须使用new出来的堆空间的指针。如果是栈中的指针,当shared_ptr对象超出作用域调用析构函数delete内存时,会报错。(make_shared类模板可以避免)
有关运行效率的问题在这里就不讨论了。其它方面,shared_ptr 的构造要求比较高,如果对象在创建的时候没有使用共享指针存储的话,之后也不能用共享指针管理这个对象了。如果有引用循环 (reference cycle), 也就是对象 a 有指向对象 b 的共享指针,对象 b 也有指向对象 a 的共享指针,那么它们都不会被析构。当然的,shared_ptr 也没有办法和 Garbage Collecting 比较,毕竟如果运行库能够干预,还是有算法可以检查到引用循环的。(例如求强连通分量的算法。)
尤其,在类的成员函数的编写的时候,有时我们经常希望得到“自己”的共享指针,但是这往往是无法得到的。此时也不能够从 this 指针构造自己的共享指针(参见 9.1.),所以有时很憋闷。
实际上上面这么多注意事项,中心思想就是一个:让 shared_ptr 正确地记录对象被引用次数。如果能悟出一点 shared_ptr 的工作原理,基本上不会弄出太危险的事情来。