亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

一篇文章弄懂C++左值引用和右值引用

 更新時間:2021年07月27日 12:40:21   作者:愛吃菠蘿不吃蘿卜  
左值(lvalue)和右值(rvalue)是 c/c++ 中一個比較晦澀基礎(chǔ)的概念,這篇文章主要給大家介紹了關(guān)于如何通過一篇文章弄懂C++左值引用和右值引用的相關(guān)資料,需要的朋友可以參考下

篇幅較長,算是從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++11?中的override詳解

    C++11?中的override詳解

    這篇文章主要介紹了C++11?中的override詳解,本文以重寫虛函數(shù)時,容易犯的四個錯誤為例,結(jié)合示例代碼給大家詳細(xì)介紹,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-03-03
  • C++中函數(shù)使用的基本知識學(xué)習(xí)教程

    C++中函數(shù)使用的基本知識學(xué)習(xí)教程

    這篇文章主要介紹了C++中函數(shù)使用的基本知識學(xué)習(xí)教程,涵蓋了函數(shù)的聲明和參數(shù)以及指針等各個方面的知識,非常全面,需要的朋友可以參考下
    2016-01-01
  • 深入探討linux下進程的最大線程數(shù)、進程最大數(shù)、進程打開的文件數(shù)

    深入探討linux下進程的最大線程數(shù)、進程最大數(shù)、進程打開的文件數(shù)

    本篇文章是對linux下進程的最大線程數(shù)、進程最大數(shù)、進程打開的文件數(shù)進行了詳細(xì)的分析介紹,需要的朋友參考下
    2013-05-05
  • 使用c語言生成隨機數(shù)的示例分享

    使用c語言生成隨機數(shù)的示例分享

    在C語言中,rand()函數(shù)可以用來產(chǎn)生隨機數(shù),但是這不是真真意義上的隨機數(shù),是一個偽隨機數(shù),這篇文章主要介紹了使用c語言生成隨機數(shù)的示例,需要的朋友可以參考下
    2014-03-03
  • 利用C++實現(xiàn)計算機輔助教學(xué)系統(tǒng)

    利用C++實現(xiàn)計算機輔助教學(xué)系統(tǒng)

    我們都知道計算機在教育中起的作用越來越大。這篇文章主要為大家詳細(xì)介紹了如何利用C++編寫一個計算機輔助教學(xué)系統(tǒng),感興趣的可以了解一下
    2023-05-05
  • C語言詳細(xì)講解if語句與switch語句的用法

    C語言詳細(xì)講解if語句與switch語句的用法

    用 if 語句可以構(gòu)成分支結(jié)構(gòu),它根據(jù)給的條件進行判定,以決定執(zhí)行哪個分支程序段,C 語言中還有另外一種分支語句,就是 switch 語句
    2022-05-05
  • 對C語言中遞歸算法的深入解析

    對C語言中遞歸算法的深入解析

    C通過運行時堆棧支持遞歸函數(shù)的實現(xiàn)。遞歸函數(shù)就是直接或間接調(diào)用自身的函數(shù)
    2013-07-07
  • C++中fork函數(shù)的使用及原理

    C++中fork函數(shù)的使用及原理

    這篇文章主要介紹了C++中fork函數(shù)的使用及原理,在C++中,fork函數(shù)用于創(chuàng)建一個新的進程稱為子進程,該進程與原始進程幾乎完全相同,需要的朋友可以參考下
    2023-05-05
  • C++ const修飾變量和修飾函數(shù)介紹

    C++ const修飾變量和修飾函數(shù)介紹

    這篇文章主要介紹了C++ const修飾變量和修飾函數(shù)介紹,本文直接用實例來講解各自的作用,并總結(jié)了各自的使用技巧,需要的朋友可以參考下
    2015-03-03
  • C++ operator關(guān)鍵字(重載操作符)的用法詳解

    C++ operator關(guān)鍵字(重載操作符)的用法詳解

    下面小編就為大家?guī)硪黄狢++ operator關(guān)鍵字(重載操作符)的用法詳解。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-01-01

最新評論