动态内存,智能指针

来源:互联网 发布:数据库 图标 编辑:程序博客网 时间:2024/06/07 23:20

一.直接使用动态内存
假设真的要用到new/delete直接使用内存,而不是使用智能指针来管理动态内存,那么需要注意几点:
(1)注意new 和 new()的区别
    
   using std::cout ;using std::endl ;int *pI = new int ;int *pJ = new int() ;cout << *pI << "," << *pJ <<endl;
*pI输出的是一个随机值,*pJ输出的是0。原因是:
不带括号的,动态分配的对象是默认初始化的,而带扩号采用的是值初始化(即初始化为0)。
这一点对于有默认构造函数的类类型来说没什么区别,这种类型都会通过默认构造函数进行初始化;
而对于内置类型或是类的内置类型成员来说就不一样了,值初始化会让这些对象有着良好定义的值。

(2)定位new(placement new)
默认情况下,new失败并不是返回NULL,而是抛出一个C++标准异常:bad_alloc;
可以向new传递额外的参数,例如在此,给它传递一个标准款定义的名为nothrow的对象:
int *p1 = new (nothrow) int ;
这时失败后,会返回一个NULL,而不会抛出异常。
这种方式叫做定位new(placement new)。

(3)动态分配的const对象

        可以new一个const对象:const int *pci = new const int(1024) ;

                                               const std::string *pcs = new const st::string;

         这时通过指针pci不能修改指针执行的对象的值。所以对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显示初始化。


          (4)直接使用动态内存总结起来无非就是下面三个不好的方面:

        A.忘记delete        B.使用已delete的内存           C.delete同一块内存两次(未定义行为)


      二.使用std::shared_ptr

        1.不要混合使用普通指针和智能指针

         当使用shared_ptr绑定到一个普通的指针时,我们就将内存的管理责任交给了这个shared_ptr,所以这时我们就不能使用内置指针来访问shared_ptr指向的对象了,因为我们不知道shared_ptr是否已经将该对象销毁了。所以不要混合使用内置指针和智能指针。


        最好的使用方式是用make_shared<>,而不是使用new,这样在分配对象的时候就同时将对象绑定到shared_ptr上了,不会暴露内置指针。

        如果我们不得已使用智能指针的成员函数get()去获取内置指针,那么我们千万不能delete该指针。

        最后一点,不能用同一个内置指针为多个智能指针初始化或reset(),这也意味着不能用get()获取到的指针去初始化另一个智能指针reset()。


         另外,std::shared_ptr的接受指针参数的构造函数是explicit的,因此,不能将一个内置指针隐式的转换成一个智能指针,必须使用直接初始化形式:

        shared_ptr<int> p1 = new int(1) ;     //这是错误的形式

        shared_ptr<int> p2(new int(1)) ;      //正确


2.使用自己的释放操作

       可以给shared_ptr传一个自己的释放操作,否则shared_ptr默认使用delete操作:

