C++11中的右值引用

2021-03-02 CPP開發前沿
來源:https://my.oschina.net/hevakelcj/blog/894483

C++11中定義了右值引用的概念,這是一個比較實用,但不好理解的內容。

右值引用,顧名思義,就是引用右值。
那什麼是右值呢?書的給出的解釋是:可以取地址的,有名字的就是左值。反之,為右值。

T a(1);
T b(a);

其中 a是左值,b也是右值。

T ReturnRValue() {
    return T(3);
}
T c = ReturnRValue()

上面這個函數調用返回過程中,首先 T(3) 生成了一個對象,然後將這個對象賦值給tmp對象,T tmp = T(3);在ReturnRValue()函數返回後,會有個 T c = tmp 的動作。這個過程中,tmp,T(3) 都是右值。

左值與右值比較,比較明顯的區別是:
右值是自動產生的,不被用戶直接操作的,沒有名稱的,將立即被銷毀的對象。
左值則是有名稱的,能夠被操作的對象。

int a; // a左值
int b = a + 1;  // (a + 1)是右值,b是左值

那為什麼出來右值引用的概念呢?

因為我們用左值引用的方式對訪問右值,是不能對其進行修改的。如:

const T &d = ReturnRValue();  //! 通過編譯
T &d = ReturnRValue();   //! 編譯錯誤
error: invalid initialization of non-const reference of type 『T&』 from an rvalue of type 『T』

如果我們想要操作右值呢?那麼必須要用右值引用。

T &&d = ReturnRValue();  //! C++11上通過編譯

之後,可以像訪問普通變量一樣去訪問變量d。

右值引用讓我們在接收到右值參數的時候,可以操作去操作右值的內容。在複製構造與賦值過程中,如果可以操作右值,那麼可以將右值中的資源直接轉換到新的對象裡,從而減免了申請資源,再複製資源的操作過程。

左值構造:

T(const T &src) { //! 左值構造(複製)
  cout << "T(const T &):" << this << "<--" << &src << endl;
  _big_block = new char [1024];
  memcpy(_big_block, src._big_block, BLOCK_SIZE);
}

右值構造:

T(T &&src) { //! 右值構造(移動)
  cout << "T(T &&):" << this << "<--" << &src << endl;
  _big_block = src._big_block;
  src._big_block = NULL;
}

如下為整體的試驗代碼:

#include <iostream>
#include <cstring>
using namespace std;

#define BLOCK_SIZE 1024

struct T {
  T() { //! 構造
  cout << "T():" << this << endl;
_big_block = new char [BLOCK_SIZE];
  }

  T(const T &src) { //! 左值構造(複製)
cout << "T(const T &):" << this << "<--" << &src << endl;
_big_block = new char [1024];
memcpy(_big_block, src._big_block, BLOCK_SIZE);
  }

  ~T() { //! 析構
delete [] _big_block;
cout << "~T():" << this << endl;
  }

  T& operator = (const T &src) { //! 左值賦值(複製)
cout << "operator=(const T&):" << this << "<--" << &src << endl;
if (_big_block != NULL)
delete [] _big_block;

_big_block = new char [BLOCK_SIZE];
memcpy(_big_block, src._big_block, BLOCK_SIZE);
  }

  T(T &&src) { //! 右值構造(移動)
cout << "T(T &&):" << this << "<--" << &src << endl;
_big_block = src._big_block;
src._big_block = NULL;
  }

  T& operator = (T &&src) { //! 右值賦值(移動)
cout << "operator=(T &&):" << this << "<--" << &src << endl;
if (_big_block != NULL)
delete [] _big_block;

_big_block = src._big_block;
src._big_block = NULL;
  }

private:
  char *_big_block;
};

/////////////////////////////////////////////////////
T ReturnRvalue() { //! 返回一個T對象
  return T();
}

void AcceptT(const T &) { //! 接受左值引用
  cout << "Accept(const T &)" << endl;
}

