事實上,重載運算符返回void、返回對象本身、返回對象引用都是可以的,並不是說一定要返回一個引用,只不過在不同的情況下需要不同的返回值。
那么什么情況下要返回對象的引用呢?
原因有兩個:
- 允許進行連續賦值
- 防止返回對象(返回對象也可以進行連續賦值(常規的情況,如a = b = c,而不是(a = b) = c))的時候調用拷貝構造函數和析構函數導致不必要的開銷,降低賦值運算符的效率。
對於第二點原因:如果用”值傳遞“的方式,雖然功能仍然正確,但由於return語句要把*this拷貝到保存返回值的外部存儲單元之中,增加了不必要的開銷,會降低賦值函數的效率。
場景:
需要返回對象引用或者返回對象(效率沒有返回引用高),需要實現連續賦值,使重載的運算符更符合C++本身的運算符語意,如連續賦值 = += -= *= 、=,<<輸出流
關於賦值 =,我們知道賦值=有連續等於的特性
int x,y,z; x=y=z=15;
同樣有趣的是,賦值采用的是右結合律,所以上述連鎖賦值被解析為:
這里15先被賦值給z,然后其結果(更新后的z)再被賦值給y,然后其結果(更新后的y)再被賦值給x。
為了實現”連鎖賦值“,賦值操作符號返回一個reference(引用)指向操作符號的左側實參(而事實上重載運算符的左側實參就是調用對象本身,比如= += -=等),這是為classes實現賦值操作符時應該遵循的協議:這個協議不僅僅適用於以上的標准賦值形式,也適用於所有賦值運算。
class Widght{ public: ..... Widget& operator=(cosnt Widget& rhs) { ... return* this; } Widget& operator+=(cosnt Widget& rhs) { ... return* this; } Widget& operator-=(cosnt Widget& rhs) { ... return* this; } Widget& operator*=(cosnt Widget& rhs) { ... return* this; } Widget& operator/=(cosnt Widget& rhs) { ... return* this; } ... };
注意,這只是個協議,並無強制性,如果不遵循它,代碼一樣可以通過編譯,然而這份協議被所有內置類型和標准程序庫提供的類型入string,vector,complex,std::trl::shared_ptr或者即將提供的類型共同遵守。因此除非你有一個標新立異的好理由,不然還是隨眾吧。
下面看一個賦值運算符重載的例子:(連續賦值,常規的情況(a = b = c))
1、首先是返回對象的情況:
#include <iostream>
using namespace std; class String { private: char *str; int len; public: String(const char* s);//構造函數聲明
String operator=(const String& another);//運算符重載,此時返回的是對象
void show() { cout << "value = " << str << endl; } /*copy construct*/ String(const String& other) { len = other.len; str = new char[len + 1]; strcpy(str, other.str); cout << "copy construct" << endl; } ~String() { delete[] str; cout << "deconstruct" << endl; } }; String::String(const char* s)//構造函數定義
{ len = strlen(s); str = new char[len + 1]; strcpy(str, s); } String String::operator=(const String &other)//運算符重載
{ if (this == &other) return *this; // return;
delete[] str; len = other.len; str = new char[len + 1]; strcpy(str, other.str); return *this; // return;
} int main() { String str1("abc"); String str2("123"); String str3("456"); str1.show(); str2.show(); str3.show(); str3 = str1 = str2;//str3.operator=(str1.operator=(str2))
str3.show(); str1.show(); return 0; }
運行結果:
2、下面是返回引用的情況(String& operator = (const String& str)),直接貼運行結果:
當運算符重載返回的是對象時,會在連續賦值運算過程的返回途中,調用兩次拷貝構造函數和析構函數(因為return的是個新的對象)
如果采用String& operator = (const String& str)這樣就不會有多余的調用(因為這里直接return一個已經存在的對象的引用)
上面的例子也說明一點:析構函數的調用是在變量作用域結束的時候(以及程序運行結束的時候)
如果采用return對象,那么第二次賦值運算調用的情況就是:
將一個新的String對象(returnStringObj)傳遞到operator = (const String& str)的參數中去 相當於
const String&str = returnStringObj;
如果采用return對象引用,那么第二次賦值運算的情況就是:
將一個已經存在的String對象的引用((其實就是str1))傳遞給operator = (const String& str)的參數中去
const String&str = returnReference; //(String& returnReference = str1;)
+=等運算符也是同樣的考慮,比如
int main() { String str1("abc"); String str2("123"); String str3("456"); str1.show(); str2.show(); str3.show(); str3 = str1 = str2;//str3.operator=(str1.operator=(str2))
str3.show(); str1.show(); int num = 10; num += (num += 100); cout << num << endl; return 0; }
如果使用+=或其它上面舉出的運算符進行連續操作時,,則這些運算符的返回值一定要是一個對象或者引用才行,不然就會出現錯誤(參數類型不符合)。什么意思呢,下面舉個例子。
現在讓運算符重載返回的類型為空,單個賦值,不使用連續賦值:
#include <iostream>
using namespace std; class String { private: char *str; int len; public: String(const char* s);//構造函數聲明
void operator=(const String& another);//運算符重載,此時返回為空
void show() { cout << "value = " << str << endl; } /*copy construct*/ String(const String& other) { len = other.len; str = new char[len + 1]; strcpy(str, other.str); cout << "copy construct" << endl; } ~String() { delete[] str; cout << "deconstruct" << endl; } }; String::String(const char* s) { len = strlen(s); str = new char[len + 1]; strcpy(str, s); } void String::operator=(const String &other) { if (this == &other) // return *this;
return; delete[] str; len = other.len; str = new char[len + 1]; strcpy(str, other.str); // return *this;
return; } int main() { String str1("abc"); String str2("123"); String str3("456"); str1.show(); str2.show(); str3.show(); str3 = str1;//這樣OK
str3.show(); str1.show(); return 0; }
運行結果:
但當把主函數中str1,str2,str3改為連續賦值時:
int main() { String str1("abc"); String str2("123"); String str3("456"); str1.show(); str2.show(); str3.show(); str3 = str1=str2;//這樣不OK
str3.show(); str1.show(); return 0; }
出錯:IntelliSense:沒有與這些操作數匹配的“=”運算符
操作數類型為:String=void
所以,當確定不使用連續賦值時,直接返回void也是可以的。要明白一點:
運算符左側的對象就是操作對象,比如
ObjectA = ObjectB 等同於ObjectA.operator=(ObjectB) ObjectA+=ObjectB 等同於ObjectA.operator+(ObjectB)
最后要說明一點:並非必須返回引用,返回引用的好處既可以避免拷貝構造函數和析構函數的調用,又可以保證= +=等運算符的原始語義清晰。
啥叫原始語義清晰呢?
如
(str3 = str1) = str2;
我們的意識里,就是先執行括號內容,即str1賦值給str3,然后str2再賦值給str3,最后str3輸出的內容是str2的。
即如果運算符重載返回的是對象引用時,
//返回的是對象引用的情況
#include <iostream>
using namespace std; class String { private: char *str; int len; public: String(const char* s);//構造函數聲明
String& operator=(const String& another);//運算符重載,此時返回為引用
void show() { cout << "value = " << str << endl; } /*copy construct*/ String(const String& other) { len = other.len; str = new char[len + 1]; strcpy(str, other.str); cout << "copy construct" << endl; } ~String() { delete[] str; cout << "deconstruct" << endl; } }; String::String(const char* s) { len = strlen(s); str = new char[len + 1]; strcpy(str, s); } String& String::operator=(const String &other) { if (this == &other) return *this; // return;
delete[] str; len = other.len; str = new char[len + 1]; strcpy(str, other.str); return *this; // return;
} int main() { String str1("abc"); String str2("123"); String str3("456"); str1.show(); str2.show(); str3.show(); (str3 = str1) = str2; cout << "str3的內容為:" << endl; str3.show(); return 0; }
運行結果:
str3得到了str2的內容,與我們認識的‘=’運算符邏輯相符。
而如果運算符重載返回的是對象時,
//這是返回類型為對象的情況
#include <iostream>
using namespace std; class String { private: char *str; int len; public: String(const char* s);//構造函數聲明
String operator=(const String& another);//運算符重載,此時返回為空
void show() { cout << "value = " << str << endl; } /*copy construct*/ String(const String& other) { len = other.len; str = new char[len + 1]; strcpy(str, other.str); cout << "copy construct" << endl; } ~String() { delete[] str; cout << "deconstruct" << endl; } }; String::String(const char* s) { len = strlen(s); str = new char[len + 1]; strcpy(str, s); } String String::operator=(const String &other) { if (this == &other) return *this; // return;
delete[] str; len = other.len; str = new char[len + 1]; strcpy(str, other.str); return *this; // return;
} int main() { String str1("abc"); String str2("123"); String str3("456"); str1.show(); str2.show(); str3.show(); (str3 = str1) = str2; cout << "賦值后str3的內容為:" << endl; str3.show(); return 0; }
運行結果:
str3只得到了str1的內容,並沒有得到str2的內容,這是因為執行(str3=str1)后,因為返回的是對象(一個臨時對象,str3的一個拷貝),不是引用,所以此時str3不在后面的‘=str2’的操作中,而是str2對一個臨時對象賦值,所以str3的內容保持不變(等於str1)。
總結
所以,對此類運算符重載時,還是老老實實的返回引用,不要瞎搞,做個乖孩子