雖然一直在用#include命令包含頭文件,但其實一致不太明白頭文件的原理。今天就研究了一下。
首先,在大型項目中,僅僅一個源文件是不夠的,巨大的代碼量需要分別放在幾個文件中,當然分開存放的更主要的目的是便於模塊化。我們把代碼按照不同的功能或作用分隔存放在不同的文件中,那麼當其中一個功能有改動時,只需要重新編譯相關的文件,而不必編譯整個項目的所有源文件。
但是,這樣就帶來了一個問題:在一個文件中定義的變量或函數,能不能在另一個文件中使用呢?或者兩個文件中同名的變量會不會引起沖突呢?
為了回答這個問題,首先要明白C語言的源代碼如何一步步生成可執行代碼的。我們先看只有一個源文件的情況:
首先經過預處理器,替換掉文件中的宏命令;
然後經過編譯器,生成匯編代碼;
接著是匯編器,生成二進制的目標代碼,然而此時的目標代碼仍然不能執行,它還缺少啟動代碼(程序和操作系統之間的接口)和庫代碼(比如printf函數的實體代碼);
最後經過鏈接器,鏈接相關的代碼,生成最終的可執行代碼。
既然提到了編譯,就不得不介紹一下C語言的編譯器gcc,假設我們寫好了一個源文件first.c,那麼對應上面的步驟,gcc的命令參數如下:
預編譯: gcc -E first.c -o first_1.c (注:-o 選項用來指定生成結果的文件名)
匯編: gcc -S first.c -o first.s
編譯: gcc -c first.s -o first.o (也可以直接編譯源碼:gcc -c first.c -o first.o)
可執行: gcc first.o -o first (當然,這裡也可以一步到位:gcc first.c -o first)
現在我們把目光集中到鏈接過程上。從上面的分析可以知道,所謂鏈接,就是把目標代碼、啟動代碼和庫代碼結合到一起形成可執行代碼。上面是只有一個源文件的情況,如果有多個文件,則把多個目標代碼與啟動代碼和庫代碼粘合在一起。那麼問題來了:多個目標代碼真的就能隨隨便便粘合在一起嗎?
要回答這個問題,還是得回到對源代碼的分析上,畢竟目標代碼只是源代碼的編譯版本。雖然源代碼被分隔成幾個部分並存放到不同的文件中,但是在邏輯或者上下文中,還是必須要保持一致的。也就是說,把幾個文件中的代碼重新放回到一個文件中,它們還是要保持“兼容”的,比如變量啊、函數啊之類的,不能重復;再比如只能有一個main函數。
然而,我們知道,變量和函數的作用域,最大的也就是文件作用域了。???比如,如何保證一個文件中的變量也被其他的文件直接使用並且不會引起沖突呢?答案就是頭文件。頭文件,就是把各個被分割的文件在邏輯上串起來的關鍵。
現在給出一個例子,在這個例子中,我用C代碼模仿游戲“石頭剪子布”,0、1、2分別代表石頭、剪子、布。游戲過程中,程序隨機產生一個數,同時提示用戶輸入一個數,然後根據規則做出輸贏判斷。完整的代碼如下:
#include#include #include int gen_rnd(void); void judge(int, int); int main(void) { int user, computer; printf("Please input a number, 0 for stone, 1 for scissors, 2 for cloth and q for quit: "); while(scanf("%d", &user) && user != 'q') { if(user > 2 || user < 0) { printf("Please input a number between 0 and 2: "); continue; } computer = gen_rnd(); judge(user, computer); printf("number: "); } return 0; } int gen_rnd(void) { int ret; time_t t; srand((unsigned)time(&t)); ret = rand() % 3; return ret; } void judge(int user, int computer) { char *name[] = {"stone", "scissors", "cloth"}; int res = abs(user - computer); if(res == 0) printf("The computer is %s and you are %s, even\n", name[computer], name[user]); else if(res == 1) { if(user < computer) printf("The computer is %s and you are %s, you win\n", name[computer], name[user]); else printf("The computer is %s and you are %s, you lose\n", name[computer], name[user]); } else { if(user < computer) printf("The computer is %s and you are %s, you lose\n", name[computer], name[user]); else printf("The computer is %s and you are %s, you win\n", name[computer], name[user]); } }
file.c
源碼中有三個函數,分別代表不同的功能:main是主函數;gen_rnd()產生隨機數用來模擬電腦;judge()用來判斷輸贏。每個函數就是一個功能模塊,現在我們把這個文件分割成三個,分別是main.c gen_rnd.c judge.c,每個文件只存放一個函數。如下:
#includemain.c#include int gen_rnd(void); void judge(int, int); int main(void) { int user, computer; printf("Please input a number, 0 for stone, 1 for scissors, 2 for cloth and q for quit: "); while(scanf("%d", &user) && user != 'q') { if(user > 2 || user < 0) { printf("Please input a number between 0 and 2: "); continue; } computer = gen_rnd(); judge(user, computer); printf("number: "); } return 0; }
#includegen_rnd.cint gen_rnd(void) { int ret; time_t t; srand((unsigned)time(&t)); ret = rand() % 3; return ret; }
#include
void judge(int user, int computer) { char *name[] = {"stone", "scissors", "cloth"}; int res = abs(user - computer); if(res == 0) printf("The computer is %s and you are %s, even\n", name[computer], name[user]); else if(res == 1) { if(user < computer) printf("The computer is %s and you are %s, you win\n", name[computer], name[user]); else printf("The computer is %s and you are %s, you lose\n", name[computer], name[user]); } else { if(user < computer) printf("The computer is %s and you are %s, you lose\n", name[computer], name[user]); else printf("The computer is %s and you are %s, you win\n", name[computer], name[user]); } }
judge.c
可以看到,由於成為了單獨的文件,judge.c必須要自己包含
m@sys:~/program/C_codes$ gcc -c judge.c judge.c: In function ‘judge’: judge.c:8:9: warning: incompatible implicit declaration of built-in function ‘printf’ [enabled by default] printf("The computer is %s and you are %s, even\n", name[computer], name[user]); ^同樣的道理,gen_rnd.c則要自己包含
gcc -c judge.c main.c gen_rnd.c
這會在當前目錄下自動生成gen_rnd.o judge.o main.o
接著就可以生成可執行文件了:gcc gen_rnd.o judge.o main.o -o exe
這三個目標文件之所以還能被正確的粘合在一起,是因為它們仍然存在著邏輯上的聯系:首先,只有main.c文件有一個main函數,這就提供了正確的入口;其次,各個文件都能包含需要的頭文件,從而正確的生成各自的目標代碼;再次,因為main.c要調用另外兩個函數,所以聲明了另外兩個函數的原型,雖然該文件中沒有它們的代碼,但是在鏈接階段兩個函數的代碼卻會一起組合到可執行文件中,同樣的道理,printf()等函數的代碼也會在鏈接階段被組合到可執行文件中,即所謂的鏈接庫文件。