智能指针梳理

C++智能指针梳理

参考:

一、 为何需要智能指针

在C/C++指针引发的错误中有如下两种:内存泄漏和指针悬挂。使用智能指针可以较好地解决这两个问题。

1.1 内存泄漏

内存泄漏的含义可以由以下几个解释中获知:
解释1:In computer science, a memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations in such a way that memory which is no longer needed is not released.
解释2:指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。
解释3:用动态存储分配函数(如malloc)动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元,直到程序结束。它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。
综合以上的解释,可以看到理解内存泄漏的关键点是“不再使用的内存没有得到释放”。
注意,内存泄漏是指堆内存泄漏(Heap leak)。
补充:关于资源泄漏
In computer science, a resource leak is a particular type of resource consumption by a computer program where the program does not release resources it has acquired. This condition is normally the result of a bug in a program. Typical resource leaks include memory leak and handle leak, particularly file handle leaks.
可见内存泄漏是资源泄漏中的一种,资源泄漏的另外一种是句柄泄漏,例如文件读写,socket操作等都有可能导致句柄的泄漏。

1.2 悬挂指针

悬挂指针也叫野指针,是未初始化或未清零的指针。与空指针(NULL)不同,悬挂指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。
悬挂指针的成因主要有两种:

  • 指针变量没有被初始化
    任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
  • 指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。
    别看free和delete的名字恶狠狠的(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。

1.3 实例

1) 忘记释放导致内存泄漏

/*
 * 忘记释放导致内存泄漏
 *
 * 析构函数未得到执行
 */

#include <iostream>
using namespace std;

class Test
{
public:
    Test(int a = 0 )
    : m_a(a)
    {}

    ~Test( )
    {
        cout<<"Calling destructor"<<endl;
    }
public:
    int m_a;
};

int main()
{
    Test *t1 = new Test(3);
    cout << t1->m_a << endl;
//  delete t1;  
    return 0;
}

2) 异常导致内存泄漏

/*
 * 异常导致内存泄漏
 *
 * 析构函数未得到执行
 */

#include <iostream>
using namespace std;

class Test
{
public:
    Test(int a = 0 )
    : m_a(a)
    {}

    ~Test( )
    {
        cout<<"Calling destructor"<<endl;
    }
public:
    int m_a;
};

int main()
{
    try
    {
        Test *t1 = new Test(3);
        cout << t1->m_a << endl;

        throw("an exception");

        delete t1;  
    }
    catch(...)
    {
        cout << "Something has gone wrong" << endl;
    }

    return 0;
}

3) 悬挂指针

/*
 * 浅copy导致的指针悬挂问题
 *
 * 输出类似如下:
 *  destructor ---> Book
 *  destructor ---> 葺葺葺葺
 *
 */

#include <iostream>
using namespace std;

class HasPtr
{
public:
    HasPtr(char *s);
    ~HasPtr();

private:
    char *ptr;
};

HasPtr::HasPtr(char *s)
{
    if (s == nullptr)
    {
        ptr = new char[1];
        *ptr = '\0';
    }
    else
    {
        ptr = new char[strlen(s) +1];
        strcpy(ptr, s);
    }
}

HasPtr::~HasPtr()
{
    cout << "destructor ---> " << ptr << endl;
    delete ptr;
}

int main()
{
    HasPtr p1("Book");
    HasPtr p2("Music");
    p2 = p1;

    return 0;
}

二、 自定义智能指针类

首先通过引入使用计数类来实现自己的智能指针类,以此加深对智能指针的理解,并解决上面例子中悬挂指针问题。
这部分具体参考《C++ Primer 第四版13.5.1节 定义智能指针类》,代码如下:

/*
 *自定义智能指针类修复-->浅copy导致的指针悬挂问题
 *
 * 定义计数类:U_ptr
 *
 * 智能指针类中需要: 
 * 构造函数、析构函数、copy构造函数、赋值运算符"="重载
 * 
 * 输出:
 * destructor ---> Book
 *
 */

