11.2 指標(pointer)
C++ 有內建的排序函式 sort。假設 n 個整數存在陣列 a 的 a[0] ~ a[n-1],把它們由小到大排好只要一行:
int a[6] = {6, 3, 2, 5, 4, 1};
int n = 6;
sort(a, a + n); // 排完 a 就變成 1 2 3 4 5 6
奇怪的地方在第二個參數:a 是陣列名稱,n 是整數,兩個居然可以相加,加出來的東西還能當參數傳進去。要看懂這個寫法,得先認識一個新型態:指標。
¶指標:儲存「記憶體位置」的型態
程式裡的每個變數都住在記憶體的某個位置,可以把位置想成門牌號碼。上冊 7.7 學過:陣列名稱單獨出現時,代表的就是陣列開頭的記憶體位置。
指標(pointer)就是專門用來儲存記憶體位置的型態。宣告時在變數名稱前加一個 *:
int *p; // p 是一個指標,可以儲存「某個 int 變數的位置」
¶兩個運算子:& 取址、* 取值
指標的操作靠兩個運算子:
&變數:取得這個變數的記憶體位置(這個動作叫「取址」)。*指標:到指標儲存的位置,把住在那裡的值取出來(這個動作俗稱「取值」,正式名稱是「解參考」)。
先用互動圖解建立畫面感,再往下讀程式碼:
#include <iostream>
using namespace std;
int main() {
int n = 10;
int *p = &n; // p 儲存的是 n 的記憶體位置
cout << n << '\n'; // 10
cout << *p << '\n'; // 10:到 p 儲存的位置,把值取出來
*p = 25; // 透過指標,把那個位置的值改掉
cout << n << '\n'; // 25:n 被改了!
return 0;
}
執行結果:
10
10
25
*p 和 n 指的是同一個位置,所以改 *p 就是改 n。順帶一提:這個 & 跟上冊 7.5 參考參數的 & 是同一個符號的兩種用法——寫在型態旁(int &x)是宣告參考,寫在變數前(&n)是取址,位置不同、意義不同。
¶位置本身長什麼樣子
指標裡面存的是位置,直接印出來會看到這種東西:
#include <iostream>
using namespace std;
int main() {
int n = 10;
int *p = &n;
cout << p << '\n'; // 位置,用十六進位顯示
cout << (long long)p << '\n'; // 同一個位置,改用十進位看
return 0;
}
執行結果(每次執行、每台電腦都不同):
0x7ffff07772e4
140737227748068
有數字又有英文字母,是因為位置習慣用十六進位顯示(a 到 f 代表 10 到 15,詳見第十六單元);轉成 long long 印出來就是我們熟悉的十進位。兩行是同一個位置的兩種寫法。
這個數字本身完全不重要——它每次執行都不一樣,換台電腦更不一樣,沒有任何題目會叫你算出某個變數的位置是多少。指標有用的地方永遠是關係:誰指向誰、兩個位置差幾格。
¶陣列名稱,骨子裡是指標
上冊 7.7 說過「陣列名稱代表開頭位置」,現在可以講得更精確:運算式裡出現陣列名稱(後面沒接中括號)時,它會隱性轉型成「指向第一個元素的指標」。而指標可以加減整數:
- 指標 + k:往後跳 k 個元素的位置。注意跳的單位是元素不是位元組——
int佔 4 個位元組(上冊 2.2),所以a + 1的位置比a大 4。 - 指標 - 指標:兩個位置之間的距離,單位一樣是元素個數。
#include <iostream>
using namespace std;
int main() {
int a[4] = {10, 20, 30, 40};
cout << a << '\n'; // 陣列開頭(a[0])的記憶體位置
cout << a + 1 << '\n'; // a[1] 的記憶體位置
cout << *a << '\n'; // 10:取出 a[0] 的值
cout << *(a + 2) << '\n'; // 30:取出 a[2] 的值
return 0;
}
執行結果(前兩行是記憶體位置,每次執行、每台電腦都不同):
0x7ffe6a2e2680
0x7ffe6a2e2684
10
30
位置用十六進位顯示,數值本身不重要,看關係:第二行恰好比第一行大 4——往後一個 int 的距離。而 *(a + 2) 的意思是「開頭往後跳兩格,取值」——你早就用了幾個月的 a[2],其實就是 *(a + 2) 的另一種寫法。
指標相減的例子:
#include <iostream>
using namespace std;
int main() {
int a[10];
int *p = &a[2]; // 指向 a[2]
int *q = &a[7]; // 指向 a[7]
cout << q - p << '\n'; // 5:兩個位置之間隔了 5 個 int
return 0;
}
執行結果:
5
動手試試看:解掉指標之間——題目給你同一個陣列裡的兩個指標,問這兩個位置之間嚴格夾著幾個元素。相減得到距離,答案就在旁邊了;但有兩件事要小心:題目沒保證哪個指標在左邊(相減可能是負的,先取絕對值),而且相鄰兩格的距離是 1、中間夾的是 0 個。這是本站的函式實作題(作答方式見上冊 7.8),你只要寫 count_between 這一個函式,不用處理輸入輸出。
(二維陣列轉成指標的規則複雜得多,超出本書的需要,我們不教——競程用不到。)
¶指標與 iterator:像親戚,但不是同一家
10.5 的 iterator 也是「代表位置」的東西,用起來也是 *it 取值、it + 1 往後跳——難怪覺得眼熟。差別在於:
| 指標(本節) | iterator(10.5) | |
|---|---|---|
| 身分 | C++ 內建的型態,任何型態後面加 * 就是 |
各個容器自己設計的型態,寫法是 vector<int>::iterator |
| 存的東西 | 明確就是記憶體位置 | 「在容器裡的哪個位置」——實際上怎麼存的,我們不需要知道 |
| 用在哪 | 陣列 | vector、string 等容器 |
換句話說,iterator 是把「位置」這個概念抽象化之後的產物,指標則是它最原始、最具體的版本。11.4 你就會看到 sort 兩種都吃——它只在乎你給它一段「從哪裡到哪裡」。
動手試試看:把上面的陣列範例打進電腦跑一次,觀察你機器上的兩個位置是不是也差 4。再加一行 *(a + 2) = 99;,然後印出 a[2]——驗證「改 *(a + 2) 就是改 a[2]」。