C++11——智能指针

1. 介绍

  一般一个程序在内存中可以大体划分为三部分——静态内存(局部的static对象、类static数据成员以及所有定义在函数或者类之外的变量)、栈内存(保存和定义在函数或者类内部的变量)和动态内存(实质上这块内存池就是堆,通常通过new/malloc操作申请的内存)。对于静态内存和栈内存来说,编译器可以根据它们的定义去自动创建和销毁的相应的内存空间。而对于动态内存,由于程序只有在运行时才知道需要分配多少内存空间,所以只能由程序员去动态的去创建和回收这块内存。

  而对于动态内存的回收是一个很复杂的问题,经常会因为一些难以观察的细节遗忘对一些对象的释放造成内存泄露,比如下面的代码:

#include <iostream>
#include <exception>
using namespace std;
class myException : public exception
{
public:
    const char* what_happened() const throw(){
        return "error: what you have down is error.";
    }
};

void check(int x){
    if(x == 0){
        throw myException();
    }
}

int main(){
    string* str = new string("testing....");
    try {
       check(0);
       //do something I really want to do
       // ....
    } catch (myException &e) {
        cout << e.what_happened() << endl;
        return -1;
    }
    delete str;
    return 0;
}

  一旦项目的代码量非常庞大时,此时像这样的内存泄露即便可以通过一些内存检测工具(比如valgrind),但去定位并改正这些错误还是很繁琐的。

  为了更方便且更安全的使用动态内存C++提供了四种智能指针来动态管理这些对象——auto_ptr(C++98,现在基本被淘汰),unique_ptr,shared_ptr,weak_ptr(后三种是C++11的新标准)。上面的程序改成如下形式,同时去掉delete str;就可以了。

std::auto_ptr<std::string> ps(new string("testing....")); 

2.智能指针

  使用智能指针,需要引入头文件#include <memory>,接受参数的智能指针的构造函数是explict,如下

template<typename _Tp>
    class auto_ptr
    {
    private:
      _Tp* _M_ptr;
      
    public:
		explicit
     		 auto_ptr(element_type* __p = 0) throw() : _M_ptr(__p) { }
        //....
    }

  因此不能自动将指针转换为智能指针对象,而是采用直接初始化的方式来初始化一个指针,显示的创建对象。如下:

shared_ptr<std::string> ps(new string("testing...."));	  //正确
shared_ptr<std::string> ps = new string("testing....");   //错误

同时,应该避免把一个局部变量的指针传给智能指针:

//error —— double free or corruption (out): 0x00007fffffffd910 ***
string s("testing.....");
shared_ptr<string> pvac(&s);

//correct
string* str = new string("testing....");
shared_ptr<string> pvac(str);

局部变量s是在栈上分配的内存,且其作用域范围仅限于当前函数,一旦执行完,该变量将被自动释放,而智能指针shared_ptr又会自动再次调用s的析构函数,导致一个变量double free。而new方式申请的内存在堆上,该部分的内存不会随着作用域范围的结束而被释放,只能等到智能指针调用析构函数再去释放。

题外话——隐式类型转换

  隐式类型转换可:能够用一个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。如下面程序:

#include <string>
#include <iostream>
using namespace std ;
class BOOK
{
private:
    string _bookISBN ;
    float _price ;

public:
    //这个函数用于比较两本书的ISBN号是否相同
    bool isSameISBN(const BOOK& other){
        return other._bookISBN==_bookISBN;
    }

    //类的构造函数,即那个“能够用一个参数进行调用的构造函数”(虽然它有两个形参,但其中一个有默认实参,只用一个参数也能进行调用)
    BOOK(string ISBN,float price=0.0f):_bookISBN(ISBN),_price(price){}
};

int main()
{
    BOOK A("A-A-A");
    BOOK B("B-B-B");
    cout<<A.isSameISBN(B)<<endl;   //正常地进行比较,无需发生转换
    cout<<A.isSameISBN(string("A-A-A"))<<endl; //此处即发生一个隐式转换:string类型-->BOOK类型,借助BOOK的构造函数进行转换,以满足isSameISBN函数的参数期待。
    cout<<A.isSameISBN(BOOK("A-A-A"))<<endl;    //显式创建临时对象,也即是编译器干的事情。

    return 0;
}

  此处发生了一个隐式类型转换,将一个string类型转化成了BOOK类,如果要阻止该类型的转换,可以将构造函数定义成如下形式:

explicit BOOK(string ISBN,float price=0.0f):_bookISBN(ISBN),_price(price){}

现在,我们只能显示的类型转换和显示的去创建BOOK对象。

2.1 auto_ptr

  auto_ptr是旧版gcc的智能指针,现在新版本的已经将其摒弃,如下程序:

