<menu id="w8yyk"><menu id="w8yyk"></menu></menu>
  • <dd id="w8yyk"><nav id="w8yyk"></nav></dd>
    <menu id="w8yyk"></menu>
    <menu id="w8yyk"><code id="w8yyk"></code></menu>
    <menu id="w8yyk"></menu>
    <xmp id="w8yyk">
    <xmp id="w8yyk"><nav id="w8yyk"></nav>
  • 網站首頁 > 物聯資訊 > 技術分享

    C++11新特性:右值引用和轉移構造函數

    2016-09-28 00:00:00 廣州睿豐德信息科技有限公司 閱讀
    睿豐德科技 專注RFID識別技術和條碼識別技術與管理軟件的集成項目。質量追溯系統、MES系統、金蝶與條碼系統對接、用友與條碼系統對接

    問題背景

     

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


    先來分析一下上述代碼的運行過程。

     

     

    [cpp] view plaincopy  
    1. vector<int> v;  
    2. for ( int i = 0; i < 100; i++ )  
    3. {  
    4.     v.push_back( i );  
    5. }  


    以上5行語句在棧上新建了一個vector的實例,并在里面放了100個數。

     

    [cpp] view plaincopy  
    1. v = doubleValues( v )  

    這條語句調用函數doubleValues,函數的參數類型的const reference,常量引用,那么在實參形參結合的時候并不會將v復制一份,而是直接傳遞引用。所以在函數體內部使用的v就是剛才創建的那個vector的實例。

     

    但是

     

    [cpp] view plaincopy  
    1. vector<int> new_values( v.size() );  

    這條語句新建了一個vector的實例new_values,并且復制了v的所有內容。但這是合理的,因為我們這是要將一個vector中所有的值翻倍,所以我們不應該改變原有的vector的內容。

    [cpp] view plaincopy  
    1. v = doubleValues( v );  

     

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

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

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

     

    左值和右值

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

    右值引用

    在C++11中,你可以使用const的左值引用來綁定一個右值,比如說: [cpp] view plaincopy  
    1. const int& val = getVal();//right  
    2. int& val = getVal();//error  

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

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

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

    轉移構造函數和轉移賦值運算符

    假設我們定義了一個ArrayWrapper的類,這個類對數組進行了封裝。 [cpp] view plaincopy  
    1. class ArrayWrapper  
    2. {  
    3.     public:  
    4.         ArrayWrapper (int n)  
    5.             : _p_vals( new int[ n ] )  
    6.             , _size( n )  
    7.         {}  
    8.         // copy constructor  
    9.         ArrayWrapper (const ArrayWrapper& other)  
    10.             : _p_vals( new int[ 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. };  

    我們可以看到,這個類的拷貝構造函數顯示新建了一片內存空間,然后又對傳進來的左值引用進行了復制。 如果傳進來的實際參數是一個右值(馬上就銷毀),我們自然希望能夠繼續使用這個右值的空間,這樣可以節省申請空間和復制的時間。 我們可以使用轉移構造函數實現這個功能: [cpp] view plaincopy  
    1. class ArrayWrapper  
    2. {  
    3. public:  
    4.     // default constructor produces a moderately sized array  
    5.     ArrayWrapper ()  
    6.         : _p_vals( new int[ 64 ] )  
    7.         , _size( 64 )  
    8.     {}  
    9.    
    10.     ArrayWrapper (int n)  
    11.         : _p_vals( new int[ n ] )  
    12.         , _size( n )  
    13.     {}  
    14.    
    15.     // move constructor  
    16.     ArrayWrapper (ArrayWrapper&& other)  
    17.         : _p_vals( other._p_vals  )  
    18.         , _size( other._size )  
    19.     {  
    20.         other._p_vals = NULL;  
    21.     }  
    22.    
    23.     // copy constructor  
    24.     ArrayWrapper (const ArrayWrapper& other)  
    25.         : _p_vals( new int[ other._size  ] )  
    26.         , _size( other._size )  
    27.     {  
    28.         for ( int i = 0; i < _size; ++i )  
    29.         {  
    30.             _p_vals[ i ] = other._p_vals[ i ];  
    31.         }  
    32.     }  
    33.     ~ArrayWrapper ()  
    34.     {  
    35.         delete [] _p_vals;  
    36.     }  
    37.    
    38. private:  
    39.     int *_p_vals;  
    40.     int _size;  
    41. };  

    第一個構造函數就是轉移構造函數。它先將other的域復制給自己。尤其是將_p_vals的指針賦值給自己的指針,這個過程相當于int的復制,所以非常快。然后將other里面_p_vals指針置成NULL。這樣做有什么用呢? 我們看到,這個類的析構函數是這樣的: [cpp] view plaincopy  
    1. ~ArrayWrapper ()  
    2.     {  
    3.         delete [] _p_vals;  
    4.     }  
    它會delete掉_p_vals的內存空間。但是如果調用析構函數的時候_p_vals指向的是NULL,那么就不會delte任何內存空間。 所以假設我們這樣使用ArrayWrapper的轉移構造函數: [cpp] view plaincopy  
    1. ArrayWrapper *aw = new ArrayWrapper((new ArrayWrapper(5)));  
    其中 [cpp] view plaincopy  
    1. (new ArrayWrapper(5)  
    獲得的實例就是一個右值,我們不妨稱為r,當整條語句執行結束的時候就會被銷毀,執行析構函數。 所以如果轉移構造函數中沒有
    [cpp] view plaincopy  
    1. other._p_vals = NULL;  
    的話,雖然aw已經獲得了r的_p_vals的內存空間,但是之后r就被銷毀了,那么r._p_vals的那片內存也被釋放了,aw中的_p_vals指向的就是一個不合法的內存空間。所以我們就要防止這片空間被銷毀。  

    右值引用也是左值

    這種說法可能有點繞,來看一個例子:   我們可以定義MetaData類來抽象ArrayWrapper中的數據: [cpp] view plaincopy  
    1. class MetaData  
    2. {  
    3. public:  
    4.     MetaData (int size, const std::string& name)  
    5.         : _name( name )  
    6.         , _size( size )  
    7.     {}  
    8.    
    9.     // copy constructor  
    10.     MetaData (const MetaData& other)  
    11.         : _name( other._name )  
    12.         , _size( other._size )  
    13.     {}  
    14.    
    15.     // move constructor  
    16.     MetaData (MetaData&& other)  
    17.         : _name( other._name )  
    18.         , _size( other._size )  
    19.     {}  
    20.    
    21.     std::string getName () const { return _name; }  
    22.     int getSize () const { return _size; }  
    23.     private:  
    24.     std::string _name;  
    25.     int _size;  
    26. };  

    那么ArrayWrapper類現在就變成這個樣子 [cpp] view plaincopy  
    1. class ArrayWrapper  
    2. {  
    3. public:  
    4.     // default constructor produces a moderately sized array  
    5.     ArrayWrapper ()  
    6.         : _p_vals( new int[ 64 ] )  
    7.         , _metadata( 64, "ArrayWrapper" )  
    8.     {}  
    9.    
    10.     ArrayWrapper (int n)  
    11.         : _p_vals( new int[ n ] )  
    12.         , _metadata( n, "ArrayWrapper" )  
    13.     {}  
    14.    
    15.     // move constructor  
    16.     ArrayWrapper (ArrayWrapper&& other)  
    17.         : _p_vals( other._p_vals  )  
    18.         , _metadata( other._metadata )  
    19.     {  
    20.         other._p_vals = NULL;  
    21.     }  
    22.    
    23.     // copy constructor  
    24.     ArrayWrapper (const ArrayWrapper& other)  
    25.         : _p_vals( new int[ other._metadata.getSize() ] )  
    26.         , _metadata( other._metadata )  
    27.     {  
    28.         for ( int i = 0; i < _metadata.getSize(); ++i )  
    29.         {  
    30.             _p_vals[ i ] = other._p_vals[ i ];  
    31.         }  
    32.     }  
    33.     ~ArrayWrapper ()  
    34.     {  
    35.         delete [] _p_vals;  
    36.     }  
    37. private:  
    38.     int *_p_vals;  
    39.     MetaData _metadata;  
    40. };  

    同樣,我們使用了轉移構造函數來避免代碼的復制。但是這里的轉移構造函數對嗎? 問題出在下面這條語句 [cpp] view plaincopy  
    1. _metadata( other._metadata )  
    我們希望的是other._metadata是一個右值,然后就會調用MetaData類的轉移構造函數來避免數據的復制。但是很可惜,右值引用是左值。 在前面已經說過,左值占用了內存上一片穩定的空間。而右值是一個臨時的數據,離開了某條語句就會被銷毀。other是一個右值引用,在ArrayWrapper類的轉移構造函數的整個作用域中都可以穩定地存在,所以確實占用了內存上的穩定空間,所以是一個左值,因為上述語句調用的并非轉移構造函數。所以C++標準庫提供了如下函數來解決這個問題: [cpp] view plaincopy  
    1. std::move  

    這條語句可以將左值轉換為右值   [cpp] view plaincopy  
    1. // 轉移構造函數  
    2.   ArrayWrapper (ArrayWrapper&& other)  
    3.       : _p_vals( other._p_vals  )  
    4.       , _metadata( std::move( other._metadata ) )  
    5.   {  
    6.       other._p_vals = NULL;  
    7.   }  

    這樣就可以避免_metadata域的復制了。

     

    函數返回右值引用

      我們可以在函數中顯示地返回一個右值引用   [cpp] view plaincopy  
    1. int x;  
    2.    
    3. int getInt ()  
    4. {  
    5.     return x;  
    6. }  
    7.    
    8. int && getRvalueInt ()  
    9. {  
    10.     // notice that it's fine to move a primitive type--remember, std::move is just a cast  
    11.     return std::move( x );  
    12. }  


    感謝Alex Allain提供的部分代碼例子。http://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html 本文是在他的文章的基礎之上寫出來的。RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成
    最近免费观看高清韩国日本大全