HANDLE h = CreateFile(...) ;std::shared_ptr<HANDLE> rH(&h , [](HANDLE *pH) { close(*pH) ;}) ;
       上面给出的例子,用Lambada表达式可以很方便的使用。


        三.使用std::unique_ptr

          1.unique_ptr的注意点

           首先,unique_ptr的接受指针参数的构造函数和shared_ptr一样是explicit的,因此,不能讲一个内置指针隐式的转换成一个unique_ptr智能指针:

           unique_ptr<int> p1 = new int(1) ;               //错误

           unique_ptr<int> p2(new int(1)) ;                //正确


           unique_ptr的不可拷贝性质使得其和share_ptr有一些使用方式的不同,首先,其没有拷贝操作,所以下面这些操作都是不行的:

           p1 = p2 ;                  //不能调用复制操作符

           unique_ptr<int> p1(p2) ; //不能调用拷贝构造函数


           unique_ptr有一个特有的release()操作,share_ptr是没有该操作的,例如:

           std::unique_ptr<int>  up1(new int(1)) ;

           int *p = up1.release() ;

           release()使得up1放弃对所所指向对象的控制权,其返回所指向指针,并将智能指针置为空;

           release()与reset()是不同的,reset()会释放所指向对象:

           up1.release()  ;              //会导致内存泄露,因为release()后,我们丢失了指针

           up1.reset() ;                   //会自动释放内存


           通常的操作是这样的,release()操作用来和reset()配合实现unique_ptr所指向对象的所有权的转移:

           std::unique_ptr<int> up1(new int(1)) ;

           std::unique_ptr<int> up2 ;                       

           up2.reset(up1.release()) ;


           最后,谈一下unique_ptr的release()成员和shared_ptr的get()成员的区别:

           两个的共同点是都返回智能指针所指向对象的内置指针,不同的是,unique_ptr的release()放弃了对内置指针的控制权,即该智能指针已经和返回的内置指针没任何关系了,所以该内置指针可以用来初始化或者reset()给另一个unique_ptr对象;而shared_ptr的get()没有放弃对内置指针的控制权,只是返回了内置指针,所以该内置指针不能用来初始化或reset()另一个shared_ptr对象。其实仔细想一想,一个shared_ptr对象放弃对内置指针的控制权是没有意义的,因为该shared_ptr对象可能已经被拷贝过了,所以会有其他的shared_ptr对象持有该内置指针。


          2.unique_ptr不能拷贝性质的例外情况:

          可以拷贝或赋值一个将要被销毁的unique_ptr,最常见的例子:从函数中返回一个unique_ptr:

std::unique_ptr<int> clone(int p){return std::unique_ptr<int>(new int(p)) ;/* 或者std::unique_ptr<int> ret(new int(p)) ;return ret ; */}
          编译器知道要返回的对象将被销毁,在这种情况下,编译器执行一种特别的"拷贝"。其实,这里的特别的"拷贝"就是使用移动构造函数或者移动复制操作,所以本质上也是没有违背不能拷贝的特性的,而只是进行了移动。


       3.使用自己的释放操作

       和shared_ptr一样,unique_ptr也可以重载默认的删除器。但是unique_ptr管理删除器的方式与shared_pr不同。在重载unique_ptr的删除器时,我们必须在模板参数中给出删除器类型:

void myDelete(int *p){std::cout << "my Delelte" << std::endl ; delete [] p;}int main(void){int *buf = new int[10] ;std::unique_ptr<int , decltype(myDelete)* > pBuf(buf , myDelete) ;return 0 ;}

        或者用lambda表达式会更方便使用:

int *buf = new int[10] ;std::unique_ptr<int , std::function<void (int *)> > pBuf(buf , [] (int *p) {delete [] p ;}) ;


       四.weak_ptr

             weak_ptr作为shared_ptr的伴随类,它指向shared_ptr管理的对象。shared_ptr的一个问题是会意外延迟管理对象的生命周期。shared_ptr是允许拷贝和赋值的,如果不小心遗留了一个拷贝,那么对象就将一直存在。基于此,才有了weak_ptr的出现。weak_ptr是一种弱引用,不会改shared_ptr的引用计数。

         一个简单的使用例子:

        std::shared_ptr<int> sp1 = std::make_shared<int>(1);        std::weak_ptr<int> wp1(sp1) ;if(auto sp2 = wp1.lock())            //lock()成员将weak_ptr提升为shared_ptr{std::cout<<"lock sp1:" << *sp2 << std::endl ;}
         其他的一些成员:

           use_count():与该weak_ptr共享对象的shared_ptr的数量

           expired():失效的,即如果use_count()返回0,则为true。


       五.shared_ptr和unique_ptr重载默认删除器的不同方式

         很多人应该都有疑问为什么这两个智能指针重载默认删除器的方式会不一样,unique_ptr需要给出在模板参数列表中给出删除器的类型,而shared_ptr不需要。刚开始我也是很疑惑,重新看了C++ primer的16.1.6节,里面讲到shared_ptr的实现方式效率比unique_ptr底,但是使用上比unique_ptr灵活。

         关于shared_ptr的效率问题,书中大概的描述如下:

