【C++】賦值運算子函式
將已有的物件拷貝給另一個物件時,會用到賦值運算子函式, 尤其是當物件的記憶體分配在堆上時
函式原型:
ClassName & operator = (const ClassName &obj)
<span style="font-family:Microsoft YaHei;">#include <iostream> using namespace std; class CMyString { public: CMyString(char* pData = NULL) ; CMyString(const CMyString &str); ~CMyString(); private: char* m_data; };</span>
為上面的類新增賦值運算子函式
CMyString & operator = (const CMyString &other) ;
注意
1. 是否把返回值的型別宣告為類的引用,並在函式結束前返回例項自身的引用(即*this)。只有返回一個引用,才可以允許連續賦值。否則,如果函式的返回值是void,那麼這個賦值運算子不能連續賦值,假設有3個CMyString物件:str1, str2, str3, 在程式語句中str1 = str2 =str3將不能通過編譯。
2. 是否把傳入的引數型別宣告為常量引用。如果傳入的引數不是引用而是例項,那麼從形參到實參會呼叫一次拷貝建構函式。把引數宣告為引用可以避免這樣無謂的消耗,提高程式碼的效率。同時,在賦值運算子內不會改變傳入的例項的狀態,因此應該為傳入的引用引數加上const關鍵字。
3. 是否釋放例項自身已有的記憶體。如果我們忘記在分配新記憶體之前釋放自身已有的空間,程式將出現記憶體洩露。
4. 是否判斷傳入的引數和當前的例項(*this)是不是同一個例項。如果是同一個,就不進行賦值操作,直接返回。如果實現不判斷就賦值,那麼在釋放例項自身的記憶體時會導致嚴重的問題:當*this和傳入的引數是同一個例項時,一旦釋放了例項自身的記憶體,傳入的引數的記憶體也同時被釋放了,因此再也找不到需要賦值的內容了。
考慮這4點,我們應該可以寫出下面的程式碼:
考慮異常安全性呢?CMyString& CMyString::operator = (const CMyString &other) { if (this == &other) { return *this; } else { delete []m_data; m_data = NULL; m_data = new char[strlen(other.m_data) + 1]; strcpy(m_data, other.m_data); return *this; } }
在上面的函式中,我們在分配記憶體之前先用delete釋放了例項m_data的記憶體。如果此時記憶體不足導致new char丟擲異常,m_data將會是一個空指標,這樣非常容易導致程式崩潰。也就是說,一旦在賦值運算子函式內部跑出一個異常,CMyString的例項就不再保有有效狀態,這違背了異常安全性原則。
要想在賦值運算子內部實現異常安全性,有兩種方法。
一個簡單的辦法時先用new分配新的內容,再用delete釋放已有的內容。這樣只在分配內容成功之後才釋放原來的內容,也就是當分配記憶體失敗時我們能確保CMyString的例項不會被修改。
還有個更好的辦法是,先建立一個臨時例項,再交換臨時例項和原來的例項。
CMyString& CMyString::operator = (const CMyString &other) {
if (this == &other) {
return *this;
}
else {
CMyString strTemp(other);
char* pTemp = strTemp.m_data;
strTemp.m_data = m_data;
m_data = pTemp;
return *this;
}
}
在這個函式中,我們先建立了一個臨時例項strTemp,接著把strTemp.m_data和例項自身的m_data互換。由於strTemp是一個區域性變數,當程式執行到if的外面時也就出了該變數的作用域,就會自動呼叫strTemp的解構函式,把strTemp.m_data所指向的記憶體釋放掉。由於strTemp.m_data指向的記憶體就是例項之前m_data的記憶體,這就相當於自動呼叫解構函式釋放例項的記憶體。
在新的程式碼中,我們在CMyString的建構函式裡用new分配記憶體,如果由於記憶體不足丟擲了異常,我們還沒有修改原來例項的狀態,因此例項的狀態還是有效的,也就保證了異常安全性。