zoukankan      html  css  js  c++  java
  • [C++11新特性] 智能指针详解

    动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极为困难的。有时我们会忘记释放内存产生内存泄漏,有时提前释放了内存,再使用指针去引用内存就会报错。

    为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,区别在于它负责自动释放所指向的对象。这两种智能指针的区别在于管理底层指针的方式:shared_ptr 允许多个shared_ptr类型指针指向同一个对象;unique_ptr 则 “独占” 所指向的对象。标准库还定义了一个名为 weak_ptr 的伴随类,它是一种弱引用,指向 shared_ptr 所管理的对象。这三种类型都定义在 memory 头文件中。


    一、shared_ptr 类

    类似 vector,智能指针也是模板。因此,当定义智能指针时,必须在尖括号内给出类型,如下所示:

    shared_ptr<string> p1; // shared_ptr,可以指向string类型的对象
    shared_ptr<list<int>> p1; // shared_ptr,可以指向int类型的list的对象
    

    默认初始化的智能指针中保存着一个空指针。智能指针的使用方式与普通指针类似,解引用一个智能指针返回它指向的对象。


    下面列出了 shared_ptr 和 unique_ptr 都支持的操作。

    shared_ptr<T> sp // 空shared_ptr智能指针,可以指向类型为T的对象
    unique_ptr<T> up // 空unique_ptr智能指针,可以指向类型为T的对象
    p // 将p用作一个条件判断,若p指向一个对象,则为ture
    *p // 解引用p,获得它指向的对象
    p->mem // 等价于(*p).mem
    p.get() // 返回p中保存的指针
    swap(p,q) // 交换p和q中的指针
    p.swap(q)
    

    下面列出了 shared_ptr 独有的操作。

    make_shared<T>(args) //返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象
    shared_ptr<T> p(q) //p是shared_ptr q的拷贝;此操作会递增q中的引用计数。q中的指针必须能转换成T*
    p = q //p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p中的引用计数,递增q中的引用计数。若p中的引用计数变为0,则将其管理的原内存释放
    p.unique() //若p.use_count()为1,返回true;否则返回false
    p.use_count() //返回与p共享对象的智能指针数量;可能很慢,主要用于调试
    

    下面介绍一些改变 shared_ptr 的其他方法:

    p.reset () //若p是唯一指向其对象的shared_ptr,reset会释放此对象。
    p.reset(q) //若传递了可选的参数内置指针q,会令P指向q,否则会将P置为空。
    p.reset(q, d) //若还传递了参数d,将会调用d而不是delete 来释放q
    

    1. 使用 make_shared 函数分配内存并返回 shared_ptr 指针

    最安全的分配和使用动态内存的方法是调用一个名为 make_shared 的标准库函数。 此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr。与智能指针一样,make_shared 也定义在头文件 memory 中。

    当要用 make_shared 时,必须指定想要创建的对象的类型。定义方式与模板类相同, 在函数名之后跟一个尖括号,在其中给出类型:

    // 指向一个值为42的int的shared_ptr
    shared_ptr<int> p3 = make_shared<int>(42);
    
    // p4 指向一个值为"9999999999"的string
    shared_ptr<string> p4 = make_shared<string>(10,'9');
    
    // p5指向一个值初始化的int
    shared_ptr<int> p5 = make_shared<int>();
    

    当然,我们通常用 auto 定义一个对象来保存 make_shared 的结果,这种方式较为简单:

    // p6指向一个动态分配的空vector<string>
    auto p6 = make_shared<vector>();
    

    2. shared_ptr 的拷贝和赋值

    我们可以认为每个 shared_ptr 都有一个关联的计数器,通常称其为引用计数(reference count)。无论何时我们拷贝一个 shared_ptr,例如,当用一个  shared_ptr 初始化另一个 shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的引用计数就会递增。而当我们给 shared_ptr 赋予一个新值或者 shared_ptr 被销毁时,引用计数就会递减。

    一旦一个 shared_ptr 的计数器变为 0,它就会自动释放自己所管理的对象:

    auto p = make_shared<int> (42); // p指向的对象只有p一个引用者 
    auto q(p); // p和q指向相同对象,此对象有两个引用者
    auto r = make_shared<int> (42); //r指向的int只有一个
    r = q; // 给r赋值,令它指向另一个地址
    	   // 递增q指向的对象的引用计数
    	   // 递减r原来指向的对象的引用计数
           // r原来指向的对象已没有引用者,会自动释放
    

    此例中我们分配了一个 int,将其指针保存在 r 中。接下来,我们将一个新值赋予 r。在此情况下,r 是唯一指向此 int 的 shared_ptr,在把 q 赋给 r 的过程中,此 int 被自动释放。


    3. shared_ptr 自动销毀所管理的对象……

    当指向一个对象的最后一个 shared_ptr 被销毁时,Shared_ptr 类会自动销毁此 对象。它是通过另一个特殊的成员函数—析构函数完成销毁工作的。shared_ptr 的析构函数会递减它所指向的对象的引用计数。如果引用计数变为 0,shared_ptr 的析构函数就会销毁对象,并释放它占用的内存。


    4. shared_ptr 和 new 结合使用

    我们还可以用 new 返回的指针来初始化智能指针,如下所示:

    shared_ptr<int> p2(new int (42)); // p2 指向一个值为 42 的 int
    

    接受指针参数的智能指针构造函数是 explicit 的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:

    shared_ptr<int> pi = new int (1024); // 错误:必须使用直接初始化形式
    shared_ptr<int> p2(new int(1024));	// 正确:使用了直接初始化形式
    

    出于相同的原因,一个返回 shared_ptr 的函数不能在其返回语句中隐式转换一个普通指针:

    shared_ptr<int> clone(int p)
    {
        return new int(p); // 错误:隐式转换为 shared_ptr<int>
    }
    

    我们必须将 shared_ptr 显式绑定到一个想要返回的指针上:

    shared_ptr<int> clone(int p)
    {
        return shared_ptr<int>(new int(p)); // 正确:显式地用int*创建shared_ptr<int>
    }
    

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

    使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。

    考虑下面对 shared_ptr 进行操作的函数:

    //在函数被调用时 ptr 被创建并初始化
    void process(shared_ptr<int> ptr)
    {
        //使用ptr
    }//ptr离开作用域,被销毁
    
    int main()
    {
    	shared_ptr<int> p( new int (42) ) ; //引用计数为 1
    	process (p);//拷贝p会递增它的引用计数;在process中引用计数值为2
    	int i = *p; //正确:引用计数值为1    
    }
    

    下面考虑混合使用普通指针和智能指针的情况。虽然不能传递给 process —个内置指针,但可以传递给它一个(临时的) shared_ptr,这个 shared_ptr 是用一个内置指针显式构造的。但是,这样做很可能会导致错误:

    int *x(new int(1024));	// 危险:x是一个普通指针,不是一个智能指针
    process (x);// 错误:不能将 int*转换为一个 shared_ptr<int> 
    process ( shared_ptr<int> (x) ); // 合法的,但内存会被释放! 
    int j = *x; // 未定义的:x是一个空悬指针!
    

    在上面的调用中,我们将一个临时 shared_ptr 传递给 process。当这个调用所在的表达式结束时,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数就变为 0 了。因此,当临时对象被销毁时,它所指向的内存会被释放。但 x 继续指向(已经释放的)内存,从而变成一个空悬指针。如果试图使用 x 的值,其行为是未定义的。


    二、unique_ptr 类

    一个 unique_ptr “拥有” 它所指向的对象。与 shared_ptr 不同,某个时刻只能有一个 unique_ptr 指向一个给定对象。当 unique_ptr 被销毁时,它所指向的对象也被销毁。

    与 shared_ptr 不同,没有类似 make_shared 的标准库函数返回一个 unique_ptr。当我们定义一个 unique_ptr 时,需要将其绑定到一个 new 返回的指针上。类似 shared_ptr,初始化 unique_ptr 必须采用直接初始化形式:

    unique_ptr<double> p1; // 指向一个double的unique_ptr
    unique_ptr<double> p2(new int(42)); // p2指向一个值为42的int
    

    由于一个 unique_ptr 拥有它指向的对象,因此 unique_ptr 不支持普通的拷贝或赋值操作:

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

    下面列出了 unique_ptr 特有的操作。

    unique_ptr<T> u1 // 空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针
    unique_ptr<T, D> u2 // u2会使用一个类型为D的可调用对象来释放它的指针
    unique_ptr<T, D> u(d) // 空unique_ptr,指向类型为T的对象,用类型为D的对象d替代delete
    u = nullptr // 释放u指向的对象,将u置为空
    u.release() // u放弃对指针的控制权,返回指针,并将u置为空
    u.reset() // 释放u指向的对象
    u.reset(q) // 如果提供了内置指针q,另u指向这个对象;否则将u置为空'    
    u.reset(nullptr)   
    

    虽然我们不能拷贝或赋值 unique_ptr,但可以通过调用 release 或 reset 将指针的 所有权从一个(非const)unique_ptr 转移给另一个 unique:

    // 将所有权从pl (指向string Stegosaurus)转移给p2 
    unique_ptr<string> p2(p1, release()); // release 将 p1 置为空 
    unique_ptr<string> p3(new string("Trex"));
    
    // 将所有权从p3转移给p2
    p2.reset(p3.release()); // reset 释放了 p2 原来指向的内存
    

    release 成员返回 unique_ptr 当前保存的指针并将其置为空。因此,p2 被初始化为 p1 原来保存的指针,而 p1 被置为空。

    调用 release 会切断 unique_ptr 和它原来管理的对象间的联系,如果我们不用另一个智能指针来保存 release 返回的指针,我们的程序就要负责资源的释放:

    p2.release(); // 错误:p2不会释放内存,而且我们丢失了指针
    auto p = p2.release(); // 正确,但我们必须记得 delete(p)
    

    传递unique_ptr参数和返回unique_ptr

    不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的 unique_ptr。最常见的例子是从函数返回一个unique_ptr:

    unique_ptr<int> clone(int p) 
    {
    	// 正确:从 int*创建一个 unique_ptr<int> 
        return unique_ptr<int>(new int(p));
    }
    

    还可以返回一个局部对象的拷贝:

    unique_ptr<int> clone (int p) 
    {
    	unique_ptr<int> ret(new int (p));
    	//… 
        return ret;
    }
    

    对于上面两段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”,在《C++ Primer》13.6.2节(第473页)中有介绍。


    三、weak_ptr 类

    weak_ptr 是一种不控制所指向对象生存期的智能指针,它指向由一个 shared_ptr 管理的对象。将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。即使有 weak_ptr 指向对象,对象也还是会被释放,因此,weak_ptr 的名字抓住了这种智能指针 “弱” 共享对象的特点。

    下面列出了 weak_ptr 的操作。

    weak_ptr<T> w // 空weak_ptr可以指向类型为T的对象
    weak_ptr<T> w(sp) // 与shared_ptr sp指向相同对象的weak_ptr。T必须能转换为sp指向的S型
    w = p // p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象
    w.reset() // 将W置为空
    w.use_count() // 与w共享对象的shared ptr的数量
    w.expired()	// 若 w.use_count()为0,返回true,否贝y返回 false
    w.lock() // 如果expired为true,返回一个空shared ptr:否则返回一个 指向w的对象的shared_ptr
    

    当我们创建一个 weak_ptr 时,要用一个 shared_ptr 来初始化它:

    auto p = make_shared<int>(42);
    weak_ptr<int> wp(p); // wp弱共享p; p的引用计数未改变
    

    本例中 wp 和 p 指向相同的对象。由于是弱共享,创建 wp 不会改变 p 的引用计数;wp 指向的对象可能被释放掉。

    由于对象可能不存在,我们不能使用 weak_ptr 直接访问对象,而必须调用 lock。 此函数检查 weak_ptr 指向的对象是否仍存在。如果存在,lock 返回一个指向共享对象的 shared_ptr。与任何其他 shared_ptr 类似,只要此 shared_ptr 存在,它所指向的底层对象也就会一直存在。例如:

    if ( shared_ptr<int> np = wp.lock() )	
    { 
        // 如果 np 不为空则条件成立
    	// 在if中,np与p共享对象
    }
    

    在这段代码中,只有当 lock 调用返回 true 时我们才会进入 if 语句体。在if中,使用 np 访问共享对象是安全的。


    参考

    《C++ Primer 第5版》


  • 相关阅读:
    Sonne的健身日志(4)
    Sonne的健身日志(13)16周腹肌计划第四周(2012.3.302012.4.6)
    Sonne的健身日志(1)
    Iphone升级ios6后很耗电的解决办法
    试驾凯迪拉克SRX
    Sonne的健身日志(6)
    Sonne的健身日志(10)16周腹肌计划第一周感受与体会
    关于Iphone 4 如何用itunes备份短信等设置
    上海人2
    签了个100万的合同,我却很失落
  • 原文地址:https://www.cnblogs.com/linuxAndMcu/p/10409723.html
Copyright © 2011-2022 走看看