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