天天看點

左值和右值

這個問題牽涉到得首先是左值(L-value)和右值(R-value)的概念;

  1. 我查資料的時候發現很多地方都引用一句話:"通俗的講,左值就是能夠出現在指派符号左面的東西,而右值就是那些可以出現在指派符号右面的東西了。"我覺得這句話在剛開始了解的時候是什麼用都沒有的一句廢話.因為我們都不知道哪些東西應該放在指派符号的左邊,哪些東西又應該放在指派符号的右邊這樣說是沒有意義的.

2.接着我找到個比較靠譜的定義:左值是指具有對應的可由使用者通路的存儲單元,并且能由使用者改變其值的量。如一個變量就是一個左值,因為它對應着一個存儲單元,并可由程式設計者通過變量名通路和改變其值。

               (下面的第三點是c++primer中的)

 3.變量和文字常量都有存儲區,并且有相關的類型。差別在于變量是可尋址的(addressable)對于每一個變量都有兩個值與其相聯:

       1).它的資料值,存儲在某個記憶體位址中。有時這個值也被稱為對象的右值(rvalue,讀做are-value).我們也可認為右值的意思是被讀取的值(read   value)。   文字常量和變量都可  被用作右值。   

       2).它的位址值——即存儲資料值的那塊記憶體的位址。它有時被稱為變量的左值(lvalue,讀作ell-value)。我們也可認為左值的意思是位置值location   value文字常量不能被用作左值

有些變量既可以當左值右可以當右值。

  • 左值(Lvalue) →→ Location

    表示記憶體中可以尋址,可以給它指派(const類型的變量例外)

  • 右值Rvalue) →→ Read

    表示可以知道它的值(例如常數)

一個典型的例子

a++ : 先使用a的值,再給a加1,作為 右值

// a++的實作
int temp = a;
a = a + 1;
return temp;
           
  • ++a : 先加再用,作為 左值
a = a + 1;
return a;
           

左值符号 &

右值符号 &&

在C++中,臨時對象不能作為左值,但是可以作為常量引用,

const &

C++ 11中的std::move可将左值引用轉化成右值引用。

左值和右值

左值:指表達式結束後依然存在的持久對象,可以取位址,具名變量或對象 

右值:表達式結束後就不再存在的臨時對象,不可以取位址,沒有名字。 

比如 

int a = b + c;

,a 就是一個左值,可以對a取位址,而

b+c

 就是一個右值,對表達式

b+c

 取位址會報錯。C++11中右值又由兩個概念組成:将亡值和純右值。

純右值和将亡值

在C++98中,右值是純右值,純右值指的是臨時變量值、不跟對象關聯的字面量值。包括非引用的函數傳回值、表達式等,比如 2、‘ch’、int func()等。将亡值是C++11新增的、與右值引用相關的表達式。

  • 純右值:非引用傳回的臨時變量( int func(void) )、運算表達式産生的臨時變量(b+c)、原始字面量(2)、lambda表達式等。
  • 将亡值:将要被移動的對象、T&&函數傳回值、std::move傳回值和轉換為T&&的類型的轉換函數的傳回值。

将亡值可以了解為通過“盜取”其他變量記憶體空間的方式擷取到的值。在確定其他變量不再被使用、或即将被銷毀時,通過“盜取”的方式可以避免記憶體空間的釋放和配置設定,能夠延長變量值的生命期。

右值引用和左值引用

介紹

右值引用就是對一個右值進行引用的類型,标記為 T&&。因為右值不具名,是以引用的形式找到它,用引用來表示,右值引用也是引用的引用(我目前是這麼想的)。

左值引用就是對一個左值進行引用的類型。

引用本身不擁有所綁定對象的記憶體,隻是該對象的一個别名,左值引用就是有名變量的别名,右值引用是不具名變量的别名。是以無論左值引用還是右值引用都必須立即進行初始化。

通過右值引用,這個将亡的右值又“重獲新生”,它的生命周期與右值引用類型變量的生命周期一樣,隻要這個右值引用類型的變量還活着,那麼這個右值臨時量就會一直活着,這是一重要特性,可利用這一點會一些性能優化,避免臨時對象的拷貝構造和析構。

左值引用包括常量左值引用和非常量左值引用。非常量左值引用隻能接受左值,不能接受右值;常量左值引用是一個“萬能”的引用類型,可以接受左值(常量左值、非常量左值)、右值。不過常量左值所引用的右值在它的“餘生”中隻能是隻讀的。

int &a = 2;       // 非常量左值引用 綁定到 右值,編譯失敗

int b = 2;        // b 是非常量左值
const int &c = b; // 常量左值引用 綁定到 非常量左值,編譯通過

const int d = 2;  // d 是常量左值
const int &e = d; // 常量左值引用 綁定到 常量左值,編譯通過
const int &f =2;  // 常量左值引用 綁定到 右值,編譯通過
           

右值引用通常不能綁定到任何的左值,要想綁定一個左值到右值引用,通常需要std::move()将左值強制轉換為右值。比如:

