C++中的三种智能指针分析(RAII思想)

智能指针
首先咱们在理解智能指针以前咱们先了解一下什么是RAII思想。RAII(Resource Acquisition Is Initialization)机制是Bjarne Stroustrup首先提出的,是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络链接、互斥量等等)的简单技术。 
对于RAII概念清楚后,咱们就能够理解为智能指针就是RAII的一种体现,智能指针呢,它是利用了类的构造和析构,用一个类来管理资源的申请和释放,这样作的好处是什么?咱们来分析一下~node

为何会有智能指针
咱们来先看一段代码c++

void Fun()
{
    int *p = new int[1000];
    throw int(); //异常的抛出
    delete[] p;
}算法

int main()
{
    try
    {
        Fun();
    }
    catch (exception e)
    {
        printf("异常\n"); // 捕捉
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上面代码咱们能够看看,在咱们写的代码中,若是在运行中咱们开出了1000个int字节的空间,可是在咱们碰见throw后,由于异常而致使Fun函数没有执行到delete[],因此就会形成内存泄漏问题,这样的话对于一个服务器程序来讲是不能容许的。 
这个只是一个简单的例子,其实还有不少,好比咱们在写代码的时候每每会打开一个文件来进行读写操做,而后又是由于异常,致使咱们打开的文件没有及时的关闭,从而形成文件描述符泄漏也能够说是内存泄漏。服务器

为了防止这种场景的发生,咱们采用了智能指针。网络

智能指针分类
在C++的历史长河中,不断发展不断进步,因此在智能指针上也作了很大优化和该进,今天咱们就看看有那些指针类。函数

auto_ptr类
scoped_ptr类
share_ptr类+weak_ptr类
1
2
3
有三种智能指针的类。如今咱们最好可靠的是share_ptr。 
咱们先在分别介绍一下各自的特色以及简单的实现。优化

auto_ptr最先期的智能指针
咱们先来简单的用代码来实现一下:ui

template<class T>
class AutoPtr
{
public:
    AutoPtr(T* _ptr) :ptr(_ptr) // 构造
    {}this

    AutoPtr(const AutoPtr<T>& a) : ptr(a.ptr) //拷贝构造
    {
        a.ptr = NULL; 
        // 这里咱们实现为NULL ,这个其实也有缺点
        // 当咱们要用两个指针的时候就不行。
    }.net

    // NULL指针也能够释放
    AutoPtr<T>& operator=(AutoPtr<T>& a) // 赋值运算符重载
    {
        if (this != &a)
        {
            delete ptr;
            // 赋值过程当中,若是涉及原有空间,必定要先释放。
            // 还有在引用计数或者写实拷贝要先判断上一个
            // 是否被析构函数要减减它的引用计数
            ptr = a.ptr;
            a.ptr = NULL;
        }
        return *this;
    }

    ~AutoPtr() //析构
    {
        delete ptr;
    }

    T& operator*()
    {
        return *ptr;
    }

    T* operator->()
    {
        return ptr;
    }

private:
    T* 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
咱们实现完aotu_ptr最早感受就是对指针的一层封装,是的!可是有所不一样的是咱们交给它管理空间,就不会有最开始说的问题!

在C++早期的智能指针,并很差,为何这么说呢?由于若是咱们要对aotu_ptr进行赋值或者拷贝构造,那么被拷贝的将会为空,这样就为咱们的写代码加大难度,同时出错率也大大的提升。很容易形成访问越界。

因此后来人们就出现第二种~

scoped_ptr防拷贝赋值智能指针
出现这种智能指针,和上面很类似,可是这个处理上面问题的方式非常暴力,直接把赋值与拷贝写成私有声明。就跟本不能用。这个必定程度上减小代码的出错率,可是同时也产生了必定的局限性。 
咱们来看代码

template<class T> //模板实现
class Scoped_ptr
{
public:
    Scoped_ptr(T* _ptr) :ptr(_ptr) // 构造
    {}

    ~Scoped_ptr() //析构
    {
        if (ptr != NULL)
        {
            delete ptr;
        }
    }

    T& operator*()
    {
        return *ptr;
    }

    T* operator->()
    {
        return ptr;
    }

private:
    Scoped_ptr(const Scoped_ptr<T>& s); // 私有防止拷贝
    Scoped_ptr<T>& operator=(const Scoped_ptr<T>& s); // 防止赋值
    T* 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
更为简单,可是这不能知足人们的工做场景,因此就有了更为稳妥的一种方式

share_ptr采用引用计数的方式
咱们先来看代码 
这个代码咱们只是简单模拟实现,不是stl库中实现方式

template<class T> //模板
class Share_ptr
{
    // weak_ptr的friend 
    friend class Weak_ptr<T>; //这里先不用在乎,稍后解释
public:
    Share_ptr(T* _ptr) :ptr(_ptr), pCount(new int(1)) //构造
    {}

    Share_ptr(const Share_ptr<T>& s)  // 拷贝构造
    :ptr(s.ptr), pCount(s.pCount) // 这是一个指针
    {
        ++(*pCount); //对引用计数进行++
    }

    ~Share_ptr() //析构
    {
        if (*pCount == 1)
        {
            delete ptr;
            delete pCount;
        }
        else
        {
            --(*pCount);
        }
    }

    Share_ptr<T>& operator=(const Share_ptr<T>& s) //赋值重载
    {
        if (ptr != s.ptr)
        {
            if (--(*pCount) == 0)
            {
                if (ptr)
                {
                    delete ptr;
                }
                delete pCount;
            }
            ptr = s.ptr;
            pCount = s.pCount;
            ++(*pCount); // 注意 
        }
        return *this;
    }

    T& operator*()
    {
        return *ptr;
    }

    T* operator->()
    {
        return ptr;
    }

private:
    T* ptr;
    int* pCount; //这个采用引用计数时的计数器
};
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
58
59
60
61
62
share_ptr采用了引用计数的方式,更好解决了赋值与拷贝的问题。 
引用计数:咱们来说解一下,就是在构造出一个对象指针后,咱们也了一个*count这样的计数器,值就是1,当咱们须要拷贝或者赋值的时候,咱们就将 *count加1,让对象的指针指向同一块空间,每一个指针都能狗经过指针对象来访问指向的空间,咱们其中某一个对象要是声明周期完了,自动掉用析构函数,这时候,咱们的析构函数中就会判断引用计数是否为1,若是不是1说明这段空间还有别的对象在用,那么将会对 *count的计数器中的值减1,当 *count值为1的时候,而且改对象要析构,这时候才会正真的释放这段空间。

虽然说share_ptr比前面的两个指针指针都要好,可是在一种场景下share_ptr是不行的,什么呢?

咱们假如用share_ptr管理一个双向链表的结构,这个时候就会出现内存泄漏,为何呢?由于在管理链表中,当一个节点的next指向下一个,下一个指向上一个的时候,咱们的引用计数也在增长,这个时候,就会出现循环引用,具体状况是什么样子呢?咱们用图来解释~ 
 
 
因此为了解决这个问题就有个一个辅助的指针类,也叫弱指针。

weak_ptr辅助share_ptr智能指针
咱们看模拟实现的代码~

template<class T>
class Weak_ptr
{
public:
    Weak_ptr(const Share_ptr<T>& s) :ptr(s.ptr) //构造
    {}

    T& operator*()
    {
        return *ptr;
    }

    T* operator->()
    {
        return ptr;
    }

private:
    T* ptr;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
其实就是把原生指针进行了一层封装。因此不用本身实现析构,用默认就能够。 
还有一点就是由于weak_prt是share_ptr的辅助,而weak_ptr中须要访问share_ptr的私有成员因此咱们要在share_ptr中声明成友元类。

关于share_ptr与weak_ptr怎么解决像上面同样的场景,咱们来看一下:


// 循环引用,会形成内存泄漏weak_ptr
struct ListNode //链表结构体
{
    Weak_ptr<ListNode> next; //这里为weak_ptr<ListNode>类型
    Weak_ptr<ListNode> prev;
    int data;

    ListNode() :next(NULL), prev(NULL) //构造
    {}
};

// 在用share_ptr的时候要注意,share_ptr<ListNode> next; 
// 这里的share_ptr<ListNode>自己就是一个指针。

void TestListNode()
{
    Share_ptr<ListNode> node1 = new ListNode; //为share_ptr
    Share_ptr<ListNode> node2 = new ListNode;

    // 这里要解释一下,node1为share_ptr类型重载->,
    // 而next是weak_ptr类型,后面node2是一个share_ptr类型
    // 这里就有一个隐式类型转换
    node1->next = node2;
    node2->next = node1;
}
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
在说完share_ptr智能指针,不知到有没有发现,咱们申请空间都是一个一个类型的大小,释放也是一个而不是 []。就好比:咱们要用share_ptr管理一个10个int的大小。 
那么咱们实现的将不能本身正确的释放空间。因此咱们要介绍一个仿函数

仿函数
关于仿函数若是有了解STL中的六大组建,就会知道其中有一个就叫作仿函数,仿函数具体是怎么实现的呢?

其实很简单就是,在另外一个类中重载一下(),这样咱们就能够经过对象的()来对进行传参数,就像是函数调用同样,咱们来用代码来看看

template<class T>
struct DeleteArray
{
    void operator()(T* ptr) // 用来释放指针所指向的空间
    {
        delete[] ptr; 
    }
};
1
2
3
4
5
6
7
8
9
这个类中就重载了(),没有作其余事情,那么咱们就在用的时候直接用它的匿名对象进行()调用,就能够实现仿函数。若是这个不是很清楚,那么咱们在看一个例子:

// 用了仿函数
struct Less
{
    // 对()的重载
    int operator()(int x, int y)
    {
        return x+y;
    }
    // 用对象调用重载的()
};
int main()
{
    Less a;
    std::cout << a(1,8) << std::endl; // 就像函数同样调用
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
用这个有什么用处呢? 
第一就是咱们前面所提到的用new[] 开辟出来的空间咱们必需要用delete[]来进行释放,因此咱们要在share_ptr中传入仿函数,用来适应不一样的场景。 
仿函数也有不少用处,好比,咱们在STL中,用算法排序的时候,算法确定要知道从大到小函数从小到大,因此咱们传一个仿函数,就能够解决,增长了灵活性。 
由于在c++库中,share_ptr实现很是复杂,同时就实如今用法上稍微简单了一点,好比:

#include <memory>  share_ptr<string> p(new string[10], 对象);   对象重载了()  // 注意:这里的对象用来给share_ptr作定制删除器 1 2 3 后面的对象就是要传入的仿函数。由于咱们前面建立了[],因此仿函数就是要有delete[].

相关文章
相关标签/搜索