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是如何使用的,而在源碼中使用,也足以說明它的高效。