#include <iostream>
#include <exception>
#include <memory>
using namespace std;
int main(){
    auto_ptr<string> day[7] = {
        auto_ptr<string>(new string("Monday")),
        auto_ptr<string>(new string("Tudsday")),
        auto_ptr<string>(new string("Wednesday")),
        auto_ptr<string>(new string("Thursday")),
        auto_ptr<string>(new string("Friday")),
        auto_ptr<string>(new string("Saturday")),
        auto_ptr<string>(new string("Sunday"))
    };
    //将Saturday的值赋给today
    auto_ptr<string> today = day[5];
    cout << "today is " << *today << endl;
    for(int i = 0; i < 7; i++){
        cout << *day[i] << " ";
    }
    cout << endl;
    return 0;
}

对于上面程序,会发现,编译的时候,没有什么问题,可以当运行的时候就会发生段错误。上面有两个变量day[5]和today都指向同一内存地址,当这两个变量的在这个作用域范围失效时,就会调用各自的析构函数,造成同一块内存被释放两次的情况。为了避免这种情况,在auto_ptr中有一种所有权的概念,一旦它指向一个对象后,这个对象的所有权都归这个指针控制,但是如果此时又有一个新的auto_ptr指针指向了这个对象,旧的auto_ptr指针就需要将所有权转让给新的auto_ptr指针,此时旧的auto_ptr指针就是一个空指针了,上面的程序通过调试可以看出这些变量值的变化过程。

程序可以编译通过,但运行时会出错,这种错误在项目中去查找是一件很痛苦的事情,C++新标准避免潜在的内存崩溃问题而摒弃了auto_ptr。

2.2 unique_ptr

  unique_ptr和auto_ptr类似,也是采用所有权模型,但是如果同样的程序,只是把指针的名字换了一下:

int main(){
    unique_ptr<string> day[7] = {
        unique_ptr<string>(new string("Monday")),
        unique_ptr<string>(new string("Tudsday")),
        unique_ptr<string>(new string("Wednesday")),
        unique_ptr<string>(new string("Thursday")),
        unique_ptr<string>(new string("Friday")),
        unique_ptr<string>(new string("Saturday")),
        unique_ptr<string>(new string("Sunday"))
    };
    //将Saturday的值赋给today
    unique_ptr<string> today = day[5];
    cout << "today is " << *today << endl;
    for(int i = 0; i < 7; i++){
        cout << *day[i] << " ";
    }
    cout << endl;
    return 0;
}
/* 编译阶段就会报错
smart_ptr.cpp:17:37: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = std::__cxx11::basic_string<char>; _Dp = std::default_delete<std::__cxx11::basic_string<char> >]’
     unique_ptr<string> today = day[5];
*/

可以看出unique比auto_ptr更加安全,在编译阶段就可以提前告知错误,而且unique_ptr还有一个很智能的地方,就是虽然不允许两个unique_ptr的赋值操作,但是允许在函数返回值处去接受这个类型的指针,如下: 

unique_ptr<string> test(const char* c){
    unique_ptr<string> temp(new string(c));
    return temp;
}
int main(){
    unique_ptr<string> ptr;
    ptr = test("haha");
    return 0;
}

  如果确实想让两个unique_ptr进行赋值操作,可以调用标准库函数std::move()函数,它可以实现对象资源的安全转移,如下:  

unique_ptr<string> today = std::move(day[5]);
cout << "today is " << *today << endl;
for(int i = 0; i < 7; i++){
    cout << *day[i] << " ";
}
cout << endl;

  上面的代码虽然可以安全编译过,day[5]将资源所有权转移到today上,会造成像auto_ptr一样出现访问day[5]这个空指针异常的错误。

2.3 shared_ptr

  现在将上面的代码换成shared_ptr:

#include <iostream>
#include <exception>
#include <memory>
using namespace std;
shared_ptr<string> test(const char* c){
    shared_ptr<string> temp(new string(c));
    return temp;
}
int main(){
    shared_ptr<string> day[7] = {
        shared_ptr<string>(new string("Monday")),
        shared_ptr<string>(new string("Tudsday")),
        shared_ptr<string>(new string("Wednesday")),
        shared_ptr<string>(new string("Thursday")),
        shared_ptr<string>(new string("Friday")),
        shared_ptr<string>(new string("Saturday")),		//指向new string("Saturday")计数器为1
        shared_ptr<string>(new string("Sunday"))
    };
    //将Saturday的值赋给today
    shared_ptr<string> today = day[5];  //指向new string("Saturday")计数器为2
    cout << "today is " << *today << endl;
    for(int i = 0; i < 7; i++){
        cout << *day[i] << " ";
    }
    cout << endl;
    return 0;
}
/* output
today is Saturday
Monday Tudsday Wednesday Thursday Friday Saturday Sunday
*/

我们会惊讶的发现这个程序是可以正常的跑过的,而且day[5]也是可以正常打印出来的,原因在于share_ptr并不是采用所有权机制,当有多个share_ptr指向同一对象时,它就会向java的垃圾回收机制一样采用引用计数器,赋值的时候,计数器加1,而指针对象过期的时候,计数器减1,直到计数器的值为0的时候,才会调用析构函数将对象的内存清空。

