c++智能指针--所有的类型的解析

Posted 轩辕泪1020

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++智能指针--所有的类型的解析相关的知识,希望对你有一定的参考价值。

参考文献:

C++智能指针简单剖析

C++弱引用智能指针weak_ptr的用处

关于shared_ptr与weak_ptr的使用

1. 智能指针背后的设计思想

1.1 无智能指针造成内存泄漏的例子

void remodel(std::string & str)

    std::string * ps = new std::string(str);//堆内存
    ...
    if (weird_thing())
        throw exception();
    str = *ps; 
    delete ps;
    return;

当出现异常时(weird_thing()返回true),delete将不被执行,因此将导致内存泄露 。

常规解决方案:

  1. throw exception()之前添加delete ps;
  2. 不要忘了最后一个delete ps;

出现问题:在一个大型的工程中,并不能保证所有的开发人员都能在合适的地方添加delete语句。

1.2 智能指针的设计思想

仿照本地变量能够自动从栈内存中删除的思想,对指针设计一个析构函数,该析构函数将在指针过期时自动释放它指向的内存,总结来说就是:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数中编写delete语句以用来删除指针指向的内存空间

转换remodel()函数的步骤:

  • 包含头义件memory(智能指针所在的头文件);
  • 将指向string的指针替换为指向string的智能指针对象;
  • 删除delete语句。

使用auto_ptr修改该函数的结果:

#include <memory>
void remodel (std::string & str)

    std::auto_ptr<std::string> ps (new std::string(str))...
    if (weird_thing ())
        throw exception(); 
    str = *ps; 
    // delete ps; NO LONGER NEEDED
    return;

2. C++智能指针简单介绍

STL一共给我们提供了四种智能指针:auto_ptrunique_ptrshared_ptrweak_ptr

其中:auto_ptr在C++11中已将其摒弃。

使用注意点

  • 所有的智能指针类都有一个explicit构造函数,以指针作为参数。比如auto_ptr的类模板原型为:
templet<class T>
class auto_ptr 
  explicit auto_ptr(X* p = 0) ; 
  ...
;

因此不能自动将指针转换为智能指针对象,必须显示调用:

shared_ptr<double> pd;
double *p_reg = new double;
pd = p_reg;//NOT ALLOWED(implicit conversion)
pd = shared_ptr<double>(p_reg);// ALLOWED (explicit conversion)
shared_ptr<double> pshared = p_reg;//NOT ALLOWED (implicit conversion)
shared_ptr<double> pshared(p_reg);//ALLOWED (explicit conversion)
  • 对全部三种智能指针都应避免的一点:
string vacation("I wandered lonely as a child."); //heap param
shared_ptr<string> pvac(&vacation);//NO!!

pvac过期时,程序将把delete运算符用于非堆(栈)内存,这是错误的!

  • 使用实例
#include <iostream>
#include <string>
#include <memory>

class report

private:
    std::string str;
public:
    report(const std::string s) : str(s)
        std::cout<<"Object created.\\n";        
    
    ~report()
        std::cout<<"Object deleted.\\n";
    
    void comment() const 
        std::cout<<str<<"\\n";
    
;

int main()
            
        std::auto_ptr<report> ps(new report("using auto ptr"));
        ps->comment();
    //auto_ptr 作用域结束
    
        std::shared_ptr<report> ps(new report("using shared_ptr"));
        ps->comment();
	//shared_ptr 作用域结束
    
        std::unique_ptr<report> ps(new report("using unique ptr"));
        ps->comment();
    //unique_ptr 作用域结束
    return 0;

3. 为什么摒弃auto_ptr?

问题来源:

auto_ptr<string> ps (new string("I reigned lonely as a cloud."));
auto_ptr<string> vocation;
vocation = ps;

如果psvocation是常规指针,则两个指针指向同一个string对象,当指针过期时,则程序会试图删除同一个对象,要避免这种问题,解决办法:

  • 定义赋值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采取此方案。
  • 建立所有权(ownership)概念。对于特定的对象,智能有一个智能对象可拥有,这样只能拥有对象的智能指针的析构函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptrunique_ptr的策略,但unique_ptr的策略更严格
  • 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,当减为0时才调用delete这是shared_ptr采用的策略

同样的策略也适用于复制构造函数

摒弃auto_ptr的例子:

#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main()
    auto_ptr<string> films[5] = 
        auto_ptr<string> (new string("Fowl Balls")),
        auto_ptr<string> (new string("Duck Walks")),
        auto_ptr<string> (new string("Chicken Runs")),
        auto_ptr<string> (new string("Turkey Errors")),
        auto_ptr<string> (new string("Goose Eggs"))
    ;
    auto_ptr<string> pwin;
    pwin = films[2];//films[2] loses owership,将所有权从films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针
    cout<<"The nominees for best avian baseball film are\\n";
    for(int i = 0;i < 5;++i)
    
        cout<< *films[i]<<endl;
    
    cout<<"The winner is "<<*pwin<<endl;
    cin.get();
    
    return 0;