shared_ptr必须能直接访问其删除器,即删除器必须保存为一个指针或一个封装了指针的类(如std::function),所以删除器时不是将删除器直接保存为一个成员,因为删除器的类型直到运行时才会知道,即删除器是间接保存的,所以调用删除器时需要一次运行时的跳转操作。

        上面这段引用C++prime的描述中,加粗的那句:“删除器必须保存为一个指针”,刚开始另我很疑惑,其实这里的指针不是指删除器的函数指针,而是一个指向删除器的指针,所以才有了间接保存删除器的说法;

         至于用std::function的方式来保存为什么也是间接保存呢?其实std::function本来就是对所有可调用对象的一个封装,所以通过std::function保存删除器后,通过std::function成员去调用删除器,也是需要一次运行时跳转的。

         用了std::function实现的大概方式如下:

template<class _Ty>class shared_ptr{public:explicit shared_ptr(_Ty *_Px , std::function<void (_Ty *)> del):_Myptr(_Px) , _Mydel(del){}~shared_ptr(){_Mydel(_Myptr) ;}private:_Ty * _Myptr ;std::function<void (_Ty *)> _Mydel ;};

          这种方式有一个限制是重载的删除器返回值必须为void,因为删除器的调用形式已经被定义成void (_Ty *)。
        再来看一下vs2010中,std::shared的实现方式,如果我们重载删除器,则调用的构造函数为:

template<class _Ux,class _Dx>shared_ptr(_Ux *_Px, _Dx _Dt){// construct with _Px, deleter_Resetp(_Px, _Dt);}
        多了一个删除器的模板参数,_Resetp()函数里面调用了_Resetp0()函数,并new了一个_Ref_count_del对象作为参数:
        _Resetp0(_Px, new _Ref_count_del<_Ux, _Dx>(_Px, _Dt));
        _Ref_count_del类型继承于基类_Ref_count_base,在其构造函数中,将删除器_Dt保存在其_Dtor成员中:
template<class _Ty,class _Dx>class _Ref_count_del: public _Ref_count_base{// handle reference counting for object with deleterpublic:_Ref_count_del(_Ty *_Px, _Dx _Dt): _Ref_count_base(), _Ptr(_Px), _Dtor(_Dt){// construct}
         进一步跟进_Resetp0()函数里面,可以看到,new出来的这个_Ref_count_del对象最终保存在shared_ptr的_Rep成员中,而_Rep是一个_Ref_count_base *指针类型。
         在shared_ptr的析构函数中,会调用_Rep的虚函数_Decef(),前面说到,_Rep是一个基类类型的指针,所以在运行时会调整到该指针实际指向的对象的成员函数中,然后,没有重载删除器的shared_ptr最终跳到:

virtual void _Destroy(){// destroy managed resourcedelete _Ptr;}
            而重载了删词器的最终跳到调用删除器的地方:
virtual void _Destroy(){// destroy managed resource_Dtor(_Ptr);}
            所以,不管是用std::function方式实现,还是用多态的方式实现,都不是直接的调用了删除器,而是多了一次跳转,因为这是在运行时才绑定的删除器。
而unique_ptr的删除器是其类型的一部分,所以在在编译时就可以绑定删除器,所以它的效率会比shared_ptr的高一点。
vs2010中的实现方式如下:
template<class _Ty,class _Dx,bool _Empty_deleter> class _Unique_ptr_base { public: typedef typename tr1::remove_reference<_Dx>::type _Dx_noref; typedef _DELETER_POINTER_TYPE(_Ty, _Dx_noref) pointer;  //省略成员函数...  pointer _Myptr;// the managed pointer _Dx _Mydel;// the deleter };
             可以看到成员函数_Dx _Mydel就是重载的删除器,直接作为unique_ptr的成员了。

          最后,因为unique_ptr是在编译期绑定的,所以在运行时不能够改变删除器了,或者直接一点,类成员的类型在运行时是不能改变的。而shared_ptr就没有这个限制了,我们可以随时改变其删除器的类型,shared_ptr的reset()成员函数允许我们传一个删除器参数,而unique_ptr的成员函数是不能这样做的。这是shared_ptr比unique_ptr灵活的一个地方。




















原创粉丝点击