跳转至

引用

引用可以看成是 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 + s1r2 是一個左值引用,可以發現 右值引用可以轉為 const 修飾的左值引用

一些例子

++ii++

++ii++ 是典型的左值和右值。++i 的實現是直接給 i 變量加一,然後返回 i 本身。因為 i 是內存中的變量,因此可以是左值。實際上前自增的函數簽名是 T& T::operator++();。而 i++ 則不一樣,它的實現是用臨時變量存下 i,然後再對 i 加一,返回的是臨時變量,因此是右值。後自增的函數簽名是 T T::operator++(int);

1
2
3
4
5
6
7
8
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 之後可用。

函數返回引用

讓函數返回引用值可以避免函數在返回時對返回值進行拷貝,如

1
char &get_val(std::string &str, int index) { return str[index]; }

你不能返回在函數中的局部變量的引用,如果一定要在函數內的變量。請使用動態內存。例如如下兩個函數都會產生懸垂引用,導致未定義行為。

1
2
3
4
5
6
7
8
9
std::vector<int>& getLVector() {  // 錯誤:返回局部變量的左值引用
  std::vector<int> x{1};
  return x;
}

std::vector<int>&& getRVector() {  // 錯誤:返回局部變量的右值引用
  std::vector<int> x{1};
  return std::move(x);
}

當右值引用指向的空間在進入函數前已經分配時,右值引用可以避免返回值拷貝。

1
2
3
4
5
6
7
8
9
struct Beta {
  Beta_ab ab;

  Beta_ab const& getAB() const& { return ab; }

  Beta_ab&& getAB() && { return std::move(ab); }
};

Beta_ab ab = Beta().getAB();  // 這裏是移動語義,而非拷貝

參考內容

  1. C++ 語言文檔——引用聲明
  2. C++ 語言文檔——值類別
  3. Is returning by rvalue reference more efficient?
  4. 淺談值類別及其歷史