11.3 指標與函式

上冊 7.5 教過一件事:函式參數預設是複本,函式裡改了,外面的變數不痛不癢;想真的改到外面,參數要加 & 變成參考。

這一節要補的是第三種寫法:把「位置」傳進函式。理由有兩個——別人的程式碼(尤其 C 語言寫的)滿滿都是這種寫法,你要看得懂;而且上冊 7.7 那三件關於陣列參數的怪事,答案全在這裡。

一個交換函式,三種寫法

以「交換兩個變數」為例(內建的 swap 先擺一邊,我們自己寫):

#include <iostream>
using namespace std;

// 版本 1:傳值——只換到複本
void swap_value(int a, int b) {
    int tmp = a;
    a = b;
    b = tmp;
}

// 版本 2:傳參考——C++ 的做法
void swap_ref(int &a, int &b) {
    int tmp = a;
    a = b;
    b = tmp;
}

// 版本 3:傳位置——C 語言只能這樣寫
void swap_ptr(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main() {
    int x = 2, y = 1;

    swap_value(x, y);
    cout << x << ' ' << y << '\n';   // 2 1:沒換到

    swap_ref(x, y);
    cout << x << ' ' << y << '\n';   // 1 2:換到了

    swap_ptr(&x, &y);
    cout << x << ' ' << y << '\n';   // 2 1:也換到了
    return 0;
}

執行結果:

2 1
1 2
2 1

三個版本並排比較:

版本 參數寫法 呼叫時寫 函式裡動的是 換得到嗎
傳值 int a swap_value(x, y) a(複本)
傳參考 int &a swap_ref(x, y) a(就是外面的 x
傳位置 int *a swap_ptr(&x, &y) *a(照位置找到的 x

指標版的關鍵在這句話:位置本身確實被複製了一份,但複本裡寫的還是同一個門牌號碼。函式拿著這張紙條走過去,開的是同一扇門——所以 *a = ... 改到的是外面的 x。傳值版之所以失敗,是因為它複製的是「值」本身,複本跟本尊從此毫無關係。

也因此,指標版有兩個地方跟參考版長得不一樣,一個都不能漏:呼叫時要寫 &x(把位置交出去)、函式裡每次用都要寫 *a(照位置取值)。

動手試試看交換指標指向的整數 要你交出的就是上面的版本 3——評分程式會呼叫 int_swap(&a, &b);,你得讓外面的 ab 真的互換。⚠️ 這題明文禁止呼叫內建的 swap(會檢查原始碼),就是要你自己用暫存變數配 * 完成,正好把這一節的手感練起來。

陣列參數的真相

上冊 7.7 留下三件說不通的怪事:陣列參數明明是傳值,函式裡卻改得動;中括號裡的大小寫了會被忽略;sizeof(arr) 在函式裡永遠是 8

其實它們是同一件事的三個側面——寫成 int arr[] 的參數,編譯器一律當成 int *arr

#include <iostream>
using namespace std;

void fill_ones(int *a, int n) {   // 寫成 int a[] 意思完全一樣
    for (int i = 0; i < n; i++) {
        a[i] = 1;                 // a[i] 就是 *(a + i)
    }
}

int main() {
    int a[3] = {7, 8, 9};
    fill_ones(a, 3);
    cout << a[0] << ' ' << a[1] << ' ' << a[2] << '\n';
    return 0;
}

執行結果:

1 1 1

於是三件怪事都有了答案:

  • 為什麼改得動:函式收到的是陣列開頭的位置,順著位置找過去就是外面那片記憶體——跟 swap_ptr 是同一招。
  • 為什麼大小可以省:傳進去的從頭到尾只有一個位置,長度根本沒跟著走,寫了也沒用;所以長度要自己另外傳一個 n
  • 為什麼 sizeof8:量到的是「一個位置」的大小(64 位元電腦上是 8 個位元組),跟陣列多長無關。

動手試試看指標反轉陣列 把這一節整個包起來考——函式收到的是 headtail 兩個位置(陣列的第一格與最後一格),要把陣列原地反轉。做法是兩個指標往中間夾:把 *head*tail 交換(拿個暫存變數,跟 swap_ptr 一模一樣),然後 head++tail--,重複到兩個指標碰頭為止。順帶學到一件新事:同一個陣列裡的指標可以互相比大小,位置在前面的比較小,所以迴圈條件就寫 while (head < tail)

動手試試看:把 swap_ptr 裡的 *a = *b; 少打一個星號改成 a = b;,猜猜會發生什麼事?(結果是印出 2 2——a = b; 只是把函式手上那張紙條改成指向 y,外面的 x 一動也沒動,接著 *b = tmp; 又把 y 寫成了 2。)再試試看把三行的星號全部拿掉,這次連編譯都過不了,錯誤訊息是 invalid conversion from 'int*' to 'int'——因為 int tmp = a; 想把一個位置塞進整數變數裡。