int a;
int &&r1 = a;             // 編譯失敗
int &&r2 = std::move(a);  // 編譯通過
           

右值引用特點

右值引用獨立于左值和右值。意思是右值引用類型的變量可能是左值也可能是右值。比如:

int&& val1 = x;
           

var1類型為右值引用,但var1本身是左值,因為具名變量都是左值。

T&& 并不一定表示右值,它綁定的類型是未定的,既可能是左值又可能是右值。

template<typename T>
void f(T&& param){}

f(10); //param是右值

int x = 10;
f(x); //param是左值
           

T&& param,表示param實際上是一個未定的引用類型,稱為universal references,可以認為它是一種未定的引用類型,它必須被初始化,它是左值還是右值引用取決于它的初始化,如果&&被一個左值初始化,它就是一個左值;如果它被一個右值初始化,它就是一個右值。 

上面例子,當參數為右值10的時候,根據universal references的特點,param 被一個右值初始化,那麼 param 就是右值;當參數為左值 x 時,param 被一個左值引用初始化,那麼 param 就是一個左值。

隻有發生自動類型推斷時(如函數模闆的類型自動推導,或auto關鍵字),T&& 才是一個universal references。比如

template<typename T>
void func(T&& param)   //T需要推導,&&是一個universal references

template<typename T>
class Test {
    Test(Test&& rhs); 
};
           

上面的例子中,param是universal references,rhs 是 Test&& 右值引用,因為模版函數 func 發生了類型推斷,而 Test&& 并沒有發生類型推導,因為 Test&& 是确定的類型了.

再看一個例子:

template<typename T>
void func(const T&& param);
           

上面的例子中param不是 universal references,它其實是一個右值引用。universal references 僅僅在 T&& 下發生,任何一點附加條件都會使之失效,是以上面被 const 修飾之後就成了右值引用了。

引用折疊規則

經過類型推導的 T&& 類型,相比右值引用(&&)會發生類型變化,這種變化被稱為引用折疊或崩塌。規則:

  • 所有的右值引用疊加到右值引用上仍然還是一個右值引用;
  • 所有的其他引用類型之間的疊加都将變成左值引用。

總之是,所有的右值引用疊加到右值引用上仍然是一個右值引用,其他引用折疊都是左值引用。

T& &、T& &&和T&& & 都折疊成類型 T& 

T&& &&折疊成 T&&

右值函數模版參數類型推導

template<typename T>
void foo(T&&);
           

1. 當傳給foo函數的參數是一個左值時,例如:

int i = 29;
foo(i);//i為左值引用
           

此時,T的類型為int的左值引用:int&,參數類型為int& &&,(即T&&),結合上面的引用折疊規則,最終參數的類型為int的左值引用:int&。 

2. 當傳給foo函數的參數是一個右值時,例如:

foo(29);
           
此時,T的類型為int,參數類型為int&&,(即T&&)。
           

還在了解ing,感覺好複雜,也不會用呢,先到這。

還有一點,忘了加上。 

編譯器會把已命名的右值引用視為左值,而将未命名的右值引用視為右值。

#include <iostream>
#include <string>
#include <stack>
#include <vector>
#include <algorithm>
#include <cstdio>

using namespace std;

void PrintValue(int & i)
{
    cout << "lvalue : " << i << endl;
}

void PrintValue(int&& i)
{
    cout << "rvalue : " << i << endl;
}

void Forward(int&& i)
{
    PrintValue(i);
}

int main()
{
    int i = 0;
    PrintValue(i);
    PrintValue(1);
    Forward(2);

    return 0;
}
           

輸出結果:

左值和右值

Forwaid函數收到一個右值,在轉發給PrintfValue時又變成了左值,在Forward中調用PrintfValue時,右值i變成了一個命名的對象,編譯器會把它當作左值處理。

左值引用, 即&i, 是一種對象類型的引用; 右值引用, 即&&i, 是一種對象值的引用;

std::move()可以把左值引用, 轉換為右值引用;

左值引用是固定的引用, 右值引用是易變的引用, 隻能引用字面值(literals)或臨時對象(temporary object);

右值引用主要應用在移動構造器(move constructor)和移動-指派操作符(move-assignment operator)上面;

代碼如下

/* 
 * cppprimer.cpp 
 * 
 *  Created on: 2013.11.7 
 *      Author: Caroline 
 */  
  
#include <iostream>  
#include <utility>  
  
int main (void) {  
    int i = 42;  
    int &lr = i;  
    int &&rr = i*42;  
    const int &lr1 = i*42;  
    int &&rr1 = 42;  
    int &&rr2 = std::move(lr);  
    std::cout << "i = " << i << std::endl;  
    std::cout << "lr = " << lr << std::endl;  
    std::cout << "rr = " << rr << std::endl;  
    std::cout << "lr1 = " << lr1  <<std::endl;  
    std::cout << "rr1  = " << rr1  << std::endl;  
    std::cout << "rr2  = " << rr2  << std::endl;  
}