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