浅析智能指针

说到智能指针,就必须得先知道RAII,因为智能指针是RAII的一种应用,RAII 其实就是资源分配即初始化,,定义一个类来封装资源的分配和释放,在构造函数中完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。

那么我们为什么要用智能指针呢?我们不是已经学了分配资源和清理资源的动态内存管理的标识符new 和delete了吗?,完全够用了啊,为什么还要用智能指针呢,因为我们是为了预防发生意外情况,导致没有及时释放空间,从而导致内存泄漏,继而程序挂掉的现象。最常见的是在有异常的程序中,,
如下

test()
{
    int* p = new int;
    dosomething();....//这段函数出现异常;
    delete p; 
}

dosomething() 函数出现异常;就会发生跳转,,从而打断执行流,从而不执行delete p;所以这就很容易造成内存泄漏。。当然程序猿可以通过捕获异常来进行处理,从而释放空间。。但是这样一来,程序猿的一直得考虑异常的发生,从而增加工作量,并且还都是手动的(有没有非常考验人,万一忘了呢),而且还写出了不好看的代码,,那么我们有没有好的方法来管理空间呢。比如不用手动释放空间,,让它自动释放空间?
当然,答案是有的。。那就是我们的智能指针,,智能指针将我们原生的指针分装起来 ,成为一个对象,来管理空间,当对象出了作用域,,就会自动调用析构函数来释放空间 。。。
是不是很好用,,再也不用担心忘了释放空间(因为自动释放的喔)
下来我们就实现第一个智能指针(最早的智能指针,c++ 98 ~03 版本)先不说了,直接撸代码<用模板类实现>

//c++标准库实现Auto_ptr
#include <iostream>
using namespace std;

template <typename T>
class Auto_ptr
{
public:
    Auto_ptr(T* ptr)
        :_ptr(ptr)
    {}

    ~Auto_ptr()
    {
        if (_ptr != NULL)
        {
            delete _ptr;
        }
        _ptr = NULL;
    }
    //s2(s1);管理权的转移
    Auto_ptr(Auto_ptr<T>& s)
        :_ptr(s._ptr)
    {       
        s._ptr = NULL;
    }

    Auto_ptr<T>& operator= (Auto_ptr<T>& s)
    {
        if (this != &s)
        {
            delete _ptr;// 防止内存泄漏
            _ptr = s._ptr;
            s._ptr = NULL;
        }
        return *this;
    }

    T& operator*()
    {
        if (_ptr != NULL)
        {
            return *_ptr;
        }
    }

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

private:
    T* _ptr;
};

void test()
{
    int *p = new int(10);
    Auto_Ptr<int> ap1<p>;

}
int main()
{
    test();
    return 0;
}

(1)首先拷贝构造函数我们可以不写,可以用默认构造函数吗?显然不行的,,因为默认构造函数是值拷贝 ,将会两个指针指向同一块空间。析构的时候会析构两次。那么用深拷贝可以吗,,显然也是不行的,两个指针指向不同的空间,,怎么用一个指针管理?
我们伟大的设计师发明了一种方法,管理权的转移 ,来解决这个问题。具体看图
这里写图片描述
可以看出确实可以,,但是它把s._ptr 置空了,万一以后还用这个指针那该怎么办,所以说它是有缺陷的;
(2),同理,看看赋值运算符的重载,也可以写出相应的代码。
这里写图片描述
(3)智能指针得像我们原生指针一样的访问对象,,那我们的重载* 和->,注意重载*时 ,返回值一定是T& ,因为返回的是 临时对象(拷贝构造出来的)的引用(别名),出了作用域对象存在,可以其修改值。。如果不加& ,返回的是临时对象,,临时对象具有常性,所有不能修改其值,不能做作值了;
(4),重载->,ap2->a1 = 30;
调用ap2.operator->() 返回_ptr, _ptr->a1 = 30 ;编译器把->优化处理了,这点应该要理解。。
(5),析构函数。。当出现异常时,出了作用域,会调用析构函数,释放空间,不会担心打断程序执行流之后,内存没有被释放的问题。
(6) 构造函数,就是传一个指针进去。。

好了,Auto_Ptr 基本重要点就是这了 ,但是它有缺陷,所有尽量不用它,,
随着发展,又出现了一种新的智能指针,,它是在三方库boost里发展的,它叫ScopedPtr ,又叫防拷贝。。它的核心是继承了Autoptr ,但是将operator= 和拷贝构造改为了只声明,,不定义,,并且变为保护;这样就能防止其他人定义了。
接下来,,看代码

boost库实现scoped_ptr,scoped_Array
template <typename T>
class Scoped_ptr
{
public:
    Scoped_ptr(T* ptr)
        :_ptr(ptr)
    {}
    ~Scoped_ptr()
    {
        if (_ptr != NULL)
        {
            delete _ptr;
            _ptr = NULL;
        }
    }
    T* operator->()
    {
        return _ptr;
    }
    T& operator*()
    {
        return *_ptr;
    }

protected:
    Scoped_ptr(Scoped_ptr<T>& s);//只声明不实现,并且声明为私有,防止其他人实现并调用
    Scoped_ptr<T>& operator=(Scoped_ptr<T>& s);

private:
    T* _ptr;
};

template <typename T>
class ScopedArray
{
public:
    ScopedArray(T* ptr)
        :_ptr(ptr)
    {}
    ~ScopedArray()
    {
        if (_ptr != NULL)
        {
            delete[] _ptr;
            _ptr = NULL;
        }
    }
    T& operator[](int index)
    {
        return _ptr[index];
    }
protected:
    ScopedArray(ScopedArray<T>& sarr);
    ScopedArray& operator=(ScopedArray<T>& sarr);
private:
    T* _ptr;

};

其他的就是boost 库还有一个管理数组的智能指针,,基本的也没啥。。

随着c++11的出台,一种新的智能指针出现了 ,,它就是SharedPtr,它是基于引用计数来实现的,它功能强大,我们先看它的具体实现,,然后在做解释,它也是用模板类实现的。

//C++标准库实现SharedPtr
#include <iostream>
using namespace std;

template <typename T>
class SharedPtr
{
public:
    SharedPtr(ptr)
        :_ptr(ptr)
        , _refcount(new int(1));
    {}

    ~SharedPtr()
    {
        realse();
    }
    void realse()
    {
        if (--*(_refcount) == 0)
        {
            delete _ptr;
            delete _refcount;
        }
    }
    T& operator*()
    {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }

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

    SharedPtr<T>& operator=(const SharedPtr<T>& sp)
    {
        if (_ptr != sp._ptr)
        {
            this->realse();
            _ptr = sp._ptr;
            _refcount = sp._refcount;
            (*_refcount)++;
        }
        return *this;
    }

private:e
    T* _ptr;
    int* _refcount;
};

一,数据成员,

首先我们来看看它的数据成员,是两个指针,,分别指向不同空间。或许,你看起来也没啥问题。。但是如果你要知道第二个数据成员为什么要用int* _refcount,而不用int _refcount或者static _refcount,,那么你就太厉害了,,最起码我得想你学习,我也是前两天才学会的,我谈谈自己的理解。
ⅰ用 int _refcout
,这样每个智能指针对象都有一个引用计数,
比如,如图所示,
这里写图片描述
这三个引用计数都是3,,当S1指针不指向这会空间时,,引用计数减1,,变为2,另外的对象引用计数不会变,还是3,,当s2指针不指向这会空间时,引用计数也减1,变为2,它们会不影响,,s3 的时候也是一样的,,也变为2,这是这块空间应该要被析构的,但是最后一个指针不指向这块空间时,它引用计数是2 ,所以系统检测不析构,就会造成内存泄漏。

ⅱ用 static _refcout,让所有的对象共享一个引用计数,可不可以呢,,答案是,不行的,我们看图
这里写图片描述
因为它为static是 共享的,所以他们共用一块-refcount,,当s4,s5指向另外一块空间时,-refcount 会被加到5,然后空间得不到释放,所以也不行,,
ⅲ 用int* _refcount, 让每一个_refcount 同管理自己的一块空间。
当智能指针对象增加了,-refcount 就加一,减少了,就减一,这样一来能很好地控制空间的释放,我们看图
这里写图片描述
它的原理基本就是这,再看成员函数

二,成员函数

