1. 程式人生 > >C++11之右值引用與移動構造

C++11之右值引用與移動構造

添加 oooo 返回對象 oat 值引用 apc 定義 tco pri

----------------------------右值引用---------------------------------

右值定義:

  通俗來講,賦值號左邊的就是左值,賦值號右邊的就是右值。可以取地址是左值,不可以取地址的是右值。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之右值引用與移動構造