標頭檔與原始檔(header files vs source files)
一個 C 專案被切成兩種扮演非常不同角色的檔案。原始檔(以 .c 結尾)裝真正的程式碼:函式的本體、變數的儲存空間、真正的工作。標頭檔(以 .h 結尾)裝宣告:簡短的承諾,說明什麼東西存在,例如函式的名稱、它的參數型別、它回傳什麼,但不說它如何做事。標頭檔是菜單;原始檔是廚房。
這種切分之所以存在,是因為分離編譯與編譯單元的運作方式。每個 .c 檔都是孤立地、各自編譯,對其他檔案一無所知。所以當 main.c 想呼叫住在 utils.c 裡的函式 add 時,main.c 看不到 utils.c 的程式碼。取而代之,你把 add 的宣告(函式原型,int add(int, int);)放進標頭檔 utils.h,並讓 main.c 寫 #include "utils.h"。現在編譯器在單獨處理 main.c 時,握有一個承諾說 add 以那種形狀存在,足以產生正確的呼叫。utils.c 裡的真正程式碼,稍後由連結器配對。
指導原則是:標頭檔承載宣告(介面),原始檔承載定義(實作)。標頭檔一般應放函式原型、型別定義與 extern 宣告,全部包在引入防護裡,好讓它能被安全地貼入多次。它不該含函式本體或一般變數的定義,因為標頭檔會被引入許多編譯單元,而被貼進其中多個的定義會產生「multiple definition」連結錯誤。保持這種切分乾淨,能讓一個標頭檔成為許多原始檔共同遵守的單一共享契約。
/* utils.h -- 介面(只有宣告) */ #ifndef UTILS_H #define UTILS_H int add(int a, int b); /* 一個承諾:add 存在 */ #endif /* utils.c -- 實作(真正的定義) */ #include "utils.h" int add(int a, int b) { return a + b; }
標頭檔宣告 add 供大家共享;恰好一個原始檔定義它。
絕不要把函式本體或一般全域變數的定義放進被好幾個檔案引入的標頭檔;把同一個定義貼進許多編譯單元會造成「multiple definition」連結錯誤。