shared_ptr原理分析及实现

来源:互联网 发布:人工智能机器人的应用 编辑:程序博客网 时间:2024/05/23 22:02

原理分析

内存管理历来是C++编程的一项需要小心费力气的活,因为C++本身不带GC机制,所有的内存管理都需要我们手动实现,从malloc / freenew / delete,再到allocator的出现,无非都是为了更合理简单的避免内存泄露。

指针本身是一个用法十分灵活并且功能强大的工具,然而它对内存的直接掌控也使得它不得不常常背起内存泄露的黑锅,因为忘记删除指针或者将一个指针删除两次的错误往往十分隐蔽,难以侦查。为了解决这个问题,boost中的智能指针终于还是在C++11中被纳入了标准库。

来看个例子:


std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
… // do something
delete stringptr1; // 删除一个对象
delete[] stringPtr2; // 删除一个由对象组成的数组


看上去一切都很好:new出来的两个对象在完成任务后都被delete掉了。然而考虑在do something的时候发生点什么,比如抛出异常,程序跳转,或者函数返回了,那么会发生什么呢?显然,这两个delete并不会得到执行,也就是说,内存泄露了。假如把上面的指针定义成智能指针,那么一切问题都将烟消云散,如下。


shared_ptr stringPtr1(new std::string);
shared_ptr stringPtr2 = new std::string[100];
… // do something


那么智能指针是如何帮助我们管理内存的呢?事实上,智能指针就是一个作用是资源管理的类,它是你在堆栈上声明的类模板,并可通过使用指向某个堆分配的对象的原始指针进行初始化(RAII)。

在初始化智能指针后,它将拥有原始指针,这意味着智能指针负责删除原始指针指定的内存, 智能指针析构函数包括要删除的调用,当智能指针超出范围时将调用其析构函数,析构函数会自动释放资源。

现代C++中的三种智能指针简单介绍如下(引自MSDN智能指针(现代 C++)):

  • unique_ptr
    只允许基础指针的一个所有者。 除非你确信需要 shared_ptr,否则请将该指针用作 POCO 的默认选项。 可以移到新所有者,但不会复制或共享。 替换已弃用的 auto_ptr。 与 boost::scoped_ptr 比较。 unique_ptr 小巧高效,大小等同于一个指针且支持 rvalue 引用,从而可实现快速插入和对 STL 集合的检索。
  • shared_ptr
    采用引用计数的智能指针。 如果你想要将一个原始指针分配给多个所有者(例如,从容器返回了指针副本又想保留原始指针时),请使用该指针。 直至所有 shared_ptr 所有者超出了范围或放弃所有权,才会删除原始指针。 大小为两个指针:一个用于对象,另一个用于包含引用计数的共享控制块。
  • weak_ptr
    结合 shared_ptr 使用的特例智能指针。 weak_ptr 提供对一个或多个 shared_ptr 实例拥有的对象的访问,但不参与引用计数。 如果你想要观察某个对象但不需要其保持活动状态,请使用该实例。 在某些情况下,用于断开 shared_ptr 实例间的循环引用。

shared_ptr 解析

shared_ptr的原理是引用计数法reference counting,每多一个智能指针指向同一个对象时,引用+1,而析构则相反,如果计数为零,则保存的指针被删除。

正常的指针的功能智能指针都有,如(以下用mySmartPtr代替shared_ptr,这是自己实现的智能指针名字)


int *pi = new int(42);     mySmartPtr<int> *hpa(new mySmartPtr<int>(pi));    // 构造函数  mySmartPtr<int> *hpb = new mySmartPtr<int>(*hpa); // 拷贝构造函数  mySmartPtr<int>  hpd = *hpa;                      // 拷贝构造函数  

同时,无论复制shared_ptr多少次,只要不出现循环引用,总是可以析构掉的,如


vector<mySmartPtr<Base> > obj;        vector<mySmartPtr<Base> > obj2;  obj2.push_back(obj[0]);

除了析构对象的工作,shared_ptr同时也需要帮助我们解决类型转换的问题。一个是多态性的体现,即以基类指针指向派生类对象:


vector<mySmartPtr<Base> > obj;        // 父类指针vectorobj.push_back(new Derived1);          // 指向子类1obj.push_back(new Derived2);          // 指向子类2

