Learning Objectives
- 說明參考是變數的別名:宣告時必須綁定,之後不能改綁到別的變數
- 用傳參考讓函式修改呼叫端的變數,或一次帶回好幾個結果
- 用 const 參考傳遞字串、vector 等較大的物件,避免複製又防止修改
- 在 range-for 裡分辨 auto、auto& 與 const auto& 的差別
- 說明回傳參考的條件,辨認懸空參考,並比較參考與指標
幫變數取第二個名字
第 6 課提到,C++ 預設是傳值呼叫:函式拿到的是複本,改不到呼叫端的變數;
第 8 課〈指標〉用「傳位址」解決了這個問題,但每次都要寫 &x、*p,還得擔心空指標。
參考(reference)是 C++ 提供的另一種做法:它是一個已經存在的變數的別名(alias),兩個名字指的是同一塊記憶體。
int score = 80;
int& alias = score; // int& 讀作「int 的參考」;alias 就是 score
alias = 90; // score 也變成 90
注意 & 的位置:寫在型別後面(int&)是宣告參考;寫在變數前面(&score)是取位址,兩者意思不同。
參考的三條規則:
- 宣告時一定要綁定一個變數:
int& r;是編譯錯誤 - 綁定之後不能改綁:之後對參考做的任何指定,都是在改它綁定的那個變數
- 一般的參考(非 const)不能綁到字面常數或運算結果:
int& r = 5;是編譯錯誤
動畫裡的參考不會顯示自己的值,而是一個指向原變數的標記,表示「這個名字就是那個變數」。
參考是別名,不能改綁
第 8、9 行印出的兩個位址完全相同,表示 alias 沒有自己的記憶體,它就是 score。第 13 行 alias = other; 看起來像是讓 alias 改綁到 other,其實是把 60 寫進 score;之後改 other,alias 不受影響。
傳參考
參數宣告成參考,呼叫時參數就直接綁定到引數,函式裡改參數就是改呼叫端的變數,稱為傳參考(pass by reference):
void swapByRef(int& a, int& b) { int t = a; a = b; b = t; }
swapByRef(x, y); // 呼叫時照常寫變數名稱,不用寫 &
和第 8 課的指標版本 swapByPointer(&x, &y) 比較:參考版本的函式裡不用寫 *,呼叫時不用寫 &,而且參考不會是空的,不必檢查 nullptr。
傳參考的兩個常見用途:
- 讓函式修改引數,例如交換、排序、把分數加分
- 一次帶回好幾個結果:
return只能回傳一個值,需要兩個以上的結果時,可以用參考參數帶回來(例如同時算出最小值與最大值)
傳參考時,引數必須是變數,swapByRef(3, 8) 是編譯錯誤,因為字面常數沒有可以修改的記憶體。
傳值與傳參考的交換
比較兩次呼叫時右邊的框架:swapByValue 的 a、b 是有自己數值的複本;swapByRef 的 a、b 則標示著指回 main 的 x、y。執行第 13、14 行時,留意 main 框架裡的 x、y 跟著改變。
想一想:別名與傳參考
下面的程式會印出什麼?
int a = 1, b = 2;
int& r = a;
r = b;
b = 9;
cout << a << r;
下面的程式會印出什麼?
void twice(int& n) {
n = n * 2;
}
int main() {
int x = 3;
twice(x);
twice(x);
cout << x;
}
下面的程式會印出什麼?
int main() {
int& r = 5;
cout << r;
}
const 參考:不複製也不修改
傳值呼叫會複製引數。int 只有 4 個位元組,複製很便宜;但 string、vector 可能有成千上萬個元素,每次呼叫都複製一份很浪費。
如果函式只需要讀,參數寫成 const 參考:
int countChar(const string& text, char target);
double average(const vector<int>& v);
- 參考:不複製,直接使用呼叫端的物件
const:函式裡不能修改它,改了就是編譯錯誤,呼叫的人可以放心- const 參考可以綁到字面常數與暫時值,所以
countChar("hello", 'l')也能呼叫(字串字面常數會先轉成一個暫時的string)
選擇參數寫法的經驗法則:
| 參數 | 寫法 |
|---|---|
int、double、char、bool 這類小型別,只讀 |
傳值:int n |
string、vector 這類較大的物件,只讀 |
const 參考:const string& s |
| 函式要修改呼叫端的變數 | 參考:int& n、vector<int>& v |
const 參考參數
第一次呼叫 countChar 時,右邊的 text 指回 main 的 fruit,沒有另外複製一份字串。第二次傳的是字面常數 "hello",text 綁定的是一個暫時的 string。第 14 行如果拿掉註解,就會是編譯錯誤。
range-for 裡的參考
C++11 加入的 range-for(for (元素 : 容器))可以走訪陣列、string、vector 的每一個元素。迴圈變數的寫法決定它是不是參考:
| 寫法 | 迴圈變數是 | 用途 |
|---|---|---|
for (int x : v) |
元素的複本 | 只讀小型別;改 x 不影響 v |
for (int& x : v) 或 for (auto& x : v) |
元素本身(別名) | 要修改容器裡的元素 |
for (const auto& x : v) |
元素本身,但唯讀 | 只讀較大的元素(例如 string),不複製 |
auto 讓編譯器依容器自動推導元素型別,auto& 就是「那個型別的參考」。
複本、別名與唯讀
第一個迴圈的 p 是有自己數值的複本,乘以 2 之後 prices 沒變;第二個迴圈的 p 標示著指向 prices[0]、prices[1]……,改它就是改 vector 裡的元素。
回傳參考與懸空參考
函式也可以回傳參考,呼叫的結果就是某個物件本身,可以直接放在 = 的左邊修改它:
int& largest(vector<int>& v); // 回傳最大的元素本身
largest(v) = 0; // 把最大的元素改成 0
條件是:回傳的物件在函式結束之後仍然存在,例如透過參考參數傳進來的物件、或它的元素。
絕對不要回傳區域變數的參考:函式結束時區域變數就消失了,呼叫端拿到的參考綁定的是已經不存在的東西,稱為懸空參考(dangling reference), 和第 8 課的懸空指標是同一種錯誤。g++ 預設就會對這種寫法提出警告,不要忽略它。
回傳參考:可以與不可以
largest 回傳的是 v[1] 本身,所以第 23 行把 0 寫進了 vector。badRef 返回時框架消失,local 也跟著消失,動畫會在第 25 行停下來說明懸空參考。
參考與指標的比較
參考在多數編譯器裡是用位址實作的,但在語言層面,它和指標的用法很不一樣:
參考 int& r = x; |
指標 int* p = &x; |
|
|---|---|---|
| 宣告時 | 一定要綁定 | 可以不初始化(但不建議),也可以是 nullptr |
| 能不能是空的 | 不能 | 可以是 nullptr,使用前要檢查 |
| 能不能改指向 | 不能改綁 | 可以:p = &y; |
| 存取值 | 直接寫 r |
要寫 *p |
| 取位址 | &r 就是 &x |
p 本身有自己的位址 &p |
| 指標運算 | 沒有 | 有:p + 1、p++ |
選擇的原則:能用參考就用參考(函式參數、range-for);需要表達「可能沒有」(nullptr)、需要改指向別的物件、或要做指標運算時,才用指標。
想一想:const 參考、range-for 與指標的比較
下面的程式會印出什麼?
vector<int> v = {1, 2, 3};
for (int x : v) {
x += 10;
}
for (int& y : v) {
y += 1;
}
cout << v[0] + v[2];
下面的程式會印出什麼?
void show(const string& s) {
s += "!";
cout << s;
}
int main() {
string name = "Amy";
show(name);
}
關於參考與指標,下列敘述何者錯誤?
vector<string> names 有很多很長的名字,只要把它們一個一個印出來,迴圈變數最適合哪一種寫法?
動手寫:調分與計算及格人數
完成兩個函式:curve 用 range-for 搭配參考,把每個分數加上 bonus,超過 100 的改成 100;countPassed 只讀不改,回傳 60 分以上的人數。注意兩個函式的參數寫法不同。main 已經寫好。
重點回顧
- 參考是變數的別名:
int& r = x;之後r就是x,兩者位址相同 - 參考宣告時一定要綁定,之後不能改綁;
r = y;是指定值,不是改綁 - 傳參考讓函式修改呼叫端的變數,也能用來帶回多個結果;呼叫時不用寫
& - 只讀的較大物件(
string、vector)用const參考傳遞:不複製、也不能改;const 參考可以綁定字面常數 - range-for:
int x是複本,auto& x可以修改元素,const auto& x只讀不複製 - 只能回傳「函式結束後仍然存在」的物件的參考;回傳區域變數的參考會產生懸空參考
- 能用參考就用參考;需要
nullptr、改指向或指標運算時才用指標