# 標頭檔的必要性與其內容

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

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

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

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

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

* 首先，作為辨識碼，需要知道函式的名字。
* 編譯器輸出呼叫函式的組合語言程式，必須把引數照規定的順序放到暫存器中，然後使用`call`指令跳到函式的開頭。引數根據型態不同可能會是整數或浮點數。並且引數的型態或數量不對的時候，必須要能顯示錯誤訊息。所以要知道函式的引數個數和各個引數的型態。
* 不管呼叫的函式做了什麼，對呼叫者來說就只知道其回傳，所以編譯時，呼叫者不需要知道被呼叫者的程式碼。
* `call`所要跳到的位址，雖然在分離編譯時還不知道，但是可以在組合語言程式中，總之先輸出跳到位址0的`call`指令，然後在目標檔中留下「目標檔的第X位元請修正為Y這個函式的位址」的資訊。連結器看到這個訊息，在執行檔佈局規劃好後，把程式的片斷接起來，然後修正跳躍的目的位址（這個步驟稱為「重新定位」（relocate））。因此，分離編譯需要知道函式的名字，但是不需要函式的位址。

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

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

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

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

```c
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`的指令。

{% hint style="info" %}

## 小知識：One-Pass 編譯器與前置宣告（Forward Declaration）

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

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

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