C++11新特性之左值右值及移動語句與完美轉發

  • C左值右值
    • 左值和右值的由來
    • 什么是左值和右值
    • 左值右值的本質
    • 引用
      • 左值引用
      • 右值引用
    • 移動語句與完美轉發
      • 移動語句
        • 實現移動構造函數和轉移賦值函數
        • stdmove
        • 完美轉發Perfect Forwarding

C++左值右值

自從C++11發布之后,出現了一個新的概念,即左值和右值,英文為lvalue和rvalue,這兩個是比較晦澀難懂的基礎概念,為什么說是基礎的概念呢?因為只有了解了它,才能真正理解move和forward語句。

左值和右值的由來

左值和右值,最早是從C語言繼承而來的。在C語言中,或者是它的繼承版本中有如下變現形式:

  • 左值是可以位于賦值運算符“=”左側的變量或表達式,也可以位于賦值運算符“=”右側
  • 右值是不可以位于賦值運算符“=”左側的表達式,只能出現在等號右邊的變量或者表達式

我們來看看例子:

例子1

int a; //聲明變量a
int b; //聲明變量ba = 3; //賦值語句,將a的值重新賦值為3,此時a為左值
b = 4; //此時b為左值a = b; //此時a為左值,b為右值
b = a;//此時b為左值,a為右值3 = a; //編譯錯誤,這里應該很好理解,3不可能再被賦值了
a + b = 4; //編譯錯誤,這里相當于7 = 4,也是不合理的

例子2

//定義了兩個函數foo1和foo2
int foo1(int number)
{return number;
}int foo2( int number )
{return number;
}main()
{foo1(1) = foo2(2); //編譯錯誤,因為foo1和foo2的返回值只能作為右值,不能放在等號的左邊int temp = foo1(1) * foo2(2); //temp為左值,foo1(1) * foo2(2)為右值
}

上面這些例子就是左值和右值的例子。

什么是左值和右值

一個變量或者表達式是左值還是右值,取決于我們使用的是它的值還是它在內存中的位置(作為實例的身份)。

int a; //聲明變量a
int b; //聲明變量ba = b; //此時a為左值,b為右值

這個例子中,將b的值賦值給a,將值保存在a的內存中,b在這里面是右值,a在這里面是左值
因為b作為實例既可以當做左值也可以當做右值。

所以判斷一個值是左值還是右值要根據實際在語句匯總的含義來確定。

總結第一點:

  • 在一般情況下,需要右值的地方可以用左值來代替,需要左值的地方必須使用左值
  • 左值存放在實例中,有持久的狀態,而右值是字面常量,要么是在表達式求值過程中創建的臨時實例,沒有持久的狀態

重點

能取得到地址的變量或者表達式就是左值,反之為右值。

那現在我們來看看下面的例子哪個是左值,哪個是右值?

int a = 0;
int b = 1;
a++;
++b;

我來宣布答案,a++為右值,++b為左值,首先我們先驗證一下:

int a = 0;
int b = 1;
a++ = 5; //error: lvalue required as left operand of assignment
++b = 5;

實驗的結果也是正確的,那我們來分析一下:
對于a++
1. a++首先產生一個臨時變量,記錄a的值
2. 然后將a+1
3. 接著返回臨時變量

根據這個過程我們知道 int a = 0;int c = a++; 的值應該是c為0;而a變為了1,
所以a++此時將臨時變量返回給了c,那么這個臨時變量我們是不能獲取地址的,也就
是使用“&”。所以結論就是a++為右值。

對于++b
1. 進行了b = b + 1
2. 返回變量b

根據這個過程,我們是可以取b的地址的,所以b是左值。

左值右值的本質

int a = 5;
int c = a + a;

a就是左值,5就是右值。 a + a 表達式中,a以右值傳入,相加之后也以右值返回。

左值就是對一塊內存區域的引用(這個并不是c++11中的int &a 之類的引用),
比如上邊的a,就對應了一塊內存區域(起始地址&a,大小為sizeof(int) )。

專業的解釋:

An object is a region of storage that can be examined and stored into. An lvalue is an expression that refers to such an object. An lvalue does not necessarily permit modification of the object it designates. For example, a const object is an lvalue that cannot be modified.

