c++:分析智慧指標與發展歷史
智慧指標是RAII思想的一種產品,那麼什麼是RAII呢?
RAII:Resource Acquistion Is Initialization
資源分配就初始化,定義一個類來封裝資源的分配和釋放。
在建構函式裡完成資源分配初始化,在解構函式裡完成資源的清理。
簡單來舉例:
template <class T>
class AutoPtr
{
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{}
~AutoPtr()
{
if (_ptr)
{
delete _ptr;
}
}
private :
T* _ptr;
};
這個類就體現了RAII思想。
那麼智慧指標存在的意義是什麼呢?c++用new/delete一樣可以完成資源分配的初始化和清理。
問題就在於執行流的改變:
void Test()
{
int* p = new int(1);
if (p)
{
return;
}
delete p;
}
這段程式碼看似簡單無誤,其實已經存在了記憶體洩漏,因為return改變了執行流,導致無法釋放。
改變執行流的語句還有很多:break,continue,gote,throw等,這些改變執行流的語句都可能導致已經new的空間無法delete,一旦程式碼非常多的時候,我們很難控制。所以使用智慧指標是必要的!
首先,剛才寫的AutoPtr這個類既然叫智慧指標,那麼它就要像個指標,我們有必要實現它的operator*和operator->。
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
(AA就是一個用來舉例的,類有兩個整形的成員變數)
要像個指標還要能賦值,但是當我賦值時: AutoPtr ap3(ap);
它崩潰了,原因很好理解,淺拷貝導致的兩個物件的指向同一片空間,那麼析構的時候這片空間必然會析構兩次。還有一個問題,就是一個的改變會影響另一個。這些是我們不希望看到的,所以c++專家們在c++98給出瞭解決方案:auto_ptr
它的核心思想就是管理權轉移,當一個指標把值賦給另一個時,它馬上賦值為NULL。
以下是它的簡單模擬實現:
template <class T>
class AutoPtr
{
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{}
AutoPtr(AutoPtr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = NULL;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
if (this != &ap)
{
if (_ptr)
{
delete _ptr;
}
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~AutoPtr()
{
if (_ptr)
{
delete _ptr;
}
}
private:
T* _ptr;
};
這樣做就不會出問題,operator=同理。
但是這樣做有很大的缺陷,ap = NULL;那麼以後就不能使用它,很容易出錯。
現在它已經被禁用。
在c++98提出auto_ptr後,由於有很大缺陷,有人忍的了繼續使用,有人忍不了,所以非官方boost總結了一套方案它們分別是:
scoped_ptr shared_ptr weak_ptr
我來分別介紹:
1.scoped_ptr:防拷貝,簡單粗暴
template <class T>
class ScopedPtr
{
public:
ScopedPtr(T* ptr)
:_ptr(ptr)
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~ScopedPtr()
{
if (_ptr)
{
delete _ptr;
}
}
private:
//1.定為私有 2.只宣告不定義
ScopedPtr(ScopedPtr<T>& sp);
ScopedPtr<T>& operator=(ScopedPtr<T>& sp);
private:
T* _ptr;
};
直接把拷貝建構函式和operator=定義為私有,而且只宣告,那麼不能賦值就不會出錯,這是個簡單粗暴的辦法!
2.shared_ptr:用了引用計數的方法
template <class T>
class SharedPtr
{
public:
SharedPtr(T* ptr)
:_ptr(ptr)
, _refCount(new int(1))
{}
SharedPtr(SharedPtr<T>& sp)
:_ptr(sp._ptr)
, _refCount(sp._refCount)
{
++(*_refCount);
}
SharedPtr<T>& operator=(SharedPtr<T>& sp)
{
if (_ptr != sp._ptr)
{
if (--(*_refCount) == 0)
{
delete _ptr;
delete _refCount;
}
_ptr = sp._ptr;
_refCount = sp._refCount;
++(*_refCount);
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~SharedPtr()
{
if (--(*_refCount) == 0)
{
delete _ptr;
delete _refCount;
}
}
private:
T* _ptr;
int* _refCount;
};
可以看出,_refCount用來計數,接下來分析它的賦值:
拷貝構造:
operator=:
它有不同的情況:
①自己給自己賦值:sp1 = sp1;或者sp2 = sp1;(sp2已經等於sp1)
②如果被賦值物件的*_refCount == 1,那麼直接釋放它,如果被賦值物件的*_refCount > 1,那就把它的_refCount減1.
然後解構函式根據*_refCount來決定是否清理物件,就不會存在淺拷貝的問題。
3.weak_ptr是用來解決shared_ptr的迴圈引用的問題,我在《c++:分析智慧指標shared_ptr存在的迴圈引用的缺陷》一文會詳細分析。
接下來在c++11,官方延用了boost總結的方法,需要注意的是,c++11把scoped_ptr改名為unique_ptr,並且沒有寫陣列。
智慧指標發展歷史的總結:
boost和c++11有一些重名的智慧指標,但是不影響使用,因為他們分別在boost::和std::