11.2 指標(pointer)

C++ 有內建的排序函式 sort。假設 n 個整數存在陣列 aa[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 變數的位置」

兩個運算子:& 取址、* 取值

指標的操作靠兩個運算子:

  • &變數:取得這個變數的記憶體位置(這個動作叫「取址」)。
  • *指標:到指標儲存的位置,把住在那裡的值取出來(這個動作俗稱「取值」,正式名稱是「解參考」)。

先用互動圖解建立畫面感,再往下讀程式碼:

圖 11-1:指標與記憶體——誰存值、誰存位置
#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

*pn 指的是同一個位置,所以改 *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

有數字又有英文字母,是因為位置習慣用十六進位顯示(af 代表 1015,詳見第十六單元);轉成 long long 印出來就是我們熟悉的十進位。兩行是同一個位置的兩種寫法。

這個數字本身完全不重要——它每次執行都不一樣,換台電腦更不一樣,沒有任何題目會叫你算出某個變數的位置是多少。指標有用的地方永遠是關係:誰指向誰、兩個位置差幾格。

陣列名稱,骨子裡是指標

上冊 7.7 說過「陣列名稱代表開頭位置」,現在可以講得更精確:運算式裡出現陣列名稱(後面沒接中括號)時,它會隱性轉型成「指向第一個元素的指標」。而指標可以加減整數:

  • 指標 + k:往後跳 k元素的位置。注意跳的單位是元素不是位元組——int4 個位元組(上冊 2.2),所以 a + 1 的位置比 a4
  • 指標 - 指標:兩個位置之間的距離,單位一樣是元素個數。
#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
存的東西 明確就是記憶體位置 「在容器裡的哪個位置」——實際上怎麼存的,我們不需要知道
用在哪 陣列 vectorstring 等容器

換句話說,iterator 是把「位置」這個概念抽象化之後的產物,指標則是它最原始、最具體的版本。11.4 你就會看到 sort 兩種都吃——它只在乎你給它一段「從哪裡到哪裡」。

動手試試看:把上面的陣列範例打進電腦跑一次,觀察你機器上的兩個位置是不是也差 4。再加一行 *(a + 2) = 99;,然後印出 a[2]——驗證「改 *(a + 2) 就是改 a[2]」。