C++智能指针实现和问题

Share_ptr实现原理

智能指针是什么

简单来说,智能指针是一个类,它对普通指针进行封装,使智能指针类对象具有普通指针类型一样的操作。具体而言,复制对象时,副本和原对象都指向同一存储区域,如果通过一个副本改变其所指的值,则通过另一对象访问的值也会改变.所不同的是,智能指针能够对内存进行进行自动管理,避免出现悬垂指针等情况。

普通指针存在的问题

C语言、C++语言没有自动内存回收机制,关于内存的操作的安全性依赖于程序员的自觉。程序员每次new出来的内存块都需要自己使用delete进行释放,流程复杂可能会导致忘记释放内存而造成内存泄漏。而智能指针也致力于解决这种问题,使程序员专注于指针的使用而把内存管理交给智能指针。

我们先来看看普通指针的悬垂指针问题。当有多个指针指向同一个基础对象时,如果某个指针delete了该基础对象,对这个指针来说它是明确了它所指的对象被释放掉了,所以它不会再对所指对象进行操作,但是对于剩下的其他指针来说呢?它们还傻傻地指向已经被删除的基础对象并随时准备对它进行操作。于是悬垂指针就形成了,程序崩溃也“指日可待”。我们通过代码+图来来探求悬垂指针的解决方法。

int * ptr1 = new int (1);
int * ptr2 = ptr1;
int * ptr3 = prt2;
        
cout << *ptr1 << endl;
cout << *ptr2 << endl;
cout << *ptr3 << endl;

delete ptr1;

cout << *ptr2 << endl;

代码简单就不啰嗦解释了。运行结果是输出ptr2时并不是期待的1,因为1已经被删除了。这个过程是这样的:

从图可以看出,错误的产生来自于ptr1的”无知“:它并不知道还有其他指针共享着它指向的对象。如果有个办法让ptr1知道,除了它自己外还有两个指针指向基础对象,而它不应该删除基础对象,那么悬垂指针的问题就得以解决了。如下图:

那么何时才可以删除基础对象呢?当然是只有一个指针指向基础对象的时候,这时通过该指针就可以大大方方地把基础对象删除了。

什么是引用计数

如何来让指针知道还有其他指针的存在呢?这个时候我们该引入引用计数的概念了。引用计数是这样一个技巧,它允许有多个相同值的对象共享这个值的实现。引用计数的使用常有两个目的:

  • 简化跟踪堆中(也即C++中new出来的)的对象的过程。一旦一个对象通过调用new被分配出来,记录谁拥有这个对象是很重要的,因为其所有者要负责对它进行delete。但是对象所有者可以有多个,且所有权能够被传递,这就使得内存跟踪变得困难。引用计数可以跟踪对象所有权,并能够自动销毁对象。可以说引用计数三个简单的垃圾回收体系。这也是本文的讨论重点。
  • 节省内存,提高程序运行效率。如何很多对象有相同的值,为这多个相同的值存储多个副本是很浪费空间的,所以最好做法是让左右对象都共享同一个值的实现。C++标准库中string类采取一种称为”写时复制“的技术,使得只有当字符串被修改的时候才创建各自的拷贝,否则可能(标准库允许使用但没强制要求)采用引用计数技术来管理共享对象的多个对象。这不是本文的讨论范围。

智能指针实现

了解了引用计数,我们可以使用它来写我们的智能指针类了。智能指针的实现策略有两种:辅助类与句柄类。这里介绍辅助类的实现方法。

基础对象类

首先,我们来定义一个基础对象类Point类,为了方便后面我们验证智能指针是否有效,我们为Point类创建如下接口:

class Point                                       
{
private:
    int x, y;
public:
    Point(int xVal = 0, int yVal = 0) :x(xVal), y(yVal) { }
    int getX() const { return x; }
    int getY() const { return y; }
    void setX(int xVal) { x = xVal; }
    void setY(int yVal) { y = yVal; }
};