另一个是dynamic_cast转型,如下,使用dynamic_cast<Derived2*>(static_cast<Base*>(new Derived1))会得到空指针:


mySmartPtr<Derived1> d1 = new Derived1;mySmartPtr<Base> b = d1;              // b指向的是Derived1mySmartPtr<Derived2> d2 = b.Cast<Derived2>();  // 转型失败返回空指针

具体来说,一个智能指针的实现要完成以下功能:

  1. 没有参数构造的时候,初始化为空,即对象和引用计数的两个指针都为0

  2. 使用指针为参数构造时,拥有此指针,在没有智能指针指向它时进行析构

  3. 智能指针复制时,两个智能指针共同拥有内部指针,引用计数同时+1

  4. 智能指针可以使用智能指针或普通指针重新赋值。重载=操作符,对于智能指针赋值,需要考虑是否自赋值,以避免将自身析构了后再重新赋值,而普通指针赋值给智能指针,则不需要考虑自赋值,因为两者本身是两个类型

  5. 获得底层指针的访问,定义getPtrPointer()getPtrCounter()来分别返回底层指针和引用计数,定义operator bool()来处理智能指针隐式转换为bool的情况

  6. 重载->×操作符 ,来实现与普通指针相同的指针访问

  7. 需要支持隐式指针类型转换,static_cast不支持而dynamic_cast支持的转换则使用Cast<T2>()成员函数来解决。考虑定义友元类,以防止指向派生类的智能指针有权限访问基类的内部对象;当转型不成功时,返回为空

  8. 如果一个裸指针直接用来创建两个智能指针的话,期望的情况是当两个智能指针析构掉的时候,该指针会被delete两次从而崩溃(这是shared_ptr的特点)

  9. 不处理循环引用(也是shared_ptr的特点),可以通过与weak_ptr协作来打破循环

  10. 暂时不实现deleter机制,即只能传递给mySmartPtr一个参数

实际上,第8/第9两点往往是使用智能指针出现问题最多的地方,平时使用过程中要多加留意。

一个简单的实现

测试用例


/**********************************************    > File Name: testmySmartPtr.cpp    > Author: ye_create    > Mail:     > Created Time: 2015年05月28日 星期四 13时03分42秒 ***********************************************/#include <iostream>#include <vector>#include "mySmartPtr.h"using namespace std;class Base             // 定义一个基类{public:    virtual ~Base(){        cout << "class Base" << endl;    }};class Derived1 : public Base   // 派生类1{public:    ~Derived1(){        cout << "class Derived1" << endl;    }};    class Derived2 : public Base    // 派生类2{public:    ~Derived2(){        cout << "class Derived2" << endl;    }};int main(){       int *pi = new int(42);         mySmartPtr<int> *hpa(new mySmartPtr<int>(pi));        // 构造函数      mySmartPtr<int> *hpb = new mySmartPtr<int>(*hpa);     // 拷贝构造函数      mySmartPtr<int>  hpd = *hpa;                          // 拷贝构造函数      // 观察引用计数的变化    cout << hpa->getPtrCounter() << " " << hpb->getPtrCounter() << " "<< hpd.getPtrCounter() << endl;      delete hpa;      cout << hpd.getPtrCounter() << endl;      delete hpb;      cout << hpd.getPtrCounter() << endl;    // 观察派生类向基类的隐式转换    vector<mySmartPtr<Base> > obj;        // 父类指针vector    obj.push_back(new Derived1);          // 存入子类    obj.push_back(new Derived2);    vector<mySmartPtr<Base> > obj2;      obj2.push_back(obj[0]);    if (obj2[0])        cout << "Cast Derived1 to Base successd" << endl;    else        cout << "Cast Derived1 to Base failed" << endl;    // 观察不同类型的显式转换    mySmartPtr<Derived1> d1 = new Derived1;    mySmartPtr<Base> b = d1;                  mySmartPtr<Derived2> d2 = b.Cast<Derived2>();      // d2是空,因为b指向的是Derived1而不是Derived2    if (d2)        cout << "Cast Derived1 to Derived2 successd" << endl;    else        cout << "Cast Derived1 to Derived2 failed" << endl;    return 0;}
  • 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

