樣本:
class MyString
{
public:
MyString(const char* value)
{
if(value != NULL)
{
data = new char[strlen(value)+1];
strcpy(data, value);
}
else
{
data = new char[1];
*data = '\0';
}
}
~String()
{
delete[] data;
}
private:
char* data;
};
此時MyString類裡沒有聲明賦值操作符和拷貝建構函式,這會帶來一些不良後果。
例如:
MyString a("Hello");
MyString b("World");
b = a;
由於沒有自訂的operator=可以調用,C++會產生並調用一個預設的operator=操作符。這個預設的賦值操作符會執行從a的成員到b的成員的逐個成員的賦值操作,對指標(a.data和b.data)來說就是逐位拷貝。
這種情況下至少有兩個問題。第一,b曾指向的記憶體永遠得不到釋放。這是產生記憶體泄露的典型例子。第二,現在a.data和b.data指向同一個字串,那麼只要其中一個離開了它的生命週期,其解構函式就會刪除掉另一個指標還指向的那塊記憶體。(/by me 即產生野指標)
拷貝建構函式的情況和賦值操作符類似,但也有不同。在傳值調用的時候,它會產生問題。
看下面例子:
void doNothing(MyString localString){}
MyString s = "The Truth Is Out There";
doNothing(s);
一切好像都很正常。但因為被傳遞的localString是一個值,它必須從s通過(預設)拷貝建構函式進行初始化。於是localString擁有了一個s內的指標(data)的拷貝。當doNothing結束運行時,localString離開了其生存空間,調用解構函式。其結果也將是:s包含了一個指向localString早已刪除的記憶體的指標。(野指標)
解決方案:
解決這類指標混亂問題的方案在於:(1)只要類裡有指標時,就要寫自己版本的拷貝建構函式和賦值操作符函數。在這些函數裡,程式員可以拷貝那些被指向的資料結構,從而使每個對象都有自己的拷貝;(2)或者可以採用某種引用計數機制去跟蹤當前有多少個對象指向某個資料結構。(2)方法更複雜,而且要求建構函式和解構函式內部做更多的工作,但是在某些(雖然不是所有)程式裡,它會大量節省記憶體並切實提高速度。
對於有些類,當實現拷貝建構函式和賦值操作符非常麻煩的時候,特別是可以確信程式中不會做拷貝和賦值操作的時候,去實現它們就會相對有點得不償失。前面提到的那個遺漏了拷貝建構函式和賦值操作符的例子固然是一個糟糕的設計,那當現實中去實現它們又不切實際的情況下,改怎麼辦?很簡單,照本條款的建議去做:可以只聲明這些函數(聲明為private成員)而不去定義(實現)它們。這就防止了會有人去調用它們,也防止了編譯器去產生它們。關於這個俏皮的小技巧的細節,參見條款27。