網(wǎng)站首頁 編程語言 正文
區(qū)分左值與右值
在C++面試的時候,有一個看起來似乎挺簡單的問題,卻總可以挖出坑來,就是問:“如何區(qū)分左值與右值?”
如果面試者自信地回答:“簡單來說,等號左邊的就是左值,等號右邊的就是右值。” 那么好了,手寫一道面試題繼續(xù)提問。
int a=1; int b=a;
問:a和b各是左值還是右值?
b是左值沒有疑問,但如果說a在上面是左值,在下面是右值的,那就要面壁思過了。C++從來就不是一門可以淺嘗輒止的編程語言,要學(xué)好它真的需要不斷地去探問。公布答案:上面代碼中的a和b都是左值。所以在很多地方都能看到的區(qū)分左右值說法是并不準(zhǔn)確的。
如果是給出描述性的說明,那么左值就是指向特定內(nèi)存具有名稱的值(具名對象),它有一個相對穩(wěn)定的內(nèi)存地址,并且有一段較長的生命周期。右值是不指向穩(wěn)定內(nèi)存地址的匿名值(不具名對象),它的生命周期很短,通常是暫時性的。
要是看著上面這段說明有些抽象,那還有一個好辦法來幫助區(qū)分,那就是是否可以用取地址符“&”來獲得地址。如果能取到地址的則為左值,否則編譯期都報錯的,那就是右值。
還是以上面的代碼為例,&a; &b;
這個一眼能看出來可以取地址成功,這是左值。而&1
這樣的寫法編譯器肯定會報錯,所以1是右值。用這樣的方法,目測也可以判斷出來了。
右值引用
說到C++中的引用,相信大家都很熟悉其用法了。在函數(shù)調(diào)用時需要對變量進行修改,或者避免內(nèi)存復(fù)制,就會使用引用的方式。當(dāng)然,使用指針也能達到一樣的效果,但引用相對來說更為安全可靠。這種使用方式就是左值引用。
那么好了,我們先從語法上來認(rèn)識一下右值引用。
int i = 0; int &j = i; //左值引用 int &&k = 10; //右值引用
我們看到,右值引用的寫法就是在變量名前加上"&&"標(biāo)識。它的作用是可以延長字面量數(shù)字10的生命周期。不過,這看起來似乎并沒什么用,不像左值引用那樣已經(jīng)深入人心。那么,我們接下來看一段有意義的示例代碼。
#include <iostream> using namespace std; static const int DataSize = 1024; class ActOne { public: ActOne() { cout << "ActOne default construct" << endl; } ActOne(const ActOne &one) { cout << "ActOne copy construct" << endl; } ~ActOne() { cout << "ActOne destructor" << endl;} void DoSomething() { cout << "ActOne work" << endl; } }; ActOne make_one() { ActOne one; return one; } int main() { ActOne one = make_one(); one.DoSomething(); cout << "++++++++++" << endl; ActOne &&one2 = make_one(); one2.DoSomething(); }
上述源碼就是實現(xiàn)生成一個對象并返回的功能。需要注意的是,如果使用g++編譯器,對這段代碼進行編譯的時候要加上-fno-elide-constructors
以屏蔽編譯器對構(gòu)造函數(shù)的優(yōu)化操作。
再來看下運行結(jié)果:
ActOne default construct
ActOne copy construct
ActOne destructor
ActOne copy construct
ActOne destructor
ActOne work
++++++++++
ActOne default construct
ActOne copy construct
ActOne destructor
ActOne work
ActOne destructor
ActOne destructor
經(jīng)過對比,我們可以發(fā)現(xiàn)未使用右值引用的寫法中,拷貝構(gòu)造函數(shù)執(zhí)行了兩次,因為這是make_one()
中的return one;
會復(fù)制一次構(gòu)造產(chǎn)生的臨時對象,接著在ActOne one = make_one();
語句中將臨時對象復(fù)制到one變量,這是第二次拷貝構(gòu)造的調(diào)用。
那么,使用了右值引用的方法中,拷貝構(gòu)造函數(shù)只調(diào)用了一次,one2實際上指向的是一個臨時存儲的變量。因為這個臨時變量被one2作為右值所引用,因此其生命期也延長到main函數(shù)結(jié)束才調(diào)用解析構(gòu)造方法。
大家可以好好體會一下右值引用的作用,對于性能敏感的C++程序員來說,它不僅是降低了程序運行的開銷,而且臨時局部變量的可引用,也意味著可以減少動態(tài)分配內(nèi)存所帶來的管理復(fù)雜度。
移動語義
可能有同學(xué)出于對技術(shù)的追求,會繼續(xù)提問:那我還想優(yōu)化程序性能,再減少一次拷貝構(gòu)造函數(shù)的開銷行不行?應(yīng)當(dāng)對這樣的提問給予積極的回應(yīng),答案是可以的,這就是C++11標(biāo)準(zhǔn)所引入的移動語義。
讓我們將上一節(jié)的代碼稍加改動,然后來體會一下移動語義的使用。main
函數(shù)和make_one
函數(shù)沒有變化,所以僅列出ActOne
類的源碼。
class ActOne { public: ActOne():data_ptr(new uint8_t[DataSize]) { cout << "ActOne default construct" << endl; } ActOne(const ActOne &one) { cout << "ActOne copy construct" << endl; } ActOne(ActOne &&one) { // 移動構(gòu)造方法 cout << "ActOne move construct" << endl; data_ptr = one.data_ptr; one.data_ptr = nullptr; } ~ActOne() { cout << "ActOne destructor" << endl; if (data_ptr != nullptr) { delete []data_ptr; } } void DoSomething() { cout << "ActOne work" << endl; } private: uint8_t *data_ptr; };
我想對于任何一名寫C/C++的代碼的程序員來說,最大的愿望就是動態(tài)內(nèi)存的分配和釋放次數(shù)越少越好。源碼中的ActOne(ActOne &&one)
就是一個移動構(gòu)造方法,它接受的是一個右值作為參數(shù),通過轉(zhuǎn)移實參對象的數(shù)據(jù)以實現(xiàn)構(gòu)造目標(biāo)對象。如果是復(fù)制構(gòu)造要怎么做?那就要先為data_ptr
分配好內(nèi)存,然后再調(diào)用內(nèi)存拷貝函數(shù)memcpy
進行一次DataSize
字節(jié)數(shù)的復(fù)制。
相比于復(fù)制構(gòu)造方法,移動構(gòu)造只需要進行指針值的替換即可,其時空消耗是不可同日而語的。程序添加了一個移動構(gòu)造方法運行之后的結(jié)果如下:
ActOne default construct
ActOne move construct
ActOne destructor
ActOne move construct
ActOne destructor
ActOne work
++++++++++
ActOne default construct
ActOne move construct
ActOne destructor
ActOne work
ActOne destructor
ActOne destructor
從上面的結(jié)果可以觀察到,在右值引用和移動語義的配合下,內(nèi)存的分配實際只發(fā)生了一次,移動構(gòu)造也只有一次。大家可以往上翻到上一節(jié)的程序打印結(jié)果,對比一下純拷貝式的構(gòu)造,進行了三次內(nèi)存的分配,兩次內(nèi)存深復(fù)制操作。這對于程序性能的影響已經(jīng)不用多說了,各位可以進行benchmark測試以驗證移動語義帶來的提升了。
從構(gòu)造函數(shù)的優(yōu)先級來說,編譯器對于右值會優(yōu)先使用移動構(gòu)造函數(shù)去生成目標(biāo)對象,如果移動構(gòu)造函數(shù)不存在,則是使用復(fù)制構(gòu)造函數(shù)。那么賦值運算符能不能進行移動操作呢?答案是可以的,這個實現(xiàn)就留給各位自己去嘗試吧。
提示一下,賦值運算符函數(shù)的聲明:
ActOne & operator=(ActOne &&one) {……}
完美轉(zhuǎn)發(fā)
我們再來學(xué)習(xí)C++11中的一個新特性,就是萬能引用。何謂萬能,這個名稱很唬人,其實就是一種引用的實現(xiàn)方法,它既可以引用左值,也可以引用右值。不廢話,還是直接上代碼。
int get_param() { return 100;} int &&a = get_param(); // a為右值引用 auto &&b = get_param(); // b為萬能引用
可以看到,a和b的區(qū)別就在于b的類型是由auto
推導(dǎo)而來,而a則是確定類型的。這是作為函數(shù)返回值的,再看一個模板參數(shù)的例子:
template <class T> void func1(T &&t){} // t為萬能引用 int a = 100; const int b = 200; func1(a); func1(b); func1(get_param());
模板方法的參數(shù)t可以接受任何類型的數(shù)據(jù),并推導(dǎo)出一個引用類型結(jié)果,是什么結(jié)果我們后面會說。所以我們會發(fā)現(xiàn),萬能引用本質(zhì)上是發(fā)生了類型推導(dǎo)。auto &&
和T &&
在初始化過程中都會發(fā)生類型推導(dǎo)。
那么推導(dǎo)結(jié)果的規(guī)則也很簡單:
- 如果源對象是左值,則目標(biāo)對象會被推導(dǎo)為左值引用;
- 如果源對象是右值,則目標(biāo)對象會被推導(dǎo)為右值引用。
萬能引用的概念大家已經(jīng)了解,那么它的用途是什么呢?這就是本節(jié)標(biāo)題所要說的完美轉(zhuǎn)發(fā)。實話說,我不太喜歡C++術(shù)語中的某些翻譯,在中文語境下很容易讓人費解、誤解或是產(chǎn)生不必要的期待。例如C++的萬能引用可以實現(xiàn)完美轉(zhuǎn)發(fā),如果你向一名初學(xué)者來上這么一句,他是不是會覺得“這門語言也太牛X了吧,竟然有萬能和完美的特性?” 竊以為換成“全值引用”和“任意轉(zhuǎn)發(fā)”會不會低調(diào)和貼切一些呢。
讓我們先從轉(zhuǎn)發(fā)的一個局限性示例說起:
template<class T> void show_info(T t) { cout << "type is: " << typeid(t).name() << endl; } template<class T> void transform(T t) { show_info(t); } int main() { string tmp("test for forward"); transform(tmp); }
上述代碼可以工作,但從性能上說string
類對象作為參數(shù)傳遞時會發(fā)生一次臨時對象復(fù)制。在實際工作中,它可能就是一個包含有大塊內(nèi)存變量的對象,顯然不能這么干。那就給參數(shù)加上一個&符使之成為左值引用吧。下一個問題又來了,如果傳的參數(shù)是個右值怎么?看到這里,大家就明白了,要想結(jié)束抬杠在這兒用上萬能引用就好了。
最終版完美引用實現(xiàn),僅列出有變動的代碼:
template<class T> void transform(T &&t) { show_info(std::forward<T>(t)); }
std::forward()
是標(biāo)準(zhǔn)庫中的模板方法,它的功能就是可以根據(jù)值的類型將其按左值引用或右值引用進行轉(zhuǎn)發(fā)。這樣,既避免了臨時對象復(fù)制的開銷,又可以支持任意類型的對象轉(zhuǎn)發(fā)。某種意義上,將其稱為“完美”似乎也并不為過。畢竟要讓挑剔的C++程序員感到滿意并不容易啊。
需要注意的是,標(biāo)準(zhǔn)庫中的std::move
()方法是將任意實參轉(zhuǎn)換為右值引用,使用這個方法不需要指定模板實參。而std::forward()
方法在使用的時候必須指定模板實參,也只有它才能按實際類型進行轉(zhuǎn)發(fā)。
結(jié)語
右值引用說到這里,相信大家已經(jīng)從一知半解的狀態(tài)到可以理解并運用了。它對于苛求性能以及強調(diào)效率的場景有著非凡的意義,例如在基礎(chǔ)庫組件的實現(xiàn)中。雖然大多數(shù)程序員都不一定會參與到基礎(chǔ)庫的開發(fā)中,但這就看個人對于技術(shù)之道的追求了。即使是調(diào)用別人做好的庫來組裝一個應(yīng)用,也會遇到性能調(diào)優(yōu)的問題,那個時候你對老板有多大的價值就體現(xiàn)在這里了。
如果大家在工作中發(fā)現(xiàn)以前的代碼在用支持C++11的編譯器重新編譯之后,運行效率居然有了提升,不用奇怪,這就是基于C++11的新特性做的編譯期優(yōu)化。例如今天學(xué)習(xí)的右值引用、移動語義、萬能引用、完美轉(zhuǎn)發(fā)等就在語法層面提供了良好的支持。
希望我們接下來在實踐中不斷練習(xí),能夠發(fā)揮出C++的最大威力來!
原文鏈接:https://blog.csdn.net/michaeluo/article/details/124298507
相關(guān)推薦
- 2023-01-21 Python?sklearn中的K-Means聚類使用方法淺析_python
- 2022-07-07 Python筆記之a(chǎn)?=?[0]*x格式的含義及說明_python
- 2023-11-23 python怎么判斷電腦環(huán)境是32位還是64位
- 2022-11-26 詳解Python中的with語句和上下文管理器_python
- 2022-11-22 Python網(wǎng)絡(luò)請求模塊urllib與requests使用介紹_python
- 2023-01-07 MPAndroidChart自定義圖表Chart的Attribute及Render繪制邏輯_Andr
- 2022-11-16 Python制作數(shù)據(jù)分析透視表的方法詳解_python
- 2022-09-27 python?實現(xiàn)打印掃描效果詳情_python
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支