C++11之右值引用與移動構造
----------------------------右值引用---------------------------------
右值定義:
通俗來講,賦值號左邊的就是左值,賦值號右邊的就是右值。可以取地址是左值,不可以取地址的是右值。C++11,之前沒有明確提出右值的概念,所以 C++11 以前這些說活都是正確的。
C++11 中的左值,仍然等同於 C++98 左值。C++11 中的右值,除了 C++98 中的右值以外,增加了將亡值的。
如下圖
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
int a; int & lref = a;
//int && rref = a; //!error:右值引用不能接受左值
//int & elref = a*34; //!error:普通引用不能接受臨時值
const int &eclref = a*34;
int && erref = a*34;
return 0;
}
右值引用主要解決什麽問題?
為什麽要引入右值引用這個概念,其實就是為了解決臨時對象帶來的效率問題。比如,我們返回一個臨時對象。
在 C++中,棧對象是可以返回的,棧對象象的引用卻不可以返回。
棧對象返回,如果在沒有優化的情況下 -fno-elide-constructors,會產生臨時對象。
過程如下:
在Qt的工程文件中,也就是pro中添加QMAKE_CXXFLAGS_DEBUG += -fno-elide-constructors
禁止Qt自動優化。
#include <iostream>
using namespace std;
class A
{
public:
A(){
cout<<"A() "<<this<<endl;
}
~A(){
cout<<"~A() "<<this<<endl;
}
A(const A &another)
{
cout <<"A(const A&)"<<&another<<"->"<<this<<endl;
}
void dis()
{
cout<<"xxxxoooooooooooo"<<endl;
}
};
A getObjectA()
{
return A();
}
int main(int argc, char *argv[])
{
//先後註掉這兩行代碼得到執行1結果 和 執行2結果
A a = getObjectA(); //!執行1
A &&a = getObjectA();//!執行2
return 0;
}
執行1結果: A() 0x61fe6f A(const A&)0x61fe6f->0x61fe9f ~A() 0x61fe6f A(const A&)0x61fe9f->0x61fe9e ~A() 0x61fe9f ~A() 0x61fe9e 執行2結果: A() 0x61fe7f A(const A&)0x61fe7f->0x61feab ~A() 0x61fe7f ~A() 0x61feab
執行1結果分析:
匿名對象(棧空間)——>臨時對象(寄存器)——>返回的對象(棧空間) :經歷了兩次拷貝
執行2結果分析:
匿名對象(棧空間)——>返回的對象(棧空間);一次拷貝
const T & 萬能常引用
其本質也是產生了臨時對象, 並且該臨時對象是 const 的。 此對於非基本數據類型也適用, 但需要轉化構造函數。
int main(int argc, char *argv[])
{
// int& ri = 5;
// const & cri = 5;
// float f = 34.5;
// int & irf = f;
// const int & cirf = f;
// A objA;
// int& irA = objA;
// const int& cirA = objA;
// cout<<cirA<<endl;
const A& ret = getObjectA();
ret.dis(); //!error:一個const對象,沒有const函數,不能再完成調用。
return 0;
}
此種舉措,也可以在返回中避免臨時對象,再次拷貝和銷毀。但時臨時對象的性質是 const 的,也會給後續的使用帶來不便。
右值引用與左值引用的對比
1) 都屬於引用類型。
2)都必須初始化。 左值引用是具名變量值的別名, 右值引用是匿名變量的別名。
3) 左值引用, 用於傳參, 好處在於, 擴展對象的作用域。 則右值引用的作用就在於延長了臨時對象的生命周期。
4) 避免“先拷貝再廢棄” 帶來的性能浪費。
5) 對將亡值進行引用的類型; 它存在的目的就是為了實現移動語義
const T & 與T && 本質對比
#include <iostream> using namespace std; int main( ) { const int & i =10;
int && iii = 10 }
/*int && iii = 10的匯編代碼
0x08048400 mov $0xa,%eax 0x08048405 mov %eax,-0xc(%ebp) 0x08048408 lea -0xc(%ebp),%eax 0x0804840b mov %eax,-0x4(%ebp)
第一句將10賦值給eax,第二句將eax放入-0xc(%ebp)處,“臨時變量會引用關聯到右值時,右值被存儲到特定位置”,-0xc(%ebp)便是該臨時變量的地址,後兩句通過eax將該地址存到iii處。
通過上述代碼,我們還可以發現,在上述的程序中-0x4(%ebp)存放著右值引用iii,-0x8(%ebp)存放著左值引用,-0xc(%ebp)存放著10,而-0x10(%ebp)存放著1,左值引用和右值引用同int一樣是四個字節(因為都是地址)
const int & i =10的匯編代碼
0x08048583 mov $0xa,%eax 0x08048588 mov %eax,-0x8(%ebp) 0x0804858b lea -0x8(%ebp),%eax 0x0804858e mov %eax,-0x4(%ebp)
-0x4(%ebp)處存放著i,-0x8(%ebp)處則存放著臨時對象10,程序將10的地址存放到了i處。看到這裏會發現const引用在綁定右值時和右值引用並沒有什麽區別。
*/
參考:https://www.cnblogs.com/likaiming/p/9045642.html
重要的事情再說一遍:
右值引用可以承接一個臨時變量,並且將臨時變量的生命周期擴展到右值引用所在的作用域。
右值引用存在的意義就在於既獲得了同const T&同樣的效率,又解決了const引用不可不可作文non-const的問題。
C++中能夠接受臨時變量(右值)的只有兩種類型,一種是const類型,另一種是右值引用。但是當使用const類型接收了一個類對象時,使用該對象是無法調用內部非const類型函數的。(const 對象只能調用const函數)。但是使用右值引用卻是可以的。並且相對於直接使用對象來接收臨時變量,使用右值引用。在系統層面只產生兩次構造(包含一次拷貝構造)和兩次析構。這在效率上來說是非常高的。
---------------------------------------移動構造--------------------------------------------
深拷貝深賦值
對於類中,含有指針的情況,要自實現其拷貝構造和拷貝賦值。也就是所謂的深拷貝和深賦值。我想這己經成為一種共識了。
比如如下類:
#include <iostream>
using namespace std;
class HasPtrMem
{
public:
HasPtrMem():_d(new int(0)){
cout<<"HasPtrMem()"<<this<<endl;
}
HasPtrMem(const HasPtrMem& another)
:_d(new int(*another._d))
{
cout<<"HasPtrMem(const HasPtrMem&
another)"<<this<<"->"<<&another<<endl;
}
~HasPtrMem(){
delete _d;
cout<<"~HasPtrMem()"<<this<<endl;
}
int * _d;
};
HasPtrMem getTemp()
{
return HasPtrMem();
}
int main(int argc, char *argv[])
{
// HasPtrMem a;
// HasPtrMem b(a);
// cout<<*a._d<<endl;
// cout<<*b._d<<endl;
HasPtrMem&& ret = getTemp();
return 0;
}
上面的過程,我們己經知曉,ret 作為右值引用,引用了臨時對象,由於臨時對象是待返回對象的復本,所以表面上看起來是,待返回對象的作用域擴展了,生命周期也延長了。
從右值引到移動構造
前面我們建立起來了一個概念,就是右值引用。用右值引用的思想,再來實現一下拷貝。這樣,順便把臨時對象的問題也解決了。
#include <iostream>
using namespace std;
class HasPtrMem
{
public:
HasPtrMem():_d(new int(0)){
cout<<"HasPtrMem()"<<this<<endl;
}
HasPtrMem(const HasPtrMem& another)
:_d(new int(*another._d)){
cout<<"HasPtrMem(const HasPtrMem& another)" <<this<<"->"<< &another<<endl;
}
HasPtrMem(HasPtrMem &&another)
{
cout<<this<<" Move resourse from "<<&another<<"->"<< another._d <<endl;
_d = another._d;
another._d = nullptr;
}
~HasPtrMem(){
delete _d;
cout<<"~HasPtrMem()"<<this<<endl;
}
int * _d;
};
HasPtrMem getTemp()
{
return HasPtrMem();
}
int main(int argc, char *argv[])
{
HasPtrMem a = getTemp();
return 0;
}
移動構造
如下是,移動構造函數。我們借用臨時變量,將待返回對象的內容“偷”了過來。
移動構造充分體現了右值引用的設計思想,通過移動構造我們也在對象層面看清了右值引用的本質。從而對於普通類型右值引用內部是怎樣操作的的也就不難理解了。
//移動構造
HasPtrMem(HasPtrMem &&another)
{
cout<<this<<" Move resourse from "<<&another<<"->"<< another._d<<endl;
_d = another._d;
another._d = nullptr;
}
再來看一下拷貝構造函數,我們對比一下區別:
HasPtrMem(const HasPtrMem& another)
:_d(new int(*another._d)){
cout<<"HasPtrMem(const HasPtrMem& another)" <<this<<"->"<< &another<<endl;
}
移動構造相比於拷貝構造的區別,移動構造通過指針的賦值,在臨時對象析構之前,及時的接管了臨時對象在堆上的空間地址。
關於默認的移動構造函數
對於不含有資源的對象來說,自實現拷貝與移動語義並沒有意義,對於這樣的類型 而言移動就是拷貝,拷貝就是移動。
拷貝構造/賦值和移動構造/賦值,必須同時提供或是同時不提供。才能保證同時俱有拷貝和移動語義。只聲明一種的話,類只能實現一種語義。
只有拷貝語義的類,也就是 C++98 中的類。而只有移動語義的類,表明該類的變量所擁有的資源只能被移動,而不能被拷貝。那麽這樣的資源必須是唯一的。只有移動語義構造的類型往往是“資源型”的類型。比如智能指針,文件流等。
效率問題
#include <iostream>
using namesapce std;
class Copyable
{
public:
Copyable(int i)
:_i(new int(i))
{
cout<<"Copyable(int i):"<<this<<endl;
}
Copyable(const Copyable & another)
:_i(new int(*another._i))
{
cout<<"Copyable(const Copyable & another):"<<this<<endl;
}
Copyable(Copyable && another)
{
cout<<"Copyable(Copyable && another):"<<this<<endl;
_i = another._i;
}
Copyable & operator=(const Copyable &another)
{
cout<<"Copyable & operator=(const Copyable &another):"<<this<<endl;
if(this == & another)
return *this;
*_i=*another._i;
return *this;
}
Copyable & operator=(Copyable && another)
{
cout<<"Moveable & operator=(Moveable && another):"<<this<<endl;
if(this != &another)
{
*_i = *another._i;
another._i = NULL;
}
return * this;
}
~Copyable()
{
cout<<"~Copyable():"<<this<<endl;
if(_i)
delete _i;
}
void dis()
{
cout<<"class Copyable is called"<<endl;
}
void dis() const
{
cout<<"const class Copyable is called"<<endl;
}
private:
int * _i;
};
void putRRValue(Copyable && a)
{
cout<<"putRRValue(Copyable && a)"<<endl;
a.dis();
}
void putCLValue(const Copyable & a)
{
cout<<"putCRValue(Copyable & a)"<<endl;
a.dis();//error!
}
//const T&和T&&重載同時存在先調用誰?
void whichCall(const Copyable & a)
{
a.dis();
}
void whichCall(Copyable && a)
{
a.dis();
}
int main(int argc, char *argv[])
{
// Copyable rrc = getCopyable();
cout<<"調用移動構造"<<endl;
Copyable a =Copyable(2);//匿名對象/臨時對象優先調用右值引用 構造-右值構造
cout<<"調拷貝構造"<<endl;
Copyable ca(a);
cout<<"直接構造右值"<<endl;
Copyable && rra =Copyable(2);
cout<<"=================================="<<endl;
//右值引用與const引用。 效率是否一樣?
cout<<"右值引用傳參"<<endl;
putRRValue(Copyable(2));
cout<<"Const 引用傳參"<<endl;
putCLValue(Copyable(2));
cout<<"----------------------"<<endl;
//優先調用哪種重載? T&& 還是 const T&?
whichCall(Copyable(2));
//這個沒什麽好糾結的!T&&的出現就是了解決 const T &接受匿名/臨時對象後,不能調用非cosnt函數的問題。
return 0;
}
C++11之右值引用與移動構造