#include <iostream>
using namespace std;

class HasPtr;
class U_ptr
{
private:
    friend class HasPtr;

    U_ptr(char *s)
    : use(1)
    {
        if (s == nullptr)
        {
            sptr = new char[1];
            sptr = '\0';
        }
        else
        {
            sptr = new char[strlen(s) +1];
            strcpy(sptr, s);
        }
    }

    ~U_ptr()
    {
        delete sptr;
    }

    char *sptr;
    int use;
};

class HasPtr
{
public:
    HasPtr(char *s);
    HasPtr(const HasPtr &other);
    HasPtr &operator=(const HasPtr &other);
    ~HasPtr();

private:
    U_ptr *ptr;
};

HasPtr::HasPtr(char *s)
{
    ptr = new U_ptr(s);
}

HasPtr::~HasPtr()
{
    if (--ptr->use == 0)
    {
        cout << "destructor ---> " << ptr->sptr << endl;
        delete ptr;
    }
}
HasPtr::HasPtr(const HasPtr &other)
{
    ptr->sptr = other.ptr->sptr;
    ptr->use ++;
}

HasPtr &HasPtr::operator=(const HasPtr &other)
{
    ++ other.ptr->use;
    if (-- ptr->use == 0)
    {
        delete ptr;
    }
    ptr = other.ptr;    

    return *this;
}

int main()
{
    HasPtr p1("Book");
    HasPtr p2("Music");
    p2 = p1;

    return 0;
}

三、 auto_ptr

auto_ptr是一个模版类,是最早出现的智能指针,在C++98标准中已经存在。
智能指针的原理都是RAII(Resource Acquisition Is Initialization),即在构造的时候获取资源,在析构的时候释放资源。
另外,为了使用智能指针,需要引入头文件 #include <memory>

3.1 使用auto_ptr

首先使用它来解决1.3节中实例的问题。

1) 使用auto_ptr修复–>内存泄漏问题

/*
 * 使用auto_ptr修复-->内存泄漏
 *
 * 析构函数可以执行到
 */

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

class Test
{
public:
    Test(int a = 0 )
    : m_a(a)
    {}

    ~Test( )
    {
        cout<<"Calling destructor"<<endl;
    }
public:
    int m_a;
};

int main()
{
    auto_ptr<Test> t1(new Test(3));
    cout << t1->m_a <<endl;

    return 0;
}

2) 使用auto_ptr修复–>浅copy导致的指针悬挂问题

/*
 * 使用auto_ptr修复-->使用auto_ptr修复-->浅copy导致的指针悬挂问题
 *
 * 输出:
 * destructor ---> Book
 *
 */

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

class HasPtr
{
public:
    HasPtr(char *s);
    ~HasPtr();

private:
    char *ptr;
};

HasPtr::HasPtr(char *s)
{
    if (s == nullptr)
    {
        ptr = new char[1];
        *ptr = '\0';
    }
    else
    {
        ptr = new char[strlen(s) +1];
        strcpy(ptr, s);
    }
}

HasPtr::~HasPtr()
{
    cout << "destructor ---> " << ptr << endl;
    delete ptr;
}

int main()
{
    auto_ptr<HasPtr>p1(new HasPtr("Book"));
    auto_ptr<HasPtr>p2(p1); // 所有权转移到p2,p1变为empty

    return 0;
}

3.2 auto_ptr的问题

1) 所有权转移(ownership transfer)

auto_ptr transfers the ownership when it is assigned to another auto_ptr. This is really an issue while passing the auto_ptr between the functions. Say, I have an auto_ptr in Foo( ) and this pointer is passed another function say Fun( ) from Foo. Now once Fun( ) completes its execution, the ownership is not returned back to Foo.
为了说明ownership transfer,参加如下代码

