快速瞭解C/C++的左值和右值

最近在segmentfault上看到一個提問《c++隱式的類類型轉換問題》:一時不知怎麼回答,查閱相關資料後整理了本文,以供參考學習。html

定義

早期的C給出的定義:左值是一個表達式,可能出如今賦值操做的左邊或右邊,但右值只能出如今右邊。好比:ios

a * b = 42; // 編譯錯誤, 說明 a * b 不是左值

由於上面的定義實在太模糊,致使左值和右值很難被理解,下面給出的定義,更簡單更好理解:
左值(lvalue)是一個表達式,它表示一個可被標識的(變量或對象的)內存位置,而且容許使用&操做符來獲取這塊內存的地址。若是一個表達式不是左值,那它就被定義爲右值。c++

int i = 42;
  i = 43; 
  int* p = &i; // ok, i 是左值
  int& foo();
  foo() = 42; // ok, foo() 是左值
  int* p1 = &foo(); // ok, foo() 是左值

  int foobar();
  int j = 0;
  j = foobar(); // ok, foobar() 是右值
  int* p2 = &foobar(); // 錯誤,不能獲取右值的地址
  j = 42; // ok, 42 是右值

左值和右值之間的轉換

通常上講,對象之間的運算,對象是以右值的形式參與的。好比二元運算符+兩邊的參數以右值傳入,加後的返回結果也是右值:segmentfault

int a = 1;     // a 是左值
int b = 2;     // b 是左值
int c = a + b; // a和b自動轉換爲右值求和

那些表示數組、函數和非完整類型的左值是不能轉換爲右值的,由於沒法對那些類型進行求值。incomplete types指的是類型定義不完整,只能用指針形式聲明的類型,在頭文件中常常會使用。數組

左值引用

C++中可使用&符定義引用,若是一個左值同時是引用,就稱爲「左值引用」,如:函數

std::string s;
std::string& sref = s;  //sref爲左值引用

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

std::string& r = std::string();  //錯誤!std::string()產生一個臨時對象,爲右值

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

但const左值引用不同,由於常量不能被修改,也就不存在上面的問題:.net

const std::string& r = std::string();  //能夠

咱們常用const左值引用做爲函數的參數類型,能夠減小沒必要要的對象複製:指針

class MyString {
public:
    ...
    MyString &MyString(string& s);  //參數類型爲左值引用
    ...
};

int main() 
{
    MyString s1("XXX");  //錯誤
    MyString s2(string("XXXX")); //同上,右值不能賦值給左值引用
}

MyString& MyString(string& a);改爲MyString& MyString(const string& a);就不會有上面的編譯錯誤。

帶CV限定符(CV-qualified)的右值

C++標準中關於左值轉右值的討論,有這樣一段話:

類型爲T的左值(非函數、非數組類型)能夠被轉換爲右值。若是T不是類(class)類型,轉換後的右值的類型將爲不帶CV限定符的T類型,不然轉換後的右值的類型爲T。

什麼是CV限定符?若是變量聲明時類型前帶有const或volatile,就說此變量類型具備CV限定符。

在C中,右值永遠沒有CV限定符,而C++中的類類型的右值能夠有CV限定符,看下面代碼:

#include <iostream>

class A {
public:
    void foo() const { std::cout << "A::foo() const\n"; }
    void foo() { std::cout << "A::foo()\n"; }
};

A bar() { return A(); }           //返回臨時對象,爲右值
const A cbar() { return A(); }    //返回帶const的右值(帶CV限定符)


int main()
{
    bar().foo();  // 非const對象調用A::foo()的非const版本
    cbar().foo(); // const對象調用A::foo()的const版本
}

也就是說,若是是類類型,從左值轉爲右值時,它的CV限定符會被保留。這裏就不給出示例代碼了。

右值引用(C++11)

右值引用及其相關的move語義是C++11新引入的最強大的特性之一。前文說到,左值(非const)能夠被修改(賦值),但右值不能。但C++11引入的右值引用特性,打破了這個限制,容許咱們獲取右值的引用,並修改之。讓咱們先看點代碼:
定義一個類Intvec及其賦值操做符重載函數以下:

class Intvec
{
public:
    ...
    Intvec& operator=(const Intvec& other)
    {
        log("copy assignment operator");
        Intvec tmp(other);  //構造一個臨時對象,由於other爲const,不能被修改
        std::swap(m_size, tmp.m_size);
        std::swap(m_data, tmp.m_data);  
        //跟臨時對象交換值,臨時對象晰構時會delete [] m_data
        return *this;
    }
private:
    size_t m_size;  
    int* m_data;     //存放int數組,構造時動態分配
};

代碼要點:

  1. 代碼使用了copy-swap策略,即先分配資源再更改自身狀態,這樣能夠保證當資源分配失敗的時候,自身可以維持原先狀態,《高效C++》有條規則描述這個主題。因此先根據other拷貝構造一個臨時對象tmp, 而後與tmp進行swap,m_data交換給了tmp以後,也會隨着tmp的晰構而被釋放。

  2. 之因此把other聲明爲const,有兩個理由,其一是賦值操做不該該更改other,其二是能夠傳入一個右值。其實這樣的聲明隨處可見。

假設現有類型爲Intvec的對象v,用一個新對象給它賦值:

v = Intvec(33);

這句代碼合法,它構造一個臨時對象,爲右值,傳入到Intvec的賦值運算符重載函數中。這個代碼是能夠工做,並且一般狀況下都比較高效。可是若是Intvec裏包含某些m_handle成員,建立和釋放m_handle比較昂貴,那麼拷貝構造越少越好。這種狀況,咱們設想一下,若是v能跟Intvec(33)臨時對象直接進行內部數據交換,而不須要在重載函數裏使用Intvec tmp(other);構造一個新對象出來swap,那該有多好!
如你所料,C++11引入的「右值引用」和「move語義」就能夠實現這個目標,新的語法很簡單,咱們重載一個新的賦值操做運算符函數:

Intvec& operator=(Intvec&& other)
{
    log("move assignment operator");
    std::swap(m_size, other.m_size);
    std::swap(m_data, other.m_data);
    return *this;
}

對於v = Intvec(33);這種寫法就會調用此版本的重載函數(即傳入一個右值)。
&&語法聲明右值引用,表示一個指向右值的引用,經過這個引用,能夠修改右值。

以上就是關於右值引用的一個簡單的示例,實際上右值引用是一個複雜的主題,在實際應用中還有不少場景要考慮,更深刻的講解見底部參考連接。

參考連接:

  1. 《Understanding lvalues and rvalues in C and C++》

  2. 《C++右值引用詳解》

相關文章
相關標籤/搜索