您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“C++11右值引用和移動語義的方法是什么”,內容詳細,步驟清晰,細節處理妥當,希望這篇“C++11右值引用和移動語義的方法是什么”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
概念1:
左值:可以放到等號左邊的東西叫左值。
右值:不可以放到等號左邊的東西就叫右值。
概念2
左值:可以取地址并且有名字的東西就是左值。
右值:不能取地址的沒有名字的東西就是右值。
概念3
左值是指那些在表達式執行結束后依然存在的數據,也就是持久性的數據。
右值是指那些在表達式執行結束后不再存在的數據,也就是臨時性的數據。
有一種很簡單的方法來區分左值和右值:對表達式取地址,如果編譯器不報錯就為左值,否則為右值。例如:int a = b + c;,a 是左值,有變量名,可以取地址,也可以放到等號左邊,表達式 b+c 的返回值是右值,沒有名字且不能取地址,&(b+c) 不能通過編譯,而且也不能放到等號左邊。
左值一般有:
變量名和函數名(注意:是函數名不是函數調用)
返回左值引用的函數調用
前置自增自減表達式++i、–i
由賦值表達式或賦值運算符連接的表達式(a=b, a += b等)
解引用表達式 *p
字符串字面值 “abcd”
純右值和將亡值都屬于右值。
純右值:運算表達式產生的臨時變量、不和對象關聯的原始字面量、非引用返回的臨時變量、lambda 表達式等都是純右值。舉例:
除字符串字面值外的字面值
返回非引用類型的函數調用
后置自增自減表達式 i++、i–
算術表達式 a+b,a*b,a&&b,a==b 等
取地址表達式等,&a
將亡值:
將亡值是指 c++11 新增的和右值引用相關的表達式,通常指將要被移動的對象、T&& 函數的返回值、std::move函數的返回值、轉換為 T&& 類型轉換函數的返回值,將亡值可以理解為即將要銷毀的值,通過“盜取”其它變量內存空間方式獲取的值,在確保其它變量不再被使用或者即將被銷毀時,可以避免內存空間的釋放和分配,延長變量值的生命周期,常用來完成移動構造或者移動賦值的特殊任務。舉例:
class A { xxx; }; A a; auto c = std::move(a); // c是將亡值 auto d = static_cast<A&&>(a); // d是將亡值
左值引用就是對左值進行引用的類型,右值引用就是對右值進行引用的類型,他們都是引用,都是對象的一個別名,并不擁有所綁定對象的堆存,所以都必須立即初始化。引用可以通過引用修改變量的值,傳參時傳引用可以避免拷貝。
type &name = exp; // 左值引用 type &&name = exp; // 右值引用
左值引用
左值引用:能指向左值,不能指向右值的就是左值引用:
int a = 5; int& b = a; // b是左值引用 b = 4; int& c = 10; // error,10無法取地址,無法進行引用 const int& d = 10; // ok,因為是常引用,引用常量數字,這個常量數字會存儲在內存中,可以取地址。
引用是變量的別名,由于右值沒有地址,沒法被修改,所以左值引用無法指向右值,等號右邊的值必須可以取地址,如果不能取地址,則會編譯失敗。
但是,const 左值引用(常量引用)是可以指向右值的:const 左值引用不會修改指向值,因此可以指向右值,這也是為什么要使用 const & 作為函數參數的原因之一。
右值引用
c++11 標準新引入了另一種引用方式,稱為右值引用,用 “&&” 表示。如果使用右值引用,那表達式等號右邊的值需要是右值(不能是左值),可以使用 std::move 函數強制把左值轉換為右值。
int a = 4; int&& b = a; // error, a 是左值 int&& c = std::move(a); // ok int num = 10; int && a = num; //error, 右值引用不能初始化為左值 int && a = 10; // ok
【注意】和聲明左值引用一樣,右值引用也必須立即進行初始化操作。
左值引用與右值引用本質
(1)右值引用指向左值
int a = 5; // a是個左值 int &ref_a_left = a; // 左值引用指向左值 int &&ref_a_right = std::move(a); // 通過std::move將左值轉化為右值,可以被右值引用指向 cout << a; // 打印結果:5
前面講過可以使用 std::move 函數強制把左值轉換為右值,實現右值引用指向左值。std::move 是一個非常有迷惑性的函數:
不理解左右值概念的人們往往以為它能把一個變量里的內容移動到另一個變量,比如在上邊的代碼里,看上去是左值 a 通過 std::move 移動到了右值 ref_a_right 中,那是不是a里邊就沒有值了?并不是,打印出a的值仍然是5。
事實上 std::move 移動不了什么,唯一的功能是把左值強制轉化為右值,讓右值引用可以指向左值。其實現等同于一個類型轉換: static_cast<T&&>(lvalue)。 所以,單純的 std::move(xxx) 不會有性能提升。
同樣的,右值引用能指向右值,本質上也是把右值提升為一個左值,并定義一個右值引用通過 std::move:
int &&ref_a = 5; ref_a = 6; // 等同于以下代碼: int temp = 5; int &&ref_a = std::move(temp); ref_a = 6;
(2)左值引用、右值引用本身是左值還是右值?
被聲明出來的左、右值引用都是左值。 因為被聲明出的左右值引用是有地址的,也位于等號左邊。仔細看下邊代碼:
// 形參是個右值引用 void change(int &&right_value) { right_value = 8; } int main() { int a = 5; // a是個左值 int &ref_a_left = a; // ref_a_left是個左值引用 int &&ref_a_right = std::move(a); // ref_a_right是個右值引用 change(a); // 編譯不過,a是左值,change參數要求右值 change(ref_a_left); // 編譯不過,左值引用ref_a_left本身也是個左值 change(ref_a_right); // 編譯不過,右值引用ref_a_right本身也是個左值 change(std::move(a)); // 編譯通過 change(std::move(ref_a_right)); // 編譯通過 change(std::move(ref_a_left)); // 編譯通過 change(5); // 當然可以直接接右值,編譯通過 cout << &a << ' '; cout << &ref_a_left << ' '; cout << &ref_a_right; // 打印這三個左值的地址,都是一樣的 }
看完后你可能有個問題,std::move 會返回一個右值引用 int && ,它是左值還是右值呢? 從表達式 int &&ref = std::move(a) 來看,右值引用 ref 指向的必須是右值,所以move返回的 int && 是個右值。所以右值引用既可能是左值,又可能是右值嗎? 確實如此:右值引用既可以是左值也可以是右值,如果有名稱則為左值,否則是右值。
或者說:作為函數返回值的 && 是右值,直接聲明出來的 && 是左值。 這同樣也符合前面章節對左值,右值的判定方式:其實引用和普通變量是一樣的, int &&ref = std::move(a) 和 int a = 5 沒有什么區別,等號左邊就是左值,右邊就是右值。
(3)無論是左值引用還是右值引用都是引用
int temp = 5; int &ref_t = temp; int &&ref_a = std::move(temp); ref_a = 6; cout << &temp << "," << &ref_t << "," << &ref_a<<endl; cout << "temp:" <<temp <<endl; // 輸出結果 // 0x61fe84 0x61fe84 0x61fe84 // temp:6
最后,從上述分析中我們得到如下結論:
從性能上講,左右值引用沒有區別,傳參使用左右值引用都可以避免拷貝。
右值引用可以直接指向右值,也可以通過 std::move 指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
作為函數形參時,右值引用更靈活。雖然 const 左值引用也可以做到左右值都接受,但它無法修改,有一定局限性。
void f(const int& n) { n += 1; // 編譯失敗,const左值引用不能修改指向變量 } void f2(int&& n) { n += 1; // ok } int main() { f(5); f2(5); }
std::move 只是類型轉換工具,不會對性能有好處;右值引用在作為函數形參時更具靈活性。他們有什么實際應用場景嗎?
1、右值引用優化性能,避免深拷貝
(1)淺拷貝重復釋放:對于含有堆內存的類,我們需要提供深拷貝的拷貝構造函數,如果使用默認構造函數,會導致堆內存的重復刪除,比如下面的代碼:
class A { public: A(int size) : size_(size) { data_ = new int[size]; } A() {} A(const A& a) { size_ = a.size_; data_ = a.data_; cout << "copy " << endl; } ~A() { delete[] data_; } int* data_; int size_; }; int main() { A a(10); A b = a; cout << "b " << b.data_ << endl; cout << "a " << a.data_ << endl; return 0; }
上面代碼中,兩個輸出的是相同的地址,a 和 b 的 data_ 指針指向了同一塊內存,這就是淺拷貝,只是數據的簡單賦值,那再析構時 data_ 內存會被釋放兩次,導致程序出問題,這里正常會出現 double free 導致程序崩潰的。
(2)深拷貝構造函數
在上面的代碼中,默認構造函數是淺拷貝,在析構的時候會導致重復刪除指針。正確的做法是提供深拷貝的拷貝構造函數,比如下面的代碼:
#include <iostream> using namespace std; class A { public: A() : m_ptr(new int(0)) { cout << "constructor A" << endl; } A(const A& a) : m_ptr(new int(*a.m_ptr)) { cout << "copy constructor A" << endl; } ~A() { cout << "destructor A, m_ptr:" << m_ptr << endl; delete m_ptr; m_ptr = nullptr; } private: int* m_ptr; }; // 為了避免返回值優化,此函數故意這樣寫 A Get(bool flag) { A a; A b; cout << "ready return" << endl; if (flag) return a; else return b; } int main() { { A a = Get(false); // 正確運行 } cout << "main finish" << endl; return 0; }
深拷貝就是在拷貝對象時,如果被拷貝對象內部還有指針引用指向其它資源,自己需要重新開辟一塊新內存存儲資源,而不是簡單的賦值。雖然深拷貝可以解決淺拷貝的問題,但是存在效率問題。
(3)移動構造函數
深拷貝構造函數可以保證拷貝構造時的安全性,但有時這種拷貝構造存在效率問題,比如上面代碼中的拷貝構造就是不必要的。上面代碼中的 Get 函數會返回臨時變量,然后通過這個臨時變量拷貝構造了一個新的對象 b,臨時變量在拷貝構造完成之后就銷毀了,如果堆內存很大,那么,這個拷貝構造的代價會很大,帶來了額外的性能損耗。
有沒有辦法避免臨時對象的拷貝構造呢?答案是肯定的。看下面的代碼:
#include <iostream> using namespace std; class A { public: A() : m_ptr(new int(0)) { cout << "constructor A" << endl; } A(const A& a) : m_ptr(new int(*a.m_ptr)) { cout << "copy constructor A" << endl; } // 移動構造函數,可以淺拷貝 A(A&& a) : m_ptr(a.m_ptr) { a.m_ptr = nullptr; // 為防止a析構時delete data,提前置空其m_ptr cout << "move constructor A" << endl; } ~A() { cout << "destructor A, m_ptr:" << m_ptr << endl; if (m_ptr) delete m_ptr; } private: int* m_ptr; }; // 為了避免返回值優化,此函數故意這樣寫 A Get(bool flag) { A a; A b; cout << "ready return" << endl; if (flag) return a; else return b; } int main() { { A a = Get(false); // 返回右值,調用移動構造函數 } cout << "main finish" << endl; return 0; }
上面的代碼中實現了移動構造( Move Construct)。從移動構造函數的實現中可以看到,它的參數是一個右值引用類型的參數 A&&,這里沒有深拷貝,只有淺拷貝,這樣就避免了對臨時對象的深拷貝,提高了性能。
在實際開發中,通常在類中自定義移動構造函數的同時,會再為其自定義一個適當的拷貝構造函數,由此當用戶利用右值初始化類對象時,會調用移動構造函數;使用左值(非右值)初始化類對象時,會調用拷貝構造函數。
這里的 A&& 用來根據參數是左值還是右值來建立分支,如果是臨時值,則會選擇移動構造函數。
移動構造函數只是將臨時對象的資源做了淺拷貝,不需要對其進行深拷貝,從而避免了額外的拷貝,提高性能。這也就是所謂的移動語義( move 語義),右值引用的一個重要目的是用來支持移動語義的(移動語義的分析詳細見下文)。
將左值的類對象稱為左值對象,將右值的類對象稱為右值對象。默認情況下,對于類中用 public 修飾的成員函數,既可以被左值對象調用,也可以被右值對象調用,舉個例子:
#include <iostream> using namespace std; class demo { public: demo(int num) : num(num) {} int get_num() { return this->num; } private: int num; }; int main() { demo a(10); cout << a.get_num() << endl; cout << move(a).get_num() << endl; return 0; }
可以看到,demo 類中的 get_num() 成員函數既可以被 a 左值對象調用,也可以被 move(a) 生成的右值 demo 對象調用,運行程序會輸出兩個 10。
某些場景中,我們可能需要限制調用成員函數的對象的類型(左值還是右值),為此 c++11 新添加了引用限定符。所謂引用限定符,就是在成員函數的后面添加 “&” 或者 “&&”,從而限制調用者的類型(左值還是右值)。【注意】引用限定符不適用于靜態成員函數和友元函數。
// 代碼修改 class demo { public: demo(int num) : num(num) {} int get_num() & { return this->num; } // 添加了 "&",限定調用該函數的對象必須是左值對象 private: int num; }; int main() { demo a(10); cout << a.get_num() << endl; // 正確 // cout << move(a).get_num() << endl; // 錯誤 return 0; }
// 代碼修改 class demo { public: demo(int num) : num(num) {} int get_num() && { return this->num; } // 添加了 "&&",限定調用該函數的對象必須是右值對象 private: int num; }; int main() { demo a(10); //cout << a.get_num() << endl; // 錯誤 cout << move(a).get_num() << endl; // 正確 return 0; }
const 也可以用于修飾類的成員函數,習慣稱為常成員函數。
const 和引用限定符修飾類的成員函數時,都位于函數的末尾。C++11 標準規定,當引用限定符和 const 修飾同一個類的成員函數時,const 必須位于引用限定符前面。如下:
#include <iostream> using namespace std; class demo { public: demo(int num, int num2) : num(num), num2(num2) {} //左值和右值對象都可以調用 int get_num() const& { return this->num; } //僅供右值對象調用 int get_num2() const&& { return this->num2; } private: int num; int num2; };
【注意】當 const && 修飾類的成員函數時,調用它的對象只能是右值對象;當 const & 修飾類的成員函數時,調用它的對象既可以是左值對象,也可以是右值對象。無論是 const && 還是 const & 限定的成員函數,內部都不允許對當前對象做修改操作。
所謂移動語義,指的就是以移動而非深拷貝的方式初始化含有指針成員的類對象:之前的拷貝是對于別人的資源,自己重新分配一塊內存存儲復制過來的資源,而對于移動語義,類似于轉讓或者資源竊取的意思,對于那塊資源,轉為自己所擁有,別人不再擁有也不會再使用,通過 c++11 新增的移動語義可以省去很多拷貝負擔,怎么利用移動語義呢,是通過移動構造函數。
移動語義可以將資源(堆、系統對象等)通過淺拷貝方式從一個對象轉移到另一個對象,這樣能夠減少不必要的臨時對象的創建、拷貝以及銷毀,可以大幅度提高 c++ 應用程序的性能,消除臨時對象的維護(創建和銷毀)對性能的影響。
class A { public: A(int size) : size_(size) { data_ = new int[size]; } A() {} A(const A& a) { size_ = a.size_; data_ = new int[size_]; cout << "copy " << endl; } A(A&& a) { // 移動構造函數 this->data_ = a.data_; a.data_ = nullptr; cout << "move " << endl; } ~A() { if (data_ != nullptr) { delete[] data_; } } int* data_; int size_; }; int main() { A a(10); A b = a; A c = std::move(a); // 返回右值,調用移動構造函數 return 0; }
如果不使用 std::move(),會有很大的拷貝代價,使用移動語義可以避免很多無用的拷貝,提供程序性能,c++ 所有的 STL 都實現了移動語義,方便我們使用。
【注意1】移動語義僅針對于那些實現了移動構造函數的類的對象,對于那種基本類型 int、float 等沒有任何優化作用,還是會拷貝,因為它們實現沒有對應的移動構造函數。
【注意2】在實際開發中,通常在類中自定義移動構造函數的同時,會再為其自定義一個適當的拷貝構造函數,由此當用戶利用右值初始化類對象時,會調用移動構造函數;使用左值(非右值)初始化類對象時,會調用拷貝構造函數。
首先,解釋一下什么是完美轉發,它指的是函數模板可以將自己的參數“完美”地轉發給內部調用的其它函數。所謂完美,即不僅能準確地轉發參數的值,還能保證被轉發參數的左、右值屬性不變。例如:
template <typename T> void function(T t) { otherdef(t); }
如上所示,function() 函數模板中調用了 otherdef() 函數。在此基礎上,完美轉發指的是:如果 function() 函數接收到的參數 t 為左值,那么該函數傳遞給 otherdef() 的參數 t 也是左值;反之如果 function() 函數接收到的參數 t 為右值,那么傳遞給 otherdef() 函數的參數 t 也必須為右值。
顯然, function() 函數模板并沒有實現完美轉發。一方面,參數 t 為非引用類型,這意味著在調用 function() 函數時,實參將值傳遞給形參的過程就需要額外進行一次拷貝操作;另一方面,無論調用 function() 函數模板時傳遞給參數 t 的是左值還是右值,對于函數內部的參數 t 來說,它有自己的名稱,也可以獲取它的存儲地址,因此它永遠都是左值,也就是說,傳遞給 otherdef() 函數的參數 t 永遠都是左值。總之,無論從那個角度看, function() 函數的定義都不“完美”。
接下來,那如何實現完美轉發呢,答案是使用 std::forward():
首先在定義模板函數時,采用右值引用的語法格式定義參數類型,由此該函數既可以接收外界傳入的左值,也可以接收右值;
其次,還需要使用 c++11 標準庫提供的 std::forword() 模板函數修飾被調用函數中需要維持左、右值屬性的參數。
由此即可輕松實現函數模板中參數的完美轉發,如下所示:
void PrintV(int& t) { cout << "lvalue" << endl; } void PrintV(int&& t) { cout << "rvalue" << endl; } template <typename T> void Test(T&& t) { // 1、采用右值引用的語法格式定義參數類型 PrintV(t); PrintV(std::forward<T>(t)); PrintV(std::move(t)); } int main() { Test(1); // lvalue rvalue rvalue int a = 1; Test(a); // lvalue lvalue rvalue // 2、使用 std::forword() 模板函數修飾被調用函數 Test(std::forward<int>(a)); // lvalue rvalue rvalue Test(std::forward<int&>(a)); // lvalue lvalue rvalue Test(std::forward<int&&>(a)); // lvalue rvalue rvalue return 0; }
Test(1):1是右值,模板中 T &&t 這種為萬能引用,右值 1 傳到 Test 函數中變成了右值引用,但是調用 PrintV() 時候,t 變成了左值,因為它變成了一個擁有名字的變量,所以打印 lvalue,而 PrintV(std::forward<T>(t)) 時候,會進行完美轉發,按照原來的類型轉發,所以打印 rvalue,PrintV(std::move(t)) 毫無疑問會打印 rvalue。
Test(a):a 是左值,模板中 T && 這種為萬能引用,左值 a 傳到 Test 函數中變成了左值引用,所以有代碼中打印。
Test(std::forward<T>(a)):轉發為左值還是右值,依賴于 T,T 是左值那就轉發為左值,T 是右值那就轉發為右值。
#include <iostream> using namespace std; //重載被調用函數,查看完美轉發的效果 void otherdef(int & t) { cout << "lvalue\n"; } void otherdef(const int & t) { cout << "rvalue\n"; } //實現完美轉發的函數模板 template <typename T> void function(T&& t) { otherdef(forward<T>(t)); } int main() { function(5); // rvalue int x = 1; function(x); // lvalue return 0; } // 打印結果 // rvalue // lvalue
對于STL容器,c++11 后引入了 emplace_back 接口。emplace_back 是就地構造,不用構造后再次復制到容器中,因此效率更高。考慮這樣的語句:
vector<string> testVec; testVec.push_back(string(16, 'a'));
上述語句足夠簡單易懂,將一個 string 對象添加到 testVec 中。底層實現:
首先,string(16, ‘a’) 會創建一個 strin g類型的臨時對象,這涉及到一次string 構造過程。
其次,vector 內會創建一個新的 string 對象,這是第二次構造。
最后在 push_back 結束時,最開始的臨時對象會被析構。加在一起,這兩行代碼會涉及到兩次 string 構造和一次析構。
c++11 可以用 emplace_back 代替 push_back,emplace_back 可以直接在vector中構建一個對象,而非創建一個臨時對象,再放進vector,再銷毀。emplace_back可以省略一次構建和一次析構,從而達到優化的目的。
emplace_back 內部沒有使用拷貝構造函數,也沒有使用移動構造函數,而是直接調用構造函數,因此更加高效。
讀到這里,這篇“C++11右值引用和移動語義的方法是什么”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。