為什麼要把程式拆成一塊一塊
到目前為止,你的程式都是一條長長的故事:宣告變數、用控制流程做決定、稍微迴圈一下、印出結果。十行的程式這樣寫沒問題,但到了兩百行就會崩壞,因為你開始把同樣的五行工作複製到三個不同的地方,當其中一份有錯誤時,另外兩份還是錯的。函式就是解藥:一段有名字、可重複使用的工作,你只寫一次,需要時就用名字呼叫它。
你可以把函式想成一台小機器,有個放輸入的投入口,和一個吐出結果的滑槽。你遞給它一些值,它在關起來的門後做完工作,再遞回一個結果給你。要使用它,你不需要知道它內部怎麼運作——你只需要它的名字,以及它期待收到什麼。其實你一直都在用函式:print 是函式,連你程式的進入點 main() 也是函式。
函式定義的解剖學
一個函式定義有四個部分,一旦你能說出它們的名字,你就能讀懂任何 C 檔裡的任何函式。先是回傳型別(會回傳什麼),接著是名字,然後是括號裡的參數列(要傳進去什麼),最後是大括號裡的本體(實際工作)。下面這個函式回傳兩個整數中較大的那個。
由左讀到右,那行標頭是 `int max_of(int a, int b)`:回傳型別 `int`、名字 `max_of`、兩個參數 `int a` 和 `int b`。大括號裡的本體做實際工作——`if (a > b) return a;` 接著 `return b;`——而呼叫處寫成 `int biggest = max_of(7, 3);`,之後 `biggest` 就裝著 7。括號裡的 `a` 和 `b` 是參數:嶄新的區域變數,只在函式執行期間存在。你在呼叫處寫的 7 和 3 則是引數。
關鍵字 return 同時做兩件事:交回一個值,並立刻結束函式——寫在 return 後面、若被執行到的程式碼都會被跳過。回傳型別是 `void` 的函式什麼都不交回,它純粹是為了效果而被呼叫,例如印東西。一旦你能認出那四個部分——回傳型別、名字、參數、本體——你就能讀懂任何 C 檔裡的任何函式。
引數是被複製進去的:傳值
這是關於 C 函式最重要的一條規則,也是初學者最常絆倒的地方:C 是傳值。當你呼叫 max_of(7, 3) 時,函式拿到的不是你原本的變數,而是它們的值的全新副本。函式對它的參數做什麼,只會改到它自己的私有副本;呼叫處那邊的變數完全不受影響。
把它講具體。假設 `void try_to_change(int n)` 只做一件事 `n = 999;`,而你這樣呼叫它:`int x = 5; try_to_change(x);`。呼叫之後,`x` 仍然是 5——那次賦值落在函式的私有副本上,而那個副本在函式回傳時就被丟掉了。對參數賦值改的是副本,永遠不是呼叫者的變數。
原型:在你說之前先告訴編譯器
C 編譯器由上而下讀你的檔案,只讀一次。如果 main() 在編譯器看到 max_of 的定義之前就呼叫了它,編譯器並不知道 max_of 期待什麼引數、會回傳什麼。解法是函式原型:在任何呼叫之前,寫一行宣告函式形狀的程式碼,把本體換成一個分號。
具體來說,你在檔案上方寫 `int max_of(int a, int b);`——和定義一模一樣的標頭,但結尾是分號而不是本體。如此一來,即使完整定義 `int max_of(int a, int b) { return a > b ? a : b; }` 出現在檔案更下面,`main` 仍能呼叫 `max_of(7, 3)`:原型早就告訴編譯器這個呼叫是合法的。有了原型,呼叫就能寫在定義之前。
原型正是大型程式跨檔案分工的方式:一個檔案列出原型(標頭檔),其他檔案提供本體,每個名字的作用域因而能被乾淨地管理。標頭檔你會在後面的一級正式認識。現在請開著警告編譯——「gcc -Wall main.c」——並把「implicit declaration of function」當成編譯器在提醒你少了原型,而不是可以忽略的雜訊。
遞迴:呼叫自己的函式
因為每次呼叫都拿到自己參數的私有副本,函式甚至可以呼叫自己。這就是遞迴,它在那些能整齊地折疊成自身較小版本的問題上特別出色。n 的階乘等於 n 乘以 (n 減 1) 的階乘——同樣的問題,小一號——一直折到小到答案顯而易見為止。那個顯而易見的情況叫基本情況,忘了它正是遞迴出錯的原因。
- 先寫基本情況:最小的、你能直接說出答案的輸入(這裡是 0 的階乘為 1)。沒有它,函式就永遠停不下來。
- 再寫遞迴情況:用同一個函式作用在更小輸入上的結果,來表達答案(n 乘以 (n 減 1) 的階乘)。
- 確保每次遞迴呼叫都嚴格朝基本情況靠近,否則它會無限遞迴,把呼叫堆疊擠爆。
unsigned long factorial(unsigned n) {
if (n == 0) return 1; /* base case */
return n * factorial(n - 1); /* recursive case */
}main 也只是個函式
你剛學的一切都適用於 main(),也就是系統用來啟動你程式的那個函式。它回傳一個 int——這個整數會成為程式的結束狀態,其中 0 代表成功——而作業系統可以把命令列上的引數交給它。那組引數通常寫成一個數量加上一串字串;那個參數列確切的寫法(包括需要用到雙星號的那一個)放在詞彙表的範例裡,但概念很單純:你在程式名稱後面打的字,會像任何其他函式的參數一樣,作為引數送進 main。
執行「$ ./a.out」,main 就在沒有額外引數的情況下執行;執行「$ ./a.out hello 42」,這兩個字就會出現在 main 裡面供你讀取。這跟 max_of 是同一套機制,只是放大到你的程式與外界之間的邊界。一旦你抓到函式的感覺,程式就不再是一整面程式碼牆,而變成一個由有名字的小塊組成的小社會,每塊做一件事、互相呼叫——這正是你接下來面對結構與標準函式庫時所需要的心態。