在C++編程中,動態分配的內存在使用完畢之後一般都要delete(釋放),否則就會造成內存洩漏,導致不必要的後果。雖然大多數初學者都會有這樣的意識,但是有些卻不以為意。我曾問我的同學關於動態內存的分配與釋放,他的回答是:”只要保證new和delete成對出現就行了。如果在構造函數中new(動態分配內存),那麼在析構函數中delete(釋放)就可以避免內存洩漏了!”
事實果真如此麼?
實例一:
(當子函數中動態分配的內存只在子函數中使用而不返回指向該動態內存的指針時,子函數中動態分配的內存即使不釋放也不會造成內存洩漏,因為在銷毀棧的同時,會自動釋放該內存。因此,我用main函數舉例)
int main(void) {
int *a = new int(35);
if (*a != 50) return 0;
delete a;
return 0;
}
在這種情況下,new和delete雖然成對出現,但是仍出現了內存洩漏的情況。因此,成對出現並不能保證不會發生內存洩漏。
實例二:
class A {
public:
A(): a(NULL) {}
A(int a) {
this->a = new int(a);
}
~A() {
if (a != NULL) delete a;
}
A operator =(A src) {
this->a = new int(src.a);
}
private:
int *a;
};
int main(void) {
A num(10);
A num1(15);
num = num1;
return 0;
}
在這種情況下,構造函數new(動態分配)一段內存,析構函數delete(釋放)這段內存。看似在對象建立時調用構造完成內存分配,在對象銷毀時調用析構釋放內存,一切都很正常不會造成內存洩漏,但是問題就出在了num = num1;
上,即是重載“=”出現問題導致內存洩漏。因為在賦值之前,num中的a已經指向堆中一段內存,而且是訪問該內存的唯一方式。一旦直接賦值,會導致這塊內存無法被訪問,因此導致了內存洩漏。
由此看來,內存洩漏的情況很容易出現,那麼有沒有方法可以避免這種情況發生呢?
這時,我們可以設計一個類似於資源管理的類,來實現對指針指向的動態內存的管理。這個類的精髓在於,成員變量用於指向動態內存,析構函數用於釋放該變量指向的動態內存。即:
template
class manage {
public:
manage(T *p) : pArr(p) {}
~manage() {
if (pArr != NULL) delete pArr;
}
T* get() { return pArr; }
private:
T *pArr;
};
這個資源管理類沒有設置無參數的構造函數的原因是:該類需滿足:RAII機制。
RAII機制:RAII,也稱為“資源獲取就是初始化”,是c++等編程語言常用的管理資源、避免內存洩露的方法。
RAII的做法:使用一個對象,在其構造時獲取資源,在對象生命期控制對資源的訪問使之始終保持有效,最後在對象析構的時候釋放資源。
那麼什麼是資源獲取就是初始化呢?那就是new(動態分配內存)後直接將地址作為參數傳入給資源管理類的對象(調用對象的構造函數)。
manage instance(new int[10]);
new(動態分配)的int[10]的地址就直接用於初始化instance對象,這就是資源獲取就是初始化。
如果我們再往這個類中加入成員方法,這就構成了一個完整的資源管理類,也就是智能指針auto_ptr。
According to cplusplus.com, auto_ptr is in
class template std::auto_ptr
template class auto_ptr;
Automatic Pointer [deprecated]
Note: This class template is deprecated as of C++11. unique_ptr is a new facility with a similar functionality, but with improved security (no fake copy assignments), added features (deleters) and support for arrays. See unique_ptr for additional information.
This class template provides a limited garbage collection facility for pointers, by allowing pointers to have the elements they point to automatically destroyed when the auto_ptr object is itself destroyed.
auto_ptr objects have the peculiarity of taking ownership of the pointers assigned to them: An auto_ptr object that has ownership over one element is in charge of destroying the element it points to and to deallocate the memory allocated to it when itself is destroyed. The destructor does this by calling operator delete automatically.
Therefore, no two auto_ptr objects should own the same element, since both would try to destruct them at some point. When an assignment operation takes place between two auto_ptr objects, ownership is transferred, which means that the object losing ownership is set to no longer point to the element (it is set to the null pointer).
Template parameters
X: The type of the managed object, aliased as member type element_type.
不過有一點十分重要,auto_ptr的對象在構造時獲得動態內存的ownership(所有權),且在析構時釋放這段內存,更重要的是,在調用復制構造函數時是實現ownership(所有權)的轉移而不是深拷貝,擁有這段動態內存的對象只能有一個。
auto_ptr 源碼如下:
template
class auto_ptr
{
private:
T*ap;
public:
//constructor & destructor-----------------------------------(1)
explicit auto_ptr(T*ptr=0)throw():ap(ptr) {}
~auto_ptr()throw() {
delete ap;
}
//Copy & assignment--------------------------------------------(2)
auto_ptr(auto_ptr& rhs)throw():ap(rhs.release()) {}
template
auto_ptr(auto_ptr&rhs)throw():ap(rhs.release()) {}
auto_ptr& operator=(auto_ptr&rhs)throw()
{
reset(rhs.release());
return *this;
}
template
auto_ptr& operator=(auto_ptr&rhs)throw()
{
reset(rhs.release());
return *this;
}
//Dereference----------------------------------------------------(3)
T& operator*()const throw()
{
return *ap;
T* operator->()const throw()
{
return ap;
}
//Helper functions------------------------------------------------(4)
//value access
T* get()const throw()
{
return ap;
}
//release owner ship
T* release()throw()
{
T* tmp(ap);
ap = 0;
return tmp;
}
//reset value
void reset(T* ptr = 0)throw()
{
if(ap != ptr)
{
delete ap;
ap = ptr;
}
}
//Special conversions-----------------------------------------------(5)
template
struct auto_ptr_ref
{
Y*yp;
auto_ptr_ref(Y*rhs):yp(rhs){}
};
auto_ptr(auto_ptr_refrhs)throw():ap(rhs.yp) {}
auto_ptr& operator=(auto_ptr_refrhs)throw()
{
reset(rhs.yp);
return*this;
}
template
operator auto_ptr_ref()throw()
{
return auto_ptr_ref(release());
}
template
operator auto_ptr()throw()
{
return auto_ptr(release());
}
};
在這之前,我要先說明源碼中重復出現的throw()函數。throw()函數類似一個聲明,保證了該函數不會拋出任何異常,因為STL需要保證異常安全。
異常安全是指,一個對象碰到異常之後,還能夠保證自身的正確性。
C++中’異常安全函數”提供了三種安全等級:(取自推薦的文章: “C++中的異常安全性”)
1. 基本承諾:如果異常被拋出,對象內的任何成員仍然能保持有效狀態,沒有數據的破壞及資源洩漏。但對象的現實狀態是不可估計的,即不一定是調用前的狀態,但至少保證符合對象正常的要求。
2. 強烈保證:如果異常被拋出,對象的狀態保持不變。即如果調用成功,則完全成功;如果調用失敗,則對象依然是調用前的狀態。
3. 不拋異常保證:函數承諾不會拋出任何異常。一般內置類型的所有操作都有不拋異常的保證。
其實不加這個throw()也是可以的,不過STL有時會要求加上。
下面我將對auto_ptr的源碼進行詳細的分析:
首先是構造函數與析構函數:
//constructor & destructor-----------------------------------(1)
explicit auto_ptr(T*ptr=0)throw():ap(ptr) {}
~auto_ptr()throw() {
delete ap; // delete dynamic storage
}
就如前面所講,這個類的核心就在於含有一個模板指針aq,以及析構函數delete(釋放)這個指針指向的動態內存。對於這個explicit,表明這個構造函數是一個顯式的構造函數。除了滿足谷歌風格以外,還限制了參數不能有隱式轉換。
接著是復制構造函數和release():
//release owner ship
T* release()throw()
{
T* tmp(ap);
/*here is a type-transformation, it have definition in this class and I will analyse it in the following passage */
ap = 0; // set it to NULL after transfer the ownership
return tmp; // return the address of dynamic storage
}
//Copy & assignment--------------------------------------------(2)
auto_ptr(auto_ptr& rhs)throw():ap(rhs.release()) {}
// call the release() to return the address of dynamic storage
template
auto_ptr(auto_ptr&rhs)throw():ap(rhs.release()) {}
// also have the same function as above one
auto_ptr& operator=(auto_ptr&rhs)throw()
{
reset(rhs.release()); // reset() will be analyse in the following passage
return *this;
}
// overload "=", and reset() must pay attention to delete the dynamic storage of (*this)
template
auto_ptr& operator=(auto_ptr&rhs)throw()
{
reset(rhs.release());
return *this;
}
// also have the same function as the above one
正如前面所講,復制構造函數是實現動態內存的ownership(所有權)的轉移,而不是深拷貝。為什麼不采用深拷貝?原因是:因為采用深拷貝就不再滿足我們設計該智能指針的初衷。
既然是實現ownership(所有權)的轉移,那麼要通過release()函數,讓原所有權擁有者放出所有權,使之返回原類型的指針,即動態內存的地址, 將之作為新所有權擁有者復制構造函數的參數。然後自身置NULL,不過所有權的新擁有者也要注意賦值前內存的釋放。
細心的人也許會感覺奇怪,auto_ptr是一個模板類,為什麼在復制構造函數裡既有尖括號,也有省略尖括號的。對於一個模板類,一般情況下不是都要加上尖括號麼?
剛開始我也感覺奇怪,後來想明白了:
在auto_ptr
的定義裡,auto_ptr默認是auto_ptr
。auto_ptr
與auto_ptr
是兩個不同的類,它們是相互獨立的。
因此就不難理解為什麼要這樣定義:
auto_ptr (auto_ptr& a) throw(); // the para's type is auto_ptr
template
auto_ptr (auto_ptr& a) throw(); // the para's type is auto_ptr instead of auto_ptr
相似的,重載”=”的兩個函數也不難理解了。
但是其實,不加auto_ptr (auto_ptr& a) throw();
好像也沒有什麼問題,因為編譯器一般都會優先調用用戶定義的函數。但是為了保險起見,還是加上為好。因為傳入相同類型,編譯器有調用默認構造函數的可能。
接著是重載 * 與 ->, 應該不難理解。
//Dereference----------------------------------------------------(3)
T& operator*()const throw()
{
return *ap;
}
T* operator->()const throw()
{
return ap;
}
再接著就是獲取成員指針的函數。
//value access
T* get()const throw() // interface for getting ap
{
return ap;
}
//release owner ship
因為ap是私有成員,需要get()函數提供一個訪問的接口。
然後繼續, 進行成員指針重置的函數。
//reset value
void reset(T* ptr = 0)throw()
{
if(ap != ptr)
{
delete ap; // very important , it avoid the storage leak
ap = ptr; // assign of ap
}
}
最後是在定義auto_ptr的一個代理類。剛開始我很疑惑,為什麼要再定義一個代理類,有什麼不能通過auto_ptr解決麼?原來auto_ptr的復制構造函數是有缺陷的。當傳入的參數為左值時,可以正常編譯,但是一旦傳入的參數為右值時,g++上就編譯不通過了。(左值右值在後面我會講解)因為右值引用必須為const引用。
//Special conversions-----------------------------------------------(5)
template
struct auto_ptr_ref // define a reference to automatic pointer
{
Y *yp;
auto_ptr_ref(Y *rhs):yp(rhs){} // constructor
};
auto_ptr(auto_ptr_refrhs)throw():ap(rhs.yp) {} // put auto_ptr_ref's object as a para
auto_ptr& operator=(auto_ptr_refrhs)throw()
{
reset(rhs.yp);
return *this;
}
/*here is the data_transformation. With the help of it, it solves the bug of auto_ptr
*/
template // transform data_type to auto_ptr_ref
operator auto_ptr_ref()throw()
{
return auto_ptr_ref(release());
}
template // transform data_type to auto_ptr
operator auto_ptr()throw()
{
return auto_ptr(release());
}
在查閱資料之前,我也是誤解了左值與右值的定義。左值與右值常見的誤區在與:認為等號左邊就是左值,等號右邊就是右值。其實不然,等號只是左值右值中的一個特例,並不能用於概括左值與右值的概念,即並不適用於所有地方。其實,左值與右值是相對於表達式而言,當一個表達式執行結束以後,若該對象仍恆定存在,那麼說明該對象是一個左值。如果在表達式結束後,該對象不存在,說明該對象是一個臨時對象,即為右值。