天天看点

SmartPtr(智能指针)为什么会有智能指针?智能指针的发展史:

为什么会有智能指针?

在我们现实生活中有这样一个使用场景,每次我们new或者malloc了一块空间,完了必须需要我们手动delete或者free释放这块空间,否则会出现内存泄漏。但是,实际应用中程序未必都会按照顺序执行到释放空间那一步,有时候会在中间部分抛出异常使执行流发生跳转或者发生其他状况,总之代码未按照我们期望的那样去执行,就有可能导致内存泄漏。这时候我们就引入智能指针(自动化管理指针所指向的内存资源的释放),主要是利用RAII机制,RAII它是在面向对象语言中的一种惯用法。资源分配以及初始化,定义一个类来封装资源的分配和释放,在构造函数完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。

智能指针的发展史:

第一个阶段(C++98):

auto_ptr:自动指针,它的主要思想是管理权转移,但其存在很大的缺陷,当要进行拷贝构造或者赋值相关操作时,原来的智能指针就失去意义,此时只有新的智能指针可以使用。简单点说,就是任何时候只能有一个智能指针指向那块空间,这样在使用上造成了很大的不便。

第二个阶段(C++03):

即第三方库Boost库中的智能指针:

scoped_ptr:守卫指针,它的主要思想就是防拷贝。

shared_ptr:共享指针,顾名思义就是共享同一块空间,它的主要思想是引入引用计数,但存在循环引用的缺陷。

weak_ptr:弱指针,它主要用来配合共享指针解决共享指针的循环引用的缺陷。

第三阶段(C++11):

unique_ptr:如同boost库中的scoped_ptr,主要思想就是防拷贝。

shared_ptr:它的主要思想是引入引用计数,但存在循环引用的缺陷。

weak_ptr:弱指针,它主要用来配合共享指针解决共享指针的循环引用的缺陷。

auto_ptr/scoped_ptr/shared_pr/weak_ptr的设计思想、缺陷?

<1>auto_ptr:

#include<iostream>
#include<string>
using namespace std;
template <class T>
class AutoPtr
{
public:
    //构造函数
    AutoPtr(T* ptr = NULL)
        :_ptr(ptr)
    {}
    //拷贝构造
    AutoPtr(AutoPtr<T>& ap)
    {
        //管理权转移
        _ptr = ap._ptr;
        ap._ptr = NULL;
    }
    //赋值运算符重载
    AutoPtr<T>& operator = (AutoPtr<T>& ap)
    {
        if(_ptr != ap._ptr)
        {
            if(_ptr)
            {
                delete _ptr;
            }
            //管理权转移
            _ptr = ap._ptr;
            ap._ptr = NULL;
        }
        return *this;
    }
    T* operator ->()
    {
        return _ptr;
    }
    T& operator *()
    {
        return *_ptr;
    }
    //析构函数
    ~AutoPtr()
    {
        cout<<"~AutoPtr()"<<endl;
        if(_ptr)
        {
            delete _ptr;
        }
    }
private:
    T* _ptr;
};
void TestAutoPtr()
{
    AutoPtr<int> ptr1(new int(10));
    AutoPtr<int> ptr2(ptr1);
    AutoPtr<int> ptr3(ptr2);
    cout<<*ptr3<<endl;
}
           

auto_ptr主要核心思想就是管理权的转移,当把ptr1赋值给ptr2或者用ptr1拷贝构造ptr2时,把ptr1的指针赋值为NULL,把管理权交给ptr2.

当我们进行简单的操作并不会出现问题:

SmartPtr(智能指针)为什么会有智能指针?智能指针的发展史:

但是,auto_ptr的思想是管理权的转移,也就是说当用ptr1去拷贝或者赋值给ptr2时,ptr1不再管理这块空间,ptr1会被赋值为NULL,当我们再去访问旧指针时,就会出错,但这并不是我们所期待的。

SmartPtr(智能指针)为什么会有智能指针?智能指针的发展史:

<2>scoped_ptr:

<span style="color:#000000">template <class T>
class ScopedPtr
{
public:
	ScopedPtr(T* ptr = NULL)
		:_Ptr(ptr)
	{}
	T* operator->()
	{
		return _Ptr;
	}
	T& operator*()
	{
		return *_Ptr;
	}
private:
	T* _Ptr;
	ScopedPtr<T>(ScopedPtr<T>& ap);
	ScopedPtr<T>& operator = (ScopedPtr<T>& ap);
};
void TestScopedPtr()
{
	ScopedPtr<int> ptr1(new int(6));
}
int main()
{
	TestScopedPtr();
	return 0;
}</span>
           

在aoto_ptr的基础上,我们发现auto_ptr的问题就是拷贝构造和赋值后,再次对旧指针进行操作时会出错。所以,scoped_ptr的思想就是防拷贝,在这里我们模拟实现,把拷贝构造和复制运算符(1)只声明不定义(2)将声明设置为私有。当用户再进行拷贝或者赋值时,程序就会报错,这样就解决了这个问题。但在实际应用场景中,我们是需要进行拷贝或者赋值的,所以这就不满足我们的需求。

SmartPtr(智能指针)为什么会有智能指针?智能指针的发展史:

<3>shared_ptr:

#include<iostream>
using namespace std;
template <class T>
class SharedPtr
{
public:
    SharedPtr(T* ptr = NULL)
        :_ptr(ptr)
        ,_refcount(new int(1))
    {}
    //ptr2(ptr1)
    SharedPtr(const SharedPtr<T>& sp)
    {
        _ptr = sp._ptr;
        _refcount = sp._refcount;
        (*_refcount)++;
    }
    //ptr2 = ptr1
    SharedPtr<T>& operator = (const SharedPtr<T>& sp)
    {
        if(_ptr != sp._ptr)
        {
            if(--(*_refcount) == 0)
            {
                delete _ptr;
                delete _refcount;
            }
            _ptr = sp._ptr;
            _refcount = sp._refcount;
            (*_refcount)++;
        }
        return *this;
    }
    T* GetPtr()
    {
        return _ptr;
    }
    T* operator->()
    {
        return _Ptr;
    }
    T& operator*()
    {
        return *_Ptr;
    }
    ~SharedPtr()
    {
        if(--(*_refcount)==0)
        {
            delete _ptr;
            delete _refcount;
        }
    }
private:
    T* _ptr;
    T* _refcount;
};
void TestSharedPtr()
{
    SharedPtr<int> ptr1(new int(1));
    SharedPtr<int> ptr2(ptr1);
    SharedPtr<int> ptr3(new int(3));
    ptr1 = ptr3;
}
//循环引用
struct ListNode
{
    SharedPtr<ListNode> _prev;
    SharedPtr<ListNode> _next;
    ~ListNode()
    {
        cout<<"~ListNode()"<<endl;
    }
};
void TestCycleRef()
{
    SharedPtr<ListNode> cur =  new ListNode;
    SharedPtr<ListNode> next = new ListNode;
    cur->_next = next;
    next->_prev = cur;
}
int main()
{
    TestSharedPtr();
    return 0;
}
           

当auto_ptr和scoped_ptr都不能满足我们实际需求时,这是我们引入shared_ptr,它的主要思想是引入了引用计数,使得多个指针可以指向同一块空间,当最后一个指针释放时才真正释放这块空间。

但是,当我们实例化为string [ ]对象时,就会出现问题程序会崩溃,因为string [ ]在开空间时会在头上多开四个字节,以致于记录所开string对象的个数,这样在析构时就知道析构多少次,但是我们析构函数中写的是delete,并不知道它会多开四个字节,就从实际存放对象的地方开始析构,前面四个字节就不会被析构造成内存泄漏,所以导致程序崩溃。所以,我们引入shared_array来解决此问题。

SmartPtr(智能指针)为什么会有智能指针?智能指针的发展史:

<3>shared_array:

#include<iostream>
#include<string>
using namespace std;
template <class T>
class SharedArray
{
public:
	SharedArray(T* ptr)
		:_ptr(ptr)
		,_refcount(new int(1))
	{}
	SharedArray(SharedArray<T>& s)
		:_ptr(s._ptr)
		,_refcount(s._refcount)
	{
		(*_refcount)++;
	}
	//传统写法
	/*SharedArray<T>& operator = (const SharedArray<T>& s)
	{
		if(_ptr != s._ptr)
		{
			if(--(*_refcount)==0)
			{
				delete[] _ptr;
				delete _refcount;
			}
			_ptr = s._ptr;
			_refcount = s._refcount;
			(*_refcount)++;
		}
		return *this;
	}*/
	//现代写法
	SharedArray<T>& operator = (SharedArray<T> s)
	{
		swap(_ptr,s._ptr);
		swap(_refcount,s._refcount);
		return *this;
	}
	int RefCount()
	{
		return *_refcount;
	}
	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}
	~SharedArray()
	{
		if(--(*_refcount)==0)
		{
			delete[] _ptr;
			delete _refcount;
		}
	}
private:
	T* _ptr;
	int* _refcount;
};
void TestSharedArray()
{
	SharedArray<int> ptr1(new int[10]);
	SharedArray<int> ptr2(ptr1);
	SharedArray<string> ptr3(new string[5]);
	SharedArray<string> ptr4(ptr3);
	ptr3[0] = "111";
	ptr3[1] = "222";
}
int main()
{
	TestSharedArray();
	return 0;
}
           

这就有效的解决了这个问题。

下面我们先来看一个实例:

struct ListNode
{
	SharedPtr<ListNode> _prev;
	SharedPtr<ListNode> _next;
	ListNode()
		:_prev(NULL)
		,_next(NULL)
	{}
	~ListNode()
	{
		cout<<"~ListNode()"<<endl;
	}
};
void TestCycleRef()
{
	SharedPtr<ListNode> cur =  new ListNode;
	SharedPtr<ListNode> next = new ListNode;
	cur->_next = next;
	next->_prev = cur;
}
int main()
{
	TestCycleRef();
	return 0;
}
           

调试窗口:

SmartPtr(智能指针)为什么会有智能指针?智能指针的发展史:

我们看到cur->_next和next->_prev共同管理cur这块空间,所以使得_refcount=2,但是想要cur->_refcoun--,这依赖于next.

next->_pre和cur->_next共同管理next这块空间,所以使得_refcount=2,但是想要next->_refcoun--,这依赖于cur.

SmartPtr(智能指针)为什么会有智能指针?智能指针的发展史:

也就是说,它们相互依赖对方,谁都释放不了,所以造成了循环引用。为了解决循环引用问题,我们引入weak_ptr.

<4>weak_ptr:

template <class T>
class WeakPtr
{
public:
	WeakPtr()
		:_ptr(NULL)
	{}
	WeakPtr(SharedPtr<T>& s)
		:_ptr(s.GetPtr())
	{}
	T* GetPtr()
	{
		return _ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	T& operator*()
	{
		return *_ptr;
	}
private:
	T* _ptr;
};

struct ListNode
{
	WeakPtr<ListNode> _prev;
	WeakPtr<ListNode> _next;
	~ListNode()
	{
		cout<<"~ListNode()"<<endl;
	}
};
void TestCycleRef()
{
	SharedPtr<ListNode> cur =  new ListNode;
	SharedPtr<ListNode> next = new ListNode;
	cur->_next = next;
	next->_prev = cur;
}
int main()
{
	TestCycleRef();
	return 0;
}
           

这时候的weak_ptr是不会增加shared_ptr的引用计数,这样在释放空间时就不会造成循环引用的问题。

但是,在我们平时使用过程中,如果是支持增加C++11的编译器,就可以直接包#include<memory>,就可以使用相应的函数,既方便又简单。

SmartPtr(智能指针)为什么会有智能指针?智能指针的发展史:

继续阅读