mySmartPtr实现


/*************************************************************************    > File Name: mySmartPtr.cpp    > Author: ye_create    > Mail:     > Created Time: 2015年05月28日 星期四 13时35分00秒 ************************************************************************/template <typename T>class mySmartPtr{public:    // 构造函数 默认为空    mySmartPtr(): pointer(0), counter(0)    {    }     // 形参为指针的构造函数    mySmartPtr(T* p): pointer(0), counter(0){        *this = p;    }    // 复制构造函数    mySmartPtr(const mySmartPtr<T> &p):     pointer(p.pointer), counter(p.counter){        Increase();    }    ~mySmartPtr(){        Decrease();    }    // 指针的赋值操作符,类型不同,不是自赋值    mySmartPtr operator=(T* p){        Decrease();        if (p){            pointer = p;            counter = new int(1);        }        else {            pointer = 0;            counter = 0;        }        return *this;    }    // 智能指针赋值操作符    mySmartPtr operator=(mySmartPtr<T> &p){        // 处理自赋值        if (this != &p){            Decrease();            pointer = p.pointer;            counter = p.counter;            Increase();        }        return *this;    }    operator bool() const{        return counter != 0;    }    // ×操作符重载    T* operator*() const{        return this;    }    // ->操作符重载    T* operator->() const{        return pointer;    }    // 返回底层指针    int getPtrPointer() const{        return *pointer;    }    // 返回引用计数    int getPtrCounter() const{        return *counter;    }    // 处理父类子类的情况, ptr<derived>不能访问 ptr<based>的内部对象    template<typename C> friend class mySmartPtr;    template<typename C>     mySmartPtr(const mySmartPtr<C> &p):         pointer(p.pointer), counter(p.counter){            Increase();        }    template<typename C>    mySmartPtr<T> & operator=(const mySmartPtr<C> &p){        Decrease();        pointer = p.pointer;        counter = p.counter;        Increase();        return *this;    }    // 处理内部使用 dynamic_cast做判断的转换,失败则空指针    template<typename C>    mySmartPtr<C> Cast() const{        C* converted = dynamic_cast<C*>(pointer);        mySmartPtr<C> result;        if (converted){            result.pointer = converted;            result.counter = counter;            result.Increase();        }        return result;    }private:    T*      pointer;    int*    counter;    void Increase(){        if (counter)             ++*counter;    }    void Decrease(){        if (counter && --*counter == 0){            delete pointer;            delete counter;            counter = 0;            pointer = 0;        }    }};
  • 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
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131

条款1:不要把一个原生指针给多个shared_ptr管理
int* ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); //logicerror
ptr对象被删除了2次
这种问题比喻成“二龙治水”,在原生指针中也同样可能发生。

条款2:不要把this指针给shared_ptr
class Test{
public:
    void Do(){ m_sp = shared_ptr<Test>(this); }
private:
   shared_ptr<Test> m_member_sp;
};

Test* t = new Test;
shared_ptr<Test>local_sp(t);
p->Do();

发生什么事呢,t对象被删除了2次!
t对象给了local_sp管理,然后在m_sp = shared_ptr<Test>(this)这句里又请了一尊神来管理t。
这就发生了条款1里“二龙治水”错误。

条款3:shared_ptr作为被保护的对象的成员时,小心因循环引用造成无法释放资源。

对象需要相互协作,对象A需要知道对象B的地址,这样才能给对象B发消息(或调用其方法)。
设计模式中有大量例子,一个对象中有其他对象的指针。现在把原生指针替换为shared_ptr.

假设a对象中含有一个shared_ptr<B>指向b对象;假设b对象中含有一个shared_ptr<A>指向a对象
并且a,b对象都是堆中分配的。很轻易就能与他们失去最后联系。
考虑某个shared_ptr<A>local_a;是我们能最后一个看到a对象的共享智能指针,其use_count==2,
因为对象b中持有a的指针。所以当local_a说再见时,local_a只是把a对象的use_count改成1。
同理b对象。然后我们再也看不到a,b的影子了,他们就静静的躺在堆里,成为断线的风筝。