辅助类

在创建智能指针类之前,我们先创建一个辅助类。这个类的所有成员皆为私有类型,因为它不被普通用户所使用。为了只为智能指针使用,还需要把智能指针类声明为辅助类的友元。这个辅助类含有两个数据成员:计数count与基础对象指针。也即辅助类用以封装使用计数与基础对象指针

class U_Ptr                                  
{
private:
    
    friend class SmartPtr;      
    U_Ptr(Point *ptr) :p(ptr), count(1) { }
    ~U_Ptr() { delete p; }
    
    int count;   
    Point *p;                                                      
};

为基础对象类实现智能指针类

引用计数是实现智能指针的一种通用方法。智能指针将一个计数器与类指向的对象相关联,引用计数跟踪共有多少个类对象共享同一指针。它的具体做法如下:

  • 当创建类的新对象时,初始化指针,并将引用计数设置为1
  • 当对象作为另一个对象的副本时,复制构造函数复制副本指针,并增加与指针相应的引用计数(加1)
  • 使用赋值操作符对一个对象进行赋值时,处理复杂一点:先使左操作数的指针的引用计数减1(为何减1:因为指针已经指向别的地方),如果减1后引用计数为0,则释放指针所指对象内存。然后增加右操作数所指对象的引用计数(为何增加:因为此时做操作数指向对象即右操作数指向对象)。
  • 析构函数:调用析构函数时,析构函数先使引用计数减1,如果减至0则delete对象。

做好前面的准备后,我们可以来为基础对象类Point书写一个智能指针类了。根据引用计数实现关键点,我们可以写出我们的智能指针类如下:

{
public:
    SmartPtr(Point *ptr) :rp(new RefPtr(ptr)) { }    
    
    SmartPtr(const SmartPtr &sp) :rp(sp.rp) { ++rp->count; }
      
    SmartPtr& operator=(const SmartPtr& rhs) {    
        ++rhs.rp->count;    
        if (--rp->count == 0)    
            delete rp;
        rp = rhs.rp;
        return *this;
    }
    
    ~SmartPtr() {       
        if (--rp->count == 0)   
            delete rp;
        else 
        cout << "还有" << rp->count << "个指针指向基础对象" << endl;
    }
    
private:
    U_Ptr *rp;  
};

智能指针类的使用与测试

至此,我们的智能指针类就完成了,我们可以来看看如何使用

int main()
{
    //定义一个基础对象类指针
    Point *pa = new Point(10, 20);

    //定义三个智能指针类对象,对象都指向基础类对象pa
    //使用花括号控制三个指针指针的生命期,观察计数的变化

    {
        SmartPtr sptr1(pa);//此时计数count=1
        {
            SmartPtr sptr2(sptr1); //调用复制构造函数,此时计数为count=2
            {
                SmartPtr sptr3=sptr1; //调用赋值操作符,此时计数为conut=3
            }
            //此时count=2
        }
        //此时count=1;
    }
    //此时count=0;pa对象被delete掉

    cout << pa->getX ()<< endl;

    system("pause");
    return 0;
}

来看看运行结果咯:

还有2个指针指向基础对象
还有1个指针指向基础对象
-17891602
请按任意键继续. . .

如期,在离开大括号后,共享基础对象的指针从3->2->1->0变换,最后计数为0时,pa对象被delete,此时使用getX()已经获取不到原来的值。

智能指针类的改进一

虽然我们的SmartPtr类称为智能指针,但它目前并不能像真正的指针那样有->、*等操作符,为了使它看起来更像一个指针,我们来为它重载这些操作符。代码如下所示:

