一篇文章弄懂C++左值引用和右值引用
篇幅較長,算是從0開始介紹的,請耐心看~
該篇介紹了左值和右值的區(qū)別、左值引用的概念、右值引用的概念、std::move()的本質(zhì)、移動構(gòu)造函數(shù)、移動復(fù)制運算符和RVO。
1. 左值和右值
首先來介紹一下左值和右值的區(qū)別,內(nèi)容參考于《C++ primer 5th》4.1。
當(dāng)一個對象被用作右值的時候,用的是對象的值(內(nèi)容);當(dāng)對象被用作左值的時候,用的是對象的身份(在內(nèi)存中的位置)。(受對象用途影響)
原則:在需要右值的地方可以用左值代替,但是不能把右值當(dāng)成左值使用(對象移動除外)。當(dāng)一個左值代替右值使用時,實際使用的是它的內(nèi)容(值)。
在C++中,不能單純的說,左值可以位于賦值語句的左側(cè),但右值不可以。如:以常量對象為代表的某些左值并不能作為賦值語句的左側(cè)運算對象。例:
const int MAX_LEN = 10; // MAX_LEN是左值 MAX_LEN = 5; // 錯誤:試圖向const對象賦值。 左值不能位于賦值語句的左側(cè)。
網(wǎng)絡(luò)上有一種說法,左值位于賦值語句的左側(cè),右值位于右側(cè)(這句話不是相對于變量說的)。例:
int a = 1; // a是左值(在內(nèi)存中的位置), 1是右值(內(nèi)容),不能給1賦值 int y = a; // 用左值代替右值,把內(nèi)容賦值給y。a依然是左值,可以取地址。但是在該表達(dá)式中,左值代替右值使用,所以賦值語句右邊也可以說是右值,只不過是a的內(nèi)容。
用到左值的運算符:
- 賦值運算符需要一個(非常量)左值作為其運算對象,得到的結(jié)果也仍然是一個左值。
- 取地址符作用于一個左值運算對象,返回一個指向該運算對象的指針,這個指針是一個右值(無法放到賦值語句的左側(cè),進行賦值)。
- 內(nèi)置解引用運算符、下標(biāo)運算符、迭代器解引用運算符、string和vertor的下標(biāo)運算符。
- 內(nèi)置類型和迭代器的遞增遞減運算符作用于左值運算對象,其前置版本所得的結(jié)果也是左值。
總結(jié):常量、有地址的變量一定是左值,臨時值是右值。左值可以當(dāng)成右值用。
2. 左值引用
左值引用:引用是變量的別名,指向左值。但const左值引用除外,由于const的不可變性,所以const引用可以指向右值,我們經(jīng)常使用const引用作為函數(shù)參數(shù)傳遞。例:
int a = 1; int &b = a; // 正確 int &c = 10; // 錯誤:10是右值。 const int &d = 10; // 正確
關(guān)于左值引用的更多內(nèi)容,可以參考我的另一篇文章(建議看完左值引用再來看這篇):深入理解左值引用 https://zhuanlan.zhihu.com/p/390611356
3. 右值引用
內(nèi)容參考于《C++ primer 5th》13.6。
3.1 出現(xiàn)
在重新分配內(nèi)存的過程中,從舊元素將元素拷貝到新內(nèi)存是不必要的,更好的方式是移動元素。還有一些可以移動但不能拷貝的類,如:IO類和unique_ptr類。索所以,為了支持移動操作,新標(biāo)準(zhǔn)引入了一種新的引用類型——右值引用。
3.2 概念
右值引用:必須綁定到右值的引用,且只能綁定到一個將要銷毀的對象。所以,可以自由地將一個右值引用地資源“移動”到另一個對象中。通過&&獲得右值引用(也可以說,接管對象的控制權(quán))。例:
int a = 1; int &b = a; // 正確:左值引用,a是左值 int &&c = a; // 錯誤:右值引用,不能綁定到一個左值上 int &d = a*3; // 錯誤:左值引用,a*3是右值 const int &e = a*3; // 正確:左值引用,const引用可以綁定到一個右值上 int &&f = a*3; // 正確:右值引用,a*3是右值 int &&g = 10; // 正確:右值引用,10是右值
變量表達(dá)式依然是左值,例:
int &&a = 10; // 正確:10是右值 int &&b = a; // 錯誤:即使a是右值引用,但a依然是左值,a不是臨時對象
從上面的例子,可以看到:左值引用有持久的狀態(tài);右值要么是字面常量,要么是在表達(dá)式求值過程中創(chuàng)建的臨時對象。
由于右值引用只能綁定到臨時對象(不管編譯器怎么做,但這個我們需要遵守),所以:
所引用的對象將要被銷毀
該對象沒有其他用戶(保證安全,在使用的時候一定要特別確定這一點)
而且,使用右值的代碼可以自由地接管所引用的對象的資源。
在移動之后,要謹(jǐn)慎操作原對象,一般不操作,因為我們不確定移動操作做了哪些內(nèi)容,原對象也是處于一種不確定的狀態(tài)。
我們一般不會使用const右值引用,當(dāng)然,編譯器也不會報錯。(和右值引用的目的沖突)
3.3 應(yīng)用
3.3.1 右值引用綁定到左值上
在左值與右值的區(qū)別中,我們知道左值是可以代替右值的。那么右值引用是不是可以引用到“左值”上呢?答案是可以的,新版標(biāo)準(zhǔn)庫給我們提供了一個函數(shù)——move(),該函數(shù)的含義是:告訴編譯器,雖然我們有一個左值,但是我們希望可以像右值一樣處理。例:
#include <utility> int a = 1; int &&c = a; // 錯誤:右值引用,不能綁定到一個左值上 int &&h = std::move(a); // 正確:使用std::move()把a當(dāng)成右值處理。
3.3.2 std::move()本質(zhì)
首先,我們來看一下std::move源碼:
// xtr1common文件 // STRUCT TEMPLATE remove_reference template<class _Ty> struct remove_reference { // remove reference using type = _Ty; }; // xtr1common文件 template<class _Ty> using remove_reference_t = typename remove_reference<_Ty>::type; // type_traits文件 // FUNCTION TEMPLATE move template<class _Ty> _NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable return (static_cast<remove_reference_t<_Ty>&&>(_Arg)); }
從源碼中,可以得知:std::move既可以傳入一個左值也可以傳入一個右值,如果是左值(這里,傳入的左值的類型是T&而不是T),則將一個左值轉(zhuǎn)換成右值(_Ty& &&會被折疊成_Ty&, type是T)。其實std::move的作用僅僅是將左值轉(zhuǎn)換成右值,也就是一次類型轉(zhuǎn)換:static_cast<_Ty&&>(_Arg)。也就是說,std::move其實不“移動”,只是轉(zhuǎn)換成右值引用。例:
int v = 5; // 正確:v是左值 int &&r_ref = 8; // 正確:re_ref引用右值 int &&r_ref_move = std::move(v); // 正確:r_ref_move=5, v是左值,std::move做了一次類型轉(zhuǎn)換。_Ty& &&會被折疊成_Ty&。(賦值之后,v的值是不確定的,這個受移動賦值運算符里的內(nèi)容影響) int &&r_ref_move2 = std::move(r_ref_move); // 正確:r_ref_move2=5, r_ref_move是左值,std::move做了一次類型轉(zhuǎn)換 int &&r_ref_move3 = std::move("hello"); // 正確:可以給一個std::move傳遞一個右值 v = 9; // 正確:v、r_ref_move、r_ref_move2=9 r_ref_move = 10; // 正確:v、r_ref_move、r_ref_move2=10 r_ref_move2 = 11; // 正確:v、r_ref_move、r_ref_move2=11
3.3.3 移動構(gòu)造函數(shù)和移動賦值運算符
接下來,介紹一下移動構(gòu)造函數(shù)和移動賦值運算符,這兩個是右值引用的典型例子。
移動拷貝構(gòu)造函數(shù)
移動構(gòu)造函數(shù)中的第一個參數(shù)是該類類型的一個右值引用,本質(zhì)是在轉(zhuǎn)移對象的控制權(quán)。所以我們需要先更新新對象的指針,然后把原對象中的指針置為nullptr。
下面看一個例子:
// 不考慮規(guī)范,僅僅是一個例子class MyClass{ // 移動構(gòu)造函數(shù) // noexcept不拋出異常 MyClass(MyClass &&c) noexcept; // ...private: std::string *p;};// 接管c中的內(nèi)存,不分配任何新內(nèi)存(與拷貝構(gòu)造函數(shù)不同)MyClass::MyClass(MyClass &&c) noexcept : p(c.p){ // 對c運行析構(gòu)函數(shù)是安全的(確保原對象進入可析構(gòu)的狀態(tài)) c.p = nullptr;}
移動賦值運算符
移動賦值運算符寫法如下:
// 不考慮規(guī)范,僅僅是一個例子class MyClass{ // 移動賦值運算符 MyClass& operator=(MyClass &&c) noexcept; // ...private: std::string *p;};MyClass& MyClass::operator=(MyClass &&c) noexcept{ // 檢查自賦值:不能在使用右側(cè)運算對象的資源之前舊釋放左側(cè)運算對象的資源(可能是相同的資源) if (this != &c) { free(); //釋放已有元素 p = c.p; c.p = nullptr; } return *this;}
關(guān)于異常
不拋出異常的移動構(gòu)造函數(shù)和移動賦值運算符必須標(biāo)記為noexcept。
但是,移動構(gòu)造函數(shù)也可能出現(xiàn)異常,這個時候就不能聲明為noexcept。比如:vector的增長,可能會導(dǎo)致內(nèi)存的重新分配。使用移動構(gòu)造函數(shù)和拷貝構(gòu)造函數(shù)的結(jié)果會不同:
- 如果使用移動構(gòu)造函數(shù),很有可能移動了部分元素后出現(xiàn)異常,這樣會導(dǎo)致——舊空間中的元素已經(jīng)被改變,新空間中未構(gòu)造的元素尚不存在。
- 如果使用拷貝構(gòu)造函數(shù)出現(xiàn)異常,則很容易處理。當(dāng)在新內(nèi)存中構(gòu)造元素時,舊元素保持不變。如果此時發(fā)生異常,vector可以釋放新分配的內(nèi)存并返回,vector原有的元素不變。
- 在重新分配內(nèi)存的過程中,必須使用拷貝構(gòu)造函數(shù)而不是移動構(gòu)造函數(shù)。(這就是noexcept的作用,讓編譯器決定是否調(diào)用移動構(gòu)造函數(shù))
合成的移動操作
我們需要注意:如果一個類沒有移動操作,類會使用對應(yīng)的拷貝操作來代替移動操作。編譯器可以將一個T&&轉(zhuǎn)換成const T&,然后調(diào)用拷貝構(gòu)造函數(shù)。所以,并不是使用了移動就一定可以提升性能。當(dāng)然,我們可以在自定義類中自己聲明定義移動操作。
那么如果我們沒有聲明定義移動操作,編譯器什么時候合成默認(rèn)的移動函數(shù)呢?答案是:一個類沒有定義任何自己版本的拷貝控制成員,且類的每個非static數(shù)據(jù)成員都可以移動時,合成。具體要求如下(忘記出處了,好像是某個翻譯過來的…):
- 如果發(fā)生以下情況,編譯器將生成移動構(gòu)造函數(shù)(move constructor)
- 用戶未聲明拷貝構(gòu)造函數(shù)(copy constructor)
- 用戶未聲明拷貝賦值運算符(copy assignment operator)
- 用戶未聲明移動賦值運算符(move assignment operator)
- 用戶未聲明析構(gòu)函數(shù)(destructor)
- 該類未被標(biāo)記為已刪除(delete)
- 所有非static成員均為可移動的(moveable)
- 如果發(fā)生以下情況,編譯器將生成移動賦值運算符(move assignment operator)
- 用戶未聲明拷貝構(gòu)造函數(shù)(copy constructor)
- 用戶未聲明拷貝賦值運算符(copy assignment operator)
- 用戶未聲明移動構(gòu)造函數(shù)(move constructor)
- 用戶未聲明析構(gòu)函數(shù)(destructor)
- 該類未被標(biāo)記為已刪除(delete)
- 所有非static成員均為可移動的(moveable)
而且,移動操作永遠(yuǎn)不會隱式定義為刪除的函數(shù)。但是,我們?nèi)绻覀兪褂?default顯示地要求編譯器生成默認(rèn)移動操作,且編譯器不能移動所有成員,編譯器會將移動操作定義為刪除的函數(shù)(安全)。
需要注意的幾點:
- 如果有類成員的移動構(gòu)造函數(shù)或移動賦值運算符被定義為刪除的或是不可訪問的,則類的移動構(gòu)造函數(shù)或移動賦值運算符被定義為刪除的。
- 如果有類的析構(gòu)函數(shù)被定義為刪除的或是不可訪問的,則類的移動構(gòu)造函數(shù)被定義為刪除的。
- 如果有類的成員是const的或是引用的,則類的移動賦值運算符被定義為刪除的。
定義了一個移動構(gòu)造函數(shù)或移動賦值運算符的類必須也定義自己的拷貝操作。否則,這些成員默認(rèn)地被定義為刪除的。
三/五原則:定義一個類時,建議定義拷貝構(gòu)造函數(shù)、拷貝賦值運算符、析構(gòu)函數(shù),當(dāng)需要拷貝資源時,建議也定義移動構(gòu)造函數(shù)、移動賦值運算符。C++并不要求我們定義所有的操作,但是這些操作通常被看成一個整體。
3.3.4 std::move()的一個例子
來源《C++程序設(shè)計語言》
來看一下交換函數(shù):
// 一種比較常規(guī)的寫法template<class T>void swap(T&a, T&b){ T tmp{a}; a = b; b = tmp;}// 當(dāng)遇到string、vector這類類型的交換,第一種方法的拷貝將會造成很大的花費,所以出現(xiàn)下面的一種寫法:template<class T>void swap(T&a, T&b){ T tmp{static_cast<T&&>(a)}; a = static_cast<T&&>(b); b = static_cast<T&&>(tmp);}// 由于move函數(shù)的本質(zhì)是static_cast<T&&>,所以對上面的函數(shù)還可以優(yōu)化一下寫法template<class T>void swap(T&a, T&b){ T tmp{std::move(a)}; a = std::move(b); b = std::move(tmp);}
在這個例子中,如果類型T存在移動賦值運算符,那么運算性可能會提高。
4. 補充—協(xié)助完成返回值優(yōu)化(RVO)
來源:《More Effective C++》條款20、《Effective C++》條款21、《C++標(biāo)準(zhǔn)庫》3.1.5
例1:
X foo(){ X x; ... return x;}
對于例1:
- 如果X有一個可取用的copy或move構(gòu)造函數(shù),編譯器可以選擇略去其中的copy版本,即RVO。(平常簡單的返回std::move()可能會出錯,這要看優(yōu)化方式以及編譯器怎么處理了)
- 否則,如果X有一個move構(gòu)造函數(shù),X就被moved(搬移)。
- 否則,如果X有一個copy構(gòu)造函數(shù),X就被copied(復(fù)制)。
- 否則,報出一個編譯器錯誤。
例2:
X&& foo(){ X x; ... return std::move(x);}
對于例2,該函數(shù)返回的是一個local nonstatic對象,返回右值引用是有風(fēng)險的。具體看編譯器優(yōu)化。(當(dāng)然,最好不這樣使用。)
例3:
// 對于返回一個對象的函數(shù)進行優(yōu)化。// Rational為分?jǐn)?shù)類,numerator是分子,denominator是分母。// plan 1:返回指針,但是寫法很難看(Rational c = *(a*b)),而且可能會導(dǎo)致資源泄露(忘記刪除函數(shù)返回的指針)。const Rational* operator*(const Rational& lhs, const Rational& rhs);// plan 2:必須付出一個構(gòu)造函數(shù)調(diào)用的代價,且可能會導(dǎo)致資源泄露const Rational& operator*(const Rational& lhs, const Rational& rhs){ Rational* result = new Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()) return *result;}// plan 3:返回引用,在函數(shù)退出前,result已經(jīng)被銷毀。所以,引用指向一個不再存活的對象,會很危險且不正確。const Rational& operator*(const Rational& lhs, const Rational& rhs){ Rational result(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()) return result; // 局部非靜態(tài)對象}// 所以,如果函數(shù)一定得以值方式返回對象,是無法消除的。所以只能盡可能地降低對象返回的成本,而不是想盡辦法消除對象本身。// plan 4:有效率且正確的方法。雖然我們構(gòu)造了臨時對象,但是C++允許編譯器將臨時對象優(yōu)化,使它們不存在。編譯器優(yōu)化后,調(diào)用operator*時沒有任何臨時對象被調(diào)用出來。只需要一個constructor(用以產(chǎn)生c的代價)。const Rational operator*(const Rational& lhs, const Rational& rhs){ return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator())}// plan 5:最有效率的做法。使用inline消除調(diào)用operator*的函數(shù)開銷。inline const Rational operator*(const Rational& lhs, const Rational& rhs){ return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator())}Rational a = 10;Rational b(1,2);Rational c = a*b;
5. 總結(jié)
移動并不移動,只是轉(zhuǎn)移控制權(quán)。
std::move()只是做了一次類型轉(zhuǎn)換,轉(zhuǎn)換成一個右值引用,然后方便后續(xù)操作,比如:構(gòu)造、賦值等。真正的內(nèi)存管理,是交由移動構(gòu)造、移動賦值等移動操作處理的。有沒有性能優(yōu)化,要看有沒有移動操作以及移動操作的處理。
到此這篇關(guān)于C++左值引用和右值引用的文章就介紹到這了,更多相關(guān)C++左值引用右值引用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++中函數(shù)使用的基本知識學(xué)習(xí)教程
這篇文章主要介紹了C++中函數(shù)使用的基本知識學(xué)習(xí)教程,涵蓋了函數(shù)的聲明和參數(shù)以及指針等各個方面的知識,非常全面,需要的朋友可以參考下2016-01-01深入探討linux下進程的最大線程數(shù)、進程最大數(shù)、進程打開的文件數(shù)
本篇文章是對linux下進程的最大線程數(shù)、進程最大數(shù)、進程打開的文件數(shù)進行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05利用C++實現(xiàn)計算機輔助教學(xué)系統(tǒng)
我們都知道計算機在教育中起的作用越來越大。這篇文章主要為大家詳細(xì)介紹了如何利用C++編寫一個計算機輔助教學(xué)系統(tǒng),感興趣的可以了解一下2023-05-05C++ operator關(guān)鍵字(重載操作符)的用法詳解
下面小編就為大家?guī)硪黄狢++ operator關(guān)鍵字(重載操作符)的用法詳解。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01