學習目標

  • 理解函式的三要素:定義(definition)、宣告/原型(prototype)、呼叫(call)。
  • 掌握參數傳遞的核心規則:C 一律傳值呼叫(call by value)。
  • 會寫有回傳值的函式與 void 函式,理解 return 的語意。
  • 重現 K&R 的經典 power 函式,理解它示範的所有觀念。
  • 建立函式設計的基本素養:單一職責、好名字、小函式。

對照 K&R 章節

  • K&R §1.7 Functions(power 函式)
  • K&R §1.8 Arguments—Call by Value
  • K&R §4.1 Basics of Functions
  • K&R §4.2 Functions Returning Non-integers

觀念講解

1. 為什麼需要函式?

函式把「一段有名字的計算」封裝起來:寫一次、到處呼叫。好處:

  • 消除重複:同樣的邏輯不必複製貼上。
  • 隱藏細節:呼叫者只需知道「做什麼」,不必知道「怎麼做」。
  • 可獨立測試:小函式可以逐一驗證。

K&R 的比喻:有了函式,你可以站在別人的工作上,而不是每次從零開始。

2. 函式定義的解剖

/* 回傳型別  函式名(參數列) */
int power(int base, int n)
{
    int result = 1;                 /* 區域變數:只活在這次呼叫 */
    for (int i = 1; i <= n; i++) {
        result = result * base;
    }
    return result;                  /* 把值帶回給呼叫者 */
}
  • 回傳型別:函式算出來的值的型別;不回傳東西就寫 void。
  • 參數(parameters):函式的「輸入」,每個參數都要寫型別。沒有參數寫 (void)。
  • return:結束函式並帶回值。void 函式可用 return;(不帶值)或執行到結尾自然結束。

3. 原型(prototype):先宣告、後使用

C 由上往下編譯。若呼叫點在定義之前,必須先給編譯器「原型」:

int power(int base, int n);    /* 原型:只有簽名,分號結尾 */

int main(void)
{
    printf("%d\n", power(2, 10));   /* 編譯器靠原型檢查引數型別與個數 */
    return 0;
}

int power(int base, int n)     /* 定義:真正的實作 */
{
    ...
}
  • 原型讓編譯器能檢查呼叫是否正確(引數個數、型別),並自動做型別轉換。
  • 原型中參數名可省略(int power(int, int);),但寫上名字可讀性更好。
  • 多檔案專案中,原型集中放在 .h 標頭檔——這是之後單元的主題。

4. 傳值呼叫(Call by Value)——C 的鐵律

函式收到的是引數的「複本」。在函式內修改參數,完全不影響呼叫端:

void tryToChange(int x)
{
    x = 999;                   /* 改的是複本 */
}

int main(void)
{
    int a = 1;
    tryToChange(a);
    printf("%d\n", a);         /* 仍然是 1 */
    return 0;
}

K&R 指出這是優點:參數可以當成「已初始化好的區域變數」放心使用。 例如 power 可以直接消耗參數 n:

int power(int base, int n)
{
    int p;
    for (p = 1; n > 0; n--)    /* 直接把 n 減到 0,呼叫端不受影響 */
        p = p * base;
    return p;
}

想讓函式修改呼叫端的變數?要傳「位址」(指標)——這是高階單元的主題, 本單元先建立「預設拿不到、改不了」的正確直覺。

5. void 函式與提早 return

void print_banner(const char *title)
{
    if (title == NULL) {
        return;                /* 提早離開:守衛子句(guard clause) */
    }
    printf("=== %s ===\n", title);
}
  • void 函式做「動作」(印東西、改狀態)而不是算「值」。
  • 守衛子句讓「異常情況先離開」,主邏輯不必層層縮排。

6. 函式設計原則

原則 說明
單一職責 一個函式只做一件事。「計算」與「列印」分開寫
好名字 動詞開頭:compute_tax、print_report、is_prime(判斷式用 is/has 開頭)
短小 一個螢幕內能看完;超過就考慮拆分
參數少 超過 4~5 個參數是壞味道
回傳值明確 判斷函式回傳 0/1;可能失敗的函式用回傳值報錯

「單一職責」的實際判斷法:你能不能用一句話(不含「和」「並且」)描述這個函式?