{
public:
    SmartPtr(Point *ptr) :rp(new RefPtr(ptr)) { }    
    
    SmartPtr(const SmartPtr &sp) :rp(sp.rp) { ++rp->count; }
      
    SmartPtr& operator=(const SmartPtr& rhs) {    
        ++rhs.rp->count;    
        if (--rp->count == 0)    
            delete rp;
        rp = rhs.rp;
        return *this;
    }
    
    ~SmartPtr() {       
        if (--rp->count == 0)   
            delete rp;
        else 
        cout << "还有" << rp->count << "个指针指向基础对象" << endl;
    }
    

    Point & operator *()        //重载*操作符  
    {
        return *(rp->p);
    }
    Point* operator ->()       //重载->操作符  
    {
        return rp->p;
    }
    
private:
    RefPtr *rp;  
};

然后我们可以像指针般使用智能指针类

  Point *pa = new Point(10, 20);
    SmartPtr sptr1(pa);
    //像指针般使用
    cout<<sptr1->getX();

智能指针改进二

目前这个智能指针智能用于管理Point类的基础对象,如果此时定义了个矩阵的基础对象类,那不是还得重新写一个属于矩阵类的智能指针类吗?但是矩阵类的智能指针类设计思想和Point类一样啊,就不能借用吗?答案当然是能,那就是使用模板技术。为了使我们的智能指针适用于更多的基础对象类,我们有必要把智能指针类通过模板来实现。这里贴上上面的智能指针类的模板版:

    //模板类作为友元时要先有声明
    template <typename T>
    class SmartPtr;
    
    template <typename T>
    class U_Ptr     //辅助类
    {
    private:
        //该类成员访问权限全部为private,因为不想让用户直接使用该类
        friend class SmartPtr<T>;      //定义智能指针类为友元,因为智能指针类需要直接操纵辅助类
    
        //构造函数的参数为基础对象的指针
        U_Ptr(T *ptr) :p(ptr), count(1) { }
    
        //析构函数
        ~U_Ptr() { delete p; }
        //引用计数
        int count;   
    
        //基础对象指针
        T *p;                                                      
    };
    
    template <typename T>
    class SmartPtr   //智能指针类
    {
    public:
        SmartPtr(T *ptr) :rp(new U_Ptr<T>(ptr)) { }      //构造函数
        SmartPtr(const SmartPtr<T> &sp) :rp(sp.rp) { ++rp->count; }  //复制构造函数
        SmartPtr& operator=(const SmartPtr<T>& rhs) {    //重载赋值操作符
            ++rhs.rp->count;     //首先将右操作数引用计数加1,
            if (--rp->count == 0)     //然后将引用计数减1,可以应对自赋值
                delete rp;
            rp = rhs.rp;
            return *this;
        }
    
        T & operator *()        //重载*操作符  
        {
            return *(rp->p);
        }
        T* operator ->()       //重载->操作符  
        {
            return rp->p;
        }
    
    
        ~SmartPtr() {        //析构函数
            if (--rp->count == 0)    //当引用计数减为0时,删除辅助类对象指针,从而删除基础对象
                delete rp;
            else 
            cout << "还有" << rp->count << "个指针指向基础对象" << endl;
        }
    private:
        U_Ptr<T> *rp;  //辅助类对象指针
    };
    
    

好啦,现在我们能够使用这个智能指针类对象来共享其他类型的基础对象啦,比如int:

int main()
{
    int *i = new int(2);
    {
        SmartPtr<int> ptr1(i);
        {
            SmartPtr<int> ptr2(ptr1);
            {
                SmartPtr<int> ptr3 = ptr2;

                cout << *ptr1 << endl;
                *ptr1 = 20;
                cout << *ptr2 << endl;

            }
        }
    }
    system("pause");
    return 0;
}

运行结果如期所愿,SmartPtr类管理起int类型来了:

        2
        20
        还有2个指针指向基础对象
        还有1个指针指向基础对象
        请按任意键继续. . .

 

Share_Ptr使用避坑条款:

条款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使用前提。

原文链接:https://blog.csdn.net/NicolasYan/article/details/50588022
原文链接:https://blog.csdn.net/peng864534630/article/details/77932574

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章