C 語言標頭檔案作用的簡單理解

Undefined443發表於2024-03-15

C 語言是一種先宣告後使用的語言。

舉個例子:

如果你要在 main() 函式里呼叫一個你的函式 foo(),那麼你有兩種寫法:

  1. foo() 的定義寫在 main() 之前。此時 foo() 的宣告和定義是同時發生的:

    int foo() {
        ...
    }
    
    int main() {
        foo();
    }
    
  2. foo() 的定義寫在 main() 之後。此時 foo() 的宣告必須出現在被 main() 呼叫之前:

    int foo();
    
    int main() {
        foo();
    }
    
    int foo() {
        ...
    }
    

實際上,我們只要保證在 foo()main() 呼叫之前宣告 foo() 就好了。無論是寫法 1 還是寫法 2,foo() 的宣告都是在被 main() 呼叫之前發生的。


對於單檔案 C 專案來講這樣還好說。然而當我們專案中的程式碼越來越多之後,把所有程式碼放到單個檔案裡會使我們的專案變得難以維護。這時我們就需要把程式碼拆分,把功能相近的程式碼放到一個檔案裡,功能不同的程式碼分別放到不同的檔案裡。這樣有利於我們後期對專案的維護。

比如說,我可以在 main.c 檔案中只寫 main() 函式,而把我的其他函式寫到一個單獨的檔案 foo.c 中,就像這樣:

// main.c
int main() {
    ...
}
// foo.c
int foo1 {
    ...
}

int foo2 {
    ...
}

然而這裡有一個問題:我們如何在 main() 函式中呼叫 foo.c 檔案中的函式呢?

首先有一個笨方法,就是你在呼叫 main() 函式之前手動加上 foo.c 檔案中函式的宣告:

int foo1();
int foo2();

int main() {
    foo1();
    foo2();
}

然後我們編譯的時候,兩個檔案都要編譯並連結:

cc -c main.c
cc -c foo.c

這將生成目標檔案 main.ofoo.o,接下來我們再對這兩個檔案進行連結:

cc main.o foo.o -o program

這樣就生成了可執行程式 program


如果我們只用到兩個函式,這樣也不算麻煩。然而現實中我們可能要呼叫成百上千個函式,這樣一來這種方法就有些過於麻煩了。

那麼有沒有一種方法能一次宣告所有函式?

首先我們介紹一下 #include 預處理指令。它的功能是將一個檔案的內容插入到這個 #include 指令所在的位置。

那我們只要把 foo.c 檔案的內容插入到 main.c 檔案中 main() 函式之前的位置不就好了?就像這樣:

#include "foo.c"

int main() {
    foo1();
    foo2();
}

我們讓編譯器對 main.c 檔案進行預處理:

cc -E main.c

編譯器輸出的內容是這樣的:

int foo1() {
    ...
}

int foo2() {
    ...
}

int main() {
    foo1();
    foo2()
}

可以看到,#include 指令將 foo.c 檔案的內容原封不動地插入到了 main.c 中。

如果要構建專案,我們只需要編譯 main.c 就夠了。因為預處理階段已經把 foo.c 的內容全部加入到 main.c 中了。

cc main.c -o program

這種方式就類似我們前面提到的方法 1 —— 將函式定義放在 main() 函式之前。

對於小專案來說,這種方式夠用了。然而對於比較大的專案,這種方式有一個顯著的缺點 —— 你會發現這種方式其實還是相當於把所有程式碼寫入到了一個檔案中。對於程式碼量大的專案,編譯一個這樣的檔案可能相當耗時。並且你一旦對專案檔案的任何部分做了改動,都要重新編譯整個專案。顯然這種方式不適合大型專案。

參考我們之前的做法,我們能不能只把函式宣告的部分提取出來,然後把它們 includemain.c 檔案中?這樣 main.c 檔案就只包含其他檔案的宣告部分,而不是全部程式碼。這樣我們在編譯的時候,就可以各個檔案分別編譯。如果其中某個檔案發生了變動,我們只需要重新編譯這個變動的檔案,再重新連結即可。而連結的過程是比較快的。相比重新編譯整個專案,顯然這是更優的選擇。

對於我們的這個例子,我們只需再建立一個 foo.h 檔案,並將 foo.c 檔案中所有函式的宣告提取出來放入其中,這樣我們只需在 main.c 檔案中加入 #include "foo.h" 命令,就可以只將這些函式宣告加入 main.c 檔案,而不是全部程式碼。

我們把這種從原始檔 foo.c 中提取函式宣告組成的檔案 foo.h 叫做標頭檔案(header)。

// main.c
#include "foo.h"

int main() {
    foo1();
    foo2();
}
// foo.h
int foo1();
int foo2();
// foo.c
#include "foo.h"

int foo1() {
    ...
}

int foo2() {
    ...
}

在這裡你看到原始檔 foo.c 也包含了其自身的標頭檔案 foo.h,是因為在實際應用中標頭檔案往往不止包括函式宣告,也包括結構體宣告、常量定義等原始檔也必須用到的資訊。因此在實際應用中原始檔也常常包括其自身的標頭檔案。

此時我們讓編譯器對 main.c 檔案進行預處理:

cc -E main.c

就會看到 main.c 檔案只包含了 foo.c 檔案中函式的宣告:

int foo1();
int foo2();

int main() {
    foo1();
    foo2();
}

讓編譯器對 foo.c 檔案進行預處理:

cc -E main.c

可以看到 foo.c 檔案也包含了自己的函式宣告:

int foo1();
int foo2();

int foo1() {
    ...
}

int foo2() {
    ...
}

如果我們想要構建專案,需要分別編譯 main.cfoo.c,最後再進行連結:

# 編譯
cc -c main.c
cc -c foo.c

# 連結
cc main.o foo.o -o program

實際上,使用標頭檔案的好處遠不止上面提到的這點。因此使用標頭檔案是程式設計中的一個好習慣。

相關文章