ⅰ构造函数,完成资源分配和初始化,没啥讲的
ⅱ析构函数,当引用计数位0时,,释放空间也没啥讲的,,代码一看就会
ⅲ,opreator* 和operator-> 和上面是一样的
ⅳ 拷贝构造,我们看图
这里写图片描述
由图看代码自然能明白
ⅴ.
operator= ,接着看图
这里写图片描述
我们注意自己不能给自己赋值,,指向同块空间的对象不能赋值,,还有就是正常情况下的赋值了,,看代码就会。。
说到这,,可以看出Sharedptr确实很强大,,但是它有循环引用的问题。。那么什么 是循环引用呢,接下来我们看一段代码

struct Node  
{  
    int _data;  
    shared_ptr<Node> _next;  
    shared_ptr<Node> _prev;  
};  
void test()  
{  
    shared_ptr<Node> sp1(new Node);  
    shared_ptr<Node> sp2(new Node);  
    cout << sp1.use_count() << endl;  
    cout << sp2.use_count() << endl;  
    sp1->_next = sp2;  
    sp2->_prev = sp1;  
    cout << sp1.use_count() << endl;  
    cout << sp2.use_count() << endl;  
}  

上边的程序最终会输出1 1 2 2,程序结束时,sp1和 sp2并不
会被析构(引用计数未被变为0),我们想要的是空间要被释放,但是它没释放,为什么呢?我们看图
这里写图片描述
那么我们该怎么解决呢,,我们引入weak_ptr弱指针,它是用来辅助shared_ptr,并不可以单独使用。将一个weak_ptr绑定到一个shared_ptr,并不会改变shared_ptr的引用计数,一旦最后一个
指向对象的shared_ptr被销毁,对象就会被销毁,即使有weak_ptr指向对象,对象还是会被释放。所以,上边定义的Node结构体改成下边这样,就能解决循环引用的问题。

struct Node  
{  
    int _data;  
    weak_ptr<Node> _next;  
    weak_ptr<Node> _prev;  
};  

二、定制删除器
智能指针的引入是为了解决因程序执行流的改变而引起的内存泄漏的问题。在构造函数中开辟和初始化,在析构函数中完成清理工作。智能指针是模板,所以就应该可以解决
各种类型的内存泄漏问题。在构造函数中打开的文件,在析构函数中完成文件的关闭,在构造函数中malloc开辟的空间,在析构函数中用free释放。所以,我们必须为每一种类型写出对应的内存释放的函数。通过仿函数(重载())来实现。下边展示定制删除器的模拟实现:

#include<iostream>  
using namespace std;  
#include<boost/shared_ptr.hpp>  

template<typename T,typename D = DefaultDelete>  
class SharedPtr  
{  
public:  
    SharedPtr(T* ptr)  
        :_ptr(ptr)  
        ,_pCount(new int (1))  
        ,_del(D())  
    {}  
    ~SharedPtr()  
    {  
        if (--*_pCount == 0)  
        {  
            //delete _ptr;  
            _del(_ptr);  
            delete _pCount;  
        }  
    }  
    SharedPtr(const SharedPtr<T,D>& sp)  
    {  
        _ptr = sp._ptr;  
        _pCount = sp._pCount;  
        ++*_pCount;  
    }  
    SharedPtr<T,D>& operator=(const SharedPtr<T,D>& sp)  
    {  
        if (_ptr != sp._ptr)  
        {  
            if (_ptr != nullptr)  
            {  
                if (--*_pCount == 0)  
                {  
                    //delete _ptr;  
                    _del(_ptr);  
                    delete _pCount;  
                }  
            }  
            else  
            {  
                _ptr = sp._ptr;  
                _pCount = sp._pCount;  
                ++(*_pCount);  
            }  
        }  
        return *this;  
    }  
private:  
    T* _ptr;  
    int* _pCount;  
    D _del;  
};  
//默认删除器  
struct DefaultDelete  
{  
    void operator() (void* ptr)  
    {  
        delete ptr;  
    }  
};  
struct Fclose  
{  
    void operator() (void* ptr)  
    {  
        fclose((FILE* )ptr);  
    }  
};  
struct Free  
{  
    void operator() (void* ptr)  
    {  
        free(ptr);  
    }  
};  
void test_del()  
{  
    SharedPtr<int>sp1(new int (1));  
    SharedPtr<FILE,Fclose>sp2(fopen("hello.txt","w"));  
}  

当智能指针管理的是文件时,出作用域时就会调用关闭文件的函数。这就是所谓的定制
删除器。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值