標籤:
<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"><span style="font-size:18px;">雖然一直在用#include命令包含標頭檔,但其實一致不太明白標頭檔的原理。今天就研究了一下。</span></span>
首先,在大型項目中,僅僅一個源檔案是不夠的,巨大的代碼量需要分別放在幾個檔案中,當然分開存放的更主要的目的是便於模組化。我們把代碼按照不同的功能或作用分隔存放在不同的檔案中,那麼當其中一個功能有改動時,只需要重新編譯相關的檔案,而不必編譯整個項目的所有源檔案。
但是,這樣就帶來了一個問題:在一個檔案中定義的變數或函數,能不能在另一個檔案中使用呢?或者兩個檔案中同名的變數會不會引起衝突呢?
為了回答這個問題,首先要明白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 <stdio.h>#include <math.h>#include <stdlib.h>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,每個檔案只存放一個函數。如下:
#include <stdio.h>#include <math.h>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;}main.c
<span style="font-size:12px;">#include <stdlib.h>int gen_rnd(void){ int ret; time_t t; srand((unsigned)time(&t)); ret = rand() % 3; return ret;}</span>gen_rnd.c
<span style="font-family: Arial, Helvetica, sans-serif;font-size:12px;">#include <stdio.h></span>
<span style="font-size:12px;"></span><pre name="code" class="plain">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必須要自己包含<stdio.h>,否則編譯目標檔案時會報錯:
[email protected]:~/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則要自己包含<stdlib.c>,而main.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()等函數的代碼也會在連結階段被組合到可執行檔中,即所謂的連結庫檔案。
C語言筆記之標頭檔與連結(一)