頭文件的作用分析

問題引入

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