構造/析構/賦值 函數

條款10:令operator=返回一個reference to *this

賦值操作符運算是由右向左運算的。例如一個連鎖賦值
[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. <span style="font-size:14px;">int x, y, z;  
  2. x=y=z=15;</span>  

編譯器解釋時時這樣的:
x=(y=(z=15));
先給z賦值,用賦值後的z再給y賦值,用賦值後的y再給x賦值。
爲了實現連鎖賦值,操作符必須返回一個reference指向操作符左側的實參。
其實,如果operator=不返回一個引用,返回一個臨時對象,照樣可以實現連鎖賦值。但這個臨時對象的構建會調用拷貝構造函數。看下面這個例子:
[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. class Widget  
  5. {  
  6.   
  7. public:  
  8.     Widget()  
  9.     {  
  10.         cout<<"Default Ctor"<<endl;  
  11.     }  
  12.     Widget(const Widget& rhs)  
  13.     {  
  14.         cout<<"Copy Ctor"<<endl;  
  15.     }  
  16.     Widget& operator=(const Widget& rhs)  
  17.     {  
  18.         cout<<"operator="<<endl;  
  19.         return *this;  
  20.     }  
  21. };  
  22. int main()  
  23. {  
  24.     Widget a,b,c;  
  25.     a=b=c;  
  26.     return 0;  
  27. }  
這樣輸出爲:
Default Ctor
Default Ctor
Default Ctor
operator=
operator=
如果把operator=返回的引用去掉,改爲Widget operator=(const Widget& rhs)
則會輸出:
Default Ctor
Default Ctor
Default Ctor
operator=
Copy Ctor
operator=
Copy Ctor
返回臨時對象,臨時對象再給左側變量賦值。多出了一步,浪費資源。

operator=是改變左側操作數的,與其類似的operator+=、operator-=等改變左側操作符的運算,都應該返回引用。這是一個協議,應該去遵守。

條款11:在operator=中實現“自我賦值”

自我賦值是指對象給自己賦值。
Widget w;
w=w;
這樣看起來有點愚蠢,但是它合法。上面這個例子很容易發現自我賦值。但有時候就不那麼容易了,例如:數組a
a[i]=a[j];
當i和j相等時,就是自我賦值。
例如,兩個指針px和py
*px=*py;
如果兩個指針指向同一個對象,這也是自我賦值。
除此之外,還有引用。更加隱晦的自我賦值發生在基類和派生類層次中,不同類型的指針或引用之間的賦值都有可能發生自我賦值。

如果遵循條款13和條款14,運用對象來管理資源,確定“資源管理對象”在copy發生時有正確的舉措,這樣自我賦值是安全的。如果自己管理資源,可能會“在停止使用資源之前意外釋放了它”。
例如使用一個class管理一個指針。
[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. class Widget  
  2. {  
  3. public:  
  4.     Widget& operator=(const Widget& rhs)  
  5.     {  
  6.         delete p;  
  7.         p=new int(ths.p);  
  8.         return *this;  
  9.     }  
  10.     int *p;  
  11. };  
如果上面代碼自我賦值,在使用指針p之前已經將其釋放掉了。
防止這種問題發生的辦法是“證同測試”,在刪除前判斷是不是自我賦值
[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. class Widget  
  2. {  
  3. public:  
  4.     Widget& operator=(const Widget& rhs)  
  5.     {  
  6.         if(this==&rhs)//證同測試  
  7.             return *this;  
  8.         delete p;  
  9.         p=new int(rhs.p);  
  10.         return *this;  
  11.     }  
  12.     int *p;  
  13. };  
這個版本的operator=可以解決自我賦值的問題。但是還有個問題:異常安全。如果delete p成功,而p=new int(rhs.)失敗會發生什麼?
這時,widget對象會持有一個指針,這個指針指向了被釋放的內存。下面方法可以實現異常安全。
[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. class Widget  
  2. {  
  3. public:  
  4.     Widget& operator=(const Widget& rhs)  
  5.     {  
  6.         int tmp=p;//記錄原先內存  
  7.         p=new int(rhs.p);  
  8.         delete tmp;//釋放原先內存  
  9.         return *this;  
  10.     }  
  11.     int *p;  
  12. };  
在實現異常安全的同時,其實也獲取了自我賦值的安全。如果p=new int(ths.p)發生異常,後面的delete tmp就不會執行。
如果你很關心效率,可以把“證同測試”放到函數起始處。但是“自我賦值”發生的頻率有多高?因爲“證同測試”也需要成本,因爲它加入了新的控制分支。
還有一個替代方案是:copy and swap技術。這個技術和異常安全關係密切,條款29詳細說明。下面看它怎麼實現
[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. class Widget  
  2. {  
  3. public:  
  4.     void swap(const Widget& rhs);//交換rhs和this  
  5.     Widget& operator=(const Widget& rhs)  
  6.     {  
  7.         Widget tmp(rhs);//賦值一份數據  
  8.         swap(tmp)//交換  
  9.         return *this;//臨時變量會自動銷燬  
  10.     }  
  11.     int *p;  
  12. };  
如果賦值操作符參數是值傳遞,那麼就不需要新建臨時變量,直接使用函數參數即可。
[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. class Widget  
  2. {  
  3. public:  
  4.     void swap(const Widget& rhs);//交換rhs和this  
  5.     Widget& operator=(const Widget rhs)  
  6.     {  
  7.         swap(rhs)  
  8.         return *this;  
  9.     }  
  10.     int *p;  
  11. };  
這個做法代碼可讀性比較差,但是將“copying動作”從函數體內移到“函數參數構造階段”,編譯器有時會生成效率更高的代碼(by moving the copying operation from the body of the function to construction of the parameter, it's fact that compiler can sometimes generate more efficient code.


條款12:複製對象時勿忘其每一部分

在一個類中,有兩個函數可以給複製對象:複製構造函數和賦值操作符,統稱爲copying函數。在條款5中講到,如果我們自己不編寫者兩個函數,編譯器會幫我們實現這兩個函數,編譯器生成的版本會將對象的所有成員變量做一份拷貝。編譯器生成的copying函數的做法通常是淺拷貝。可以參考這裏
如果我們自己實現了copying函數,編譯器就不再幫我們實現。但是編譯器不會幫我們檢查copying函數是否給對象的每一個變量都賦值。
下面有一個消費者的類
[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. class Cutsomer  
  2. {  
  3. public:  
  4.     Cutsomer()  
  5.     {  
  6.         name="nobody";  
  7.     }  
  8.     Cutsomer(const Cutsomer& rhs)  
  9.         :name(rhs.name)  
  10.     {  
  11.         cout<<"Customer Copy Ctor"<<endl;  
  12.     }  
  13.     Cutsomer& operator=(const Cutsomer& rhs)  
  14.     {  
  15.         cout<<"assign operator"<<endl;  
  16.         name=rhs.name;  
  17.         return *this;  
  18.     }  
  19. private:  
  20.     string name;  
  21. };  
這樣的copying函數沒有問題,但是如果再給類添加變量,例如添加一個電話號碼
[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. class Cutsomer  
  2. {  
  3. ……  
  4. private:  
  5.     string name;  
  6.     string telphone;  
  7. };  
這時copying函數不做更改,即便是在最高警告級別,編譯器也不會報錯,但是我們的確少拷貝了內容。
由此可以得出結論:一旦給類添加變量,那麼自己編寫的copying函數也要修改,因爲編譯器不會提醒你。
在派生類層次中,這樣的bug更難發現。假如有優先級的客戶類,它繼承自Customer
[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. class PriorityCustomer:public Cutsomer  
  2. {  
  3. public:  
  4.     PriorityCustomer()  
  5.     {  
  6.         cout<<"PriorityCustomer Ctor"<<endl;  
  7.     }  
  8.     PriorityCustomer(const PriorityCustomer& rhs)  
  9.         :priority(rhs.priority)  
  10.     {  
  11.         cout<<"PriorityCustomer Copy Ctor"<<endl;  
  12.     }  
  13.     PriorityCustomer& operator=(const PriorityCustomer& rhs)  
  14.     {  
  15.         cout<<"PriorityCustomer assign operator"<<endl;  
  16.         priority=rhs.priority;  
  17.         return *this;  
  18.     }  
  19. private:  
  20.     int priority;  
  21. };  
在PriorityCustomer的copying函數中,只是複製了PriorityCustomer部分的內容,基類內容被忽略了。那麼其基類內容部分怎麼初始化的呢?
在派生類中構造函數沒有初始化的基類部分是通過基類默認構造函數初始化的(沒有默認構造函數就會報錯)。
但是在copy assignment操作符中,不會調用基類的默認構造函數,因爲copy assignment只是給對象重新賦值,不是初始化,因此不會調用基類的構造函數,除非我們顯示調用。
正確的PriorityCustomer的copying函數應該這樣寫:
[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. PriorityCustomer(const PriorityCustomer& rhs)  
  2.         :Cutsomer(rhs),priority(rhs.priority)  
  3.     {  
  4.         cout<<"PriorityCustomer Copy Ctor"<<endl;  
  5.     }  
  6.     PriorityCustomer& operator=(const PriorityCustomer& rhs)  
  7.     {  
  8.         cout<<"PriorityCustomer assign operator"<<endl;  
  9.         Cutsomer::operator=(rhs);  
  10.         priority=rhs.priority;  
  11.         return *this;  
  12.     }  

可以發現複製構造函數和賦值操作符有類似的代碼。但是者兩個函數不能相互調用。複製構造函數是構造一個不存在的對象,而賦值操作符是給一個存在的對象重新賦值。消除重複代碼的方法編寫一個private方法,例如void Init()。在這個函數中操作重複代碼。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章