void AcceptT(T &&) { //! 接受右值引用
  cout << "Accept(T &&)" << endl;
}

/////////////////////////////////////////////////////
int main() {
  cout << "> T a" << endl;
  T a;
  cout << "> T b = a" << endl;
  T b = a; //! 左值構造
  cout << "> T c" << endl;
  T c;
  cout << "> c = a" << endl;
  c = a; //! 左值賦值

  cout << ">" << endl;
  cout << ">" << endl;

  cout << "> T d = ReturnRvalue()" << endl;
  T d = ReturnRvalue(); //! 以右值構造d
  cout << "> T && e = ReturnRvalue()" << endl;
  T && e = ReturnRvalue(); //! 定義e引用右值,而e本身是左值
  cout << "> T f = e" << endl;
  T f = e; //! 以左值e構造f,e是左值
  cout << "> d = ReturnRvalue()" << endl;
  d = ReturnRvalue(); //! 右值賦值

  cout << ">" << endl;
  cout << ">" << endl;

  cout << "> AcceptT(a)" << endl;
  AcceptT(a); //! 接受左值
  cout << "> AcceptT(ReturnRvalue())" << endl;
  AcceptT(ReturnRvalue()); //! 接受右值
  cout << ">" << endl;
  return 0;
}

編譯命令:g++ -o test test.cpp -std=c++11 -fno-elide-constructors

運行結果:

> T aT():0x7ffd354d68c0> T b = aT(const T &):0x7ffd354d68b0<--0x7ffd354d68c0  --a是左值,所以採用左值複製構造函數> T cT():0x7ffd354d68a0> c = aoperator=(const T&):0x7ffd354d68a0<--0x7ffd354d68c0  --a是左值,所以採用左值賦值函數>>> T d = ReturnRvalue()T():0x7ffd354d6850   --創建T()對象T(T &&):0x7ffd354d68d0<--0x7ffd354d6850  --將T()移動到tmp對象,由於T()為右值,所以用右值移動構造函數~T():0x7ffd354d6850  --析構T()對象T(T &&):0x7ffd354d6890<--0x7ffd354d68d0  --將tmp移動到d對象,由於tmp為右值,所以用右值移動構造函數~T():0x7ffd354d68d0  --析構tmp對象> T && e = ReturnRvalue()T():0x7ffd354d6850   --創建T()對象T(T &&):0x7ffd354d68e0<--0x7ffd354d6850  --將T()移動到tmp對象,由於T()為右值,所以用右值移動構造函數~T():0x7ffd354d6850  --析構T()對象,e引用的是tmp對象> T f = eT(const T &):0x7ffd354d6880<--0x7ffd354d68e0   --e雖然代表的是tmp對象,但e會被繼續訪問,所以是左值,這裡採用的是左值複製構造函數> d = ReturnRvalue()T():0x7ffd354d6850T(T &&):0x7ffd354d68f0<--0x7ffd354d6850  --將tmp移動到d對象,由於tmp為右值,所以用右值移動賦值函數~T():0x7ffd354d6850operator=(T &&):0x7ffd354d6890<--0x7ffd354d68f0~T():0x7ffd354d68f0>>> AcceptT(a)Accept(const T &)  --a是左值> AcceptT(ReturnRvalue())T():0x7ffd354d6850T(T &&):0x7ffd354d6900<--0x7ffd354d6850~T():0x7ffd354d6850Accept(T &&)  --ReturnRValue()返回的是右值~T():0x7ffd354d6900>~T():0x7ffd354d6880  --左值對象析構,不解析~T():0x7ffd354d68e0~T():0x7ffd354d6890~T():0x7ffd354d68a0~T():0x7ffd354d68b0~T():0x7ffd354d68c0

使用右值引用可以提升賦值效率,避免臨時變量賦值或構造過程中沒有必要的複製過程。