解决方案是:Use weak_ptr to "break cycles."(boost文档里写的)或者显示的清理

条款4:不要在函数实参里创建shared_ptr

function ( shared_ptr<int>(newint), g( ) );  //有缺陷
可能的过程是先new int,然后调g( ),g()发生异常,shared_ptr<int>没有创建,int内存泄露

shared_ptr<int> p(newint());
f(p, g());  //Boost推荐写法

条款5:对象内部生成shared_ptr

前面说过,不能把this指针直接扔给shared_ptr. 但是没有禁止在对象内部生成自己的shared_ptr

//这是Boost的例子改的。
class Y: publicboost::enable_shared_from_this<Y>
{
   boost::shared_ptr<Y> GetSelf()
    {
       return shared_from_this();
    }
};

原理是这样的。普通的(没有继承enable_shared_from_this)类T的shared_ptr<T>p(new T).
p作为栈对象占8个字节,为了记录(new T)对象的引用计数,p会在堆上分配16个字节以保存
引用计数等“智能信息”。share_ptr没有“嵌入(intrusive)”到T对象,或者说T对象对share_ptr毫不知

情。Y对象则不同,Y对象已经被“嵌入”了一些share_ptr相关的信息,目的是为了找到“全局性”的
那16字节的本对象的“智能信息”。

原理说完了,就是陷阱
Y y;
boost::shared_ptr<Y> p= y.GetSelf(); //无知的代码,y根本就不是new出来的

Y* y = new Y;
boost::shared_ptr<Y> p= y->GetSelf(); //似是而非,仍旧程序崩盘。
Boost文档说,在调用shared_from_this()之前,必须存在一个正常途径创建的shared_ptr

boost::shared_ptr<Y> spy(newY)
boost::shared_ptr<Y> p = spy->GetSelf(); //OK

条款6 :处理不是new的对象要小心。

int* pi = (int*)malloc(4)
shared_ptr<int> sp( pi ) ;//delete马嘴不对malloc驴头。

条款7:多线程对引用计数的影响。

如果是轻量级的锁,比如InterLockIncrement等,对程序影响不大
如果是重量级的锁,就要考虑因为share_ptr维护引用计数而造成的上下文切换开销。
1.33版本以后的shared_ptr对引用计数的操作使用的是Lock-Free(类似InterLockIncrement函数族)
的操作,应该效率不错,而且能保证线程安全(库必须保证其安全,程序员都没有干预这些隐藏事物的机会)。
Boost文档说read,write同时对shared_ptr操作时,行为不确定。这是因为shared_ptr本身有两个成员px,pi。
多线程同时对px读写是要出问题的。与一个int的全局变量多线程读写会出问题的原因一样。

条款8:对象数组用shared_array

int* pint = new int[100];
shared_array<int> p (pint);

既然shared_ptr对应着delete;显然需要一个delete[]对应物shared_array

条款9:学会用删除器

struct Test_Deleter
{   
    void operator ()( Test* p){  ::free(p);   }
};
Test* t = (Test*)malloc(sizeof(Test));
new (t) Test;

shared_ptr<Test> sp( t , Test_Deleter() ); //删除器可以改变share_ptr销毁对象行为

有了删除器,shared_array无用武之地了。
template<class T>
struct Array_Deleter
{   
    void operator ()( T*){   delete[] p;  }
};
int* pint = new int[100];
shared_ptr<int> p (pint,Array_Deleter<int>() );

条款10:学会用分配器

存放引用计数的地方是堆内存,需要16-20字节的开销。
如果大量使用shared_ptr会造成大量内存碎片。
shared_ptr构造函数的第3个参数是分配器,可以解决这个问题。

shared_ptr<Test> p( (new Test),Test_Deleter(), Mallocator<Test>());
注意删除器Test_Deleter是针对Test类的。分配器是针对shared_ptr内部数据的。

Mallocator<Test>()是个临时对象(无状态的),符合STL分配器规约。

