智能指针 auto_ptr、scoped_ptr、shared_ptr、weak_ptr

  什么是RAII?

RAII是Resource Acquisition Is Initialization的简称,是C++语言的一种管理资源、避免泄漏的惯用法。

RAII又叫做资源分配即初始化,即:定义一个类来封装资源的分配和释放,在构造函数完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。

  为什么要使用RAII?

在计算机系统中,资源是数量有限且对系统正常运行具有一定作用的元素。比如:网络套接字、互斥锁、文件句柄和内存等等,它们属于系统资源。由于系统的资源是有限的,所以,我们在编程使用系统资源时,都必须遵循一个步骤:
  1. 申请资源;
  2. 使用资源;
  3. 释放资源。

第一步和第三步缺一不可,因为资源必须要申请才能使用的,使用完成以后,必须要释放,如果不释放的话,就会造成资源泄漏。

  什么是智能指针?

 所谓智能指针就是智能/自动化的管理指针所指向的动态资源的释放。它是一个类,有类似指针的功能。 

  智能指针的实现原理

  当类中有指针成员时,一般有两种方式来管理指针成员:
  • 一是采用值型的方式管理,每个类对象都保留一份指针指向的对象的拷贝;
  • 另一种更优雅的方式是使用智能指针,从而实现指针指向的对象的共享。
  智能指针(smart pointer)的通用实现技术是使用引用计数(reference count)。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象的指针指向同一对象。每次创建类的新对象时,初始化指针就将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,析构函数减少引用计数(如果引用计数减至0,则删除基础对象)。

  常见的智能指针

包括:std::auto_ptrboost::scoped_ptr、boost::shared_ptr、boost::scoped_array、boost::shared_array、boost::weak_ptr、boost::intrusive_ptr

Boost库的智能指针(ps:新的C++11标准中已经引入了unique_ptr/shared_ptr/weak_ptr):

 

  auto_ptr  独占所有权,转移所有权 

第一种实现:最开始auto_ptr的成员变量主要有T* _ptr 和 bool _owner,主要实现原理是在构造对象时赋予其管理空间的所有权,在拷贝或赋值中转移空间的所有权,在析构函数中当_owner为true(拥有所有权)时来释放所有权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
template < typename T>
class AutoPtr
{
public :
    //构造函数
    explicit AutoPtr(T* ptr = NULL)
        :_ptr(ptr)
        , _owner( true )
    {}
    //拷贝构造
    AutoPtr(AutoPtr<T>& ap) //参数不能写成const的,这里要修改ap对象的成员<br>      :_ptr(ap._ptr)
        , _owner( true )
    {
        ap._owner = false //转让权限
    }
    //赋值运算符重载
    AutoPtr& operator=(AutoPtr<T>& ap)
    {
        if ( this ! = &ap)
        {
            delete this ->_ptr;
            this ->_ptr = ap._ptr;
            // 转让权限
            this ->_owner = true ;
            ap._owner = false ;
        }
        return * this ;
    }
    //析构函数
    ~AutoPtr()
    {
        // 只删除拥有权限的指针
        if (_owner)
        {
            this ->_owner = false ;
            delete this ->_ptr;
        }
    }
 
    T& operator*()
    {
        return *( this ->_ptr);
    }
 
    T* operator->()
    {
        return this ->_ptr;
    }
 
    T* AutoPtr<T>::GetStr()
    {
        return ( this ->_ptr);
    }
protected :
    T* _ptr;
    bool _owner; //权限拥有者
};

 出现的主要问题如果拷贝出来的对象比原来的对象先出作用域或先调用析构函数,则原来的对象的_owner虽然为false,但却在访问一块已经释放的空间,原因在于拷贝对象的释放会导致原对象的_ptr指向的内容跟着被释放!

问题体现如下

ap1将析构的权限给予了ap2,由于ap1._ptr和ap2._ptr指向同一份空间,ap2在出了if作用域之后自动被释放,进而导致ap1._ptr也被释放。

但是在if作用域之外,又对ap1._ptr进行访问,导致程序崩溃,这时候 ap1._ptr已经是一个野指针了,这就造成指针的悬挂的问题!

1
2
3
4
5
6
7
8
9
10
11
void Test()
{
        AutoPtr< int > ap1( new int (1));
        if ( true )
        {
            AutoPtr< int > ap2(ap1);
        }
 
        //这里的ap1._ptr已经是一个野指针了,这就造成指针的悬挂的问题
        *(ap1.GetStr() )= 10;
}

  auto_ptr的第二种实现方法:还是管理空间的所有权转移,但这种实现方法中没有_owner权限拥有者。构造和析构和上述实现方法类似,但拷贝和赋值后直接将_ptr赋为空,禁止其再次访问原来的内存空间,比较简单粗暴。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
template < typename T>
class AutoPtr
{
public :
    //构造函数
    explicit AutoPtr(T* ptr = NULL)  //不能写成const T* ptr,否则出现const类型赋值给非const类型的问题
        :_ptr(ptr)
    {}
    //拷贝构造
    AutoPtr(AutoPtr<T>& ap)
        :_ptr(ap._ptr)
    {
        ap._ptr=NULL; 
    }
    //赋值运算符重载
    AutoPtr& operator=(AutoPtr<T>& ap)
    {
        if ( this ! = &ap)
        {
            delete this ->_ptr;
            this ->_ptr = ap._ptr;
            ap._ptr = NULL;
        }
        return * this ;
    }
    //析构函数
    ~AutoPtr()
    {
        // 只删除拥有权限的指针
        if (_ptr)
        {
            delete _ptr;
        }
    }
 
    T& operator*()
    {
        return *_ptr;
    }
 
    T* operator->()
    {
        return _ptr;
    }
 
    T* GetStr()
    {
        return _ptr;
    }
protected :
    T* _ptr;
};

  这种实现方式很好的解决了旧版本野指针问题,但是由于它实现了完全的权限转移,所以导致在拷贝构造和赋值之后只有一个指针可以使用,而其他指针都置为NULL,使用很不方便,而且还很容易对NULL指针进行解引用,导致程序崩溃,其危害也是比较大的。

  scoped_ptr  独占所有权,防拷贝

scoped_ptr的实现原理是防止对象间的拷贝与赋值。具体实现是将拷贝构造函数和赋值运算符重载函数设置为保护或私有,并且只声明不实现,并将标志设置为保护或私有,防止他人在类外拷贝,简单粗暴,但是也提高了代码的安全性。 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
template < typename T>
class  ScopedPtr
{
public :
    ScopedPtr(T* ptr = NULL) 
        :_ptr(ptr)
    {}
 
    T& operator*()
    {
        return *_ptr;
    }
 
    T* operator->()
    {
        return _ptr;
    }
 
    T* GetStr()
    {
        return _ptr;
    }
    //析构函数
    ~ScopedPtr()
    {
        if (_ptr!=NULL)
        {
            delete _ptr;
        }
    }
protected :
    //防拷贝
    ScopedPtr(ScopedPtr<T>& ap);
    ScopedPtr& operator=(ScopedPtr<T>& ap);
 
    T* _ptr;
};

 scoped_ptr的实现和auto_ptr非常类似,不同的是 scoped_ptr有着更严格的使用限制——不能拷贝,这就意味着scoped_ptr 是不能转换其所有权的。

在一般的情况下,如果不需要对于指针的内容进行拷贝,赋值操作,而只是为了防止内存泄漏的发生,scoped_ptr完全可以胜任。

猜你喜欢

转载自www.linuxidc.com/Linux/2016-08/134315.htm