引用
引用可以看成是 C++ 封裝的指針,用來傳遞它所指向的對象。在 C++ 代碼中實際上會經常和引用打交道,但是通常不會顯式地表現出來。引用的基本原則是在聲明時必須指向對象,以及對引用的一切操作都相當於對原對象操作。另外,引用不是對象,因此不存在引用的數組、無法獲取引用的指針,也不存在引用的引用。
注意引用類型不屬於對象類型,所以才需要 reference_wrapper 這種設施。
引用主要分為兩種,左值引用和右值引用。此外還有兩種特殊的引用:轉發引用和垂懸引用,不作詳細介紹。另外,本文還牽涉到一部分常值的內容,請用 常值 一文輔助閲讀。
左值引用
左值和右值
如果你不知道什麼是左值和右值,可以參考 值類別 頁面。
左值表達式
如果一個表達式返回的是左值,那麼這個表達式被稱為左值表達式。右值表達式亦然。
通常我們會接觸到的引用為左值引用,即綁定到左值的引用,但 const 的左值引用可以綁定到右值。以下是來自 參考手冊 的一段示例代碼。
1
2
3
4
5
6
7
8
9
10
11
12 #include <iostream>
#include <string>
int main () {
std :: string s = "Ex" ;
std :: string & r1 = s ;
const std :: string & r2 = s ;
r1 += "ample" ; // 修改 r1,即修改了 s
// r2 += "!"; // 錯誤:不能通過到 const 的引用修改
std :: cout << r2 << '\n' ; // 打印 r2,訪問了s,輸出 "Example"
}
左值引用最常用的地方是函數參數,通過左值引用傳參可以起到與通過指針傳參相同的效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 #include <iostream>
#include <string>
// 參數中的 s 是引用,在調用函數時不會發生拷貝
char & char_number ( std :: string & s , std :: size_t n ) {
s += s ; // 's' 與 main() 的 'str' 是同一對象
// 此處還説明左值也是可以放在等號右側的
return s . at ( n ); // string::at() 返回 char 的引用
}
int main () {
std :: string str = "Test" ;
char_number ( str , 1 ) = 'a' ; // 函數返回是左值,可被賦值
std :: cout << str << '\n' ; // 此處輸出 "TastTest"
}
右值引用 (C++ 11)
右值引用是綁定到右值的引用。右值 可以在內存裏也可以在 CPU 寄存器中 。另外,右值引用可以被看作一種 延長臨時對象生存期的方式 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 #include <iostream>
#include <string>
int main () {
std :: string s1 = "Test" ;
// std::string&& r1 = s1; // 錯誤:不能綁定到左值
const std :: string & r2 = s1 + s1 ; // 可行:到常值的左值引用延長生存期
// r2 += "Test"; // 錯誤:不能通過到常值的引用修改
std :: string && r3 = s1 + s1 ; // 可行:右值引用延長生存期
r3 += "Test" ; // 可行:能通過到非常值的右值引用修改
std :: cout << r3 << '\n' ;
}
在上述代碼中,r3 是一個右值引用,引用的是右值 s1 + s1。r2 是一個左值引用,可以發現 右值引用可以轉為 const 修飾的左值引用 。
一些例子
++i 和 i++
++i 和 i++ 是典型的左值和右值。++i 的實現是直接給 i 變量加一,然後返回 i 本身。因為 i 是內存中的變量,因此可以是左值。實際上前自增的函數簽名是 T& T::operator++();。而 i++ 則不一樣,它的實現是用臨時變量存下 i,然後再對 i 加一,返回的是臨時變量,因此是右值。後自增的函數簽名是 T T::operator++(int);。
int n1 = 1 ;
int n2 = ++ n1 ;
int n3 = ++ ++ n1 ; // 因為是左值,所以可以繼續操作
int n4 = n1 ++ ;
// int n5 = n1++ ++; // 錯誤,無法操作右值
// int n6 = n1 + ++n1; // 未定義行為
int && n7 = n1 ++ ; // 利用右值引用延長生命期
int n8 = n7 ++ ; // n8 = 5
移動語義和 std::move(C++11)
在 C++11 之後,C++ 利用右值引用新增了移動語義的支持,用來避免對象在堆空間的複製(但是無法避免棧空間複製),STL 容器對該特性有完整支持。具體特性有 移動構造函數 、移動賦值 和具有移動能力的函數(參數裏含有右值引用)。
另外,std::move 函數可以用來產生右值引用,需要包含 <utility> 頭文件。
注意:一個對象被移動後不應對其進行任何操作,無論是修改還是訪問 。被移動的對象處於有效但未指定的狀態,具體內容依賴於 stl 的實現。如果需要訪問(即指定一種狀態),可以使用該對象的 swap 成員函數或者偏特化的 std::swap 交換兩個對象(同樣可以避免堆空間的複製)。
1
2
3
4
5
6
7
8
9
10
11
12 // 移動構造函數
std :: vector < int > v { 1 , 2 , 3 , 4 , 5 };
std :: vector < int > v2 ( std :: move ( v )); // 移動v到v2, 不發生拷貝
// 移動賦值函數
std :: vector < int > v3 ;
v3 = std :: move ( v2 );
// 有移動能力的函數
std :: string s = "def" ;
std :: vector < std :: string > numbers ;
numbers . push_back ( std :: move ( s ));
注意上述代碼僅在 C++11 之後可用。
函數返回引用
讓函數返回引用值可以避免函數在返回時對返回值進行拷貝,如
char & get_val ( std :: string & str , int index ) { return str [ index ]; }
你不能返回在函數中的局部變量的引用,如果一定要在函數內的變量。請使用動態內存。例如如下兩個函數都會產生懸垂引用,導致未定義行為。
std :: vector < int >& getLVector () { // 錯誤:返回局部變量的左值引用
std :: vector < int > x { 1 };
return x ;
}
std :: vector < int >&& getRVector () { // 錯誤:返回局部變量的右值引用
std :: vector < int > x { 1 };
return std :: move ( x );
}
當右值引用指向的空間在進入函數前已經分配時,右值引用可以避免返回值拷貝。
struct Beta {
Beta_ab ab ;
Beta_ab const & getAB () const & { return ab ; }
Beta_ab && getAB () && { return std :: move ( ab ); }
};
Beta_ab ab = Beta (). getAB (); // 這裏是移動語義,而非拷貝
參考內容
C++ 語言文檔——引用聲明
C++ 語言文檔——值類別
Is returning by rvalue reference more efficient?
淺談值類別及其歷史
本页面最近更新: ,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:OI-wiki
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用