运行下发现程序崩溃了,原因是films[2]已经是空指针了,输出空指针就会崩溃。如果把auto_ptr换成shared_ptrunique_ptr后,程序就不会崩溃,原因如下:

  • 适用shared_ptr时运行正常,因为shared_ptr采用引用计数,pwinfilms[2]都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小,因此不会出现多次删除一个对象的错误。

  • 适用unique_ptr时编译出错,与auto_ptr一样,unique_ptr也采用所有权模型,但在适用unique_ptr时,程序不会等到运行阶段崩溃,在编译阶段下属代码就会出现错误:

  unique_ptr<string> pwin;
  pwin = films[2];//films[2] loses ownership

这就是为何摒弃auto_ptr的原因:避免潜在的内存泄漏问题。

4.unique_ptr为何优于auto_ptr

4.1 使用规则更严格

  auto_ptr<string> p1(new string("auto"));  //#1
  auto_ptr<string> p2;					//#2
  p2 = p1;							   //#3

在语句#3中,p2接管string对象的所有权后,p1的所有权将被剥夺。–>可防止p1和p2的析构函数试图删除同一个对象。但如果随后试图使用p1,则会出现错误。

  unique_ptr<string> p3(new string("auto"));//#4
  unique_ptr<string> p4;//#5
  p4=p3;//#6

编译器会认为#6语句为非法,可以避免上述问题。

4.2 对悬挂指针的操作更智能

总体来说:允许临时悬挂指针的赋值,禁止其他情况的出现

示例:函数定义如下:

  unique_ptr<string> demo(const char *s)
      unique_ptr<string> temp (new string(a));
      return temp;
  

在程序中调用函数:

  unique_ptr<string> ps;
  ps = demo("unique special");

编译器允许此种赋值方式。总之:当程序试图将一个unique_ptr赋值给另一个时,如果源unique_ptr是个临时右值,编译器允许这么做;如果源unique_ptr将存在一段时间,编译器将禁止这么做。

  unique_ptr<string> pu1(new string("hello world"));
  unique_ptr<string> pu2;
  pu2 = pu1;//#1 not allowed
  unique_ptr<string> pu3;
  pu3 = unique_ptr<string>(new string("you"));//#2 allowed

如果确实想执行类似#1的操作,仅当以非智能的方式使用摒弃的智能指针时(如解除引用时),这种赋值才不安全。要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),可以将原来的指针转让所有权变成空指针,可以对其重新赋值。

  unque_ptr<string> ps1,ps2;
  ps1 = demo("hello");
  ps2 = move(ps1);
  ps1 = demo("alexia");
  cout<<*ps2<<*ps1<<endl;

5. 如何选择智能指针

使用指南:

  1. 如果程序要使用多个指向同一个对象的指针,应选用shared_ptr。这样的情况包括:
    • 有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;
    • 连个对象包含指向第三个对象的指针;
    • STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr
  2. 如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。可将unique_ptr储存到STL容器中,只要不调用将unique_ptr复制或赋值给另一个算法(如sort())。例如,可在程序中使用类似于下面的代码段:
unique_ptr<int> make_int(int n)
    return unique_ptr<int>(new int(n));

void show(unique_ptr<int> &p1)
    cout<<*a<<' ';

int main()
    ...
    vector<unique_ptr<int>> vp(size);
    for(int i=0; i<vp.size();i++)
        vp[i] = make_int(rand() %1000);//copy temporary unique_ptr
    
    vp.push_back(make_int(rand()%1000));// ok because arg is temporary
    for_each(vp.begin(),vp.end(),show); //use for_each();

其中push_back调用没有问题,因为它返回一个临时unique_ptr,该unique_ptr被赋值给vp中的一个unique_ptr。另外,如果按值而不是按引用给show()传递对象,for_each()将非法,因为这将导致使用一个来自vp的非临时unique_ptr初始化p1,而这是不允许的。前面说过,编译器将发现错误使用unique_ptr的企图。

unique_ptr为右值时,可将其赋给shared_ptr,这与将一个unique_ptr赋给一个需要满足的条件相同。与前面一样,在下面的代码中,make_int()的返回类型为unique_ptr<int>

unique_ptr<int> pup(make_int(rand() % 1000));   // ok
shared_ptr<int> spp(pup);                       // not allowed, pup as lvalue
shared_ptr<int> spr(make_int(rand() % 1000));   // ok

模板shared_ptr包含一个显式构造函数,可用于将右值unique_ptr转换为shared_ptrshared_ptr将接管原来归unique_ptr所有的对象。

在满足unique_ptr要求的条件时,也可使用auto_ptr,但unique_ptr是更好的选择。如果你的编译器没有unique_ptr,可考虑使用Boost库提供的scoped_ptr,它与unique_ptr类似。

6. 弱引用智能指针 weak_ptr

