• C++相关:动态内存和智能指针


    前言

    在C++中,动态内存的管理是通过运算符newdelete来完成的。但使用动态内存很容易出现问题,因为确保在正确的时间释放内存是及其困难的。有时候我们会忘记内存的的释放,这种情况下就会产生内存泄露;有时候又会在尚有指针引用的情况下就用delete释放了内存,这样又会产生引用非法内存的指针(野指针)。因此,为了更容易地使用动态内存,C++标准库提供了两种智能指针,shared_ptrunique_ptr。shared_ptr允许多个指针指向同一个对象,unique_ptr则独占指向的对象。另外,还有一种叫weak_ptr的伴随类,他是一种弱引用,指向shared_ptr所管理的对象。三者定义于memory头文件中。

    shared_ptr类

    声明方式类似vector,属于模板类,如下

    shared_ptr<string> p1;     //声明了一个指向string的智能指针,默认空
    

    解引用等使用的方式类似普通指针

    if( p1 && p1->empty())
      *p1 = "hi!"; //如果p1指向一个空string,解引用并赋一个新值
    

    两种智能指针公用的操作

    shared_ptr<T> sp;
    unique_ptr<T> up;
    
    //假设声明了两个名为p、q的智能指针
    p->mem;  //等价于(*p).mem
    p.get();     //返回p中存放的指针
    
    //交换二者保存的指针
    swap(p,q);
    p.swap(q);
    

    shared_ptr独有的操作

    p.unique();  //是否独有
    p.use_count; //返回p共享对象的智能指针数量
    p = q;  //该操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放
    make_shared<T>(args);//该方法返回一个shared_ptr,指向一个T类型的对象,并使用args初始化该对象,具体见下文 

    make_shared函数

    最安全的分配和使用动态内存的方法。

    shared_ptr<int> p3 = make_shared<int>(42);//指向值为42的int的智能指针。
    //或者也可以
    auto p3 = make_shared<int>(42);

    每一个shared_ptr都有关联的计数器,称为引用计数。当用一个shared_ptr ——p去初始化另个一个q时,或者将p作为参数传递给函数,或者作为函数返回值时,它关联的对象的引用计数就会递增;而如果给它赋一个新值或者是shared_ptr被销毁时,之前关联的计数器就减1,当一个shared_ptr的计数器为0时,他就会自动释放管理的对象的内存。

    动态分配的const对象

    const int *pci = new int(1024);
    const string *pcs = new string;
    /*const修饰的对象必须初始化
    虽然对象的值不能被修改,但是本身可以销毁的*/
    delete pcs;//这是可以的

    PS:用delete释放一个空指针总是没有错误的。

    内存耗尽

    如果内存耗尽的情况下,使用new会分配失败并抛出std::alloc异常。

    可以使用

    int *p2 = new (nothrow) int;

    的形式来向new传递额外的参数,从而告诉它不能抛出异常,这种称为定位new。

     动态对象的生存周期直到被释放为止

    Foo* factory(T arg)
    {
       return new Foo(arg);
    }
    
    void use_factory(T arg)
    {
       Foo *p = factory(arg);
    }

    上述的代码,虽然p在离开作用域以后被销毁了,但他所指向的动态内存并没有被释放,不注意的话很可能内存泄漏!

    所以,正确的做法如下:

    void use_factory(T arg)
    {
       Foo *p = factory(arg);
       //这块内存不再使用了就释放
       delete p;
    }

    概括来说,由内置指针(不是智能指针)管理的动态内存在被显式地释放前会一直存在,直到手动释放或者程序结束时才会被回收。因此,智能指针的使用能够避免很多忘记释放内存等失误带来的麻烦。

    另外,delete之后,虽然指针已经无效,但是它依然保存着释放的内存的地址,因此为了避免误操作最好将指针置空。

    int *p(new int(42));
    auto q = p;
    delete p;
    p = nullptr;

    但是这样提供的保护还是有限的,如上述代码虽然将p置空,但是q仍然指向那块内存,仍然存在隐患。

     shared_ptr和new结合使用

    //错误的方式,智能指针的构造函数由explicit修饰,不支持将内置指针隐式转换为智能指针
    shared_ptr<int> p1= new int(1024);
    //正确方式
    shared_ptr<int> p2(new int(1024));
    
    p2.reset(); //若p2是唯一指向,则释放其指向的内存并置空
    p2.reset(q) //令p2指向q,否则置空
    
    //同样的,返回值如果时内置指针也会报错
    shared_ptr<int> clone(int p)
    {
        return new int(p);  //错误,无法隐式转换为智能指针
    }
    

      

     智能指针和普通指针最好不要混合使用

    void process(shared_ptr<int> ptr)
    {
    
    }
    /*ptr离开作用域被销毁
    -----------------
    如果使用普通指针*/
    int *x(new int(1024));
    process(x);//出错,无法转换
    process(shared_ptr<int>(x));//合法,但是x指向的内存在内部会被释放掉!!
    int j = *x; //错误,未定义,x是一个空悬指针

    上述代码中,shared_ptr通过x拷贝构造了一个智能指针ptr传递进process,这个时候的引用计数为1,而离开作用域后ptr被销毁,其指向的对象不再被引用,因此内存被回收,指针x因此无家可指变为野指针。

    另外,也尽量避免使用get初始化另一个智能指针,也不要delete get()返回的内置指针。

    使用自定义的释放操作

    struct destination;       //连接目的地
    struct connection;       //连接信息
    connection connect(destination *);  //打开连接
    void disconnect(connection);          //关闭指定连接
    void end_connection(connection *p)
    {
         disconnect(*p);
    }
    
    void f(destination &d /*其他参数*/)
    {
        connection c = connect(&d);
        shared_ptr<connection> p(&c,end_connection);
        //使用连接
       //当f退出时(即使为异常退出),connection也会被正确关闭
    }

    上述代码模拟的一个网络库的代码使用。

    当p被销毁时,她不会对保存的的指针delete,而是调用end_connection,接下来end_connection会调用disconnect,从而确保连接被关闭。如果f正常退出,那么p的销毁会作为结束处理的一部分,如果发生了异常,p同样被销毁,连接从而被关闭。

    unique_ptr类

    顾名思义,独一无二的指针,与shared_ptr不同,某个时刻只能由一个unique_ptr指向一个给定对象。声明以及初始化如下

    unique_ptr<int> p2(new int(42));

    由于unique_ptr独享其对象,所以它不支持普通的拷贝和赋值操作

    unique_ptr<string> p1(new string("Stegosaurus"));
    unique_ptr<string> p2(p1);  //错误:不支持拷贝
    unique_ptr<string> p3'
    p3 = p2;                             //错误,不支持赋值

    unique_ptr的相关操作

    unique_ptr<T> u1; //空unique_ptr指针
    unique_ptr<T,D> u2; //使用D对象来代替delete释放
    unique_ptr<T,D> u(new class());
    
    u = nullptr; //释放u指向的对象并置空
    u.release(); //u会放弃对该对象的控制权(内存不会释放),返回一个指向对象的指针,并置空自己
    u.reset();   //释放u所指对象
    u,reset(q);//如果提供了内置指针q,则指向q所指对象;否则u置空

    当unique_ptr将要被销毁时,可以“特殊地”被拷贝或者赋值,比如下面这种情况

    unique_ptr<int> clone(int p)
    {
        return unique_ptr<int>(new int(p));  //正确
    }
    
    //或者
    unique_ptr<int> clone(int p)
    {
       unique_ptr<int> ret(new int(p));
       //......
       return ret; //正确
    }

    weak_ptr类

    weak_ptr是一种不控制所指向对像生命周期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数,当最后一个指向对象的shared_ptr被销毁时,对象会被释放(即使有weak_ptr指向)。

    weak_ptr的操作

    weak_ptr<T>w;
    weak_ptr<T>w(sp);//使用一个shared_ptr初始化
    
    w = p;               //p可以是一个sp也可是一个wp。赋值后w,p共享对象
    
    w.reset();//置空
    w.use_count();       //同w共享对象的shared_ptr的数量
    w.expired();        //w.use_count()为0返回true,否则返回false
    w.lock();            //expired为true,返回一个空的shared_ptr;否则返回一个指向w的对象的shared_ptr

    allocator类

    定义在memory中,它提供了一种类型感知的内存分配方式,将内存分配和对象构造分离开来。它分配的内存是原始的、未构造的。基本用法如下:

    allocator<string> alloc;  //分配string的allocator对象
    auto const p = alloc,allocate(n); //分配n个未初始化的string

    allocator的操作

    allocator<T> a;
    a.allocate(n);
    
    a.deallocate(p,n); /*释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是
    一个先前由allocate返回的指针,且n必须是p创建时所要求的大小。在调用deallocate之后,用户必须对
    每个在这块内存中创建的对象调用destroy*/
    
    a.construct(p,args);/*p必须是一个类型为T*的指针,指向一块原始内存;args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象*/
    
    a.destroy(p); //p为T*类型的指针,此算法对p所指向的对象执行析构函数

    allocator分配的内存是未构造的,所以我们必须用construct构造对象,并且只能对构造了的对象执行destroy操作。

    销毁的参考代码如下:

    while(q != p)
       alloc.destroy(--q);
    

     一旦所有元素被销毁后,就可以重新使用这部分内存来保存其他的string,也可以使用 alloc.deallocate(p,n)来释放内存。

    参考资料

    《C++ Primer 第5版》 电子工业出版社    作者:【美】  Stanley B. Lippman  && Josee Lajoie && Barbara E.Moo

  • 相关阅读:
    iphone的苹果式营销体会
    上海自驾游之水博园游记
    2011上海车展参观记(多图)
    一部烂电影《让子弹飞》
    坚持自己读罗永浩《我的奋斗》感悟
    转:心理测试
    我们只要世界第一
    一篇小学课文
    iphone 4 初体验
    Setting Expires and CacheControl: maxage headers for static resources in ASP.NET
  • 原文地址:https://www.cnblogs.com/0kk470/p/7903779.html
Copyright © 2020-2023  润新知