讀書筆記_Effective_C++_條款十一:在operator=中處理自我賦值


 

直觀的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)     確定任何函數如果操作一個以上的對象,而其中多個對象是同一個對象時,其行為仍然正確。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM