標頭檔的必要性與其內容

在分離編譯中,編譯器只看見程式的一部份程式碼,但編譯器並不能編譯任意的程式碼片段。舉例來說,我們考慮底下的程式碼:

void print_bar(struct Foo *obj) {
  printf("%d\n", obj->bar);
}

上述的程式碼中,只要知道Foo的資料結構,就能輸出對應這段程式碼的組合語言指令,但若不知道的話就無法編譯。

分離編譯時,需要把足以編譯每一個C程式檔的資訊,放進每一個檔案裡。不過,如果把別的檔案的程式碼全部寫進去的話,根本就不是分離編譯了,所以得做一定程度的資訊取捨。

舉個例子,我們來考慮要呼叫別的C檔案裡的函式,到底需要什麼樣的資訊吧。編譯器需要以下這些資訊:

  • 首先,作為辨識碼,需要知道函式的名字。

  • 編譯器輸出呼叫函式的組合語言程式,必須把引數照規定的順序放到暫存器中,然後使用call指令跳到函式的開頭。引數根據型態不同可能會是整數或浮點數。並且引數的型態或數量不對的時候,必須要能顯示錯誤訊息。所以要知道函式的引數個數和各個引數的型態。

  • 不管呼叫的函式做了什麼,對呼叫者來說就只知道其回傳,所以編譯時,呼叫者不需要知道被呼叫者的程式碼。

  • call所要跳到的位址,雖然在分離編譯時還不知道,但是可以在組合語言程式中,總之先輸出跳到位址0的call指令,然後在目標檔中留下「目標檔的第X位元請修正為Y這個函式的位址」的資訊。連結器看到這個訊息,在執行檔佈局規劃好後,把程式的片斷接起來,然後修正跳躍的目的位址(這個步驟稱為「重新定位」(relocate))。因此,分離編譯需要知道函式的名字,但是不需要函式的位址。

綜上所述,只要省略函式本體的{...},就有充份的資訊可以呼叫出該函式了。像這樣省去函式本體的就叫「宣告」(declaration)。宣告只告訴編譯器型態和名字,並不包含函式的程式碼。舉例來說底下就是strncmp的宣告:

int strncmp(const char *s1, const char *s2, size_t n);

編譯器只要看到上面這1行,就能知道有strncmp的存在與其型態。反之,包含函式程式碼的就稱為「定義」(definition)。

在函式的宣告前,也可以加上代表宣告的關鍵字extern

extern int strncmp(const char *s1, const char *s2, size_t n);

但是函式的宣告,只要省略函式的本體就足以區分定義和宣告,所以不加上extern也沒問題。

此外,宣告中只要知道引數的型態就可以了,所以宣告中可以省略變數的名字,但是一般來說為了要讓人類容易理解,所以就算是宣告通常也會寫上名字。

另一個例子,我們來看型態為結構的情況。如果有2個以上的C程式檔用到同一個結構的話,需要在每個檔案中都宣告同樣的結構。如果該結構只有一個C程式檔用到的話,就不需要讓其他的C檔案知道該結構。

在C語言中,會把像這類編譯其他C程式檔案需要的宣告做整理,寫進標頭檔(副檔名為 .h)中。在 foo.h 裡寫上宣告,在別的C程式檔需要的它們時寫上#include "foo.h",就可以把#include這行換成 foo.h 裡的內容。

typedef這類用來告訴編譯器型態的語法,如果要在複數個C程式檔中用到,也需要寫進標頭檔中。

編譯器讀到宣告並不會輸出組合語言指令。因為,宣告只是在要用到別的檔案裡的函式或是變數時,所需要的資訊,其本身並不是函式或是變數的定義。

關於分離編譯,談到這邊應該可以知道「使用printf時像咒語一樣寫的#include <stdio.h>」具體來說到底在做什麼了。它會默默地把C標準函式庫丟給連結器,連結器就可以做出連結到包含printf的目標檔的執行檔。同時,編譯器自己其實並不知道關於printf相關的知識。printf並不是內嵌的函式,C語言也沒有自動讀進標準函式庫標頭檔的規則,所以剛啟動的編譯器是完全不知道printf的。在這個狀態下,透過讀進C標準函式庫隨附的標頭檔,編譯器才知道printf的存在和其型態,於是可以編譯出呼叫printf的指令。

小知識:One-Pass 編譯器與前置宣告(Forward Declaration)

C語言中,就算把所有函式寫在同一個檔案裡,有時也需要宣告。C語言的規格中,編譯器並不會一次讀進整個檔案,而是可以從上到下1個1個函式編譯。於是,不論哪個函數,都必須要可以根據到該函數在檔案中的位址為止的資訊進行編譯。所以,如果像要用到在檔案中比較後面定義的函式的話,就必須事先寫好宣告。像這樣的宣告就稱作「前置宣告」。

花點功夫調整函式在檔案中的順序的話,也可以幾乎不用前置宣告,但是如果想寫互相為遞迴的函數時,前置宣告就是必要的。

不讀完整個檔案,而是允許只讀取部份檔案編譯的C語言規格,在主記憶體很小的時代可能有意義,現在看起來可能是相當落後的規格也說不定。如果編譯器稍微聰明一點,寫在同一個檔案中的定義應該可以不寫宣告。但是這個步驟已經是語言規格的一部份,所以得放在心上。

Last updated