/*
 * auto_ptr’s ownership transfer
 *
 * 该程序运行会崩溃
 * 执行完Fun(t1),t1变成empty
 */

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

class Test
{
public:
    Test(int a = 0 )
    : m_a(a)
    {}

    ~Test( )
    {
        cout<<"Calling destructor"<<endl;
    }
public:
    int m_a;
};

void Fun(auto_ptr<Test> p1 )
{
    cout << p1->m_a << endl;
}

int main()
{
    auto_ptr<Test> t1(new Test(3));
    Fun(t1);
    cout << t1->m_a <<endl;

    return 0;
}

调试显示执行完Fun(t1)的情况:
auto_ptr控制权转移

2) 多个auto_ptr不能同时拥有同一个对象

//error code
Test *t1 = new Test(3);
auto_ptr<Test> ptr1(t1);
auto_ptr<Test> ptr2(t1);
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

这里ptr1与ptr2都认为指针t1是归它管的,在析构时都试图删除t1,这样就造成了重复释放问题。程序中释放已经不属于自己的空间,而是非常危险的,比起内存泄露,重复释放是一个更加严重的问题。有可能你第二次释放的空间已经被别的程序所使用,所以C/C++中视这种错误为致命错误,也就是说,我容许你局部的浪费,但绝对不容许你释放(使用)别人的东西。

3) 不能用auto_ptr管理数组指针

因为auto_ptr的析构函数中删除指针用的是delete,而不是delete [],所以我们不应该用auto_ptr来管理一个数组指针。如下的代码虽然可以编译通过,但是不要这样使用。

//bad code
auto_ptr<Test> p(new Test[5]);

4) auto_ptr不可做为容器(vector, list, map)元素

//error code
vector<auto_ptr<int> > vec;
auto_ptr<int> ptr1(new int(3));
vec.push_back(ptr1);

四、 unique_ptr

从第三节的内容可以看出auto_ptr具有比较多的缺陷,使用时容易出错。在C++ 11标准中出现了新的智能指针unique_ptr、 shared_ptr与weak_ptr等,这里首先介绍unique_ptr,可以将unique_ptr看成是auto_ptr的升级替代品。

4.1 基本操作

unique_ptr类中有get()、reset()、release()等函数。
get(): 获得原生指针
reset():重置,显式释放资源
reset(new XX):重置,重新指定对象
release():释放所有权到某一原生指针上
另外可以通过std::move将所有权由一个unique_ptr对象转移到另一个unique_ptr对象上,具体见下例。

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

int main()
{
    //1. unique_ptr的创建
    //1.1)创建空的,然后利用reset指定对象
    unique_ptr<int> up1; 
    up1.reset(new int(3)); 
    //1.2)通过构造函数在创建时指定动态对象
    unique_ptr<int> up2(new int(4));

    //2. 获得原生指针(Getting raw pointer )
    int* p = up1.get();

    //3.所有权的变化
    //3.1)释放所有权,执行后变为empty
    int *p1 = up1.release();
    //3.2)转移所有权,执行后变为empty
    unique_ptr<int> up3 = std::move(up2);

    //4.显式释放资源
    up3.reset();

    return 0;
}

4.2 禁止赋值和复制

unique_ptr禁止赋值和复制,“唯一”地拥有其所指对象,同一时刻只能有一个unique_ptr实例指向给定对象。也就是说模板类unique_ptr的copy构造函数以及等号(“=”)操作符是无法使用的。
通过禁止复制和赋值可以较好的改善auto_ptr的所有权转移问题。下面是其禁止复制和赋值的例子:

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

void Fun1( unique_ptr<int> up )
{
}

int main()
{
    unique_ptr<int> up1 = unique_ptr<int>(new int(10));

    //不允许复制(Copy construction is not allowed),所以以下三个均错误
    unique_ptr<int> up2 = up1;  // error
    unique_ptr<int> up3(up1);   // error
    Fun1(up1);                  // error

    //不允许赋值('='),所以下面错误
    unique_ptr<int> up4;
    up4 = up1;                  // error

    return 0;
}