對于每個變量,都有2個值與其相關聯:

  1. 數據值,存儲在某個內存地址中,也稱為右值,右值是被讀取的值,不可修改。
  2. 地址值,即存儲數據值的那塊內存地址,也稱左值。

所以左值既可以當作左值也可以作為右值。就是這么神奇。

引用

在C++中,有兩種對實例的引用:左值引用和右值引用。

左值引用

左值引用是常見的引用,C++中可以使用“&”符號定義引用,如果一個左值同時也是引用,那么就稱其為“左值引用”。如

std::string str;
std::string& strRef = str; // strRef為左值也為引用,稱其為左值引用

非const左值引用不能使用右值對其賦值

std::string& strRef = "abc"; // error: abc字符串為右值,

假設上面可以的話,就會遇到一個問題:如何修改右值的值?因為引用是可以后續被賦值的。根據上面的定義,右值連可被獲取的內存地址都沒有,也就談不上對其進行賦值。

但是const左值引用就可以使用右值,因為常量不能被修改,也不存在上面糾結的問題:

const std::string strRef = "abc";

再比如,我們經常使用左值作為函數的參數類型,可以減少不必要的對象復制:

int foo( int& number )
{return number;
}main()
{int a = foo(1); //錯誤int b = 1;int c = foo(b); //通過
}

我們將上面的int& number改為 const int& number即可。

補充知識:

什么是CV限定符(CV-qualified),如果變量聲明時類型帶有const或者volatile,就說此變量類型具有CV限定符。

右值引用

右值引用也是引用,但是它只能且必須綁定在右值上。

int a = 5;
int& b = a; // a綁定在左值引用b上
int&& c = a; // error:a可以是左值,所以不能將它綁定在右值引用上。
int&& d = 30; // 將右值30綁定在右值引用上
int&& e = a * 1 // a * 1的結果是一個臨時對象,為右值,所以可以綁定在右值引用上

結論:

由于右值引用只能綁定在右值上,而右值要么是字面常量,要么是臨時對象,所以:

右值引用的對象,是臨時的,即將被銷毀; 并且右值引用的對象,不會在其它地方使用。

移動語句與完美轉發

這里涉及前面提過的move和forward兩個函數,即移動語句和轉發。

右值引用 (Rvalue Referene) 是 C++ 新標準 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它實現了轉移語義 (Move Sementics) 和精確傳遞 (Perfect Forwarding)。它的主要目的有兩個方面:

  1. 消除兩個對象交互時不必要的對象拷貝,節省運算存儲資源,提高效率。
  2. 能夠更簡潔明確地定義泛型函數。

移動語句

右值引用是用來支持移動語句的。移動語句可以將資源(堆,系統對象等)從一個對象轉移到另一個對象,這樣能夠減少不必要的臨時對象的創建、拷貝以及銷毀,能夠大幅度提高C++應用程序的性能。臨時對象的維護(創建和銷毀)對性能有嚴重影響。

移動語句是和拷貝語句相對的,可以類比文件的剪切和拷貝,當我們將文件從一個目錄拷貝到另一個目錄時,速度比剪切慢很多。

通過移動語句,臨時對象中的資源能夠轉移其他的對象里。

在現有的 C++ 機制中,我們可以定義拷貝構造函數和賦值函數。要實現移動語句,需要定義移動構造函數,還可以定義移動賦值操作符。對于右值的拷貝和賦值會調用移動構造函數和移動賦值操作符。如果移動構造函數和移動拷貝操作符沒有定義,那么就遵循現有的機制,拷貝構造函數和賦值操作符會被調用。

實現移動構造函數和轉移賦值函數

class MyString { public: MyString() { m_data = nullptr; m_len = 0; }private: char* m_data; size_t   m_len; void initData(const char *s) { m_data = new char[m_len+1]; memcpy(m_data, s, m_len); m_data[m_len] = '\0'; } MyString(const char* p) { m_len = strlen (p); initData(p); } MyString(const MyString& str) { m_len = str.m_len; initData(str.m_data); std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl; } MyString& operator=(const MyString& str) { if (this != &str) { m_len = str.m_len; initData(str.m_data); } std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl; return *this; } virtual ~MyString() { } }; int main() 
{ MyString a; a = MyString("Hello");  //調用拷貝構造函數,MyString("Hello")為臨時對象,即右值std::vector<MyString> vec; vec.push_back(MyString("World")); 
}運行結果: 
Copy Assignment is called! source: Hello 
Copy Constructor is called! source: World

