Skip to content

函數

函數是打造高復用程式的核心。好的函數規劃可以使程式簡潔、易於測試,並讓邏輯像樂高積木一樣好維護。

什麼是函數

你可以把函數想像成工廠裡的「專業員工」。

  • 輸入 (Input):你交給員工的材料(參數)。
  • 處理 (Process):員工根據專業知識進行運算。
  • 輸出 (Output):員工交回給你的成品(回傳值)。
純函數

在函數式程式設計(Functional Programming)中,有一種哲學是「函數不應有副作用」。這意味著相同的輸入永遠會得到相同的輸出,這能極大地減少程式在橫向擴充時的 Bug。

函數宣告與定義

在 C 語言中,編譯器是「由上而下」閱讀程式碼的。如果你在函數還沒出現前就呼叫它,編譯器會報錯。

Forward Declaration (前置宣告)

有時兩個函數會互相呼叫、引用,如果直接定義會出錯,因此需要 foward declaration,先宣告,後定義的模式

Example
c
// 示意程式
void func1(){
    func2(); // func2 還沒有宣告,因此會 compile error
}
void func2(){
    func1();
}
c
void func2(); // forward declaration
void func1(){
    func2();
}
void func2(){
    func1();
}

參數

參數傳遞有很多種類別

  • Pass by Value
  • Pass by Reference
  • Pass by Pointer

由於我們還沒介紹指標(Pointer),目前 C 語言的參數傳遞你可以理解為 「影印」

  • 當你把變數傳進函數時,函數拿到的是一份 副本

  • 你在函數裡對參數做的任何更改,都不會影響到原本的變數。

請注意不要傳遞太大的物件,這會很占用時間

遞迴

遞迴常見於各種程式中,能夠對複雜的任務進行拆解、簡化。它的核心概念是:「要解決這個大問題,我可以先解決一個縮小版的自己。」

經典範例: 取指數

c
int pow(int base, int times) {
    if (times == 0) return 1; // 終止條件:一定要有,否則會變成無窮遞迴
    return base * pow(base, times - 1); // 拆解任務
}
流程講解

如果我呼叫了 pow(3, 3)
首先會計算出 3 * pow(3, 2)
然後是 3 * (3 * pow(3, 1))
最後是 3 * (3 * (3 * pow(3, 0))),pow(3, 0) 為 1,也就得到了3的3次方

雖然這個例子可以使用簡單的迴圈解決,但是在面對更複雜的情境時,迴圈無法如此簡潔的呈現

進階: Stack Frame 記憶體布局

在 C 中,這些函數呼叫都是儲存在 stack 裡面(包含 main),在進行呼叫的時候,可以想像成在 stack 上多開了一塊區域(stack frame),程式控制權轉移到這個地方,並在執行完後將控制權轉移回呼叫者的 frame。

而遞迴呼叫則會在一個 frame 上加一個又一個的 frame,但記憶體是有限的,因此這種疊疊樂不可能無限的進行,太多的話有可能發生 stack overflow。

運作範圍 (Scope)

Scope 在任何程式語言中都是很重要的概念,他保證了變數的「生命週期」與「取用限制」。在 C 語言中,Scope 主要以 大括號 {} 做劃分:

  • 內部可以看到外部:大括號內可以使用外面定義的變數。

  • 外部看不見內部:在大括號內宣告的變數,一旦出了大括號就會被銷毀(釋放記憶體)。

c
int k = 1;
for (int i = 0; i < n; i++) {
    printf("%d\n", k); // 可以找到 k
}
printf("%d\n", i); // i 已經隨著迴圈結束而消失了

遮蔽現象 (Shadowing)

如果在內部與外部使用了一樣的名稱,程式會優先選擇「最接近」的那一個。

c
int i = 1;
int main()
{
    int i = 2;
    for (int j = 0; j < 3; j++) {
        printf("%d\n", i); // 2 2 2
    }
    for (int i = 0; i < 3; i++) {
        printf("%d\n", i); // 0 1 2
    }
    return 0;
}

多檔案編譯時,不同的檔案都是一個獨立的 scope

靜態變數 (Static)

在函數中,一般的區域變數在函數執行完後就會「死掉」,下次呼叫時會重新出生。但如果你希望函數能記住上一次執行的狀態,就可以使用 static

區域靜態變數

加上 static 後,該變數只會被初始化一次,並且在程式執行期間永遠存在。

c
void count_calls() {
    static int counter = 0; // 只在第一次呼叫時初始化
    counter++;
    printf("%d\n", counter);
}

int main() {
    count_calls(); // 1
    count_calls(); // 2
    count_calls(); // 3
}
進階:Static 變數存哪裡?

一般的區域變數存在 Stack (函數結束就回收);但 static 變數存在 Data 段 (跟全域變數住在一起)。

全域靜態變數

當 static 用在「函數外面」或「函數名稱前面」時,它的意義會變成:「這個變數/函數只能在這個檔案內使用」,這會在後面多檔案編譯部份仔細說明。