頭文件的作用分析
問題引入
假設有一個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)時產生一個符號,具體符號的使用是在鏈接期間綁定的,而不同的編譯方式(靜態編譯或動態編譯)綁定的時機也不相同,以後再詳細介紹。