C++ 右值引用
能出現在賦值號左邊的表達式稱為“左值”,不能出現在賦值號左邊的表達式稱為“右值”。一般來說,左值是可以取地址的,右值則不可以。
非 const 的變量都是左值。函數調用的返回值若不是引用,則該函數調用就是右值。之前我們了解的“引用”是引用變量的,而變量是左值,因此它們都是“左值引用”。
C++11 新增了一種引用,可以引用右值,因而稱為“右值引用”。無名的臨時變量不能出現在賦值號左邊,因而是右值。右值引用就可以引用無名的臨時變量。定義右值引用的格式如下:
類型 && 引用名 = 右值表達式;例如:
class A{}; A & rl = A(); //錯誤,無名臨時變量 A() 是右值,因此不能初始化左值引用 r1 A && r2 = A(); //正確,因 r2 是右值引用引入右值引用的主要目的是提高程序運行的效率。有些對象在復制時需要進行深復制,深復制往往非常耗時。合理使用右值引用可以避免沒有必要的深復制操作。例如下面的程序:
#include <iostream> #include <string> #include <cstring> using namespace std; class String { public:char* str;String() : str(new char[1]) { str[0] = 0; }String(const char* s) {str = new char[strlen(s) + 1];strcpy(str, s);}String(const String & s) {//復制構造函數cout << "copy constructor called" << endl;str = new char[strlen(s.str) + 1];strcpy(str, s.str);}String & operator = (const String & s) {//復制賦值號cout << "copy operator = called" << endl;if (str != s.str) {delete[] str;str = new char[strlen(s.str) + 1];strcpy(str, s.str);}return *this;}String(String && s) : str(s.str) { //移動構造函數cout << "move constructor called" << endl;s.str = new char[1];s.str[0] = 0;}String & operator = (String && s) { //移動賦值號cout << "move operator = called" << endl;if (str != s.str) {str = s.str;s.str = new char[1];s.str[0] = 0;}return *this;}~String() { delete[] str; } }; template <class T> void MoveSwap(T & a, T & b) {T tmp(move(a)); //std::move(a) 為右值,這里會調用移動構造函數a = move(b); //move(b) 為右值,因此這里會調用移動賦值號b = move(tmp); //move(tmp) 為右值,因此這里會調用移動賦值號 } int main() {String s;s = String("this"); //調用移動賦值號cout << "* * * *" << endl;cout << s.str << endl;String s1 = "hello", s2 = "world";MoveSwap(s1, s2); //調用一次移動構造函數和兩次移動賦值號cout << s2.str << endl;return 0; }程序的輸出結果如下:
move operator = called **** this move constructor called move operator = called move operator = called hello第 33 行重載了一個移動賦值號。它和第 19 行的復制賦值號的區別在于,其參數是右值引用。在移動賦值號函數中沒有執行深復制操作,而是直接將對象的 str 指向了參數 s 的成員變量 str 指向的地方,然后修改 s.str 讓它指向別處,以免 s.str 原來指向的空間被釋放兩次。
該移動賦值號函數修改了參數,這會不會帶來麻煩呢?答案是不會。因為移動賦值號函數的形參是一個右值引用,則調用該函數時,實參一定是右值。右值一般是無名臨時變量,而無名臨時變量在使用它的語句結束后就不再有用,因此其值即使被修改也沒有關系。
第 53 行,如果沒有定義移動賦值號,則會導致復制賦值號被調用,引發深復制操作。臨時無名變量String(“this”)是右值,因此在定義了移動賦值號的情況下,會導致移動賦值號被調用。移動賦值號使得 s 的內容和 String(“this”) 一致,然而卻不用執行深復制操作,因而效率比復制賦值號高。
雖然移動賦值號修改了臨時變量 String(“this”),但該變量在后面已無用處,因此這樣的修改不會導致錯誤。
第 46 行使用了 C++ 11 中的標準模板 move。move 能接受一個左值作為參數,返回該左值的右值引用。因此本行會用定義于第 28 行、以右值引用作為參數的移動構造函數來初始化 tmp。該移動構造函數沒有執行深復制,將 tmp 的內容變成和 a 相同,然后修改 a。由于調用 MoveSwap 本來就會修改 a,所以 a 的值在此處被修改不會產生問題。
第 47 行和第 48 行調用了移動賦值號,在沒有進行深復制的情況下完成了 a 和 b 內容的互換。對比 Swap 函數的以下寫法:
template <class T> void Swap(T & a, T & b) {T tmp(a); //調用復制構造函數a=b; //調用復制賦值號b=tmp; //調用復制賦值號 }Swap 函數執行期間會調用一次復制構造函數,兩次復制賦值號,即一共會進行三次深復制操作。而利用右值引用,使用 MoveSwap,則可以在無須進行深復制的情況下達到相同的目的,從而提高了程序的運行效率。
總結