這個 string 類已經基本滿足我們演示的需要。在 main函數中,實現了調用拷貝構造函數的操作和拷貝賦值操作符的操作。
MyString(“Hello”) 和 MyString(“World”)都是臨時對象,也就是右值。雖然它們是臨時的,但程序仍然調用了拷貝構造和拷貝賦值,造成了沒有意義的資源申請和釋放的操作。如果能夠直接使用臨時對象已經申請的資源,既能節省資源,有能節省資源申請和釋放的時間。這正是定義轉移語義的目的。

MyString(MyString&& str) 
{ std::cout << "Move Constructor is called! source: " << str.m_data << std::endl; m_len = str.m_len; m_data = str.m_data; str.m_len = 0; str.m_data = nullptr; 
}MyString& operator=(MyString&& str) 
{ std::cout << "Move Assignment is called! source: " << str.m_data << std::endl; if (this != &str) { m_len = str.m_len; m_data = str.m_data; str.m_len = 0; str.m_data = nullptr; } return *this; 
}增加后的運行結果:Move Assignment is called! source: Hello 
Move Constructor is called! source: World

由此看出,編譯器區分了左值和右值,對右值調用了移動構造函數和移動賦值操作符。節省了資源,提高了程序運行的效率。

有了右值引用和移動語義,我們在設計和實現類時,對于需要動態申請大量資源的類,應該設計移動構造函數和轉移賦值函數,以提高應用程序的效率。

std::move

我們先來看一個例子:

void processValue( int& value )
{std::cout << "lvalue process: " << value << std::endl;
}void processValue( int&& value )
{std::cout << "rvalue process: " << value << std::endl;
}main()
{int a = 0;processValue(a); //傳入左值processValue(1); //傳如右值
}結果:
lvalue process: 0
rvalue process: 1

通過這個例子來看,processValue函數被重載,分別接受左值和右值。由輸出結果可以看出,臨時對象是作為右值處理的。

但是如果臨時對象通過一個接受右值的函數傳遞給另一個函數時,就會變成左值,因為這個臨時對象在傳遞過程中,變成了命名對象。

void processValue( int& value )
{std::cout << "lvalue process: " << value << std::endl;
}void processValue( int&& value )
{std::cout << "rvalue process: " << value << std::endl;
}void forwardValue( int&& value )
{processValue(value);
}main()
{int a = 0;processValue(a);processValue(1);forwardValue(2);
}結果:  
lvalue process: 0
rvalue process: 1
lvalue process: 2

我們可以看出最后一個函數調用,2是右值,可以返回的時候卻變成了左值。這里面我們可以使用std::move(var)將變量轉移為右值語句。

修改為:

...
void forwardValue( int&& value )
{processValue(std::move(value) );
}
...

既然編譯器只對右值引用才能調用轉移構造函數和轉移賦值函數,而所有命名對象都只能是左值引用,如果已知一個命名對象不再被使用而想對它調用轉移構造函數和轉移賦值函數,也就是把一個左值引用當做右值引用來使用,怎么做呢?標準庫提供了函數 std::move,這個函數以非常簡單的方式將左值引用轉換為右值引用。

std::move在提高 swap 函數的的性能上非常有幫助,一般來說,swap函數的通用定義如下:

template <class T> swap(T& a, T& b) 
{ T tmp(a);   // copy a to tmp a = b;      // copy b to a b = tmp;    // copy tmp to b 
}

有了 std::move,swap 函數的定義變為 :

template <class T> swap(T& a, T& b) 
{ T tmp(std::move(a)); // move a to tmp a = std::move(b);    // move b to a b = std::move(tmp);  // move tmp to b 
}

通過 std::move,一個簡單的 swap 函數就避免了 3 次不必要的拷貝操作。

完美轉發(Perfect Forwarding)

Perfect Forwarding也被翻譯成完美轉發,精準轉發等,說的都是一個意思。

用于這樣的場景:需要將一組參數原封不動的傳遞給另一個函數。

原封不動”不僅僅是參數的值不變,在 C++ 中,除了參數值之外,還有一下兩組屬性:

  • 左值/右值
  • const/non-const。

