移動構造-C++11
移動構造:
移動構造是C++11標準中提供的一種新的構造方法。
在現實中有很多這樣的例子,我們將錢從一個賬號轉移到另一個賬號,將手機SIM卡轉移到另一臺手機,將檔案從一個位置剪下到另一個位置……
移動構造可以減少不必要的複製,帶來效能上的提升。
有些複製構造是必要的,我們確實需要另外一個副本;而有些複製構造是不必要的,我們可能只是希望這個物件換個地方,移動一下而已。
在C++11之前,如果要將源物件的狀態轉移到目標物件只能通過複製。
而現在在某些情況下,我們沒有必要複製物件——只需要移動它們。
C++11引入移動語義:
~源物件資源的控制權全部交給目標物件
對比一下複製構造和移動構造:
複製構造是這樣的:
在物件被複制後臨時物件和複製構造的物件各自佔有不同的同樣大小的堆記憶體,就是一個副本。
移動構造是這樣的:
就是讓這個臨時物件它原本控制的記憶體的空間轉移給構造出來的物件,這樣就相當於把它移動過去了。
複製構造和移動構造的差別:
這種情況下,我們覺得這個臨時物件完成了複製構造後,就不需要它了,我們就沒有必要去首先產生一個副本,然後析構這個臨時物件,這樣費兩遍事,又佔用記憶體空間,所幸將臨時物件它的原本的資源直接轉給構造的物件即可了。
當臨時物件在被複制後,就不再被利用了。我們完全可以把臨時物件的資源直接移動,這樣就避免了多餘的複製構造。
什麼時候該觸發移動構造呢?
如果臨時物件即將消亡,並且它裡面的資源是需要被再利用的,這個時候我們就可以觸發移動構造。
移動構造是需要通過移動建構函式來完成的。
移動建構函式定義形式:
class_name(class_name && )
例:函式返回含有指標成員的物件
有兩種版本:
版本一:使用深層複製建構函式
~ 返回時構造臨時物件,動態分配臨時物件返回到主調函式,然後刪除臨時物件。
版本二:使用移動建構函式
~ 將要返回的區域性物件轉移到主調函式,省去了構造和刪除臨時物件的過過程。
版本一舉例:使用複製構造的程式碼:
#include<iostream>
using namespace std;
class IntNum{
public:
IntNum(int x = 0):xptr(new int(x)){//建構函式
cout<<"Calling constructor..."<<endl;
}
IntNum(const IntNum &n):xptr(new int(*n.xpr)){//複製建構函式
cout<<"Calling copy constructor..."<<endl;
}
~IntNum(){//解構函式
delete xpr;
cout<<"Destructing..."<<endl;
}
int getInt(){return *ptr;}//返回指標所指向的值,而不是返回指標本身
private:
int *ptr;
};
//返回值為IntNum類物件
IntNum getNum(){
//定義了一個區域性物件,然後將區域性物件作為結果返回
IntNum a;
//返回值是IntNum型別
return a;
}
int main(){
//getNum()函式返回了一個IntNum型別的物件(臨時無名物件),之後呼叫類的函式
cout<<"getNum().getInt()"<<endl;
return 0;
}
版本一通過深層複製建構函式實現的返回含有指標成員的物件,在這個過程中,做了一些無用功,既需要構造臨時無名(注:是無名物件,不是上例中的物件a)物件,還要對其進行析構。
在上例中不構造臨時無名物件不行嗎?其實呢我們也不用構造臨時無名物件了,物件a反正是要消亡的,我們把要消亡的物件a佔用的資源轉移給臨時物件,不行嗎?!這樣的方案就是使用移動構造
版本二:使用移動構造
使用移動建構函式的程式碼舉例:
#include<iostream>
using namespace std;
class IntNum{
public:
IntNum(int x = 0):xptr(new int(x)){//建構函式
cout<<"Calling constructor..."<<endl;
}
IntNum(const IntNum &n):xptr(new int(*n.xpr)){//複製建構函式
cout<<"Calling copy constructor..."<<endl;
}
IntNum(IntNum && n):xptr(n.xptr){//移動建構函式
n.xptr = nullptr;
cout<<"Calling move constructor..."<<endl;
}
~IntNum(){//解構函式
delete xpr;
cout<<"Destructing..."<<endl;
}
int getInt(){return *ptr;}//返回指標所指向的值,而不是返回指標本身
private:
int *ptr;
};
//返回值為IntNum類物件
IntNum getNum(){
//定義了一個區域性物件,然後將區域性物件作為結果返回
IntNum a;
//返回值是IntNum型別
return a;
}
int main(){
//getNum()函式返回了一個IntNum型別的物件(臨時無名物件),之後呼叫類的函式
cout<<"getNum().getInt()"<<endl;
return 0;
}
在該例中的移動建構函式
IntNum(IntNum && n):xptr(n.xptr){//移動建構函式
n.xptr = nullptr;
cout<<"Calling move constructor..."<<endl;
}
好像幹了件很危險的事情:直接用引數物件(n)裡面的指標(n.xptr)來初始化當前物件的指標(xptr),(按理說這不是淺層複製嗎?!說了有指標成員,我們複製時不能做這種淺層複製的呀,怎麼在這裡我們恰恰做淺層複製了呢)
看函式體裡面,我們發現再做完xptr(n.xptr)這種指標對指標的複製(也就是把引數指標所指向的物件轉給了當前正在被構造的指標)後,接著就把引數n裡面的指標置為空指標(n.xptr = nullptr;),物件裡面的指標置為空指標後,將來解構函式析構該指標(delete xpr;)時,是delete一個空指標,不發生任何事情,這就是一個移動建構函式。
移動建構函式中的引數型別,&&符號表示是右值引用;即將消亡的值就是右值,函式返回的臨時變數也是右值,這樣的單個的這樣的引用可以繫結到左值的,而這個引用它可以繫結到即將消亡的物件,繫結到右值
左值和右值都是針對表示式而言的,左值是指表示式結束後依然存在的持久物件,右值指表示式結束時就不再存在的臨時物件——顯然右值不可以被取地址。