C/C++-左值、右值及引用


C和C++中定義了引用類型(reference type),存在左值引用(lvalue reference)。而在C++11中,新增了右值引用(rvalue reference)這一概念, 雖然個人感覺右值引用用處不大,但在此一並討論。

1.左值and右值

首先,我們討論左值和右值兩個概念。
左值(lvalue):一個標識非臨時性對象的表達式。通常來說,可以將程序中所有帶名字的變量看做左值。
右值(rvalue):相對的,右值標識是臨時性對象的表達式,這類對象沒有指定的變量名,都是臨時計算生成的。
考慮以下代碼:

vector<string> arr(3);
const int x = 2;
int y;
int z = x + y;
string str = "foo";
vector<string> *ptr = &arr;

在上述代碼中,arr, str, y, z等都是左值,x也是一個左值,且他不是一個可修改的左值;而類似於2, x+y這類臨時(沒有專屬變量名)的值則是右值。

2.引用

我們可以這樣理解引用:一個引用是它所引用對象的同義詞,是其另一個變量名。
看這樣一段代碼:

#inclulde <stdio.h>

int main()
{
    int x = 5;
    int & y = x;
    printf("引用y = %d\n", y);
    return 0;
}

運行結果如下:

(1)左值引用
左值引用的聲明是通過在某個類型后放置一個符號&來進行的。前文代碼中的int & y = x;便是一個左值引用。
需要注意的是,在定義左值引用時,=右邊的要求是一個可修改的左值。因此下面幾種左值引用都是錯誤的:

#include <stdio.h>

int main()
{
    const int x = 5;
    int y = 1;
    int z = 1;
    int & tmp1 = x;     // ERROR:x不是一個可修改的左值
    int & tmp2 = 5;     // ERROR:5是一個右值
    int & tmp3 = y + z; // ERROR:y+z是一個右值
    return 0;
}

編譯運行,報錯如下:

(2)右值引用
類似於左值引用,右值引用便是對右值的引用,它是通過兩個&&來聲明的。

#include <stdio.h>

int main()
{
    int && x = 5;
    printf("x = %d\n", x);
    return 0;
}

運行結果如下:

引用和指針的區別
我們知道,指針是在內存中存放地址的一種變量,cpu能夠直接通過而變量名訪問唯一對應的內存單元,且每個內存單元的地址都是唯一的。
而變量名和引用,都可以看做內存的一個標簽或是標識符,計算機通過是否符合標識符判斷是否為目標內存,而一個內存可以有多個標識符

3.左值引用的用途

因為目前右值引用用途不大,故此處僅列舉較為常見的左值引用用途
(1)作為復雜名稱變量的別名
我們可以寫出類似如下的語句:

auto & whichList = theList[myHash(x, theList.size())];

可以看到,我們用簡短的whichList代替了其原本復雜的名稱,這能夠簡化我們的代碼書寫。
(2)用於rangeFor循環
設想我們希望通過rangeFor循環使一個vector對象所有值都增加1,下面的rangeFor循環是做不到的、

for (auto x : arr)   // x僅相當於每個元素的拷貝
    ++x;

但我們可以通過使用引用達到這一目的

for (auto & x : arr)
    ++x;

(3)避免復制大的對象
假定有一個findMax函數,它返回一個vector中最大的元素。若給定vector存儲的是某些大的對象時,下述代碼中的x拷貝返回的最大值到x的內存中:

auto x = finaMax(vector);

在大型的項目中這顯然會增大程序的開銷,這時我們可以通過引用來減小這類開銷

auto & x = findMax(vector);

類似的,我們在處理函數返回值的時候也可以使用傳引用返回。但是要注意,當返回的是類中私有屬性時,傳回的引用會導致外界能夠對其修改。
(4)參與函數中的參數傳遞
在C和C++的函數中,addSelf(int x)這類函數對直接傳入的參數進行修改並不會改變原有參數的值。而有時我們希望能夠實現類似swap(int a, int b)這類能夠修改原參數的函數時,我們可以通過1.傳入指針2.傳入引用實現
swap函數的實現是一個很好的例子

#include <stdio.h>

void swap_non(int, int);    // 直接傳入參數 
void swap_p(int *, int *);  // 傳入指針
void swap_r(int &, int &);  // 傳入引用

int main()
{
    int a = 1;
    int b = 2;
    printf("直接傳入參數時:\n");
    swap_non(a, b);
    printf("a = %d, b = %d\n", a, b);
    printf("傳入指針時:\n");
    swap_p(&a, &b);
    printf("a = %d, b = %d\n", a, b);
    printf("傳入引用時:\n");
    swap_r(a, b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

void swap_non(int a, int b)
{
    int temp = a;
    a = b;
    b = temp;
}

void swap_p(int * a, int * b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

void swap_r(int & a, int & b)
{
    int temp = a;
    a = b;
    b = temp;
}

運行結果如下:

可以看到,只有在傳入指針和傳入引用時變量才真正交換。

直接傳入參數傳入指針仍不理解的可以看這篇博客,在此不再描述。

4.std::move和std::swap

前面我們討論3.(3)時,談到可以使用引用來減少復制產生的內存開銷。
考慮這樣一種情況,在進行元素交換時,我們通常使用一個緩存變量temp來臨時保存數據;而對temp直接進行=的賦值操作時,實際上temp復制了一次原有對象的內存,但我們需要只是對象之間的移動而不是復制,而C++STL中的std::move函數便可以達成這一操作。
這可能有些抽象,讓我們通過例子來看看:

void swap(vector<string> & x, vector<string> & y)
{
    vector<string> temp = std::move(x);
    x = std::move(y);
    y = std::move(temo);
}

上述例子是C++STL中std::swap的源碼之一,相信它很好的示范了std::move是如何使用的,而在源碼中使用,也足以說明它的高效。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM