1. 程式人生 > >C++11 新特性之右值引用和轉移建構函式

C++11 新特性之右值引用和轉移建構函式

問題背景

  1. #include <iostream>
  2. usingnamespace std;  
  3. vector<int> doubleValues (const vector<int>& v)  
  4. {  
  5.     vector<int> new_values( v.size() );  
  6.     for (auto itr = new_values.begin(), end_itr = new_values.end(); itr != end_itr; ++itr )  
  7.     {  
  8.         new_values.push_back( 2 * *itr );  
  9.     }  
  10.     return new_values;  
  11. }  
  12. int main()  
  13. {  
  14.     vector<int> v;  
  15.     for ( int i = 0; i < 100; i++ )  
  16.     {  
  17.         v.push_back( i );  
  18.     }  
  19.     v = doubleValues( v );  
  20. }  

先來分析一下上述程式碼的執行過程。
  1. vector<int> v;  
  2. for ( int i = 0; i < 100; i++ )  
  3. {  
  4.     v.push_back( i );  
  5. }  

以上5行語句在棧上新建了一個vector的例項,並在裡面放了100個數。
  1. v = doubleValues( v )  
這條語句呼叫函式doubleValues,函式的引數型別的const reference,常量引用,那麼在實參形參結合的時候並不會將v複製一份,而是直接傳遞引用。所以在函式體內部使用的v就是剛才建立的那個vector的例項。

但是

  1. vector<int> new_values( v.size() );  
這條語句新建了一個vector的例項new_values,並且複製了v的所有內容。
但這是合理的,因為我們這是要將一個vector中所有的值翻倍,所以我們不應該改變原有的vector的內容。
  1. v = doubleValues( v );  

函式執行完之後,new_values中放了翻倍之後的數值,作為函式的返回值返回。但是注意,這個時候doubleValue(v)的呼叫已經結束。開始執行 = 的語義。

賦值的過程實際上是將返回的vector<int>複製一份放入新的記憶體空間,然後改變v的地址,讓v指向這篇記憶體空間。總的來說,我們剛才新建的那個vector又被複制了一遍。

但我們其實希望v能直接得到函式中複製好的那個vector。在C++11之前,我們只能通過傳遞指標來實現這個目的。但是指標用多了非常不爽。我們希望有更簡單的方法。這就是我們為什麼要引入右值引用和轉移建構函式的原因。

左值和右值

在說明左值的定義之前,我們可以先看幾個左值的例子。
  1. int a;  
  2. a = 1; // here, a is an lvalue
上述的a就是一個左值。 臨時變數可以做左值。同樣函式的返回值也可以做左值。
  1. int x;  
  2. int& getRef ()   
  3. {  
  4.         return x;  
  5. }  
  6. getRef() = 4;  
以上就是函式返回值做左值的例子。 其實左值就是指一個擁有地址的表示式。換句話說,左值指向的是一個穩定的記憶體空間(即可以是在堆上由使用者管理的記憶體空間,也可以是在棧上,離開了一個block就被銷燬的記憶體空間)。上面第二個例子,getRef返回的就是一個全域性變數(建立在堆上),所以可以當做左值使用。 與此相反,右值指向的不是一個穩定的記憶體空間,而是一個臨時的空間。比如說下面的例子:
  1. int x;  
  2. int getVal ()  
  3. {  
  4.     return x;  
  5. }  
  6. getVal();  
這裡getVal()得到的就是臨時的一個值,沒法對它進行賦值。
下面的語句就是錯的。
  1. getVal() = 1;//compilation error
所以右值只能夠用來給其他的左值賦值。

右值引用

在C++11中,你可以使用const的左值引用來繫結一個右值,比如說:
  1. constint& val = getVal();//right
  2. int& val = getVal();//error

因為左值引用並不是左值,並沒有建立一片穩定的記憶體空間,所以如果不是const的話你就可以對它的內容進行修改,而右值又不能進行賦值,所以就會出錯。因此只能用const的左值引用來繫結一個右值。 在C++11中,我們可以顯示地使用“右值引用”來繫結一個右值,語法是"&&"。因為指定了是右值引用,所以無論是否const都是正確的。
  1. const string&& name = getName(); // ok
  2. string&& name = getName(); // also ok 

有了這個功能,我們就可以對原來的左值引用的函式進行過載,過載的函式引數使用右值引用。比如下面這個例子:
  1. printReference (const String& str)  
  2. {  
  3.         cout << str;  
  4. }  
  5. printReference (String&& str)  
  6. {  
  7.         cout << str;  
  8. }  
可以這麼呼叫它。
  1. string me( "alex" );  
  2. printReference(  me ); // 呼叫第一函式,引數為左值常量引用
  3. printReference( getName() ); 呼叫第二個函式,引數為右值引用。  

好了,現在我們知道C++11可以進行顯示的右值引用了。但是我們如果用它來解決一開始那個複製的問題呢? 這就要引入與此相關的另一個新特性,轉移建構函式和轉移賦值運算子

轉移建構函式和轉移賦值運算子

假設我們定義了一個ArrayWrapper的類,這個類對陣列進行了封裝。
  1. class ArrayWrapper  
  2. {  
  3.     public:  
  4.         ArrayWrapper (int n)  
  5.             : _p_vals( newint[ n ] )  
  6.             , _size( n )  
  7.         {}  
  8.         // copy constructor
  9.         ArrayWrapper (const ArrayWrapper& other)  
  10.             : _p_vals( newint[ other._size  ] )  
  11.             , _size( other._size )  
  12.         {  
  13.             for ( int i = 0; i < _size; ++i )  
  14.             {  
  15.                 _p_vals[ i ] = other._p_vals[ i ];  
  16.             }  
  17.         }  
  18.         ~ArrayWrapper ()  
  19.         {  
  20.             delete [] _p_vals;  
  21.         }  
  22.     private:  
  23.     int *_p_vals;  
  24.     int _size;  
  25. };  

我們可以看到,這個類的拷貝建構函式顯示新建了一片記憶體空間,然後又對傳進來的左值引用進行了複製。 如果傳進來的實際引數是一個右值(馬上就銷燬),我們自然希望能夠繼續使用這個右值的空間,這樣可以節省申請空間和複製的時間。 我們可以使用轉移建構函式實現這個功能:
  1. class ArrayWrapper  
  2. {  
  3. public:  
  4.     // default constructor produces a moderately sized array
  5.     ArrayWrapper ()  
  6.         : _p_vals( newint[ 64 ] )  
  7.         , _size( 64 )  
  8.     {}  
  9.     ArrayWrapper (int n)  
  10.         : _p_vals( newint[ n ] )  
  11.         , _size( n )  
  12.     {}  
  13.     // move constructor
  14.     ArrayWrapper (ArrayWrapper&& other)  
  15.         : _p_vals( other._p_vals  )  
  16.         , _size( other._size )  
  17.     {  
  18.         other._p_vals = NULL;  
  19.     }