條款11:在**重點內容**operator=中處理“自我賦值”
“自我賦值”發生在對象被賦值給自己時:
class Widget{…}
Widget w;
…
w=w;//賦值給自己
a[i]=a[j];//潛在的自我賦值,如果i和j有相同的值,這便是個自我賦值
*px=*py;//潛在的自我賦值,如果px和py指向同一個問題,這也是自我賦值
甚至如下這樣也可以:
class Base {…};
class Derived:public Base {…};
void doSomething(const Base& rb,Derived* pd);//rb和pd可能其實是同一個對象
很多情況下“自我賦值”是安全的,不需要額外操心,然而如果你嘗試自行管理資源(如果你打算寫一個用於資源管理的class就這樣做),可能會掉進“在停止使用資源之前以外釋放了它”的陷阱,如:
class Bitmap {...};class Widget{ ...private: Bitmap* pb;};Widget& Widget::operator=(const Widget& rhs){ delete pb; pb=new Bitmap(*rhs.pb); return *this;}
這裡的自我賦值問題是,operator=函數內部*this和rhs有可能是同一個對象,如果這樣的話,delete pb銷毀的不只是當前對象那個的bitmap,同時rhs的bitmap也被銷毀掉了,那麼在自我賦值的時候程式會發現rhs.pb指標指向的是一個已經被刪除的對象。
如何來阻止這種錯誤呢,具體有多種方法:
1)傳統做法是operator=最前面加一個“證同測試”達到“自我賦值”的校正目的,具體見如下代碼:
Widget& Widget::operator=(const Widget& rhs){ if(this==&rhs) return *this;//證同測試 delete pb; pb=new Bitmap(*rhs.pb); return *this;}
這樣的代碼有沒有什麼錯誤呢。可以看到,具備了“自我賦值安全性”,那還有什麼問題呢。答案就是“異常安全性”,這種解決方案對於異常並沒有什麼很好的作用,具體來說,如果new Bitmap(*rhs.pb)出現異常的話(不論是因為分配時記憶體不足或者Bitmap的copy建構函式拋出異常),pb最終指向一個被刪除的Bitmap,這樣的指標會出現null 指標異常等乙烯類問題。
2)這種方法主要側重於“異常安全性”方面,我們只需注意在賦值pb所指的東西之前別刪除pb:
Widget& Widget::operator=(const Widget& rhs){ Bitmap* pOrig=pb; pb=new Bitmap(*rhs.pb); delete pOrig;//刪除原先的pb return *thsis;}
這樣的話,如果“new Bitmap”拋出異常,pb保持原狀,即使沒有“證同測試”,這段代碼還是可以處理自我賦值,因為我們對原bitmap做了一份複件、刪除原bitmap、然後指向新製造的那個複件。PS:如果此時需要考慮效率的話,需要先估計“自我賦值”發生的頻率有多高。如果高的話,我們就將“證同測試”加在函數起始處。如果“證同測試”發生頻率不高的話,推薦不要添加“證同測試”,因為添加這項測試也需要耗費成本,降低程式執行效率。
3)如果對上述兩種方法還是不太滿意的話,還有一個替代方案,不僅“異常安全”同時“自我賦值安全”,即使用copy and swap技術實現,參看以下代碼:
class Widget{ ... void swap(Widget& rhs); ...};Widget& Widget::operator=(const Widget& rhs){ Widget temp(rhs);//使用copy建構函式,為rhs資料製作一份副本 swap(temp); //將*this資料和上述所述複件的資料交換 return *this;}
怎樣,這種方法不錯吧。但是代碼看起來邏輯並沒有多麼清晰,這也是這種方法的缺點吧。
總結:
1)確保當前對象自我複製時operator=有良好的行為,其中包括“來來源物件”和“目標對象”的地址、精心周到的語句順序以及copy-and-swap操作;
2)確定任何函數如果操作一個以上的對象,而其中多個對象是同一個對象那個時候,其行為依然正確。