C++之--智能指针Smart Pointer

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Zhang_1218/article/details/78622759

原文见www.louhang.xin

设计思想

最近在看《C++ Primer Plus》,不得不说这本书确实非常棒,里面把C++很多的语法都讲到的很清楚。这几天看了智能指针,所以在此,简单的进行一下剖析。
智能指针,顾名思义,既其首先是指针,其次其具有智能部分。具体是什么意思呢?

  • RAII(Resource Acquisition Is Initialization)

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

我们来考虑这种情况,当我们new一块空间后,应用完后就要进行delete,但是在我们delete前,程序throw了一个异常,跳转了程序,那么可想而知,造成的后果必然是内存的泄露。

可能有人会想到,那为什么不把delete放在异常前面呢?对的,这样妥协的话,确实是可以解决现在的问题。但是,有多少人在书写代码时都会忘记释放动态空间,更何况现在还要刻意的去避免这种情况;再者,当在一个比较大的项目或工程中时,代码的情况更为复杂,这时候只要有一点点的空间没有得到释放,那么对这个工程来说,就是一场灾难。

那么这个时候智能指针派上用场了。可想而知,智能指针则能自动获取内存空间,并且在应用完毕后自动完成空间的释放,不会存在内存泄漏。那么这一点是不是和类创建的对象很相像。当创造类对象时,会自动调用构造函数来进行初始化,而当类对象失效时,则会自动的调用析构函数来进行清理工作。实质上,智能指针也正是利用了这个思想。

- 智能指针

 用类对象的思想创建一个模板类(以适应不同类型的需求),而后将基本类型指针进行了一层封装,使其成为类对象指针,能够自适应开辟空间,并且在析构函数内部调用delete来完成空间的释放。

深入剖析

1. auto_ptr

最早期的智能指针当属C++98提出的auto_ptr,这个版本的智能指针相当简单,既只是简单的完成了空间释放上的问题,当期存在赋值操作的时候,则进行的是所有权权的转移,因此其存在巨大的缺陷。简单来说,既可能会造成一定程度上的内存泄漏和系统崩溃。
所有权(ownership):对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。既称此指针具有该对象的所有权。

#include <iostream>
#include<memory>
void auto_ptrTest()
{
    std::auto_ptr<int> ptr1(new int(100));
    std::auto_ptr<int> ptr2;
    ptr2 = ptr1;
    std::cout << *ptr2 << std::endl;
    *ptr1 = 50;
    std::cout << *ptr1 << std::endl;
}

程序运行结果:
这里写图片描述
可以很清楚的看到程序运行崩溃,这就是因为ptr1进行了所有权的转移,之后其已经成为了一个空指针,因此不能够再去进行访问。
下面来看我简单模拟实现的auto_ptr指针:

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

    AutoPtr(AutoPtr& p)
        :_ptr(p._ptr)
    {
        p._ptr=NULL;
    }

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

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

    AutoPtr<T>& operator=(AutoPtr<T>& a)
    {
        if (this != &a)
        {
            if (_ptr)
            {
                delete _ptr;
            }
            //所有权转移
            _ptr = a._ptr;
            a._ptr = NULL;
        }
        return *this;
    }

    ~AutoPtr()
    {
        if (_ptr!=NULL)
        {
            delete _ptr;
        }
    }
private:
    T* _ptr;
};

2. scoped_ptr / unique_ptr

由于auto_ptr中存在的巨大缺陷,对于很多工程和项目来说都是潜在危险,所以随之就诞生了相对来说更完善的智能指针模型。
boost库中的 scoped_ptr 以及 C++11中的unique_ptr。这两个智能指针模型底层实现基本一样,因此,在这里概为一体,进行论述。
这两个智能指针人送外号:守卫指针。
”守卫“既保护,scoped_ptr/unique_ptr为了避免auto_ptr的所有权转移问题,进行了相当简单粗暴的规定,既直接规定,不允许进行拷贝构造和赋值运算等相关运算。

#include <iostream>
#include<memory>
void unique_ptrTest()
{
    std::unique_ptr<int> ptr1(new int(100));

    std::unique_ptr<int> ptr2;
    ptr2 = ptr1;

    std::unique_ptr<int> ptr3(ptr1);
}

程序运行结果:
这里写图片描述
这样一来因为不允许拷贝构造和赋值运算,所以也就压根就不会存在内存崩溃的问题。但是这样却不像指针了。
下面是我模拟实现的scoped_ptr 指针:

//scoped_ptr
template <class T>
class ScopedPtr
{
public:
    ScopedPtr(T* ptr)
        :_ptr(ptr)
    {}

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

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

    ~ScopedPtr()
    {
        if (_ptr!=NULL)
        {
            delete _ptr;
            _ptr = NULL;
        }
    }
private:
    //防拷贝
    //1.定义为私有的
    //2.只声明,不定义
    ScopedPtr(const ScopedPtr<T>& p);
    ScopedPtr<T> operator=(const ScopedPtr<T>& p);

private:
    T* _ptr;
};

以下是测试用例:

void ScopedPtrTest()
{
    ScopedPtr<int> p1(new int(2));
    *p1 = 10;
    cout << *p1 << endl;

    ScopedPtr<double> p2(new double(1));
    *p2 = 1.0;
    cout << *p2 << endl;

    //不可访问
    //ScopedPtr<int> p3(p1);
    //ScopedPtr<int> p4(new int(2));
    //p4 = p1;
}

可以看到,scoped_ptr为了达到守卫效果,首先是将函数定义为私有的,这样外界无法访问,其次只声明,不定义。这样即便是在类内部业无法进行函数的调用。这样通过两方面,来达到守卫效果。

3. shared_ptr

shared_ptr 人称:共享指针。
boost库中的shared_ptr与C++11中的shared_ptr具有同样的思想,运用引用计数来进行拷贝构造和赋值运算。这样较为完美的解决了之前的问题(有关引用计数详见之间博客)。虽然这样解决了问题,但是也同样带来了麻烦,shared_ptr相比于其他两个智能指针要更为复杂,应用起来也更为麻烦。

#include <iostream>
#include<memory>
void shared_ptrTest()
{
    std::shared_ptr<int> ptr1(new int(10));
    std::shared_ptr<int> ptr2(ptr1);
    std::shared_ptr<int> ptr3;
    ptr3 = ptr2;
    std::cout << "ptr1->" << *ptr1 << std::endl;
    std::cout << "ptr2->" << *ptr2 << std::endl;
    std::cout << "ptr3->" << *ptr3 << std::endl;
}

程序运行结果:
这里写图片描述

下面的是我简单模拟实现的shared_ptr指针:


//shared_ptr
template <class T>
class SharedPtr
{
public:
    SharedPtr(T* ptr=NULL)
        :_ptr(ptr)
        ,_refcount(new int(1))
    {}

    SharedPtr(const SharedPtr<T>& s1)
        :_ptr(s1._ptr)
        ,_refcount(s1._refcount)
    {
        (*_refcount)++;
    }

    SharedPtr<T>& operator=(const SharedPtr<T>& s1)
    {
        if (this != &s1)
        {
            if (--(*_refcount) == 0)
            {
                delete _ptr;
                delete _refcount;
            }

            _ptr = s1._ptr;
            _refcount = s1._refcount;
            (*_refcount)++;
        }

        return *this;
    }

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

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

    int UseCount()
    {
        return *_refcount;
    }

    ~SharedPtr()
    {
        if (--(*_refcount)==0)
        {
            delete _ptr;
            delete _refcount;
        }

        cout << "~SharedPtr()" << endl;
    }

private:
    T* _ptr;
    int* _refcount;//引用计数
};

以下为测试用例:

void SharedPtrTest()
{
    SharedPtr<int> s1(new int(1));
    *s1 = 3;
    SharedPtr<int> s2(new int(2));
    *s2 = 3;
    SharedPtr<int> s3(s1);
    cout << *s3 << endl;
    SharedPtr<int> s4;
    s4 = s2;
    cout << *s2 << endl;
}

这里写图片描述
我们可以看到上面的程序都运行无误,那么是不是说shared_ptr就已经完美解决了我们的问题呢?
来看代码:


#include <iostream>
#include<memory>

struct ListNode
{
    int _data;