4.3 针对auto_ptr缺陷的改善

1) 管理数组指针

因为unique_ptr有unique_ptr< X[ ] >重载版本,销毁动态对象时调用delete[],所以可以用unique_ptr来管理数组指针。

unique_ptr< Test[ ] > uptr1(new Test[3]);
//注意 unique_ptr<Test> uptr3(new Test[3]);是不对的
unique_ptr<int[]> uptr2(new int[5]);
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

2) 做容器(vector, list, map)元素

vector<unique_ptr<int> > vec;
unique_ptr<int> ptr1(new int(3));
vec.push_back(std::move(ptr1));
//vec.push_back(ptr1); //由于禁止复制这样不行

五、 shared_ptr

shared_ptr has the notion called shared ownership. The goal of shared_ptr is very simple: Multiple shared pointers can refer to a single object and when the last shared pointer goes out of scope, memory is released automatically.
从上面这段英文可以看出,shared_ptr是共享所有权的,其内部有一个计数机制,类似于第二节中我们自定义的智能指针类。

5.1 使用shared_ptr

shared_ptr类中有get()、reset()、unique()、swap()等函数。
get(): 获得原生指针
reset():重置,显式释放资源
reset(new XX):重新指定对象
unique ():检测对象管理者是否只有一个shared_ptr实例
在shared_ptr的RAII实现机制中,默认使用delete实现资源释放,也可以定义自己的函数来释放,比如当其管理数组指针时,需要delete[],这时就需要自定义释放函数。自定义释放的方法有两种:lambda表达式和括号操作符的重载。
另外可以通过dynamic_pointer_cast实现继承中的转换,具体见下例。

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

class Dealloc
{
public:
    Dealloc()
    {}

    //括号()操作符的重载
    void operator() (int* p )
    {
        if (p != nullptr)
        {
            //Do the custom deallocation job
            cout << "Dealloc called to release the resource " << p 
                 << " whose value is " << *p<<endl;
            delete p;
            p = nullptr;
        }
    }
};

class Base
{
public:
    Base() {}
    // 虚函数保证Base的多态性,以便在dynamic_pointer_cast中使用
    virtual void Foo() {}
};

class Derived : public Base
{
public:
    Derived() {}
};

int main()
{
    //1. 创建
    shared_ptr<int> sp1 = shared_ptr<int>(new int(100));
    shared_ptr<int> sp2 = make_shared<int>(int(10));
    auto sp3 = shared_ptr<int>(nullptr);
    if( sp3 == nullptr )
    {
        cout<<"Null pointer" << endl;
    }

    //2. 自定义资源释放函数
    {
        // lamdba表达式
        auto sp4 = shared_ptr<int>(new int[5], [ ](int* p){
            cout<<"In lambda releasing array of objects..."<<endl;
            delete[ ] p;});
    }
    {
        // 括号()操作符的重载
        auto sp5 = shared_ptr<int>(new int(1000), Dealloc() );
    }

    //3. 复制
    auto sp6(sp1);
    auto sp7 = sp1;

    //4. Getting raw pointer
    int* pRaw = sp2.get( );

    //5. Get how many shared pointers sharing the resource
    long nCount1 = sp1.use_count();
    long nCount2 = sp2.use_count();

    //6. Is this only shared pointer sharing the resource
    bool b1 = sp1.unique();
    bool b2 = sp2.unique();

    //7. swap
    sp1.swap(sp2);

    //8. reset
    sp1.reset();
    sp1.reset(new int(20));

    //9.Using dynamic_cast_pointer on shared pointer
    auto sp10 = shared_ptr<Derived>(new Derived( ));
    shared_ptr<Base> sp11 = dynamic_pointer_cast<Base>(sp10);
    if (sp11.get( ) != nullptr )
    {
        cout << "Dynamic casting from sp10 to sp11 succeeds...." << endl;
    }

    auto sp12 = shared_ptr<Base>(new Base());
    shared_ptr<Derived> sp13 = dynamic_pointer_cast<Derived>(sp12);
    if (sp13 != nullptr)
    {
        cout << "Dynamic casting from 12 to 13 succeeds...." << endl;
    }
    else
    {
        cout << "Dynamic casting from sp12 to sp13 failed ...." << endl;
    }

    return 0;
}

