智能指针干什么用的

  分配与释放内存空间中的动态内存,是C++内存管理中重要环节。相比静态内存(static)和栈内存,动态内存的管理更加困难,因为这部分内存完全由程序员自己掌控。C++语法提供了new和delete两个关键字进行动态内存的管理,new运算符会首先申请内存空间,然后调用对象的构造函数,delete运算符会先调用析构函数,然后释放内存。由于代码编写不当或者逻辑问题,很容易造成内存释放错误,产生悬空指针,内存泄露等内存问题。为了更好地对动态内存进行管理,标准库定义了一些智能指针,这篇文章就来介绍这些智能指针。

智能指针的分类以及使用方式

    智能指针是一个模板类,当我们使用智能指针时,需要智能指针指向的数据类型。

XXX_ptr<T>p;  // XXX_ptr是具体的智能指针,T是数据类型,如int,dobble

  智能指针存在与memory库当中,使用时需要引入这个库。智能指针包含下面几种:

  • auto_ptr
  • unique_ptr
  • shared_ptr
  • weak_ptr

auto_ptr

  auto_ptr是一个独占性指针,它最明显的特点是,某一个对象只能被一个auto_ptr所指,不存在多个auto_ptr指向同一对象的情况

    auto_ptr<Object>Object1(new Object());       // 指向一个对象
    Object1->doSomething();
    auto_ptr<Object>Object2;
    Object2 = Object1;
    Object2->doSomething();
    Object1->doSomething();     // 错误,Object1已经把指针的所有权给了Object2,它已经不再指向原来的对象

  从C++11开始,auto_ptr已经被废除了,所以这里就不过多介绍了。它的替代品是unique_ptr。

unique_ptr

  unique_ptr是auto_ptr的替代品。它比auto_ptr更加安全。unique_ptr禁止了拷贝构造函数和拷贝赋值运算符,所以无法进行拷贝和赋值:

    unique_ptr<Object>p1;  

    unique_ptr<Object>p2(p1); // 错误,unique_ptr不支持拷贝
    p2 = p1;                  // 错误,unique_ptr不支持赋值

  想要初始化unique_ptr有两种常见的方法,一种方法是使用构造函数初始化,另一种是使用make_unique函数初始化:

 

    unique_ptr<int>p1(new int);
    unique_ptr<int>p2 = make_unique<int>();

 

  unique_ptr不支持拷贝语义,只支持移动语义。如果需要转移unique_ptr指向的对象,需要使用std::move()语法。

    unique_ptr<Object>p1;  

    unique_ptr<Object>p2(std::move(p1)); 

   或者可以调用unique_ptr的release和reset函数,转移对象的所有权。reset接收一个可选的指针参数,令unique_ptr重新指向给定的指针。若unique_ptr原来不为空,则先释放原来指向的对象。release切断unique_ptr和它管理对象之间的联系,一般常用来初始化另一个智能指针或给另一个智能指针赋值。

    unique_ptr<Object>p1(new Object);  

    unique_ptr<Object>p2;
    p2.reset(p1.release());
    p2->doSomething();

  注意,release仅仅是切断指针和动态资源之间的联系,并不能释放资源。程序要负责释放从release接管过来的资源:

    unique_ptr<int>p1(new int);
    auto p2 = p1.release();
    delete p2;    // 释放资源

  不能拷贝unique_ptr的规则也有例外,当拷贝或赋值的是一个临时右值,也就是将要被销毁的unique_ptr时,编译器会通过unique_ptr的拷贝。例如从函数中返回一个unique_ptr:

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

  当unique_ptr被销毁时,它所指向的对象也被销毁。

