轉自html
http://www.javashuo.com/article/p-ymiycxlp-e.html安全
C++11 引入的新特性中,除了併發內存模型和相關設施,這些高帥富以外,最引人入勝且接地氣的特性就要屬『右值引用』了(rvalue reference)。加入右值引用的動機在於效率:減小沒必要要的資源拷貝。考慮下面的程序:併發
std::vector<string> v; v.push_back("string");
首先看push_back:函數
push_back(const T& x)優化
實際上傳進來的是引用ui
而在push_back中構造時,使用的是construct:this
void construct(T1* p,const T2& value) { new(p) T1(value); }
T1*爲迭代器所指的位置,而構造的值value也是引用。T1爲迭代器類型,而p爲該迭代器所處的地址,也就是說construct其實是在迭代器所屬位置上,用value來構造一個T1類型的迭代器。spa
這裏值得要注意的是,容器對其中存儲元素的管理都是在堆上的(用自由存儲區更精確),也就是說容器自己多是在棧上,但它管理的元素必定是在堆上。3d
在回到上面的過程:指針
首先"string"這是一個char*指向的區域,而push_back調用的實際上裏面是一個string&變量,所以首先進行隱式轉換,調用string的string(const char*)這個含參的構造函數。
而這個生成的變量其實上是一個臨時變量。
push_back能夠看到他是傳參的,所以這個臨時變量直接被傳進去而沒有被拷貝構造。
到目前爲止,使用了隱式轉換,而此次隱式轉換調用了含參(char*)的string的構造函數。
進入以後進入construct,而construct也是傳引用,所以這裏也沒有調用構造。
可是在construct內部,在new時,使用了T1的含參構造,至關於在堆上利用這個臨時變量構造了一個T1類型的變量
在這裏調用了一次構造。
而因爲vector的迭代器是原始指針,所以這裏T1與T2是同一種類型(固然對其餘容器就不必定是這樣了)
於是調用的其實是拷貝構造
在堆上構造完畢後,construct退棧,push_back退棧,而後這個臨時變量由於離開做用域而被析構
實際上這個過程經歷了一次含參構造,一次拷貝構造,一次析構
std::vector<string> v; v.push_back("string");
移動語義:
上面程序操做的問題癥結在於,臨時對象的構造和析構帶來了沒必要要的資源拷貝。若是有一種機制,能夠在語法層面識別出臨時對象,在使用臨時對象構造新對象(拷貝構造)的時候,將臨時對象所持有的資源『轉移』到新的對象中,就能消除這種沒必要要的拷貝(將「string」直接轉移給construct構造處的新對象)。這種語法機制就是『右值引用』,相對地,傳統的引用被稱爲『左值引用』。左值引用使用 ‘&’ 標識(好比 string&),右值引用使用 ‘&&’ 標識(好比 string&&)。順帶提一下什麼是左值(lvalue)什麼是(rvalue):能夠取地址的具名對象是左值;沒法取值的對象是右值,包括匿名的臨時對象和全部字面值(literal value)。有了右值的語法支持,爲了實現移動語義,須要相應類以右值爲參數重載傳統的拷貝構造函數和賦值操做符,畢竟哪些資源能夠移動、哪些只能拷貝只有類的實現者才知道。對於移動語義的拷貝『構造』,通常流程是將源對象的資源綁定到目的對象,而後解除源對象對資源的綁定;對於賦值操做,通常流程是,首先銷燬目的對象所持有的資源,而後改變資源的綁定。另外,固然,與傳統的構造和賦值類似,還要考慮到構造的異常安全和自賦值狀況。做爲演示:
其中,重載了String的"=",使遇到使用右值來進行構造時,轉移對象的資源。
class String { public: String(const String &rhs) { ... } String(String &&rhs) { s_ = rhs.s_; rhs.s_ = NULL; } String& operator=(const String &rhs) { ... } String& operator=(String &&rhs) { if (this != &rhs) { delete [] s_; s_ = rhs.s_; rhs.s_ = NULL; } return *this; } private: char *s_; };
值得注意的是,一個綁定到右值的右值引用是『左值』,由於它是有名字的。考慮:
其中,D的構造函數重載了一個右值引用版本,在這個版本中使用了初始化列表來初始化基類,但B(rhs)中rhs其實是個左值,也就是說對B調用構造時將會調用拷貝構造版本,而不是B的移動構造版本
class B { public: B(const B&) {} B(B&&) {} }; class D : public B { D(const D &rhs) : B(rhs) {} D(D &&rhs) : B(rhs) {} }; D getD(); D d(getD());
上面程序中,B::B(B&&) 不會被調用。爲此,C++11 中引入 std::move(T&& t) 模板函數,它 t 轉換爲右值:
class D : public B { D(D &&rhs) : B(std::move(rhs)) {} };
引入右值引用後,『引用』到『值』的綁定規則也獲得擴充:
考慮下面這段代碼:
當調用不一樣的構造函數時,會輸出不一樣的語句
class HasPtrMem{ public: HasPtrMem():d(new int(3)) { cout<<"Construct:"<<++n_cstr<<endl; } HasPtrMem(const HasPtrMem& tmp):d(new int(3)) { cout<<"copy constructor:"<<++n_cptr<<endl; } HasPtrMem(HasPtrMem&& h) { d=h.d; h.d=nullptr; cout<<"move constructor:"<<++n_mvtr<<endl; } ~HasPtrMem() { delete d; cout<<"destruct:"<<++n_dstr<<endl; } int * d; static int n_cstr; static int n_dstr; static int n_cptr; static int n_mvtr; }; int HasPtrMem::n_cstr=0; int HasPtrMem::n_dstr=0; int HasPtrMem::n_cptr=0; int HasPtrMem::n_mvtr=0; HasPtrMem GetTemp() { HasPtrMem n; return n; } int main() { HasPtrMem a=GetTemp();//a
//HasPtrMem a;//b1
//a=GetTemp;//b2
//GetTemp//c
cout<<endl; }
咱們分別執行a,b1與b2和c:
執行a時:
分析一下這個過程,首先進入GetTemp,生成一個n,此處調用默認構造函數
而後返回n,在返回n的時候,理論上應該生成一個臨時變量,此處應該是編譯器優化過了,將a當作這個臨時變量直接進來構造。
而此處是調用的移動構造函數,儘管n是一個左值,難道說對臨時變量的構造都是調用的移動構造函數嗎???
構造完畢後,析構掉這個n
執行b1,b2時:
分析一下這個過程,首先建立a,此時調用默認構造函數。
而後進入GetTemp內部,首先建立n,此處調用默認構造函數。
因爲咱們這裏是使用的賦值函數,所以不會像上面同樣進行優化。
首先用n移動構造一個臨時變量,構造完畢後n析構。
而後用這個臨時變量複製給a,因爲咱們沒有重載=,所以此處用的默認的賦值函數。
最後臨時變量析構
執行c時:
咱們分析一下這個過程
注意這裏與a的區別,因爲這裏只調用了GetTemp,所以編譯期是沒法優化的。
首先建立n,調用默認構造函數
而後調用移動構造去構造臨時變量
而後n析構
最後臨時變量析構
對於b1,b2,當咱們加上移動賦值函數後:
HasPtrMem& operator=(HasPtrMem&& s) { d=s.d; s.d=nullptr; cout<<"move operator=:"<<endl; return *this; }
結果爲:
咱們分析這個過程,前面不表。
在臨時變量調用移動構造函數構造完畢後,n析構。
而後對a,調用了移動賦值函數,而後臨時變量析構。
看完上面,有一個問題是:
難道對臨時變量的構造必定使用移動構造函數嗎?這裏的n是一個左值!