设计weak_ptr的原因:解决使用shared_ptr因循环引用而不能释放资源的问题。

6.1 空悬指针问题

有两个指针p1p2,指向堆上的同一个对象Object,p1p2位于不同的线程中。假设线程A通过p1指针将对象销毁了(尽管把p1置为NULL),那p2就成了空悬指针。这是一种典型的C/C++内存错误。

使用weak_ptr能够帮助我们轻松解决上述的空悬指针问题(直接使用shared_ptr也是可以的)。

weak_ptr不控制对象的生命期,但是它知道对象是否还活着,如果对象还活着,那么它可以提升为有效的shared_ptr(提升操作通过lock()函数获取所管理对象的强引用指针);如果对象已经死了,提升会失败,返回一个空的shared_ptr

举个栗子 :

#include <iostream>
#include <memory>

int main()

    // OLD, problem with dangling pointer
    // PROBLEM: ref will point to undefined data!

    int* ptr = new int(10);
    int* ref = ptr;
    delete ptr;

    // NEW
    // SOLUTION: check expired() or lock() to determine if pointer is valid
    // empty definition
    std::shared_ptr<int> sptr;
    // takes ownership of pointer
    sptr.reset(new int);
    *sptr = 10;
    // get pointer to data without taking ownership
    std::weak_ptr<int> weak1 = sptr;
    // deletes managed object, acquires new pointer
    sptr.reset(new int);
    *sptr = 5;
    // get pointer to new data without taking ownership
    std::weak_ptr<int> weak2 = sptr;
    // weak1 is expired!
    if(auto tmp = weak1.lock())
        std::cout << *tmp << '\\n';
    else
        std::cout << "weak1 is expired\\n";
    // weak2 points to new data (5)
    if(auto tmp = weak2.lock())
        std::cout << *tmp << '\\n';
    else
        std::cout << "weak2 is expired\\n";

6.2 循环引用问题

栗子 大法:

#include <iostream>
#include <boost/smart_ptr.hpp>
using namespace std;
using namespace boost;

class BB;
class AA

public:
    AA()  cout << "AA::AA() called" << endl; 
    ~AA()  cout << "AA::~AA() called" << endl; 
    shared_ptr<BB> m_bb_ptr;  //!
;

class BB

public:
    BB()  cout << "BB::BB() called" << endl; 
    ~BB()  cout << "BB::~BB() called" << endl; 
    shared_ptr<AA> m_aa_ptr; //!
;

int main()

    shared_ptr<AA> ptr_a (new AA);
    shared_ptr<BB> ptr_b ( new BB);
    cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
    cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
    //下面两句导致了AA与BB的循环引用,结果就是AA和BB对象都不会析构
    ptr_a->m_bb_ptr = ptr_b;
    ptr_b->m_aa_ptr = ptr_a;
    cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
    cout << "ptr_b use_count: " << ptr_b.use_count() << endl;

运行结果:

可以看到由于AA和BB内部的shared_ptr各自保存了对方的一次引用,所以导致了ptr_aptr_b销毁的时候都认为内部保存的指针计数没有变成0,所以AA和BB的析构函数不会被调用。解决方法就是把一个shared_ptr替换成weak_ptr

#include <iostream>
#include <boost/smart_ptr.hpp>
using namespace std;
using namespace boost;

class BB;
class AA

public:
    AA()  cout << "AA::AA() called" << endl; 
    ~AA()  cout << "AA::~AA() called" << endl; 
    weak_ptr<BB> m_bb_ptr;  //!
;

class BB

public:
    BB()  cout << "BB::BB() called" << endl; 
    ~BB()  cout << "BB::~BB() called" << endl; 
    shared_ptr<AA> m_aa_ptr; //!
;

int main()

    shared_ptr<AA> ptr_a (new AA);
    shared_ptr<BB> ptr_b ( new BB);
    cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
    cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
    //下面两句导致了AA与BB的循环引用,结果就是AA和BB对象都不会析构
    ptr_a->m_bb_ptr = ptr_b;
    ptr_b->m_aa_ptr = ptr_a;
    cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
    cout << "ptr_b use_count: " << ptr_b.use_count() << endl;

运行结果:

最后值得一提的是,虽然通过弱引用指针可以有效的解除循环引用,但这种方式必须在能预见会出现循环引用的情况下才能使用,即这个仅仅是一种编译期的解决方案,如果程序在运行过程中出现了循环引用,还是会造成内存泄漏的。因此,不要认为只要使用了智能指针便能杜绝内存泄漏。

以上是关于c++智能指针--所有的类型的解析的主要内容,如果未能解决你的问题,请参考以下文章

c++复习笔记——智能指针详细解析(智能指针的使用,原理分析)

C++的智能指针auto_ptrunqiue_ptr源码解析

C++智能指针shared_ptrweak_ptr源码解析

现代 C++一文读懂智能指针

c++中的智能指针auto_ptr解析

C++智能指针简单剖析