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

智能指针

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

为什么会有智能指针

我们来先看一段代码

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

int main()
{
    try
    {
        Fun();
    }
    catch (exception e)
    {
        printf("异常\n"); // 捕捉
    }
    return 0;
}

上面代码我们可以看看,在我们写的代码中,如果在运行中我们开出了1000个int字节的空间,但是在我们遇见throw后,因为异常而导致Fun函数没有执行到delete[],所以就会造成内存泄漏问题,这样的话对于一个服务器程序来说是不能允许的。
这个只是一个简单的例子,其实还有很多,比如我们在写代码的时候往往会打开一个文件来进行读写操作,然后又是因为异常,导致我们打开的文件没有及时的关闭,从而造成文件描述符泄漏也可以说是内存泄漏。

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

智能指针分类

在C++的历史长河中,不断发展不断进步,所以在智能指针上也做了很大优化和该进,今天我们就看看有那些指针类。

auto_ptr类
scoped_ptr类
share_ptr类+weak_ptr类

有三种智能指针的类。现在我们最好可靠的是share_ptr。
我们先在分别介绍一下各自的特点以及简单的实现。

auto_ptr最早期的智能指针

我们先来简单的用代码来实现一下:

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

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

    // NULL指针也可以释放
    AutoPtr& operator=(AutoPtr& 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;
};

我们实现完aotu_ptr最先感觉就是对指针的一层封装,是的!但是有所不同的是我们交给它管理空间,就不会有最开始说的问题!

在C++早期的智能指针,并不好,为什么这么说呢?因为如果我们要对aotu_ptr进行赋值或者拷贝构造,那么被拷贝的将会为空,这样就为我们的写代码加大难度,同时出错率也大大的提高。很容易造成访问越界。

所以后来人们就出现第二种~

scoped_ptr防拷贝赋值智能指针

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

template //模板实现
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& s); // 私有防止拷贝
    Scoped_ptr& operator=(const Scoped_ptr& s); // 防止赋值
    T* ptr;
};

更为简单,但是这不能满足人们的工作场景,所以就有了更为稳妥的一种方式

share_ptr采用引用计数的方式

我们先来看代码
这个代码我们只是简单模拟实现,不是stl库中实现方式

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

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

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

    Share_ptr& operator=(const Share_ptr& 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; //这个采用引用计数时的计数器
};

share_ptr采用了引用计数的方式,更好解决了赋值与拷贝的问题。
引用计数:我们来讲解一下,就是在构造出一个对象指针后,我们也了一个*count这样的计数器,值就是1,当我们需要拷贝或者赋值的时候,我们就将 *count加1,让对象的指针指向同一块空间,每个指针都能狗通过指针对象来访问指向的空间,我们其中某一个对象要是声明周期完了,自动掉用析构函数,这时候,我们的析构函数中就会判断引用计数是否为1,如果不是1说明这段空间还有别的对象在用,那么将会对 *count的计数器中的值减1,当 *count值为1的时候,并且改对象要析构,这时候才会正真的释放这段空间。

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

我们假如用share_ptr管理一个双向链表的结构,这个时候就会出现内存泄漏,为什么呢?因为在管理链表中,当一个节点的next指向下一个,下一个指向上一个的时候,我们的引用计数也在增加,这个时候,就会出现循环引用,具体情况是什么样子呢?我们用图来解释~
C++中的三种智能指针分析(RAII思想)_第1张图片
C++中的三种智能指针分析(RAII思想)_第2张图片
所以为了解决这个问题就有个一个辅助的指针类,也叫弱指针。

weak_ptr辅助share_ptr智能指针

我们看模拟实现的代码~

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

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

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

private:
    T* ptr;
};

其实就是把原生指针进行了一层封装。所以不用自己实现析构,用默认就可以。
还有一点就是因为weak_prt是share_ptr的辅助,而weak_ptr中需要访问share_ptr的私有成员所以我们要在share_ptr中声明成友元类。

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


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

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

// 在用share_ptr的时候要注意,share_ptr next; 
// 这里的share_ptr本身就是一个指针。

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;
}

在说完share_ptr智能指针,不知到有没有发现,我们申请空间都是一个一个类型的大小,释放也是一个而不是 []。就比如:我们要用share_ptr管理一个10个int的大小。
那么我们实现的将不能自己正确的释放空间。所以我们要介绍一个仿函数

仿函数

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

其实很简单就是,在另一个类中重载一下(),这样我们就可以通过对象的()来对进行传参数,就像是函数调用一样,我们来用代码来看看

template<class T>
struct DeleteArray
{
    void operator()(T* ptr) // 用来释放指针所指向的空间
    {
        delete[] ptr; 
    }
};

这个类中就重载了(),没有做其他事情,那么我们就在用的时候直接用它的匿名对象进行()调用,就可以实现仿函数。如果这个不是很清楚,那么我们在看一个例子:

// 用了仿函数
struct Less
{
    // 对()的重载
    int operator()(int x, int y)
    {
        return x+y;
    }
    // 用对象调用重载的()
};
int main()
{
    Less a;
    std::cout << a(1,8) << std::endl; // 就像函数一样调用
    return 0;
}

用这个有什么用处呢?
第一就是我们前面所提到的用new[] 开辟出来的空间我们必须要用delete[]来进行释放,所以我们要在share_ptr中传入仿函数,用来适应不同的场景。
仿函数也有很多用处,比如,我们在STL中,用算法排序的时候,算法肯定要知道从大到小函数从小到大,所以我们传一个仿函数,就可以解决,增加了灵活性。
因为在c++库中,share_ptr实现非常复杂,同时就实现在用法上稍微简单了一点,比如:

#include 
 share_ptr<string> p(new string[10], 对象);   对象重载了()
 // 注意:这里的对象用来给share_ptr做定制删除器

后面的对象就是要传入的仿函数。因为我们前面创建了[],所以仿函数就是要有delete[].

如果有误, 还望多多指导!谢谢!

你可能感兴趣的:(C++)