另外,shared_ptr对象的引用是强引用(stong_ref),如下图:
shared_ptr强引用

5.2 shared_ptr的三个问题

1) 多个独立的shared_ptr实例不能共享一个对象

类似于3.2节中auto_ptr的问题

int* p = new int;
shared_ptr<int> sptr1( p);

//right
shared_ptr<int> sptr2(sptr1); 

//wrong
//shared_ptr<int> sptr2(p);

2) 循环引用问题

循环引用例子:

/*
 * shared_ptr的循环引用(Cyclic Reference)问题
 *
 * 类A,B的析构函数没有得到执行
 * 资源没得到释放
 */

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

class B;
class A
{
public:
    A(  ) : m_sptrB(nullptr) { };
    ~A( )
    {
        cout<<" A is destroyed"<<endl;
    }
    shared_ptr<B> m_sptrB;
};

class B
{
public:
    B(  ) : m_sptrA(nullptr) { };
    ~B( )
    {
        cout<<" B is destroyed"<<endl;
    }
    shared_ptr<A> m_sptrA;
};

int main( )
{
    shared_ptr<B> sptrB( new B );
    shared_ptr<A> sptrA( new A );
    sptrB->m_sptrA = sptrA;
    sptrA->m_sptrB = sptrB;

    return 0;
}

过程分析:
shared_ptr循环引用过程

为了解决shared_ptr的循环引用问题,需要用到weak_ptr

3) shared_ptr的this问题

假设如下情况,类Thing的一个成员函数需要传递其this指针到一个普通函数,如果不用智能指针,没有问题,如下:

#include <iostream>
using namespace std;

class Thing;
void UseThingThisPointer(Thing *);

class Thing 
{
public:
    void foo()
    {
//注意这里this
        UseThingThisPointer(this);
    }

    void print()
    {
        cout << "class Thing" << endl;
    }
};

void UseThingThisPointer(Thing * ptr)
{
    ptr->print();
}

int main()
{
    Thing * t1 = new Thing;
    t1->foo();
    delete t1;
}
此时,我们需要使用shared_ptr智能指针来自动地管理Thing内存,如何使用呢?若我们简单的认为将所有的Thing *用shared_ptr<Thing>替换,代码如下,这时虽然可以编译通过,但是代码存在较大问题。
 //...
class Thing 
{
public:
    void foo()
    {
        //注意这里this
        shared_ptr<Thing> sp_for_this(this); // danger! a second manager object!
        UseThingThisPointer(sp_for_this);
    }
    //...
};

void UseThingThisPointer(shared_ptr<Thing> ptr)
{
    ptr->print();
}

int main()
{
    shared_ptr<Thing> t1(new Thing);
    t1->foo();
    return 0;
}

通过调试可以发现,在执行shared_ptr t1(new Thing)后创建了类Thing的一个强引用(管理对象),在foo函数中使用shared_ptr sp_for_this(this)也在原生指针上创建了一个强用(管理对象),这样共有两个shared_ptr管理同一个对象。但是在foo()执行完成时,sp_for_this超出范围,其管理的Thing对象会被释放,留下t1指向一个不存在的对象。最后程序运行结束,释放t1管理的对象时便出现问题。此现象引发的问题和shared_ptr的第一个问题(如下代码)一样。

int* p = new int;
shared_ptr<int> sptr1( p);
shared_ptr<int> sptr2( p );
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