shared_ptr内存也可以这样申请:

std::shared_ptr<ClassA> p1 = std::shared_ptr<ClassA>();
std::shared_ptr<ClassA> p2 = std::make_shared<ClassA>();

  

第一种方式会先申请A类对象所需的空间,然后再去申请针对对该空间控制的内存控制块。而第二种方式是数据块和控制块会一块申请,所以它的效率会更高一点。

2.4 wek_ptr

先来看一个例子,假设有两个对象,他们之间重存在这相互引用的关系:

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

class ClassB;

class ClassA
{
public:
    ClassA() { cout << "ClassA Constructor..." << endl; }
    ~ClassA() { cout << "ClassA Destructor..." << endl; }
    shared_ptr<ClassB> pb;  // 在A中引用B
};

class ClassB
{
public:
    ClassB() { cout << "ClassB Constructor..." << endl; }
    ~ClassB() { cout << "ClassB Destructor..." << endl; }
    shared_ptr<ClassA> pa;  // 在B中引用A
};

int main02() {
    //也可以通过make_shared来返回一个shared_ptr对象,它的效率会更高
    shared_ptr<ClassA> spa = make_shared<ClassA>();
    shared_ptr<ClassB> spb = make_shared<ClassB>();
    spa->pb = spb;
    spb->pa = spa;
    // 函数结束:spa和spb会释放资源么?
    return 0;
}

/** valgrind 一部分报告
==812== LEAK SUMMARY:
==812==    definitely lost: 32 bytes in 1 blocks
==812==    indirectly lost: 32 bytes in 1 blocks
==812==      possibly lost: 0 bytes in 0 blocks
==812==    still reachable: 72,704 bytes in 1 blocks
*/

  

使用valgrind可以看出确实造成了内存泄露,因为ClassA和ClassB相互循环的引用对方,造成各自的引用计数器都会加1,使得最终析构函数调用无法将其置为0。

这个时候可以用到wek_ptr,weak_ptr是一种“弱”共享对象的智能指针,它指向一个由share_ptr管理的对象,讲一个weak_ptr绑定到shared_ptr指向的对象去,并不会增加对象的引用计数器的大小,即使weak_ptr还指向某一个对象,也不会阻止该对象的析构函数的调用。这个时候需要判断一个对象是否存在,然后才可以去访问对象,如下代码:

class C
{
public:
    C() : a(8) { cout << "C Constructor..." << endl; }
    ~C() { cout << "C Destructor..." << endl; }
    int a;
};
int main() {
    shared_ptr<C> sp(new C());
    weak_ptr<C> wp(sp);
    if (shared_ptr<C> pa = wp.lock())
    {
        cout << pa->a << endl;
    }
    else
    {
        cout << "wp指向对象为空" << endl;
    }
    sp.reset(); //reset--释放sp关联内存块的所有权,如果是最后一个指向该资源的(引用计数为0),就释放这块内存
    //wp.lock()检查和shared_ptr绑定的对象是否还存在
    if (shared_ptr<C> pa = wp.lock())
    {
        cout << pa->a << endl;
    }
    else
    {
        cout << "wp指向对象为空" << endl;
    }
}
/* output
C Constructor...
8
C Destructor...
wp指向对象为空
*/

  然后将最开始的程序改成如下形式,则可以避免循环引用而造成的内存泄漏问题。

class ClassB;

class ClassA
{
public:
    ClassA() { cout << "ClassA Constructor..." << endl; }
    ~ClassA() { cout << "ClassA Destructor..." << endl; }
    weak_ptr<ClassB> pb;  // 在A中引用B
};

class ClassB
{
public:
    ClassB() { cout << "ClassB Constructor..." << endl; }
    ~ClassB() { cout << "ClassB Destructor..." << endl; }
    weak_ptr<ClassA> pa;  // 在B中引用A
};

int main() {
    shared_ptr<ClassA> spa = make_shared<ClassA>();
    shared_ptr<ClassB> spb = make_shared<ClassB>();
    spa->pb = spb;
    spb->pa = spa;
    // 函数结束,思考一下:spa和spb会释放资源么?
    return 0;
}

/* valgrind报告
==5401== LEAK SUMMARY:
==5401==    definitely lost: 0 bytes in 0 blocks
==5401==    indirectly lost: 0 bytes in 0 blocks
==5401==      possibly lost: 0 bytes in 0 blocks
==5401==    still reachable: 72,704 bytes in 1 blocks
==5401==         suppressed: 0 bytes in 0 blocks
*/

参考资料

  1. C++ Primer(第五版)

  1. C++智能指针简单剖析

  2. C++ 隐式类类型转换

  3. 【C++11新特性】 C++11智能指针之weak_ptr

  

posted @ 2019-08-30 14:51  晓乎  阅读(1435)  评论(1编辑  收藏  举报
总访问: counter for blog 次