前言:
引用是給對象取別名,本質(zhì)是為了減少拷貝。以前我們學(xué)習(xí)的引用都是左值引用,右值引用是C++11新增的語法,它們的共同點都是給對象取別名。既然如此,有了左值引用,為什么還要有右值引用?右值引用具體是怎樣的?以及它有哪些應(yīng)用場景?接下來,會詳細(xì)分析~~
一、左值引用和右值引用
1.1 什么是左值和左值引用
左值是一個表示數(shù)據(jù)的表達(dá)式,可以是變量名、解引用的指針和前置++。左值可以取地址和賦值,它出現(xiàn)在賦值符號的左邊。如果定義的左值被const修飾,那么它就不能被賦值,但是可以取地址。
//左值
int a = 10;
const int b = 20;
int* p = new int(0);
前置++是左值是因為該運(yùn)算符先進(jìn)行自增,再使用,返回值還是它自己,所以是左值
左值引用就是給左值的引用,給左值取別名
//左值引用
int& c = a;
const int& d = b;
int*& pp = p;
1.2 什么是右值和右值引用
右值也是一個表示數(shù)據(jù)的表達(dá)式,可以是常量、表達(dá)式、函數(shù)返回值(不能是左值引用返回)和后置++。右值不可以被賦值和取地址,它出現(xiàn)在賦值符號的右邊。
int x = 1, y = 2;
//右值
10;//常量
x + y;//表達(dá)式
func(x, y);//函數(shù)返回值
后置++是右值是因為該運(yùn)算符先使用,再++,即它會返回當(dāng)前沒有自增的臨時變量,然后再自己++
右值引用就是給右值的引用,給右值取別名
//右值引用
int&& r1 = 10;
int&& r2 = x + y;
int&& r3 = func(x,y);
總結(jié):
左值是具有存儲性質(zhì)的對象,是要占內(nèi)存空間的;右值是沒有存儲性質(zhì)的對象,也就是臨時對象
判斷是左值還是右值,不能以是否可以賦值來確定,右值是不可以賦值的,左值沒有const時可以,有const時不行,所以左值和右值的本質(zhì)區(qū)別是能否取地址,左值可以取地址,右值不可以取地址。
二、左值引用和右值引用比較
前面說過,左值引用是給左值取別名,右值引用是給右值取別名,那么有個小問題,左值引用能給右值取別名嗎?右值引用又能否給左值取別名呢?答案是可以的,這里作了特殊處理:
- const左值引用可以給右值取別名
- 右值引用可以給move(左值)取別名
const int& a = 10;
int&& p = move(x);
move函數(shù)的作用是強(qiáng)制把左值轉(zhuǎn)換為右值
我們知道,引用的最主要的作用是給對象取別名,減少拷貝。既然左值引用都可以給左值和右值取別名,那右值引用的出現(xiàn)有什么意義?
先來看下左值引用有哪些應(yīng)用場景:
- 解決函數(shù)傳參的拷貝問題。函數(shù)傳參時如果沒有左值引用,就要進(jìn)行拷貝;有左值引用,不需要拷貝。
- 解決部分返回對象拷貝問題。返回對象出了函數(shù)作用域還在,沒有問題;如果出了作用域就銷毀了,就有問題。
1??函數(shù)傳參
string& operator=(const string& s)
2??返回的對象,出了作用域還在
// 賦值重載
string& operator=(const string& s)
{
string tmp(s);
swap(tmp);
return *this;//this指針指向的成員變量的作用域在整個類中
}
3??返回的對象是局部的,出了作用域就銷毀
int& Func()
{
int b = 10;
return b;
}
第一個和第二個沒問題,第三個就有問題,返回對象是一個局部對象,出了作用域就銷毀,用其他變量接收會出問題。
從這里可以發(fā)現(xiàn),函數(shù)返回一個對象時用左值引用在某些場景是不適合的,但把左值引用去掉,只能傳值返回,要拷貝。對上面的例子,返回的是一個int類型的對象,沒有多大的消耗;但是如果返回的對象消耗很大,就影響效率,比如:
yss::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
yss::string str;//是局部對象
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;//出了函數(shù)作用域就會銷毀
}
既然左值引用返回不行,傳值返回有拷貝存在,那換成右值引用返回呢?其實也是不行的。
因為就算把要返回的對象轉(zhuǎn)換為右值,還是避免不了返回對象出了作用域就銷毀的情況。
三、右值引用使用場景
3.1 傳值返回使用場景
怎么解決前面的問題呢?先來看看傳值返回的場景:
在編譯器沒有作優(yōu)化的情況下,要返回的對象是局部的,出了作用域就會銷毀,所以拷貝構(gòu)造給臨時對象,臨時對象是右值,臨時對象再拷貝構(gòu)造給ret1。整個過程拷貝構(gòu)造了兩次,拷貝了就算了,第一次拷貝構(gòu)造后,str銷毀了;第二次拷貝構(gòu)造后,臨時對象銷毀了。也就是說,產(chǎn)生的臨時空間,用完就將被銷毀,這樣是不是太浪費(fèi)資源了。所以編譯器一般都會作優(yōu)化處理,盡可能的減少拷貝次數(shù)。先看下運(yùn)行結(jié)果:
只調(diào)用了一次拷貝構(gòu)造:
3.2 移動構(gòu)造
有了編譯器的優(yōu)化,拷貝的次數(shù)減少,但還是不夠。因此,右值引用的就有它的用武之地了。先說明一下,在前面的例子中,用右值引用作返回值是不行,因為沒有解決局部對象出作用域就銷毀的根本問題;也就是說,右值引用并不是像左值引用那樣,你用了,就直接起作用,右值引用是間接起作用的。
右值引用是怎么間接起作用的呢?對比以下兩個函數(shù):
//函數(shù)1
void func(const int& x)
{
cout << "void func(const int& x)" << endl;
}
//函數(shù)2
void func(int&& x)
{
cout << "void func(int&& x)" << endl;
}
int main()
{
int x = 2;
func(x);
func(10);
return 0;
}
函數(shù)2是函數(shù)1的重載,函數(shù)1的參數(shù)是左值引用,函數(shù)2的是右值引用,先注釋掉函數(shù)2,運(yùn)行一下:
第一次調(diào)用傳入?yún)?shù)X,第二次調(diào)用傳入?yún)?shù)10都可以調(diào)用函數(shù)1,這里其實也順便驗證了左值引用既可以引用左值(參數(shù)x),也可以引用右值(常數(shù)10,特殊處理的要記得帶const)。
取消注釋,函數(shù)1和函數(shù)2都在的情況下如何:
傳入?yún)?shù)x調(diào)用函數(shù)1,參數(shù)為10調(diào)用函數(shù)2,說明調(diào)用哪個函數(shù)是根據(jù)傳的參數(shù)是左值還是右值決定的,也就是哪個更合適用哪個。
在上面例子的基礎(chǔ)上,可以對拷貝構(gòu)造進(jìn)行重載,變成移動構(gòu)造,移動構(gòu)造的作用:竊取別人的資源來構(gòu)造自己。下面是拷貝構(gòu)造和移動構(gòu)造:
// 拷貝構(gòu)造
string(const string& s)
{
cout << "string(const string& s) -- 深拷貝" << endl;
string tmp(s._str);//調(diào)用構(gòu)造函數(shù)
swap(tmp);
}
// 移動構(gòu)造
string(string&& s)
{
cout << "string(string&& s) -- 移動構(gòu)造" << endl;
swap(s);
}
/
yss::string ret1 = yss::to_string(1234);
在拷貝構(gòu)造函數(shù)和移動構(gòu)造函數(shù)都在的情況下運(yùn)行,只有移動構(gòu)造,也就是說沒有拷貝了,這得益于編譯器的優(yōu)化。
在編譯器沒有優(yōu)化的情況下:
編譯器有優(yōu)化的情況下:
對比下拷貝構(gòu)造和移動構(gòu)造:
- 根據(jù)函數(shù)調(diào)用匹配原則,如果傳入的參數(shù)是左值,調(diào)用的是拷貝構(gòu)造;如果傳入的參數(shù)是右值,調(diào)用的是移動構(gòu)造。
- 拷貝構(gòu)造(深拷貝)是比較浪費(fèi)資源的,產(chǎn)生的臨時對象tmp用完就銷毀了;移動構(gòu)造只需將被拷貝對象的資源占為己有,不需要深拷貝,提高了效率。
- 如果沒有移動構(gòu)造,不管是左值還是右值都會調(diào)用拷貝構(gòu)造,也就是前面例子中返回對象有兩次拷貝構(gòu)造的情況(假設(shè)沒有優(yōu)化)
是不是所有的類都要有移動構(gòu)造呢?
首先要清楚的是,移動構(gòu)造是為了減少拷貝。也不是所有的拷貝都需要移動構(gòu)造來解決,如果是要開空間的(深拷貝),比如string類,list等就要移動構(gòu)造減少拷貝,否則拷貝的消耗很大。如果是不需要開空間的(淺拷貝),比如日期類,成員變量都是int類型,像這樣的內(nèi)置類型直接拷貝即可。
總結(jié):
- 淺拷貝的類不需要移動構(gòu)造
- 深拷貝的類需要移動構(gòu)造
3.3 移動賦值
右值引用不僅可以用在移動構(gòu)造,還可以用在移動賦值。如果一個對象已經(jīng)存在,調(diào)用的函數(shù)返回值賦值給這個對象,就會調(diào)用移動賦值。
比如:
yss::string ret1;
ret1 = yss::to_string(1234);
拷貝賦值(賦值重載)和移動賦值:
// 賦值重載
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷貝" << endl;
string tmp(s);//調(diào)用拷貝構(gòu)造
swap(tmp);
return *this;
}
// 移動賦值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移動賦值" << endl;
swap(s);
return *this;
}
運(yùn)行一下:
接下來,作幾組對比:
1??沒有移動構(gòu)造和移動賦值
函數(shù)返回值是拷貝構(gòu)造出來的臨時對象,再賦值給已經(jīng)存在的對象ret1,賦值的過程中調(diào)用賦值構(gòu)造,賦值構(gòu)造里又有拷貝構(gòu)造,總共兩次拷貝。
2??有移動構(gòu)造,沒有移動賦值
返回對象時是移動構(gòu)造,沒有拷貝,但是賦值時要調(diào)用拷貝構(gòu)造
3??有移動構(gòu)造,有移動賦值
返回對象時沒有拷貝,str是出了作用域就銷毀,直接給返回值,返回值也是臨時對象,直接給ret1,不需要拷貝,減少了資源浪費(fèi),效率提高。
拷貝賦值與移動賦值對比:
- 如果沒有移動賦值,那么無論是左值還是右值都會調(diào)用拷貝復(fù)制,這點與拷貝構(gòu)造與移動構(gòu)造相同
- 根據(jù)函數(shù)調(diào)用匹配原則,參數(shù)是左值調(diào)用拷貝賦值,參數(shù)是右值調(diào)用移動賦值
- 拷貝賦值會先調(diào)用拷貝構(gòu)造,再進(jìn)行資源交換,交換后那個臨時的對象用完就銷毀了,整個過程比較浪費(fèi)資源。移動賦值直接將自己的資源與臨時對象的資源進(jìn)行交換,交換后自己原來的資源只需交給臨時對象處理(銷毀)
注:有可能要賦值的對象不是臨時對象,即不是右值,有可能是左值,那么情況就會有變化(對應(yīng)函數(shù)調(diào)用匹配原則),下面來看看是左值的:
yss::string ret1;
yss::string ret2;//左值
ret1 = ret2;
運(yùn)行:
3.4 STL容器接口也增加右值引用
有了右值引用,STL容器接口也作出了調(diào)整。以list的構(gòu)造為例:
不僅是構(gòu)造函數(shù),在其他接口也有增加與右值引用相關(guān)的功能。通過STL中l(wèi)ist的尾插函數(shù)來看:
list<yss::string> lt;
yss::string s1("1111");
lt.push_back(s1);//有名對象
cout << "-----------------" << endl;
lt.push_back(yss::string("2222"));//匿名對象
cout << "-----------------" << endl;
lt.push_back("3333");//隱式類型轉(zhuǎn)換
有名對象是左值,調(diào)用拷貝構(gòu)造;匿名對象和隱式類型轉(zhuǎn)換(構(gòu)造+拷貝構(gòu)造-》構(gòu)造)是右值,調(diào)用移動構(gòu)造。當(dāng)然,在調(diào)用對應(yīng)的構(gòu)造函數(shù)前,尾插函數(shù)傳參的過程需要先看下:
注:有名對象——左值可以通過move轉(zhuǎn)換為右值,但是不要輕易使用,因為一旦使用這個左值的資源將會被拿走
上面的list是C++標(biāo)準(zhǔn)庫中的list,用我們之前模擬實現(xiàn)的list試下,看有沒有同樣的效果。
第一個有兩次拷貝構(gòu)造,是因為定義空的鏈表時也有拷貝構(gòu)造,第一個下面的深拷貝才是按照圖示走的,所以第一個上面的深拷貝暫時先忽略掉
發(fā)現(xiàn)全是深拷貝,因為我們沒有重載拷貝構(gòu)造函數(shù)的傳參為右值引用,重載后再運(yùn)行看看:
ListNode(const T& x = T())
:_prev(nullptr)
, _next(nullptr)
, _val(x)
{}
ListNode(T&& x)
:_prev(nullptr)
, _next(nullptr)
, _val(x)
{}
//
//尾插
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(), x);
}
///
//pos位置插入
iterator insert(iterator pos, const T& x)
{
Node* newnode = new Node(x);//創(chuàng)建新節(jié)點
//......
}
iterator insert(iterator pos, T&& x)
{
Node* newnode = new Node(x);//創(chuàng)建新節(jié)點
//......
}
為什么還全是深拷貝呢?先來看一小段代碼:
右值引用接收右值常量10,右值引用r可以自增++,也就是說,右值引用r的屬性是左值。根據(jù)這點,所以前面的代碼用右值引用參數(shù)接收后,它的屬性變成了左值,左值再調(diào)用到下一個函數(shù),接收的是左值引用。這里需要修改下代碼,傳參時move下,讓它的參數(shù)(進(jìn)入右值引用的)變成左值后再重新變成右值
ListNode(T&& x)
:_prev(nullptr)
, _next(nullptr)
, _val(move(x))
{}
///
void push_back(T&& x)
{
insert(end(), move(x));
}
///
iterator insert(iterator pos, T&& x)
{
Node* newnode = new Node(move(x));//創(chuàng)建新節(jié)點
//......
}
運(yùn)行一下:正是我們想要的結(jié)果。
那為什么右值引用后它的屬性要變成左值呢?
因為只有右值引用的屬性是左值可以被改變,資源才可以轉(zhuǎn)移。
3.5 完美轉(zhuǎn)發(fā)
模板中的萬能引用——&&
作用:可以接收左值,也可以接收右值
template<class T>
void PerfectForward(T&& t)
{
cout << "void PerfectForward(T&& t)" << endl;
}
int main()
{
PerfectForward(10); // 右值
int a = 1;
PerfectForward(a);// 左值
PerfectForward(move(a)); // 右值
return 0;
}
注意:萬能引用雖然和右值引用都是兩個取地址符,但是要有所區(qū)分。右值引用接收右值,或者是move后的左值;萬能引用左、右值都能接收,包括const左值和const右值
const int b = 8;
PerfectForward(b);// const 左值
PerfectForward(std::move(b)); // const 右值
這樣來看好像萬能引用很不錯,但其實還是有些局限:
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); //右值
int a;
PerfectForward(a); //左值
PerfectForward(std::move(a)); //右值
const int b = 8;
PerfectForward(b); //const左值
PerfectForward(std::move(b)); //const右值
return 0;
}
以上代碼中我們的思路是:傳入右值,在PerfectForward函數(shù)中調(diào)用的函數(shù)打印右值引用;傳入左值,在PerfectForward函數(shù)中調(diào)用的函數(shù)打印左值引用;傳入const右值,在PerfectForward函數(shù)中調(diào)用的函數(shù)打印const右值引用;傳入const左值,在PerfectForward函數(shù)中調(diào)用的函數(shù)打印const左值引用。
運(yùn)行結(jié)果:
發(fā)現(xiàn)都是左值,為什么?因為萬能引用只是接收了而已,對后面該引用是左值引用還是右值引用就不歸它管了。前面提過,左值經(jīng)過左值引用后,還是左值;右值經(jīng)過右值引用后,屬性改變?yōu)樽笾?。所以這段代碼里無論左值進(jìn)來還是右值進(jìn)來最后都是調(diào)用左值引用的函數(shù)(const對應(yīng)const的)。
既然這樣,那么在調(diào)用Fun函數(shù)時把參數(shù)move下行不行呢?
Fun(move(t));
全都是右值引用了……
為了解決該問題,有一新語法:完美轉(zhuǎn)發(fā)——std::forward
作用:在傳參的過程中保留對象原生類型屬性
Fun(std::forward<T>(t));
文章來源:http://www.zghlxwxcb.cn/news/detail-847554.html
對比move和forward文章來源地址http://www.zghlxwxcb.cn/news/detail-847554.html
- move就是簡單粗暴的把左值屬性變成右值屬性
- forward是保持原來的屬性。如果本身是左值,就不變;如果本身是右值,右值引用后屬性會變成左值,但是這里面的過程相當(dāng)于被move了,又變成了右值
到了這里,關(guān)于【C++】右值引用的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!