为了解决shared_ptr的this问题问题,也需要用到weak_ptr。

六、 weak_Ptr

weak_ptr是为了配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针,因为它没有重载operator*和->,故而不具有普通指针的行为。它的最大作用在于协助shared_ptr工作。
weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。

6.1 使用weak_ptr

weak_ptr类中有use_count()、expired()等函数。
use_count():观测资源的引用计数
expired():等价于use_count()==0,但更快
lock():获取shared_ptr,当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr
具体见下例

/*
 * weak_ptr的使用
 */

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

class Test
{
public:
    Test(int a = 0) : m_a(a) { }
    ~Test( )
    {
        cout<<"Test object is destroyed whose value is = "<<m_a<<endl;
    }
public:
    int m_a;
};

int main( )
{
    //1. Create a shared pointer
    shared_ptr<Test> sp1(new Test(5) );

    //2. Create a weak pointer from the shared pointer
    weak_ptr<Test> wp1 = sp1;//(sp1);// = sp1;

    //3. Create a weak pointer from another weak pointer
    weak_ptr<Test> wp2(wp1);// = wp1;

    //4. Get the reference count of the shared pointer
    int nShared = sp1.use_count();
    int nWeak = wp1.use_count();

    //5.lock
    shared_ptr<Test> sp2 = wp1.lock();

    nShared = sp1.use_count();
    nWeak = wp1.use_count();

    //6. expired()
    {
        shared_ptr<Test> sp3 = shared_ptr<Test> (new Test(100));
        wp1 = sp3;       
    }
    if (wp1.expired())
    {
        cout << "expired" << endl;
    }

    return 0;
}

weak_ptr对象的引用是弱引用(weak ref),如下图:
弱引用(weak ref)

6.2 解决shared_ptr的问题

1) 解决shared_ptr循环引用问题

/*
 * 使用weak_ptr修复-->shared¬_ptr循环引用问题
 *  
 * 析构函数可以执行到
 * 
 */

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

class B;
class A
{
public:
    A(  )
    : m_a(5)  
    { };

    ~A( )
    {
        cout<<"A is destroyed"<<endl;
    }

    void PrintSpB( );

    weak_ptr<B> m_sptrB;
    int m_a;
};

class B
{
public:
    B(  )
    : m_b(10) 
    { };

    ~B( )
    {
        cout<<"B is destroyed"<<endl;
    }

    weak_ptr<A> m_sptrA;
    int m_b;
};

void A::PrintSpB( )
{
    if( !m_sptrB.expired() )
    {  
        cout<< m_sptrB.lock( )->m_b<<endl;
    }
}

int main( )
{
    shared_ptr<B> sptrB(new B );
    shared_ptr<A> sptrA(new A );
    sptrB->m_sptrA = sptrA;
    sptrA->m_sptrB = sptrB;
    sptrA->PrintSpB( ); 

    return 0;
}

2) 解决shared_ptr的this问题

/*
 * 使用weak_ptr修复--> shared_ptr的this问题
 *
 * 1) 继承模板类enable_shared_from_this
 * 2) 用shared_from_this()替换this
*
 */
#include <iostream>
#include <memory>
using namespace std;

class Thing;
void UseThingThisPointer(shared_ptr<Thing> ptr);

class Thing : public enable_shared_from_this<Thing>
{
public:
    void foo()
    {
        // get a shared_ptr from the weak_ptr in this object
        UseThingThisPointer(shared_from_this());
    }

    void print()
    {
        cout << "class Thing" << endl;
    }
};

void UseThingThisPointer(shared_ptr<Thing> ptr)
{
    ptr->print();
}

int main()
{
    shared_ptr<Thing> t1(new Thing);
    t1->foo();

    return 0;
}

分享:
silenceofwater Asked on 2016年10月31日 in Java.
Add Comment
0 Answer(s)

Your Answer

By posting your answer, you agree to the privacy policy and terms of service.