完美轉發就是在參數傳遞過程中,所有這些屬性和參數值都不能改變。

重點:完美轉發僅與類模板或函數模板的環境有關。

示例:

class Person
{
public:template<typename T1, typename T2>Person(T1&& first, T2&& second ) : firstname{std::forward<T1>(first)},secondname{std::forward<T2>(second)}{}string getName() const{return firstname.getName() + " " + secondname.getName(); }
private:Name firstname;Name secondname;
}class Name
{
public:Name( const string& aName ): name{aName}{cout << "Lvalue Name constructor." << endl;}Name( string&& aName ): name{std::move(aName)}{cout << "Rvalue Name constructor." << endl;}const string& getName() const { return name; }
private:string name;
}main()
{Person me{string{"abc"}, string{"def"}};string first{"lll"};string second{"ggg"};Person other{first, second};
}輸出結果:
Rvalue Name constructor.
Rvalue Name constructor.
Lvalue Name constructor.
Lvalue Name constructor.

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/536151.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/536151.shtml
英文地址,請注明出處:http://en.pswp.cn/news/536151.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

nginx中的nginx.conf.default配置

#運行用戶 user nobody; #啟動進程,通常設置成和cpu的數量相等 worker_processes 1;#全局錯誤日志及PID文件 #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info;#pid logs/nginx.pid;#工作模式及連接數上限 events {…

C++11新特性之泛型編程與模板

模板泛型編程函數模板普通函數模板成員函數模板函數模板重載模板函數的特化 類模板類模板中的成員函數模板類模板的特化與偏特化類模板成員特化 模板 Template所代表的泛型編程是C語言中的重要組成部分。 泛型編程 泛型編程&#xff08;Generic Programming&#xff09;是…

WordPress更改“固定鏈接”后 頁面404原因及解決方法(Nginx版)

網上盛傳的方法是&#xff1a; 在 /etc/nginx/nginx.conf文件的 loction / {} 中添加 if (-f $request_filename/index.html){rewrite (.*) $1/index.html break; }if (-f $request_filename/index.php){rewrite (.*) $1/index.php; }if (!-f $request_filename){rewrite (.*…

C++類型萃取之type_traits和type_info

類型萃取類型判斷typeiddecltype和declvalenable_if 類型萃取 通過type_traits可以實現在編譯期計算、查詢、判斷、轉換和選擇&#xff0c;增強了泛型編程的能力&#xff0c;也增強了我們程序的彈性&#xff0c;讓我們能夠在編譯期就能夠優化改進甚至排錯&#xff0c;進一步提…

使用Phpstorm實現遠程開發

Phpstorm除了能直接打開本地文件之外&#xff0c;還可以連接FTP&#xff0c;除了完成正常的數據傳遞任務之外&#xff0c;還可以進行本地文件與服務端文件的異同比較&#xff0c;同一文件自動匹配目錄上傳&#xff0c;下載&#xff0c;這些功能是平常IDE&#xff0c;FTP軟件中少…

什么是遞歸函數?

文章目錄遞歸函數遞歸例題特點效率優點遞歸函數 遞歸 遞歸就是一個函數在它的函數體內調用它自身。執行遞歸函數將反復調用其自身&#xff0c;每調用一次就進入新的一層。遞歸函數必須有結束條件。 當函數在一直遞推&#xff0c;直到遇到墻后返回&#xff0c;這個墻就是結束條…

apache ab壓力測試報錯

今天用apache 自帶的ab工具測試&#xff0c;當并發量達到1000多的時候報錯如下&#xff1a; [rootaa~]# This is ApacheBench, Version 2.3 <Revision:655654> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Sof…

ngOnInit與constructor的區別

前世今生 Angular會管理一個組件的生命周期,包括組件的創建、渲染、子組件創建與渲染、當數據綁定屬性變化時的校驗、DOM移除之前毀銷。 Angular提供組件生命周期鉤子便于我們在某些關鍵點發生時添加操作。 組件生命周期鉤子 指令和組件實例有個生命周期用于創建、更新和銷…

Nginx配置性能優化

大多數的Nginx安裝指南告訴你如下基礎知識——通過apt-get安裝&#xff0c;修改這里或那里的幾行配置&#xff0c;好了&#xff0c;你已經有了一個Web服務器了。而且&#xff0c;在大多數情況下&#xff0c;一個常規安裝的nginx對你的網站來說已經能很好地工作了。然而&#xf…

