左值(lvalue)和右值(rvalue)

左值(lvalue)和右值(rvalue)是編程中兩個非常基本的概念,但是也非常容易讓人誤解,看了很多文章,自我感覺真正將這個問題講的很透徹的文章還沒有看見,所以自告奮勇來嘗試一下。如果左值右值的概念不是非常清楚的話,它們遲早會像攔路虎一樣跳出來,讓你煩心不已,就像玩電腦遊戲的時候每隔一段時間總有那麼幾個地雷考驗你的耐性,如果一次把所有地雷掃盡就好了。

左值(lvalue)和右值(rvalue)最先來源於編譯理論(感謝南大小百合的programs)。在C語言中表示位於賦值運算符兩側的兩個值,左邊的就叫左值,右邊的就叫右值。比如:

int ii = 5;//ii是左值,5是右值

int jj = ii;//jj是左值,ii是右值

上面表明,左值肯定可以作爲右值使用,但反之則不然。左值和右值的最早區別就在於能否改變。左值是可以變的,右值不能變。【注1】

注1:這一點在C++中已經豬羊變色,不再成立。拱豬遊戲還是挺好玩的,我還真抓過好幾次全紅心,不過真的好險。:)

在很多文章中提到,在C++中,左值更多的指的是可以定位,即有地址的值,而右值沒有地址。【注2】

注2:這一點仍然不準確,我在程序中生成一個臨時右值std::vector(),你能夠說它沒有地址嗎?難道它是沒有肉體的鬼魂或幽靈?它是有地址的,而且它也是絕對的右值。

在現代C++中,現在左值和右值基本上已經失去它們原本所具有的意義,對於左值表達式,通過具體名字和引用(pointer or reference)來指定一個對象。非左值就是右值。我來下一個定義:

左值表示程序中必須有一個特定的名字引用到這個值。

右值表示程序中沒有一個特定的名字引用到這個值。

跟它們是否可以改變,是否在棧或堆(stack or heap)中有地址毫無關係。

1.左值

在下面的代碼中:

int ii = 5;

int const jj = ii;

int a[5];

a[0] = 100;

*(a+3) = 200;

int const& max( int const& a, int const& b ) //call by reference

{

  return a > b ? a : b;

}

int& fun(int& a) //call by reference

{

  a += 5;

  return a;

}

ii,jj,a[0],*(a+3),還有函數max的返回值比如max(ii, jj),【注3】函數fun的返回值fun(ii)都是左值。,它們都是有特定的引用名字的值。ii,jj,a[0],*(a+3),max(ii, jj),fun(ii)分別就是它們的名字。

注3:在這裏有一個不太容易分清楚的盲點。那就是有人會問max(8, 9)到達是左值還是右值,C++標準規定常量引用(reference to const)可以引用到右值,所以max(8, 9)似乎應該是右值,不過不管它是左值,還是右值,我們都不能試圖去改變它。爲了與前面的概念一致,我認爲它是左值,不可改變的常量左值。

左值有不能改變的,即被const所修飾的左值,比如上面的jj,max(ii, jj)都是被常量(const)魔咒所困住的左值。

沒有被const困住的左值當然是可以改變的,比如下面的代碼都是成立的:

ii = 600;

a[0] = 700;

fun(ii) = 800; //OK!

我們的眼睛沒有問題,fun(ii) = 800;完全正確,因爲它是可以改變的左值。所以我們看STL的源碼,就會理解std::vector中的重載operator[]運算符的返回值爲什麼要寫成引用,因爲operator[]必須返回左值。

 

2.右值

沒有特定名字的值是右值。先看下面的代碼:

std::list();

std::string(“It is a rvalue!”);

int fun1() //call by value

{

  …

}

int* fun2() //call by reference

{

  …

}

其中std::list(),std::string(“It is a rvalue!”),函數fun1的返回值fun1(),函數fun2的返回值fun2()都是右值,它們的值都沒有特定的名字去引用。也許有人會奇怪,fun2()也是右值?最前面的max(a,b)不是左值嗎?

請看清楚,函數fun2的返回值是pointer,pointer也是call by value,而函數max的返回值是reference,reference是call by reference。所以說C++中引入reference不僅僅是爲了方便,它也是一種必須。【注4】

注4:SCOtt Meyer寫的《More Effective C++》的條款1專門講了pointer和reference的區別,寫的很好,辨別的非常清楚。

fun2()是右值,但 *fun2()卻是左值,就跟經常看到的*p一樣,所以看C++庫代碼的時候,會發現重載operator*的函數返回值是reference。

當然我還遺漏了一種右值,那就是字面上的(literal)值,比如5,8.23,’a’等等理所當然的都是右值。

右值最初出現的時候,一個最大的特徵就是不可改變。但就跟我們的道德標準一樣,時代不同了,標準也變化了,以前的三綱五常早已經被扔到歷史的垃圾堆裏面了。

C++中有可以改變的右值,而且這個特性還非常有用。那就是用戶自定義的類(class)的構造函數生成的臨時對象。比如:

std::vector(9),std::deque(),……都是可以改變的右值。在Herb Sutter的《More Exceptional C++》中的條款7的page51頁有這樣幾行代碼:

// Example 7-2(b): The right way to shrink-to-fit a vector.

vector<Customer> c( 10000 );

// ...now c.capacity() >= 10000...

// erase all but the first 10 elements

c.erase( c.begin()+10, c.end() );

// the following line does shrink c's

// internal buffer to fit (or close)

vector<Customer>( c ).swap( c );

// ...now c.capacity() == c.size(), or

// perhaps a little more than c.size()

認真看幾遍,你會發現但vector的大小增大到一定程度,你又用不着這麼多空間的時候,你會想辦法把它收縮到最合適的大小,但利用別的辦法比如調用成員函數reserve()都無法辦到,這個時候就必須利用右值可以改變這個性質了。

vector<Customer>( c ).swap( c );這行代碼就是點睛之處。

首先使用複製構造函數生成臨時右值vector<Customer>( c ),這個右值正好是合適大小,然後和c交換【注5】,c就變成合適大小了,最後在整個表達式結束的時候,這個臨時右值析構歸還內存空間。真是紳士一般的優雅!

注5:這個時候這個臨時右值就發生了改變。

如果還不理解,可以看看書,或者直接看庫的源代碼

至於爲什麼會這樣?我思考了一下,我想是這樣的,我們看類(class)的數據佈置結構,會發現它的每一個數據成員都是有名字的,我想編譯器在編譯的過程中,都會生成一個外部不所知的對這個臨時對象右值的名字引用,但需要改變這個臨時對象的時候,這個名字就用上了。比如:

class Point

{

public: //純粹爲了方便,我把數據成員公開,現實中儘量不要這樣用

  int x, y ,z;

  ……//其他各種成員函數

};

我們現在就可以改變右值,用到了匿名的引用名字。

Point().x = 6;//改變了右值

Point().y = 6;//同意改變了右值,不過注意,這個右值跟上面的不是同一個。

總結

左值和右值的真正區別我想就是這些了,左值表示有特定的名字引用,而右值沒有特定的名字引用。當然我仍然會有疏忽,希望大家能夠提醒我,指正我的不足。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章