本文最后更新于:2022年11月22日 下午
前言 回望最初版本的shared_ptr
,其实存在着很多问题,比如没有考虑 Constructor Failure 以及未使用 copy-and-swap idiom 来解决 code duplicate 等.
在这里我打算保留最初的版本,并添加说明对此进行更正
Old Version 成员变量ptr
用于保存共享的指针,而 refCount
则是计数器,在这里注意 refCount
是指针,这样的话可以做到多个共享指针共享同一份计数值(这一点非常重要)
关于拷贝构造函数,注意在自增refCount
前,需要检验被拷贝对象是否为空:
1 2 3 4 5 6 7 8 my_shared_ptr (const my_shared_ptr & obj){ this ->ptr = obj.ptr; this ->refCount = obj.refCount; if (obj.ptr != nullptr ){ (*this ->refCount) ++; } }
而对于拷贝赋值函数,首先要注意返回值是引用(拷贝赋值函数的惯例),其次相比于拷贝构造函数,调用了__cleanup__
来清理原先共享指针维护的指针对象资源:
1 2 3 4 5 6 7 8 9 10 11 my_shared_ptr& operator =(const my_shared_ptr &obj){ __cleanup__(); this ->ptr = obj.ptr; this ->refCount = obj.refCount; if (obj.ptr != nullptr ){ (*this ->refCount) ++; } }
1 2 3 4 5 6 7 8 9 10 void __cleanup__(){ (*refCount)--; if (*refCount == 0 ){ if (ptr != nullptr ){ delete ptr; } delete refCount; } }
__cleanup__
中会将计数减一,一旦计数值变为0,便会使用delete
来回收资源,不过同样要注意的是,在delete ptr
前会去检验ptr
是否为空。如果不这样做,在以下场景中就会去delete
一个空指针,这是一件很危险的事情
1 2 3 my_shared_ptr<SomeClass> msp;my_shared_ptr<SomeClass> another_msp (new SomeClass) msp = another_msp
下面再使得my_shared_ptr
支持move
语义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 my_shared_ptr (my_shared_ptr && dyingObj){ this ->ptr = dyingObj.ptr; this ->refCount = dyingObj.refCount; dyingObj.ptr = dyingObj.refCount = nullptr ; } my_shared_ptr& operator =(my_shared_ptr &&dyingObj){ __cleanup__(); this ->ptr = dyingObj.ptr; this ->refCount = dyingObj.refCount; dyingObj.ptr = dyingObj.refCount = nullptr ; }
同时我们还需要实现一些重载函数,使得共享指针使用起来跟其维护的指针对象没有什么差别。
1 2 3 4 5 6 7 8 9 T* operator ->() const { return this ->ptr; } T& operator *() const { return this ->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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 using namespace std;typedef unsigned int uint;template <class T >class my_shared_ptr {private : T *ptr = nullptr ; uint *refCount = nullptr ; void __cleanup__(){ (*refCount)--; if (*refCount == 0 ){ if (ptr != nullptr ){ delete ptr; } delete refCount; } }public : my_shared_ptr ():ptr (nullptr ), refCount (new uint (0 )){ } my_shared_ptr (T* ptr):ptr (ptr), refCount (new uint (1 )){ } my_shared_ptr (const my_shared_ptr & obj){ this ->ptr = obj.ptr; this ->refCount = obj.refCount; if (obj.ptr != nullptr ){ (*this ->refCount) ++; } } my_shared_ptr& operator =(const my_shared_ptr &obj){ __cleanup__(); this ->ptr = obj.ptr; this ->refCount = obj.refCount; if (obj.ptr != nullptr ){ (*this ->refCount) ++; } } my_shared_ptr (my_shared_ptr && dyingObj){ this ->ptr = dyingObj.ptr; this ->refCount = dyingObj.refCount; dyingObj.ptr = dyingObj.refCount = nullptr ; } my_shared_ptr& operator =(my_shared_ptr &&dyingObj){ __cleanup__(); this ->ptr = dyingObj.ptr; this ->refCount = dyingObj.refCount; dyingObj.ptr = dyingObj.refCount = nullptr ; } T* operator ->() const { return this ->ptr; } T& operator *() const { return this ->ptr; } uint get_count () const { return *refCount; } ~my_shared_ptr (){ __cleanup__(); } };
New Version 关于下面提到的一些概念可以去参考我的有关 UniquePtr 的博客文章,实现上可以参考一下
cleanup 首先是关于__cleanup__
,其中提到delete
之前会去检查ptr
是否为空,其实这边是多余的,因为delete nullptr
并不是一个危险的行为
Constructor Failure 1 2 3 4 5 6 7 8 9 my_shared_ptr ():ptr (nullptr ), refCount (new uint (0 )){ }my_shared_ptr (T* ptr):ptr (ptr), refCount (new uint (1 )){ }
在构造函数中使用了new
来分配内存,而new
如果分配内存失败会产生std::bad_alloc
异常,如果程序在构造函数外抛出异常,那么析构函数不会被调用,那么就有可能会造成内存泄露。我们需要更正这个问题:
1 2 3 4 5 6 7 8 9 explicit my_shared_ptr (T* ptr) : ptr(ptr), refCount(new (std::nothrow) int(1 )){ if (refCount == nullptr ){ delete data; throw std::bad_alloc (); } }
在这面使用了new
的 nothrow 版本,内存分配失败后其并不会抛出std::bad_alloc
异常,而是返回nullptr
,在构造函数中我们需要检验到这种情况,进行资源的回收,同时抛出异常(在 constructor 抛出异常要区别于在 constructor 外抛出异常)
同时使用explicit
避免隐式转换,而由于explicit
的添加,在这里为了处理nullptr
并简化其使用,可以参考之前实现的MyUniquePtr
,加入以nullptr_t
类型为参数的构造函数和拷贝赋值函数。
而有了对nullptr
的专门处理,可以删去一些计数增加前检查指针是否为空的逻辑。
copy-and-swap idiom copy-and-swap idiom 可以在做到在减少重复代码的同时,又做到 strong exception guarantee .
首先需要实现自己的swap
函数
1 2 3 4 void swap (my_shared_ptr& other) noexcept { std::swap (ptr, other.ptr); std::swap (count, other.count); }
接着将copy semantics 和 move semantics 使用swap
进行改写
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 my_shared_ptr (const my_shared_ptr & obj):ptr (obj.ptr), refCount (obj.refCount){ (*this ->refCount) ++; } my_shared_ptr& operator =(my_shared_ptr obj){ obj.swap (*this ); return *this ; } my_shared_ptr& operator =(T* newData){ myshared_ptr tmp (newData); tmp.swap (*this ); return *this ; }my_shared_ptr (myshared_ptr && dyingObj){ dyingObj.swap (*) } my_shared_ptr& operator =(my_shared_ptr &&dyingObj){ dyingObj.swap (*this ); return *this ; }
这边一个值得注意的细节是 copy semantics 的拷贝赋值函数的参数是 pass by value ,如要非要使用引用可以参考下面的代码
1 2 3 4 5 my_shared_ptr& operator =(my_shared_ptr &obj){ my_shared_ptr tmp (obj); tmp.swap (*this ); return *this ; }
参考