template <typenameT> 
class Mallocator { 
    //略。。。。。。
    T * allocate(constsize_t n) const {
       returnsingleton_pool<T,sizeof(T)>::malloc();
    }
    //略。。。。。。

Mallocator传入Test,实际分配的类型确是
class boost::detail::sp_counted_impl_pda<classTest *,
                                   structTest_Deleter,
                                   classMallocator<class Test>>
这是用typeid(T).name()打印出来的。可能和rebind相关。

条款11 weak_ptr在使用前需要检查合法性。
weak_ptr<K> wp;
{
shared_ptr<K> sp(new K); //sp.use_count()==1
wp = sp; //wp不会改变引用计数,所以sp.use_count()==1
shared_ptr<K> sp_ok = wp.lock();//wp没有重载->操作符。只能这样取所指向的对象
}
shared_ptr<K> sp_null =wp.lock(); //sp_null .use_count()==0;
因为上述代码中sp和sp_ok离开了作用域,其容纳的K对象已经被释放了。
得到了一个容纳NULL指针的sp_null对象。在使用wp前需要调用wp.expired()函数判断一下。
因为wp还仍旧存在,虽然引用计数等于0,仍有某处“全局”性的存储块保存着这个计数信息。
直到最后一个weak_ptr对象被析构,这块“堆”存储块才能被回收。否则weak_ptr无法直到自己
所容纳的那个指针资源的当前状态。

条款12 不要newshared_ptr<T>

本来shared_ptr就是为了管理指针资源的,不要又引入一个需要管理的指针资源shared_ptr<T>*

条款13  尽量不要get

class B{...};
class D : public B{ ...};  //继承层次关系

shared_ptr<B> sp (new D);   //通过隐式转换,储存D的指针。
B* b = sp.get();           //shared_ptr辛辛苦苦隐藏的原生指针就这么被刨出来了。
D* d = dynamic_cast<D*>(b); //这是使用get的正当理由吗?

正确的做法
shared_ptr<B> spb (new D) ;
shared_ptr<D> spd =shared_dynamic_cast<D>(spb);//变成子类的指针
shared_ptr在竭尽全力表演的像一个原生指针,原生指针能干的事,它也基本上能干。

另一个同get相关的错误
shared_ptr<T> sp(new T);
shared_ptr<T> sp2( sp.get() );//又一个“二龙治水”实例,指针会删2次而错误。

条款14 不要memcpy shared_ptr

shared_ptr<B> sp1 (new B) ;
shared_ptr<B> sp2;
memcpy(&sp2,&sp1,sizeof(shared_ptr<B>));//sp2.use_count()==1
很显然,不是通过正常途径(拷贝构造,赋值运算),引用计数是不会正确增长的。

条款15 使用BOOST预定义的宏去改变shared_ptr行为。

shared_ptr行为由类似BOOST_SP_DISABLE_THREADS这样的宏控制。需要去学习他们到底是干什么的。
大师Andrei Alexandrescu设计了一种基于模板策略设计模式的智能指针,通过几个模板参数去定制化
智能指针的行为。Boost却不以为然,官方解释是:需要统一的接口,这样利于大规模书写。
smart_ptr<T,OwnershipPolicy,ConversionPolicy,CheckingPolicy,StoragePolicy>sp(new T);
上述接口缺点是外形复杂,看上去像个大花脸。优点是客户程序员可以轻易的定制行为。

条款17 构造函数里调用shared_from_this抛例外

class Holder:publicenable_shared_from_this<Holder>{
public:
    Holder() {
       shared_ptr<Holder>sp = shared_from_this();
       int x = sp.use_count();
    }
};
同前面条款5,不符合enable_shared_from_this使用前提。

总结:
学习了一天就总结出10多条条款,长期研究一下恐怕就出现条款100了。为什么还要使用shared_ptr呢?
有很多开源库用shared_ptr,而且shared_ptr具有“传染性”(某网友语:像毒品沾上就甩不掉),
抛开它就会有更严重的多龙治水现象。shared_ptr作为原生指针的替代品,能解决一定的内存泄露问题。
实际上初学原生指针时,每个人都遇到过野指针,删两次,忘记删除等问题。学习shared_ptr也会遇到。
shared_ptr的确能改善上述问题,并不能完全解决问题。shared_ptr可能在将来占主流,它最可能号令江湖,
否则一大堆auto_ptr,weak_ptr,原生指针,scoped_ptr共存就把人搞糊涂了。

原创粉丝点击