一、賦值運算符需要考慮的因素
一個賦值運算符需要考慮的因素,有以下四點:
1. 檢查返回值類型是否是該類類型的引用,並且返回值也是自身引用(即*this)。
- 如果返回類型是一個void,那麼就不能進行連續賦值(s1=s2=s3)。s1=s2=s3實際上是s1=(s2=s3),首先是s3對s2進行賦值,賦值完畢之後,會返回一個NULL,這就相當於s1=NULL,這樣是C++不允許的,所以會出現編譯錯誤。
- 如果返回值不是一個引用,那麼會多調用一次拷貝構造和析構函數,程序的效率相對而言會更低
2. 檢查函數的形參是否是const引用類型。
因爲如果傳入的參數不是引用,而是一個實例的話,那麼在實參對形參結合的這個過程中會調用拷貝構造函數。而如果是一個引用就會避免這種無端的銷燬,從而提高效率。之所以聲明爲const,是因爲在賦值運算符中,我們可以保證不會對對象進行修改。
3. 檢查是否釋放掉自身已有內存。
如果我們忘記在賦值之前釋放掉自身已有內存,那麼就會出現內存泄漏。
4. 檢查是否判斷自我賦值
如果沒有判斷是否爲自我賦值,那麼當是同一個對象進行賦值時,釋放掉自身對象,其實也是將傳入參數的內存也釋放掉了,因此就找不到要賦值的內容了。
二、賦值運算符的常用寫法
根據上面的四點,我以模擬String類的賦值運算符爲例,我們可以寫出下面的賦值運算符重載函數:
String& operator=(const String& s)
{
if (this != &s) //判斷是否自我賦值
{
delete[] _str; //釋放自身原有空間
char* pStr = new char[s._capacity + 1];
strcpy(pStr, s._str);
_str = pStr;
}
return *this;
}
上面的賦值運算符重載函數有問題嗎?
乍一看是沒有問題的,但是假如我們深入的考慮的話,就會發現一個很嚴重的問題。如果我們使用new時空間不足,那麼new就會拋出異常。那麼pStr就是一個空指針,這樣很容易導致程序崩潰,這就是一個異常安全問題。
解決辦法:
方法一:先用new分配新空間,再使用delete釋放原空間。如下所示:
String& operator=(const String& s)
{
if (this != &s) //判斷是否自我賦值
{
char* pStr = new char[s._capacity + 1];
strcpy(pStr, s._str);
delete[] _str; //釋放自身原有空間
_str = pStr;
}
return *this;
}
這樣在new分配成功後才釋放空間,也就是在內存分配失敗時,我們可以保證對象不會被修改。
方法二:先創建一個臨時對象,然後再將這個臨時對象與原來的對象進行交換。
String& operator=(const String& s)
{
if (this != &s) //判斷是否自我賦值
{
String s1(s);
char* pStr=s1._str;
s1._str=_str;
_str=pStr;
}
return *this;
}
在上面的代碼中,首先創建一個臨時對象s1,並且調用拷貝構造函數用傳入的形參s爲s1初始化。然後將s1._str與自身的_str進行交換。當運行出s1的作用域時,s1會被銷燬,因爲這時s1._str指向的內存就是自身原來的內存,所以銷燬的相當於自身原來的空間。過程如下圖所示:
(1)開始時,創建臨時對象
(2)執行交換語句後。
(3)s1出作用域,臨時對象被銷燬
(4)如此便完成了賦值工作