Angular的@Output與@Input理解

@Output與@Input理解 Output和Input是兩個裝飾器,是Angular2專門用來實現跨組件通訊,雙向綁定等操作所用的。 @Input Component本身是一種支持 nest 的結構,Child和Parent之間,如果Parent需要把數據傳輸給child并在child自己的頁面中顯示,則需要在Child的對應 directiv…

騰訊云CDN配置

第一步&#xff1a;先去領取騰訊云CDN免費包23333333 以下為正式步驟&#xff1a; 在這里體現大家&#xff0c;域名一定要備案&#xff0c;另外要明白域名如何解析 前邊問題不大&#xff0c;一切跟著騰訊云的套路來即可&#xff0c;需要注意的是網上后優化的配置大家可以自行…

Promise.all的深入理解

異步之Promise Promise.all Promise.all接收的promise數組是按順序執行的還是一起執行的&#xff0c;也就是說返回的結果是順序固定的嗎&#xff1f; 目前有兩種答案&#xff1a; 應該是同步執行的&#xff0c;但是這樣就有效率問題了&#xff0c;如果想改成異步執行怎么辦…

wordpress后臺無法登錄問題

之前給自己的WordPress加了個標簽云&#xff0c;今天登錄的時候突然發現網站后臺進不去了&#xff0c;無奈各種找材料&#xff0c;這算是皇天不負有心人&#xff0c;總算是給我找到了&#xff0c;現在做一下記錄 登錄不上的原因在于&#xff1a;wp-admin和wp-admin/是不同的&a…

深入理解Angular訂閱者模式

深入理解Angular訂閱者模式 如果正在讀此篇文章的你學過java,c++等面向對象語言,知道兩個模式觀察者模式和訂閱者模式,分別為:Observer pattern,Pub-sub pattern(Subscriber) 接下來我們結合Angular來說明這兩個模式。 Observer pattern This is a pattern of developme…

Ubuntu中安裝python3

通過命令行安裝Python3.*&#xff0c;只需要在終端中通過命令行安裝即可&#xff1a; sudo apt-get install python3 Ubuntu的底層大多數采用的是Python2.*&#xff0c;Python3和Python2是互相不兼容的&#xff0c;完全沒法通用的&#xff08;也不知道他們怎么想的o(TヘTo)&a…

Angular深入理解之指令

Angular深入理解之指令 指令有什么功能 Attribute directives 屬性指令Structural directives 結構指令自定義屬性指令自定義結構指令Angular深入理解之指令 對于初學Angular的同學來說,指令無疑是最痛苦的,那么我們怎么使用自定義的指令呢?指令到底怎么實現呢?為什么要寫…

windows下Apache虛擬主機配置

找到host文件&#xff1a;C:\Windows\System32\drivers\etc\hosts 在hosts這么增加&#xff1a; 127.0.0.1 666.666.com 127.0.0.1 777.777.com 修改httpd.conf文件&#xff1a; 打開文件&#xff1a;xxx\xampp\apache\conf\httpd.conf 找到#LoadModule vhost_…

Angular深入理解基本組成

Angular深入理解基本組成 在講指令時,我們先來了解一下Angular的基本概念和結構。 Module 模塊 Angular 是模塊化的.Modules 導出 classes, function, values , 以便在其他模塊導入使用.angular應用由模塊組成,每個模塊都做此模塊相關的事情組件、方法、類、服務等,他們都…

1607: 字符棱形

1607: 字符棱形 根據讀入的字符和邊長&#xff0c;勾畫字符棱形。 Input 輸入數據含有不超過50組的數據&#xff0c;每組數據包括一個可見字符c和一個整數n&#xff08;1≤n≤30&#xff09;。 Output 輸出以c為填充字符&#xff0c;邊長為n的棱形&#xff0c;勾畫每個棱形…

Angular深入理解管道Pipe

Angular深入理解管道 純管道與非純管道區別的本質 Pure FunctionImpure Function內置Pipe pipe使用自定義Pipe 管道性能優化Angular深入理解管道 管道的鏈接 有學過linux shell的同學,應該知道管道,在shell中的管道是IPC,linux的進程間通訊有pipe,FIFO,signal。這里只是簡單…