C++?11新特性之右值引用使用案例與應(yīng)用場(chǎng)景
概述
C++ 11中引入了一項(xiàng)關(guān)鍵特性——右值引用,極大地增強(qiáng)了C++在資源管理、性能優(yōu)化和表達(dá)力方面的能力。通過理解并合理運(yùn)用右值引用,我們可以編寫出更高效、更簡(jiǎn)潔且不易出錯(cuò)的代碼。本文將深入探討右值引用的概念、工作原理及其在C++編程實(shí)踐中的應(yīng)用場(chǎng)景。
右值引用是C++中的一種特殊引用類型,它只能綁定到臨時(shí)對(duì)象或即將銷毀的對(duì)象上,也就是那些沒有命名且不再需要的對(duì)象。語法上,右值引用以&&表示,可參考下面的示例代碼。
// 右值引用,綁定到字面量整數(shù) int&& nData = 42;
工作原理
理解右值引用是學(xué)習(xí)“移動(dòng)語義”的基礎(chǔ),而要理解右值引用,就必須先區(qū)分左值與右值。
對(duì)左值和右值的一個(gè)最常見的誤解是:等號(hào)左邊的就是左值,等號(hào)右邊的就是右值。左值和右值都是針對(duì)表達(dá)式而言的,左值是指表達(dá)式結(jié)束后依然存在的持久對(duì)象,右值是指表達(dá)式結(jié)束時(shí)就不再存在的臨時(shí)對(duì)象。一個(gè)區(qū)分左值與右值的便捷方法是:看能不能對(duì)表達(dá)式取地址,如果能,則為左值,否則為右值。下面我們結(jié)合一些實(shí)際例子來進(jìn)行說明。
int a = 100; int b = 200; int *pFlag = &a; vector<int> vctTemp; vctTemp.push_back(66); string str1 = "Hello "; string str2 = "World"; const int &m = 99;
請(qǐng)問:a、b、a+b、a++、++a、pFlag、*pFlag、vctTemp[0]、100、string("CSDN")、str1、str1+str2、m分別是左值還是右值?
1、a和b都是持久對(duì)象(可以對(duì)其取地址),是左值。
2、a+b是臨時(shí)對(duì)象(不可以對(duì)其取地址),是右值。
3、a++是先取出持久對(duì)象a的一份拷貝,再使持久對(duì)象a的值加1,最后返回那份拷貝,而那份拷貝是臨時(shí)對(duì)象(不可以對(duì)其取地址),故其是右值。
4、++a則是使持久對(duì)象a的值加1,并返回那個(gè)持久對(duì)象a本身(可以對(duì)其取地址),故其是左值。
5、pFlag和*pFlag都是持久對(duì)象(可以對(duì)其取地址),是左值。
6、vctTemp[0]調(diào)用了重載的[]操作符,而[]操作符返回的是一個(gè)int &,為持久對(duì)象(可以對(duì)其取地址),是左值。
7、100和string("CSDN")是臨時(shí)對(duì)象(不可以對(duì)其取地址),是右值。
8、str1是持久對(duì)象(可以對(duì)其取地址),是左值。
9、str1+str2是調(diào)用了+操作符,而+操作符返回的是一個(gè)string(不可以對(duì)其取地址),故其為右值。
10、m是一個(gè)常量引用,引用到一個(gè)右值,但引用本身是一個(gè)持久對(duì)象(可以對(duì)其取地址),為左值。
區(qū)分清楚了左值與右值,我們?cè)賮砜纯醋笾狄谩W笾狄酶鶕?jù)其修飾符的不同,可以分為非常量左值引用和常量左值引用。
非常量左值引用只能綁定到非常量左值,不能綁定到常量左值、非常量右值和常量右值。如果允許綁定到常量左值和常量右值,則非常量左值引用可以用于修改常量左值和常量右值,這明顯違反了其常量的含義。如果允許綁定到非常量右值,則會(huì)導(dǎo)致非常危險(xiǎn)的情況出現(xiàn)。因?yàn)榉浅A坑抑凳且粋€(gè)臨時(shí)對(duì)象,非常量左值引用可能會(huì)使用一個(gè)已經(jīng)被銷毀了的臨時(shí)對(duì)象。
常量左值引用可以綁定到所有類型的值,包括:非常量左值、常量左值、非常量右值和常量右值。
可以看出,使用左值引用時(shí),我們無法區(qū)分出綁定的是否是非常量右值的情況。那么,為什么要對(duì)非常量右值進(jìn)行區(qū)分呢?區(qū)分出來了,又有什么好處呢?這就牽涉到C++中一個(gè)著名的性能問題——拷貝臨時(shí)對(duì)象??紤]下面的示例代碼。
vector<int> GetAllScores() { vector<int> vctTemp; vctTemp.push_back(90); vctTemp.push_back(95); return vctTemp; }
當(dāng)使用vector<int> vctScore = GetAllScores()進(jìn)行初始化時(shí),實(shí)際上調(diào)用了三次構(gòu)造函數(shù)。盡管有些編譯器可以采用RVO(Return Value Optimization)來進(jìn)行優(yōu)化,但優(yōu)化工作只在某些特定條件下才能進(jìn)行。可以看到,上面很普通的一個(gè)函數(shù)調(diào)用,由于存在臨時(shí)對(duì)象的拷貝,導(dǎo)致了額外的兩次拷貝構(gòu)造函數(shù)和析構(gòu)函數(shù)的開銷。當(dāng)然,我們也可以修改函數(shù)的形式為:void GetAllScores(vector<int> &vctScore),但這并不一定就是我們需要的形式。另外,考慮下面字符串的連接操作:
string s1("Hello "); string s = s1 + "W" + "o" + "r" + "l" + "d";
在對(duì)s進(jìn)行初始化時(shí),會(huì)產(chǎn)生大量的臨時(shí)對(duì)象,并涉及到大量字符串的拷貝操作,這顯然會(huì)影響程序的效率和性能。怎么解決這個(gè)問題呢?如果我們能確定某個(gè)值是一個(gè)非常量右值(或者是一個(gè)以后不會(huì)再使用的左值),則我們?cè)谶M(jìn)行臨時(shí)對(duì)象的拷貝時(shí),可以不用拷貝實(shí)際的數(shù)據(jù),而只是“竊取”指向?qū)嶋H數(shù)據(jù)的指針(類似于STL中的auto_ptr,會(huì)轉(zhuǎn)移所有權(quán))。C++ 11中引入的右值引用,正好可用于標(biāo)識(shí)一個(gè)非常量右值。C++ 11中用&表示左值引用,用&&表示右值引用,比如:int &&a = 10。
右值引用根據(jù)其修飾符的不同,也可以分為:非常量右值引用和常量右值引用。
非常量右值引用只能綁定到非常量右值,不能綁定到非常量左值、常量左值和常量右值。如果允許綁定到非常量左值,則可能會(huì)錯(cuò)誤地竊取一個(gè)持久對(duì)象的數(shù)據(jù),而這是非常危險(xiǎn)的;如果允許綁定到常量左值和常量右值,則非常量右值引用可以用于修改常量左值和常量右值,這明顯違反了其常量的含義。
常量右值引用可以綁定到非常量右值和常量右值,不能綁定到非常量左值和常量左值(理由同上)。
使用案例
有了右值引用的概念,我們可以用它來實(shí)現(xiàn)下面的CMyString類。
#include <iostream> using namespace std; class CMyString { public: // 構(gòu)造函數(shù) CMyString(const char *pszSrc = NULL) { cout << "CMyString(const char *pszSrc = NULL)" << endl; if (pszSrc == NULL) { m_pData = new char[1]; *m_pData = '\0'; } else { m_pData = new char[strlen(pszSrc) + 1]; strcpy(m_pData, pszSrc); } } // 拷貝構(gòu)造函數(shù) CMyString(const CMyString &s) { cout << "CMyString(const CMyString &s)" << endl; m_pData = new char[strlen(s.m_pData) + 1]; strcpy(m_pData, s.m_pData); } // move構(gòu)造函數(shù) CMyString(CMyString &&s) { cout << "CMyString(CMyString &&s)" << endl; m_pData = s.m_pData; s.m_pData = NULL; } // 析構(gòu)函數(shù) ~CMyString() { cout << "~CMyString()" << endl; delete [] m_pData; m_pData = NULL; } // 拷貝賦值函數(shù) CMyString &operator =(const CMyString &s) { cout << "CMyString &operator =(const CMyString &s)" << endl; if (this != &s) { delete [] m_pData; m_pData = new char[strlen(s.m_pData) + 1]; strcpy(m_pData, s.m_pData); } return *this; } // move賦值函數(shù) CMyString &operator =(CMyString &&s) { cout << "CMyString &operator =(CMyString &&s)" << endl; if (this != &s) { delete [] m_pData; m_pData = s.m_pData; s.m_pData = NULL; } return *this; } private: char *m_pData; }; int main() { CMyString strText("Hello CSDN"); CMyString strText2; strText2 = CMyString("BeiJing"); return 0; }
可以看到,上面我們添加了move版本的構(gòu)造函數(shù)和賦值函數(shù)。那么,添加了move版本后,對(duì)類的自動(dòng)生成規(guī)則有什么影響呢?唯一的影響就是,如果提供了move版本的構(gòu)造函數(shù),則不會(huì)生成默認(rèn)的構(gòu)造函數(shù)。另外,編譯器永遠(yuǎn)不會(huì)自動(dòng)生成move版本的構(gòu)造函數(shù)和賦值函數(shù),它們需要你手動(dòng)顯式地添加。
當(dāng)添加了move版本的構(gòu)造函數(shù)和賦值函數(shù)的重載形式后,某一個(gè)函數(shù)調(diào)用應(yīng)當(dāng)使用哪一個(gè)重載版本呢?下面是按照判決的優(yōu)先級(jí)列出的3條規(guī)則。
1、常量值只能綁定到常量引用上,不能綁定到非常量引用上。
2、左值優(yōu)先綁定到左值引用上,右值優(yōu)先綁定到右值引用上。
3、非常量值優(yōu)先綁定到非常量引用上。
當(dāng)給構(gòu)造函數(shù)或賦值函數(shù)傳入一個(gè)非常量右值時(shí),依據(jù)上面給出的判決規(guī)則,可以得出會(huì)調(diào)用move版本的構(gòu)造函數(shù)或賦值函數(shù)。而在move版本的構(gòu)造函數(shù)或賦值函數(shù)內(nèi)部,都是直接“移動(dòng)”了其內(nèi)部數(shù)據(jù)的指針。因?yàn)樗欠浅A坑抑担且粋€(gè)臨時(shí)對(duì)象,移動(dòng)了其內(nèi)部數(shù)據(jù)的指針不會(huì)導(dǎo)致任何問題,它馬上就要被銷毀了,我們只是重復(fù)利用了其內(nèi)存,這樣就省去了拷貝數(shù)據(jù)的大量開銷。
一個(gè)需要注意的地方是:拷貝構(gòu)造函數(shù)可以通過直接調(diào)用*this = s來實(shí)現(xiàn),但move構(gòu)造函數(shù)卻不能。這是因?yàn)樵趍ove構(gòu)造函數(shù)中,s雖然是一個(gè)非常量右值引用,但其本身卻是一個(gè)左值(是持久對(duì)象,可以對(duì)其取地址),因此調(diào)用*this = s時(shí),會(huì)使用拷貝賦值函數(shù)而不是move賦值函數(shù),而這已與move構(gòu)造函數(shù)的語義不相符。要使語義正確,我們需要將左值綁定到非常量右值引用上,C++ 11提供了move函數(shù)來實(shí)現(xiàn)這種轉(zhuǎn)換,因此我們可以修改為*this = move(s),這樣move構(gòu)造函數(shù)就會(huì)調(diào)用move賦值函數(shù)。
應(yīng)用場(chǎng)景
1、智能指針。
C++ 11標(biāo)準(zhǔn)庫中的std::unique_ptr和std::shared_ptr等智能指針利用了右值引用來安全有效地轉(zhuǎn)移所有權(quán)。
2、容器。
C++ STL容器,比如:std::vector、std::string等,在C++11之后都開始支持移動(dòng)構(gòu)造和移動(dòng)賦值,從而避免了在添加元素或重新分配內(nèi)存時(shí)不必要的數(shù)據(jù)拷貝。
3、字符串字面值。
C++ 11中引入了std::string_view,它可以通過右值引用高效地引用字符串字面值,或現(xiàn)有std::string對(duì)象的內(nèi)容,而無需復(fù)制。
4、RAII原則強(qiáng)化。
RAII,也就是資源獲取即初始化(英文為:Resource Acquisition Is Initialization),是C++中一種重要的資源管理手段。右值引用使資源在生命周期結(jié)束時(shí)能被自動(dòng)回收,并允許在對(duì)象之間高效地“移動(dòng)”資源控制權(quán)。
總結(jié)
右值引用無疑是C++ 11的一項(xiàng)重大革新,它不僅提高了程序的運(yùn)行效率,也極大地簡(jiǎn)化了程序員對(duì)資源管理的復(fù)雜度。掌握這一特性對(duì)于寫出更現(xiàn)代、更高效的C++代碼至關(guān)重要。同時(shí),隨著C++后續(xù)版本的發(fā)展,右值引用在標(biāo)準(zhǔn)庫中的使用越來越廣泛,成為現(xiàn)代C++開發(fā)者的必備知識(shí)之一。
到此這篇關(guān)于C++ 11新特性之右值引用使用案例與應(yīng)用場(chǎng)景的文章就介紹到這了,更多相關(guān)C++ 11新特性右值引用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++實(shí)現(xiàn)圖的鄰接表存儲(chǔ)和廣度優(yōu)先遍歷實(shí)例分析
這篇文章主要介紹了C++實(shí)現(xiàn)圖的鄰接表存儲(chǔ)和廣度優(yōu)先遍歷,實(shí)例分析了C++實(shí)現(xiàn)圖的存儲(chǔ)與遍歷技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04C++實(shí)現(xiàn)類似延時(shí)停頓的打字效果
這篇文章主要介紹的是使用C++實(shí)現(xiàn)類似延時(shí)停頓的打字效果的代碼,非常的簡(jiǎn)單,推薦給大家,有需要的小伙伴可以參考下。2015-03-03C語言實(shí)現(xiàn)共享單車管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)共享單車管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08詳解C語言結(jié)構(gòu)體中的char數(shù)組如何賦值
這篇文章主要給大家介紹了關(guān)于C語言結(jié)構(gòu)體中的char數(shù)組如何賦值的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-03-03淺析結(jié)束程序函數(shù)exit, _exit,atexit的區(qū)別
在一個(gè)程序中最多可以用atexit()注冊(cè)32個(gè)處理函數(shù),這些處理函數(shù)的調(diào)用順序與其注冊(cè)的順序相反,也即最先注冊(cè)的最后調(diào)用,最后注冊(cè)的最先調(diào)用2013-09-09