問題引入
假設有一個C/C++語言專案,專案中包含了很多模組,每個模組中又包含了很多功能函式。對於這個專案,稍稍學習過程式設計知識的開發者都會將模組做成動態或者靜態庫。在動態或者靜態庫中,往往包含了很多標頭檔案和原始檔。現在思考一個問題,為什麼需要標頭檔案?似乎從開始學習程式設計開始老師就教導我們要寫標頭檔案和原始檔,但是有沒有認真思考過它們的作用?本篇來通過一個簡單例子來簡要分析一下標頭檔案的作用。
背景介紹
為了簡化問題,我們假設需要開發一個可執行專案,在這個專案中需要一個模組,這個模組中僅僅包含兩個函式,它們的作用就是列印出傳入的引數值。程式碼如下:
1 #include <stdio.h> 2 3 void test(int val) 4 { 5 printf("test : %d\n", val); 6 } 7 8 void fun(int val) 9 { 10 printf("fun : %d\n", val); 11 }
再增加main函式,程式碼如下:
1 int main(int argc, char* argv[]) 2 { 3 test(123); 4 fun(456); 5 return 0; 6 }
不劃分模組
最簡單的情況就是將這個模組和main函式寫在一個檔案中,但是我們都知道這樣做不但模組無法重用,同時也會給後面擴充功能帶來繁重的工作。劃分模組,可以將功能封裝,隱藏實現細節,還可以更好的實現模組複用,並且獨立模組間的低耦合也為了擴充套件升級提供了便利。
將這個原始檔編譯:
gcc -o nomodule main.c
使用標頭檔案
加入標頭檔案test.h,程式碼如下:
1 #ifndef TEST_H_ 2 #define TEST_H_ 3 4 void test(int val); 5 void fun(int val); 6 7 #endif //TEST_H_
修改main.c,程式碼如下:
1 #include "test.h" 2 3 int main(int argc, char* argv[]) 4 { 5 test(123); 6 fun(456); 7 return 0; 8 }
重新編譯
gcc -o header main.c test.c
執行程式,可以看到效果。再來看下標頭檔案的作用,使用gcc -E來檢視預處理的結果
gcc -E test.c -o test.i
檢視test.i,關鍵的程式碼如下:
1 # 1 "./test.h" 1 2 3 void test(int val); 4 void fun(int val);
可以看到#include "test.h"被展開了,它的內容就是我們在標頭檔案中寫的兩個函式宣告。其實,這就是標頭檔案的作用體現了,也就是說在test.c檔案中將標頭檔案中的內容加入了進來。可以再使用上述命令檢視main.c檔案,結果也是一樣的。我們將i檔案編譯成o檔案:
1 gcc -c test.i -o test.o 2 gcc -c main.i -o main.o
然後將o檔案編譯成可執行程式:
gcc -o header main.o test.o
執行與上一節結果是一樣的。你會不會有疑問?難道可以不用標頭檔案?別急,我們繼續。
沒有標頭檔案
是的,這次沒有標頭檔案。
test.c中定義兩個函式,內容省略。
在main.c中,程式碼如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 void test(int); 5 void fun(int); 6 7 int main(int argc, char* argv[]) 8 { 9 test(123); 10 fun(456); 11 return 0; 12 }
將test.c編譯為目標檔案o檔案
gcc -c test.c
再將main.c編譯為目標檔案
gcc -c main.c
將兩個目標檔案連結為可執行程式
gcc -o noheader main.o test.o
執行與前兩節是一樣的。至此,可以完全看明白標頭檔案的作用了,它在預處理期間就被展開了,然後嵌入到原始檔當中。
總結
通過以上三種情況的分析可以看到標頭檔案在編譯連結過程中的作用,從編譯連結的角度來說,不寫標頭檔案也是行得通的,只要你不怕寫N多函式或者類的宣告就行。其實,函式宣告只是在生成目標檔案(*.o)時產生一個符號,具體符號的使用是在連結期間繫結的,而不同的編譯方式(靜態編譯或動態編譯)繫結的時機也不相同,以後再詳細介紹。