相關焦點

  • 深入淺出 C++ 11 右值引用
    推薦 閱讀原文:舊文翻新,最新版本請閱讀原文😉1 寫在前面如果你還不知道 C++ 11 引入的右值引用是什麼,可以讀讀這篇文章,看看有什麼 啟發;如果你已經對右值引用了如指掌,也可以讀讀這篇文章,看看有什麼 補充。
  • 左值、右值、左值引用、右值引用
    【導讀】:本文主要詳細介紹了左值、右值、左值引用、右值引用以及move、完美轉發。Lvalue引用和rvalue引用在語法和語義上是相似的。右值引用支持移動語義的實現,可以顯著提升應用程式的性能。移動語義允許您編寫將資源(例如動態分配的內存)從一個對象傳輸到另一個對象的代碼,移動語義行之有效,因為它允許從程序中其他地方無法引用的臨時對象轉移資源。
  • C++11 中的左值、右值和將亡值
    在c++11以後,表達式按值類別分,必然屬於以下三者之一:左值(left value,lvalue),將亡值(expiring value,xvalue),純右值(pure rvalue,pralue)。其中,左值和將亡值合稱泛左值(generalized lvalue,glvalue),純右值和將亡值合稱右值(right value,rvalue)。見下圖,
  • c++11新特性,所有知識點都在這了!
    左值右值眾所周知C++11新增了右值引用,這裡涉及到很多概念:左值:可以取地址並且有名字的東西就是左值。右值:不能取地址的沒有名字的東西就是右值。純右值:運算表達式產生的臨時變量、不和對象關聯的原始字面量、非引用返回的臨時變量、lambda表達式等都是純右值。
  • c++ 之布爾類型和引用的學習總結!
    a : b = 3;printf("a=%d,b=%d\n",a,b);上面的三目運算符語句看起來怎麼有點奇怪,它作為左值了,一般在c語言裡面它應該是作為右值賦值給一個變量的,那這樣寫在c++中有沒有錯誤,答案肯定是沒有錯的,我們還是來看一下這種寫法在c語言中報了啥錯誤:
  • (C++) 快速辨別左值和右值的兩個方法
    Lu's Blog作者:Gu Lu(顧露)連結:http://gulu-dev.com/post/2016-02-07-lvalue-rvalue(點擊尾部閱讀原文前往)問題:C++左值和右值區別問題:關於C++左值和右值區別有沒有什麼簡單明了的規則可以一眼辨別?
  • 跟我學C++中級篇——STL中的字符串
    的右值語義支持         mystring(mystring&& mstr) noexcept: str_(mstr.str_)         {             mstr.str_ = nullptr;         }         mystring(const mystring& mstr): str_(new
  • C++ vector詳解
    2. size代表vector中的實際含有元素數量,而capacity表示容量。3. resize()調整size的時候會生成或釋放元素,釋放的本質只是調用了析構,但是那塊內存塊還在vector中。4. reserve()調整capacity的時候只會增大但是不會減小。
  • c++11-17 模板核心知識(零)—— 導語
    Guide為什麼要整理 cpp11-17TemplateTutorial緣起於看 folly 源碼時有好多模板相關的語法,遇到不會的需要經常查,給閱讀帶來極大的困難。其中,內容零散不系統對於我來說是一個最大的問題,因為我 C++模板沒有寫過多少,對於某一個特性或者用法,不知道其在模板的知識體系框架中處在什麼位置、解決了什麼問題,想要查相關的語法都不知道怎麼查、查的關鍵詞是什麼。 所以,我根據後面列出的三個參考資料,整理了模板的核心知識點,內容不求涉及到所有方面,但求梳理出主要脈絡,同時融入 C++11-17 模板的新特性。
  • C/C+編程筆記:C語言中的左值和右值,帶你快速弄懂它!
    如果標識符引用一個內存位置並且其類型是算術,結構,聯合或指針,則它是可修改的左值。例如,如果ptr是指向存儲區域的指針,則* ptr是可修改的l值,用於指定ptr指向的存儲區域。
  • C++、java 和 C 的區別
    的區分 ,這個跟java和c# 的區別比較大,但c#裡面有unit ulong ushort 這三種就相當於c++的修飾詞unsigned,當c++李明的變量類型定義unsigned,就默認是整數。2.java和c#裡面都有字符串型 和byte型, 但c++裡面沒有,但它是以另外的形式存儲這類型的數據的,比如 java和c#裡面的 byte其實就是unsigned char類型;c++中字符數組就能存儲字符串 (char a[]={"hello"}; ps:注意c++裡面定義數組 變量必須在中括號前面)。
  • 淺談C++類的拷貝控制
    第二個構造函數接收一個 const char * 類型指針,動態申請內存並從參數中拷貝構造字符串。(函數返回)用花括號列表初始化一個數組中的元素或者一個聚合類中的成員ClassName & operator= (const ClassName &s) {     }注意點:拷貝賦值運算符是一種運算符重載,可以看作名字為:operator= 的函數,其參數類型為 const 引用類型,
  • C++11特性:decltype關鍵字
    同時在C++11中typeid還提供了hash_code這個成員函數,用於返回類型的唯一哈希值。RTTI會導致運行時效率降低,且在泛型編程中,我們更需要的是編譯時就要確定類型,RTTI並無法滿足這樣的要求。編譯時類型推導的出現正是為了泛型編程,在非泛型編程中,我們的類型都是確定的,根本不需要再進行推導。而編譯時類型推導,除了我們說過的auto關鍵字,還有本文的decltype。
  • 再論C++中的const和引用
    (1)指針是一個常量:值為一個內存地址,不需要初始化,可以保存不同的地址通過指針可以訪問對應內存地址中的值指針可以被const修飾成為常量或者只讀變量(2)引用只是一個變量的新名字:對引用的操作(賦值、取地址等)都會傳遞到代表的變量上const引用使其代表的變量具有隻讀屬性引用必須在定義時初始化
  • 還不懂c++vector的用法,你憑什麼勇氣來的!
    今天給大家帶來一篇c++vector的介紹,難以置信這篇文章寫了我三天,不過總算整理完畢,現在分享給大家。模板類vector 和 array是數組的替代品。模板類vector 類似於string類,也是一種動態數組。 在 c++ 中,vector 是一個十分有用的容器。
  • 「最佳實踐」C++陷阱與套路
    只要可能就應該減少拷貝,比如通過共享,比如通過引用指針的形式傳遞參數和返回值。運行過程中需要動態增刪的vector,不宜存放大的對象本身 ,因為擴容會導致所有成員拷貝構造,消耗較大,可以通過保存對象指針替代。
  • C++學習,關於字符串 remove_ctrl() 函數,你知道嗎?
    函數的功能是從一個由 ASCII 字符組成的字符串中移除控制字符。看起來它似乎很無辜,但是出於多種原因,這種寫法的函數確實性能非常糟糕。而代碼清單的 remove_ctrl() 函數的執行時間在程序整體執行時間中所佔的比例非常大。
  • C++ 的門門道道 | 技術頭條 - CSDN
    運行過程中需要動態增刪的vector,不宜存放大的對象本身 ,因為擴容會導致所有成員拷貝構造,消耗較大,可以通過保存對象指針替代。resize()是重置大小;reserve()是預留空間,並未改變size(),可避免多次擴容; clear()並不會導致空間收縮 ,如果需要釋放空間,可以跟空的vector交換,std::vector <t>.swap(v),c++11裡shrink_to_fit()也能收縮內存。
  • C++11實現的100行線程池
    我來講講人話:你的函數需要在多線程中運行,但是你又不能每來一個函數就開啟一個線程,所以你就需要固定的N個線程來跑執行,但是有的線程還沒有執行完,有的又在空閒,如何分配任務呢,你就需要封裝一個線程池來完成這些操作,有了線程池這層封裝,你就只需要告訴它開啟幾個線程,然後直接塞任務就行了,然後通過一定的機制獲取執行結果。