7. 回傳非整數:為什麼原型特別重要(K&R §4.2)

double atof_like(const char *s);   /* 若沒有這行原型…… */

double x = atof_like("3.14");      /* ……舊式 C 會假設回傳 int,值會全毀 */

K&R 時代沒有原型的函式一律假設回傳 int,回傳 double 的函式會拿到垃圾值。 現代 C(C99 起)要求呼叫前必須看得到宣告——永遠先寫原型,這類 bug 就絕跡了。

範例程式

檔案 內容 編譯與執行
01_power_kr.c K&R 經典 power 函式:原型、定義、呼叫三件套,含「消耗參數」版本 cc -std=c11 -Wall -Wextra -o 01_power_kr 01_power_kr.c && ./01_power_kr
02_call_by_value.c 傳值呼叫實證:函式內改參數、swap 失敗示範、參數當區域變數 cc -std=c11 -Wall -Wextra -o 02_call_by_value 02_call_by_value.c && ./02_call_by_value
03_function_design.c 函式設計實戰:單一職責拆分、守衛子句、判斷函式、小工具函式組合 cc -std=c11 -Wall -Wextra -o 03_function_design 03_function_design.c && ./03_function_design

常見錯誤與陷阱

陷阱 1:呼叫點之前沒有原型

int main(void)
{
    double r = half(5);        /* C99 起:錯誤——隱式宣告已被移除 */
    ...
}
double half(int x) { return x / 2.0; }

修正:在檔案頂端加上 double half(int x);,或把定義移到 main 之前。

陷阱 2:以為函式能修改引數(swap 陷阱)

void swap(int a, int b)        /* 天真版 swap */
{
    int tmp = a; a = b; b = tmp;   /* 只交換了複本! */
}

呼叫 swap(x, y) 之後 x、y 完全沒變。正確的 swap 需要指標(高階單元)。

陷阱 3:非 void 函式忘記 return

int max(int a, int b)
{
    if (a > b) {
        return a;
    }
    /* b >= a 時沒有 return:回傳值是垃圾,使用它是未定義行為 */
}

修正:確保每條路徑都有 return。-Wall 會警告 control reaches end of non-void function。

陷阱 4:回傳區域變數的位址

int *make_array(void)
{
    int arr[10];
    return arr;                /* 函式結束後 arr 就消失了:懸空指標 */
}

區域變數存活期只到函式結束。本單元先記住「不要回傳區域變數的位址」,原理在指標單元詳解。

陷阱 5:引數求值順序未定義

printf("%d %d\n", next_id(), next_id());   /* 哪個先被呼叫?沒有規定! */

兩個引數都有副作用時,不同編譯器可能給出不同順序。修正:先存進具名變數再傳入。

練習題

  1. 基本題:寫 int square(int x) 與 int cube(int x),印出 1~10 的平方與立方表。
  2. 溫度轉換函式化(K&R 習題 1-15):把單元 05 的溫度對照表改寫成呼叫 double c_to_f(double c) 函式的版本。
  3. 判斷函式:寫 int is_even(int n)、int is_leap_year(int y),用一組寫死的測試資料驗證,輸出格式為 is_leap_year(1900) = 0。
  4. 負指數的 power:K&R 的 power 不支援負指數。寫 double power_d(double base, int n),讓 n < 0 時回傳 1 / base^(-n),測試 power_d(2.0, -3) 是否為 0.125。
  5. 最大公因數:寫 int gcd(int a, int b)(輾轉相除法:gcd(a, b) = gcd(b, a % b),b == 0 時答案是 a)。先寫迴圈版,行有餘力寫遞迴版。
  6. 挑戰題(重構練習):找一段你以前寫的 30 行以上的 main,把它拆成至少三個「一句話說得清楚」的函式,比較拆分前後的可讀性。

延伸閱讀

  • K&R 2/e §1.7–1.8、§4.1–4.2;習題 1-15、4-1
  • 《The Practice of Programming》(Kernighan & Pike)第 1 章:風格與函式設計
  • 《Code Complete》第 7 章:高品質常式(routine)的特徵
  • 遞迴入門:K&R §4.10(本教程中階單元會深入)
  • 下一單元預告:陣列——一次管理一整排資料