shared_ptr

  shard_ptr采用共享模型,允许多个指针指向相同的对象。初始化shared_ptr时,可以使用拷贝构造函数和拷贝赋值构造函数,也可以使用shared_ptr特有的make_shared函数。使用make_shared函数是最安全的分配动态内存的方法,应该被优先考虑:

    shared_ptr<int>p1(new int(5));
    shared_ptr<int>p2(p1);   // shared_ptr支持拷贝初始化
    shared_ptr<int>p3 = p2;  // shared_ptr支持赋值
    shared_ptr<int>p4 = make_shared<int>(6);  

  shared_ptr有一个与它指向对象相关联的计数器,每当拷贝一个shared_ptr,计数器就递增,每当给shared_ptr赋一个新值或销毁shared_ptr,计数器就递减。一旦计数器清零,shared_ptr会自动销毁所指向的对象。使用use_count函数可以得到与指针共享对象的智能指针的数量。

    shared_ptr<int>p1(new int(5));
    shared_ptr<int>p2(p1);   // shared_ptr支持拷贝初始化
    shared_ptr<int>p3 = p2;  // shared_ptr支持赋值
    shared_ptr<int>p4 = make_shared<int>(6);  
    cout << p3.use_count() << endl;   // 3,p3指向的对象有3个引用者
    cout << p4.use_count() << endl;   // 1,p4指向的对象有1个引用者
    p3 = p4;                          //拿p4给p3赋值,p3指向对象的引用减1,p4指向对象的引用加1
    cout << p1.use_count() << endl;   //2
    cout << p2.use_count() << endl;   //2
    cout << p3.use_count() << endl;   //2
    cout << p4.use_count() << endl;   //2

  当引用计数计为0的时候,shared_ptr本身的析构函数会销毁对象,并释放占用的内存。在某种情况下,shared_ptr会出现循环引用的问题:

class father;
class son;
class father{
public:
    shared_ptr<son>child;
};

class son{
public:
    shared_ptr<father>father;
};
shared_ptr<father>f(new father());
shared_ptr<son>c(new son());
f->child = c;
c->father = f;

  如以上代码所示,father类和son类各有一个指向对方的shared_ptr。经过初始化,f指向一个father对象,c指向son对象,之后,f中的child指针指向son对象,c中的father指针指向father对象,此时两者的引用计数全部2,且形成了一个循环。

  当程序运行结束时,程序试图释放father对象和son对象占据的内存,可是我们发现,要释放father对象,必须要释放它的child指针。而释放的必要条件是智能指针的引用计数清零,然后才会调用析构函数,释放内存,可是因为Son对象的father指针正在指向father对象自己,如果father对象不释放,则引用计数不可能清零。son对象和father都在等待对方释放资源,产生了死锁,导致双方都不会主动释放资源。这种现象就是循环等待,循环等待会造成内存泄露的发生。

 

weak_ptr

  weak_ptr被用来解决shared_ptr造成的循环引用的问题。weak_ptr不控制指向对象的生命周期,仅仅提供对管理对象的访问手段。它指向一个有shared_ptr管理的对象。初始化weak_ptr需要shared_ptr,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

    shared_ptr<int>p1(new int(5));
    weak_ptr<int>wp(p1);
    cout << p1.use_count() << endl;   //1,不改变p1的引用计数
    cout << wp.use_count() << endl;      //1    

 

  一旦一个对象不再有shared_ptr指向它,无论是不是还有weak_ptr指向该对象,它也还是会被释放。由于指向的对象不存在,所以不能直接使用weak_ptr访问对象,而是应该判断指向的对象是否存在。我们可以通过调用expired()方法实现这一点:

 

    if (!wp.expired()){
        cout << "exist!" << endl;
    }

 

  我们不能直接通过weak_ptr来对shared_ptr进行操作,正确的做法应该是调用lock()函数,然后实现对这个指针进行操作:

 

    if (!wp.expired()){
        shared_ptr<int>p5 = wp.lock();
        *p5 = 8;  
        cout << *p1 << endl;
        //cout << "exist!" << endl;
    }

 

  利用weak_ptr可以解决循环等待问题。我们将上文father类的shared_ptr改成weak_ptr

 

class father{
public:
    weak_ptr<son>child;
};

 

  使用weak_ptr替换shared_ptr之后,可以直接销毁father对象并释放内存。father对象释放以后,son对象的shared_ptr的引用计数清零,这样son对象就也能被顺利地销毁和释放内存。

 

总结

  智能指针被设计用来管理动态内存,体现了RAII的思想。RAII(资源获取即初始化)是C++语言中一种管理资源的方法。应用程序往往忘记释放申请来的资源,最终造成系统资源的消耗殆尽。RAII机制将资源和对象的生命周期捆绑在一起,来解决这个问题。RAII强调在类的构造函数中实现资源的分配和初始化,在析构函数中释放与清理资源。智能指针正是根据这样的设想实现的。