學習目標
- 理解函式的三要素:定義(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()); /* 哪個先被呼叫?沒有規定! */
兩個引數都有副作用時,不同編譯器可能給出不同順序。修正:先存進具名變數再傳入。
練習題
- 基本題:寫
int square(int x)與int cube(int x),印出 1~10 的平方與立方表。 - 溫度轉換函式化(K&R 習題 1-15):把單元 05 的溫度對照表改寫成呼叫
double c_to_f(double c)函式的版本。 - 判斷函式:寫
int is_even(int n)、int is_leap_year(int y),用一組寫死的測試資料驗證,輸出格式為is_leap_year(1900) = 0。 - 負指數的 power:K&R 的 power 不支援負指數。寫
double power_d(double base, int n),讓n < 0時回傳1 / base^(-n),測試power_d(2.0, -3)是否為 0.125。 - 最大公因數:寫
int gcd(int a, int b)(輾轉相除法:gcd(a, b) = gcd(b, a % b),b == 0時答案是a)。先寫迴圈版,行有餘力寫遞迴版。 - 挑戰題(重構練習):找一段你以前寫的 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(本教程中階單元會深入)
- 下一單元預告:陣列——一次管理一整排資料