C++過載=(C++過載賦值運算子)

2020-07-16 10:04:23
賦值運算子=要求左右兩個運算元的型別是匹配的,或至少是相容的。有時希望=兩邊的運算元的型別即使不相容也能夠成立,這就需要對=進行過載。C++ 規定,=只能過載為成員函數。來看下面的例子。

要編寫一個長度可變的字串類 String,該類有一個 char* 型別的成員變數,用以指向動態分配的儲存空間,該儲存空間用來存放以結尾的字串。String 類可以如下編寫:
#include <iostream>
#include <cstring>
using namespace std;
class String {
private:
    char * str;
public:
    String() :str(NULL) { }
    const char * c_str() const { return str; };
    String & operator = (const char * s);
    ~String();
};
String & String::operator = (const char * s)
//過載"="以使得 obj = "hello"能夠成立
{
    if (str)
        delete[] str;
    if (s) {  //s不為NULL才會執行拷貝
        str = new char[strlen(s) + 1];
        strcpy(str, s);
    }
    else
        str = NULL;
    return *this;
}
String::~String()
{
    if (str)
        delete[] str;
};
int main()
{
    String s;
    s = "Good Luck,"; //等價於 s.operator=("Good Luck,");
    cout << s.c_str() << endl;
    // String s2 = "hello!";   //這條語句要是不注釋掉就會出錯
    s = "Shenzhou 8!"; //等價於 s.operator=("Shenzhou 8!");
    cout << s.c_str() << endl;
    return 0;
}
程式的執行結果:
Good Luck,
Shenzhou 8!

第 8 行的建構函式將 str 初始化為 NULL,僅當執行了 operator= 成員函數後,str 才會指向動態分配的儲存空間,並且從此後其值不可能再為 NULL。在 String 物件的生存期內,有可能從未執行過 operator= 成員函數,所以在解構函式中,在執行delete[] str之前,要先判斷 str 是否為 NULL。

第 9 行的函數返回了指向 String 物件內部動態分配的儲存空間的指標,但是不希望外部得到這個指標後修改其指向的字串的內容,因此將返回值設為 const char*。這樣,假定 s 是 String 物件,那麼下面兩條語句編譯時都會報錯,s 物件內部的字串就不會輕易地從外部被修改了 :
char* p = s.c_str ();
strcpy(s.c_str(), "Tiangong1");
第一條語句出錯是因為=左邊是 char* 型別,右邊是 const char * 型別,兩邊型別不匹配;第二條語句出錯是因為 strcpy 函數的第一個形參是 char* 型別,而這裡實參給出的卻是 const char * 型別,同樣型別不匹配。

如果沒有第 13 行對=的過載,第 34 行的s = "Good Luck,"肯定會因為型別不匹配而編譯出錯。經過過載後,第 34 行等價於s.operator=("Good Luck,");,就沒有問題了。

在 operator= 函數中,要先判斷 str 是否已經指向動態分配的儲存空間,如果是,則要先釋放那片空間,然後重新分配一片空間,再將引數 s 指向的內容複製過去。這樣,物件中存放的字串就和 s 指向的字串一樣了。分配空間時,要考慮到字串結尾的,因此分配的位元組數要比 strlen(s) 多 1。

需要注意一點,即使對=做了過載,第 36 行的String s2 = "hello!";還是會編譯出錯,因為這是一條初始化語句,要用到建構函式,而不是賦值運算子=。String 類沒有編寫引數型別為 char * 的建構函式,因此編譯不能通過。

就上面的程式而言,對 operator= 函數的返回值型別沒有什麼特別要求,void 也可以。但是在對運算子進行過載時,好的風格是應該盡量保留運算子原本的特性,這樣其他人在使用這個運算子時才不容易產生困惑。賦值運算子是可以連用的,這個特性在過載後也應該保持。即下面的寫法應該合法:

a = b = c;

假定 a、b、c 都是 String 物件,則上面的語句等價於下面的巢狀函數呼叫:

a.operator=( b.operator=(c) );

如果 operator= 函數的返回值型別為 void,顯然上面這個巢狀函數呼叫就不能成立。將返回值型別改為 String 並且返回 *this 可以解決問題,但是還不夠好。因為,假設 a、b、c 是基本型別的變數,則

(a =b) = c;

這條語句執行的效果會使得 a 的值和 c 相等,即a = b這個表示式的值其實是 a 的參照。為了保持=的這個特性,operator= 函數也應該返回其所作用的物件的參照。因此,返回值型別為 String & 才是風格最好的寫法。在 a、b、c 都是 String 物件時,(a=b)=c;等價於

( a.operator=(b) ).operator=(c);

a.operator=(b) 返回對 a 的參照後,通過該參照繼續呼叫 operator=(c),就會改變 a 的值。