直觀的operator=是這樣定義的:
1 class SampleClass 2 { 3 private: 4 int a; 5 double b; 6 float* p; 7 public: 8 SampleClass& operator= (const SampleClass& s) 9 { 10 a = s.a; 11 b = s.b; 12 p = s.p; 13 return *this; 14 } 15 };
就是將自身的私有成員的值全部賦值成另一個對象的私有成員的值。若沒有顯式定義operator=,編譯器會生成的默認的operator=,生成的結果也是這個樣子。但注意此時私有成員中含有指針float *p,為了達到深拷貝的目的(不拷貝指針的地址,而拷貝指針所指向的空間內容),應該這樣寫:
1 class SampleClass 2 { 3 private: 4 int a; 5 double b; 6 float* p; 7 public: 8 SampleClass& operator= (const SampleClass& s) 9 { 10 a = s.a; 11 b = s.b; 12 delete p; 13 p = new float(*s.p); 14 return *this; 15 } 16 };
大致思路就是刪除指針所指向的舊內容,而后再用這個指針指向一塊新的空間,空間的內容填充s.p所指向的內容。但有兩件事會導致這段代碼崩潰,其一就是本條款所說的“自我賦值”。讀者不妨想想看,如果這樣:
1 SampleClass obj; 2 obj = obj;
所發生的事情。在賦值語句執行時,檢測到obj.p已經有指向了,此時會釋放掉obj.p所指向的空間內容,但緊接着下一句話就是:
p = new float(*s.p);
注意*s.p會導致程序崩潰,因為此時s.p也就是obj.p,對其取值*obj.p(根據優先級,這相當於*(obj.p)),obj.p已經在前一句話被釋放掉了,所以這樣的操作會有bug。
也許讀者不以為意,認為用戶不可能傻到會寫obj = obj這樣的代碼出來。事實上也確實如此,明顯的錯誤不大可能會犯,但萬一寫一個:
1 SampleClass obj; 2 … 3 SampleClass& s = obj; 4 … 5 s = obj;
或者
1 SmapleClass* p = &obj; 2 … 3 *p = obj;
這種錯誤就不那么直觀了,甚至*pa = *pb也有可能出問題,因為pa與pb大有可能指向的是同一個地址空間。自我賦值一個不小心就會發生,決不要假設用戶不用寫出自我賦值的語句來。
解決自我賦值只要一句話:
1 class SampleClass 2 { 3 private: 4 int a; 5 double b; 6 float* p; 7 public: 8 SampleClass& operator= (const SampleClass& s) 9 { 10 if(this == &s) return *this; // 解決自我賦值的一句話 11 a = s.a; 12 b = s.b; 13 delete p; 14 p = new float(*s.p); 15 return *this; 16 } 17 };
我以前曾經這樣寫過:
1 class SampleClass 2 { 3 private: 4 int a; 5 double b; 6 float* p; 7 public: 8 SampleClass& operator= (const SampleClass& s) 9 { 10 if(*this == s) return *this; // 注意條件判斷的不同,這樣寫有問題! 11 a = s.a; 12 b = s.b; 13 delete p; 14 p = new float(*s.p); 15 return *this; 16 } 17 };
但這樣是不對的,因為==經常是用於對象內每一個成員變量是否相同的判斷,而不是地址是否重疊的判斷。所以用this == &s才能從地址上來捕捉到是否真的是自我賦值。
這樣做確實能解決上面所說的第一問題:自我賦值。事實上還可能出現另一個問題導致代碼崩潰,試想,如果p = new float(*s.p)不能正常分配空間怎么辦,突然拋出了異常怎么辦,這將導致原有空間的內容被釋放,但新的內容又不能正常填充。有沒有一個好的方法,在出現異常時,還能保持原有的內容不變呢?(可以提升程序的健壯性)
這有兩種思路,書上先給出了這樣的:
1 SampleClass& operator= (const SampleClass& s) 2 { 3 if(this == &s) return *this; //可以刪掉 4 a = s.a; 5 b = s.b; 6 float* tmp = p; // 先保存了舊的指針 7 p = new float(*s.p); // 再申請新的空間,如果申請失敗,p仍然指向原有的地址空間 8 delete tmp; // 能走到這里,說明申請空間是成功的,這時可以刪掉舊的內容了 9 return *this; 10 }
大致的思路是保存好舊的,再試着申請新的,若申請有問題,舊的還能保存。這里可以刪掉第一句話,因為“讓operator具備異常安全往往自動獲得自我賦值安全的回報”。
還有一種思路,就是先用臨時的指針申請新的空間並填充內容,沒有問題后,再釋放到本地指針所指向的空間,最后用本地指針指向這個臨時指針,像這樣:
1 SampleClass& operator= (const SampleClass& s) 2 { 3 if(this == &s) return *this; //可以刪掉 4 a = s.a; 5 b = s.b; 6 float* tmp = new float(*s.p); // 先使用臨時指針申請空間並填充內容 7 delete p; // 若能走到這一步,說明申請空間成功,就可以釋放掉本地指針所指向的空間 8 p = tmp; // 將本地指針指向臨時指針 9 return *this; 10 }
上述兩種方法都是可行,但還要注意拷貝構造函數里面的代碼與這段代碼的重復性,試想一下,如果此時對類增加一個私有的指針變量,這里面的代碼,還有拷貝構造函數里面類似的代碼,都需要更新,有沒有可以一勞永逸的辦法?
本書給出了最終的解決方案:
1 SampleClass& operator= (const SampleClass& s) 2 { 3 SampleClass tmp(s); 4 swap(*this, tmp); 5 return *this; 6 }
這樣把負擔都交給了拷貝構造函數,使得代碼的一致性能到保障。如果拷貝構造函數中出了問題,比如不能申請空間了,下面的swap函數就不會執行到,達到了保持本地變量不變的目的。
一種進一步優化的方案如下:
1 SampleClass& operator= (const SampleClass s) 2 { 3 swap(*this, s); 4 return *this; 5 }
注意這里去掉了形參的引用,將申請臨時變量的任務放在形參上了,可以達到優化代碼的作用。
最后總結一下:
(1) 確保當對象自我賦值時operator=有良好的行為,其中技術包括比較“來源對象”和“目標對象”的地址、精心周到的語句順序、以及copy-and-swap;
(2) 確定任何函數如果操作一個以上的對象,而其中多個對象是同一個對象時,其行為仍然正確。