    std::shared_ptr<ListNode> _next;
    std::shared_ptr<ListNode> _prev;

    ListNode()
    {
        std::cout << "调用析构函数" << std::endl;
    }
};

void shared_ptrTest()
{

    std::shared_ptr<ListNode> p1(new ListNode);     
    std::shared_ptr<ListNode> p2(new ListNode);
    std::cout << "p1->" << p1.use_count() << std::endl;
    std::cout << "p1->" << p2.use_count() << std::endl;

    p1->_next = p2;
    p2->_prev = p1;

    std::cout << "p1->" << p1.use_count() << std::endl;
    std::cout << "p1->" << p2.use_count() << std::endl;
}

程序运行结果:
这里写图片描述

上面程序运行没有报错,没有警告,看似很成功的运行了代码。但是仔细考虑一下就会发现问题。程序结束后应该要调用析构函数的,这里怎么没有调用?shared_ptr的引用计数在析构之前应该是1呀,这里怎么变成了2?
这就是shared_ptr的一个缺陷。

这里存在的问题是循环引用。下面我通过一副简单的图来进行分析:
这里写图片描述

可以看到,由于_next和_prev的指向关系,导致p1与p2的引用计数分别加1,而当释放空间的时候,由于引用计数为2,减减不为0,所以就没有调用析构函数进行空间的清理,造成了内存的泄露。
为了解决这个问题,就需要用到另外一个智能指针。

4. weak_ptr

weak_ptr 又称弱指针。其是专为shared_ptr而生的。首先来看代码:

struct ListNode
{
    int _data;

    //std::shared_ptr<ListNode> _next;
    //std::shared_ptr<ListNode> _prev;

    std::weak_ptr<ListNode> _next;
    std::weak_ptr<ListNode> _prev;


    ~ListNode()
    {
        std::cout << "调用析构函数" << std::endl;
    }
};

void shared_ptrTest()
{

    std::shared_ptr<ListNode> p1(new ListNode);     
    std::shared_ptr<ListNode> p2(new ListNode);
    std::cout << "p1->" << p1.use_count() << std::endl;
    std::cout << "p1->" << p2.use_count() << std::endl;

    p1->_next = p2;
    p2->_prev = p1;

    std::cout << "p1->" << p1.use_count() << std::endl;
    std::cout << "p1->" << p2.use_count() << std::endl;
}

程序运行结果:
这里写图片描述

可以看到,在将ListNode内部的指针更改为weak_ptr型的后,成功的调用了析构函数,完成了空间的释放。
那么weak_ptr是怎么样实现的呢?
以下是我简单模拟的weak_ptr指针:

template <class T>
class WeakPtr
{
public:
    WeakPtr()
        :_ptr(NULL)
    {}

    WeakPtr(const SharedPtr<T>& sp)
        :_ptr(sp._ptr)
    {}

    WeakPtr<T>& operator=(const SharedPtr<T>& p)
    {
        _ptr = p._ptr;
        return *this;
    }

private:
    T* _ptr;
};

可以看到,weak_ptr 对shared_ptr进行了引用,而且只是单纯的进行了赋值操作,并未改变引用计数。

可以分析:weak_ptr 是 shared_ptr 的观察者(Observer)对象,观察者意味着 weak_ptr 只对 shared_ptr 进行引用,而不改变其引用计数,当被观察的 shared_ptr 失效后,相应的 weak_ptr 也相应失效。
所以,weak_ptr的作用就是观测shared_ptr的对象,时刻掌握资源的使用情况。而其本身并未具备资源的管理权。

总结

智能指针在实际项目和工程中的应用很广泛,所以,要清楚熟悉掌握其特性,最后在此略作总结:

  1. 拒绝使用auto_ptr ,除非万不得已,因为auto_ptr 危害太大,而且企业不符合C++的编程思想。

  2. 如果对象需要进行共享的话,那么就用boost库中的scope_ptr 或者C++11的unique_ptr。

  3. 如果需要共享对象的话,则用shared_ptr,但是切记weak_ptr要和shared_ptr配合使用,否则会出现内存泄漏等未知情况。

猜你喜欢

转载自blog.csdn.net/Zhang_1218/article/details/78622759