建構函式,解構函式和賦值操作符
幾乎所有的類都有一個或多個建構函式,一個解構函式和一個賦值操作符。這沒什麼奇怪的,因為它們提供的都是一些最基本的功能。建構函式控制對象產生時的基本操作,並保證對象被初始化;解構函式摧毀一個對象並保證它被徹底清除;賦值操作符則給對象一個新的值。在這些函數上出錯就會給整個類帶來無盡的負面影響,所以一定要保證其正確性。本章我將指導如何用這些函數來搭建一個結構良好的類的主幹。
條款11: 為需要動態分配記憶體的類聲明一個拷貝建構函式和一個賦值操作符
看下面一個表示string對象的類:
// 一個很簡單的string類
class string {
public:
string(const char *value);
~string();
... // 沒有拷貝建構函式和operator=
private:
char *data;
};
string::string(const char *value)
{
if (value) {
data = new char[strlen(value) + 1];
strcpy(data, value);
}
else {
data = new char[1];
*data = '/0';
}
}
inline string::~string() { delete [] data; }
請注意這個類裡沒有聲明賦值操作符和拷貝建構函式。這會帶來一些不良後果。
如果這樣定義兩個對象:
string a("hello");
string b("world");
其結果就會如下所示:
a: data——> "hello/0"
b: data——> "world/0"
對象a的內部是一個指向包含字串"hello"的記憶體的指標,對象b的內部是一個指向包含字串"world"的記憶體的指標。如果進行下面的賦值:
b = a;
因為沒有自訂的operator=可以調用,c++會產生並調用一個預設的operator=操作符(見條款45)。這個預設的賦值操作符會執行從a的成員到b的成員的逐個成員的賦值操作,對指標(a.data和b.data) 來說就是逐位拷貝。賦值的結果如下所示:
a: data --------> "hello/0"
/
b: data --/ "world/0"
這種情況下至少有兩個問題。第一,b曾指向的記憶體永遠不會被刪除,因而會永遠丟失。這是產生記憶體流失的典型例子。第二,現在a和b包含的指標指向同一個字串,那麼只要其中一個離開了它的生存空間,其解構函式就會刪除掉另一個指標還指向的那塊記憶體。
string a("hello"); // 定義並構造 a
{ // 開一個新的生存空間
string b("world"); // 定義並構造 b
...
b = a; // 執行 operator=,
// 丟失b的記憶體
} // 離開生存空間, 調用
// b的解構函式
string c = a; // c.data 的值不能確定!
// a.data 已被刪除
例子中最後一個語句調用了拷貝建構函式,因為它也沒有在類中定義,c++以與處理賦值操作符一樣的方式產生一個拷貝建構函式並執行相同的動作:對對象裡的指標進行逐位拷貝。這會導致同樣的問題,但不用擔心記憶體流失,因為被初始化的對象還不能指向任何的記憶體。比如上面代碼中的情形,當c.data用a.data的值來初始化時沒有記憶體流失,因為c.data沒指向任何地方。不過,假如c被a初始化後,c.data和a.data指向同一個地方,那這個地方會被刪除兩次:一次在c被摧毀時,另一次在a被摧毀時。
拷貝建構函式的情況和賦值操作符還有點不同。在傳值調用的時候,它會產生問題。當然正如條款22所說明的,一般很少對對象進行傳值調用,但還是看看下面的例子:
void donothing(string localstring) {}
string s = "the truth is out there";
donothing(s);
一切好象都很正常。但因為被傳遞的localstring是一個值,它必須從s通過(預設)拷貝建構函式進行初始化。於是localstring擁有了一個s內的指標的拷貝。當donothing結束運行時,localstring離開了其生存空間,調用解構函式。其結果也將是:s包含一個指向localstring早已刪除的記憶體的指標。
順便指出,用delete去刪除一個已經被刪除的指標,其結果是不可預測的。所以即使s永遠也沒被使用,當它離開其生存空間時也會帶來問題。
解決這類指標混亂問題的方案在於,只要類裡有指標時,就要寫自己版本的拷貝建構函式和賦值操作符函數。在這些函數裡,你可以拷貝那些被指向的資料結構,從而使每個對象都有自己的拷貝;或者你可以採用某種引用計數機制(見條款 m29)去跟蹤當前有多少個對象指向某個資料結構。引用計數的方法更複雜,而且它要求建構函式和解構函式內部做更多的工作,但在某些(雖然不是所有)程式裡,它會大量節省記憶體並切實提高速度。
對於有些類,當實現拷貝建構函式和賦值操作符非常麻煩的時候,特別是可以確信程式中不會做拷貝和賦值操作的時候,去實現它們就會相對來說有點得不償失。前面提到的那個遺漏了拷貝建構函式和賦值操作符的例子固然是一個糟糕的設計,那當現實中去實現它們又不切實際的情況下,該怎麼辦呢?很簡單,照本條款的建議去做:可以只聲明這些函數(聲明為private成員)而不去定義(實現)它們。這就防止了會有人去調用它們,也防止了編譯器去產生它們。關於這個俏皮的小技巧的細節,參見條款27。
關於本條款中所用到的那個string類,還要注意一件事。建構函式體內,在兩個調用new的地方都小心地用了[],儘管有一個地方實際只需要單個對象。正如條款5所說,在配套使用new和delete時一定要採用相同的形式,所以這裡也這麼做了。一定要經常注意,若且唯若相應的new用了[]的時候,delete才要用[]。
From:http://blog.csdn.net/steven216/article/category/275386