-
簡單的說其實要理解C文件與頭文件(即.h)有什么不同之處,首先需要弄明白編譯器的工作過程,一般說來編譯器會做以下幾個過程:
Compiler
1.預處理階段
2.詞法與語法分析階段
3.編譯階段,首先編譯成純匯編語句,再將之匯編成跟CPU相關的二進制碼,生成各個目標文件 (.obj文件)
4.連接階段,將各個目標文件中的各段代碼進行絕對地址定位,生成跟特定平臺相關的可執(zhí)行文件,當然,最后還可以用objcopy生成純二進制碼,也就是去掉了文件格式信息。(生成.exe文件)

編譯器在編譯時是以C文件為單位進行的,也就是說如果你的項目中一個C文件都沒有,那么你的項目將無法編譯,連接器是以目標文件為單位,它將一個或多個目標文件進行函數(shù)與變量的重定位,生成最終的可執(zhí)行文件,在PC上的程序開發(fā),一般都有一個main函數(shù),這是各個編譯器的約定,當然,你如果自己寫連接器腳本的話,可以不用main函數(shù)作為程序入口!?。?!
(main .c文件 目標文件 可執(zhí)行文件)
有了這些基礎知識,再言歸正傳,為了生成一個最終的可執(zhí)行文件,就需要一些目標文件,也就是需要C文件,而這些C文件中又需要一個main函數(shù)作為可執(zhí)行程序的入口,那么我們就從一個C文件入手,假定這個C文件內容如下:
#include <stdio.h>
#include "mytest.h"
int main(int argc,char **argv)
{
test = 25;
printf("test.................%d ",test);
}
mytest.h 中的內容如下:
int test;
舉個栗子??講解下編譯器的工作:
1.預處理階段
編譯器以C文件作為一個單元,首先讀這個C文件,發(fā)現(xiàn)第一句與第二句是包含一個頭文件,就會在所有搜索路徑中尋找這兩個文件,找到之后,就會將相應頭文件中再去處理宏,變量,函數(shù)聲明,嵌套的頭文件包含等,檢測依賴關系,進行宏替換,看是否有重復定義與聲明的情況發(fā)生,最后將那些文件中所有的東東全部掃描進這個當前的C文件中,形成一個中間"C文件"。
2.編譯階段
在上一步中相當于將那個頭文件中的test變量掃描進了一個中間C文件,那么test變量就變成了這個文件中的一個全局變量,此時就將所有這個中間C文件的所有變量,函數(shù)分配空間,將各個函數(shù)編譯成二進制碼,按照特定目標文件格式生成目標文件,在這種格式的目標文件中進行各個全局變量,函數(shù)的符號描述,將這些二進制碼按照一定的標準組織成一個目標文件。
3.連接階段
將上一步成生的各個目標文件,根據(jù)一些參數(shù),連接生成最終的可執(zhí)行文件,主要的工作就是重定位各個目標文件的函數(shù),變量等,相當于將個目標文件中的二進制碼按一定的規(guī)范合到一個文件中再回到C文件與頭文件各寫什么內容的話題上:理論上來說C文件與頭文件里的內容,只要是C語言所支持的,無論寫什么都可以的,比如你在頭文件中寫函數(shù)體,只要在任何一個C文件包含此頭文件就可以將這個函數(shù)編譯成目標文件的一部分(編譯是以C文件為單位的,如果不在任何C文件中包含此頭文件的話,這段代碼就形同虛設),你可以在C文件中進行函數(shù)聲明,變量聲明,結構體聲明,這也不成問題!?。?/p>
- 那為何一定要分成頭文件與C文件呢?又為何一般都在頭件中進行函數(shù),變量聲明,宏聲明,結構體聲明呢?而在C文件中去進行變量定義,函數(shù)實現(xiàn)呢??原因如下:
1.如果在頭文件中實現(xiàn)一個函數(shù)體,那么如果在多個C文件中引用它,而且又同時編譯多個C文件,將其生成的目標文件連接成一個可執(zhí)行文件,在每個引用此頭文件的C文件所生成的目標文件中,都有一份這個函數(shù)的代碼,如果這段函數(shù)又沒有定義成局部函數(shù),那么在連接時,就會發(fā)現(xiàn)多個相同的函數(shù),就會報錯。
2.如果在頭文件中定義全局變量,并且將此全局變量賦初值,那么在多個引用此頭文件的C文件中同樣存在相同變量名的拷貝,關鍵是此變量被賦了初值,所以編譯器就會將此變量放入DATA段,最終在連接階段,會在DATA段中存在多個相同的變量,它無法將這些變量統(tǒng)一成一個變量,也就是僅為此變量分配一個空間,而不是多份空間,假定這個變量在頭文件沒有賦初值,編譯器就會將之放入BSS段,連接器會對BSS段的多個同名變量僅分配一個存儲空間。
3.如果在C文件中聲明宏,結構體,函數(shù)等,那么我要在另一個C文件中引用相應的宏,結構體,就必須再做一次重復的工作,如果我改了一個C文件中的一個聲明,那么又忘了改其它C文件中的聲明,這不就出了大問題了,程序的邏輯就變成了你不可想象的了,如果把這些公共的東東放在一個頭文件中,想用它的C文件就只需要引用一個就OK了?。?!這樣豈不方便,要改某個聲明的時候,只需要動一下頭文件就行了。
4.在頭文件中聲明結構體,函數(shù)等,當你需要將你的代碼封裝成一個庫,讓別人來用你的代碼,你又不想公布源碼,那么人家如何利用你的庫呢?也就是如何利用你的庫中的各個函數(shù)呢??一種方法是公布源碼,別人想怎么用就怎么用,另一種是提供頭文件,別人從頭文件中看你的函數(shù)原型,這樣人家才知道如何調用你寫的函數(shù),就如同你調用printf函數(shù)一樣,里面的參數(shù)是怎樣的??你是怎么知道的??還不是看人家的頭文件中的相關聲明?。。?!當然這些東東都成了C標準,就算不看人家的頭文件,你一樣可以知道怎么使用。
c++中 .c 和 .h 的困惑
本質上沒有任何區(qū)別。 只不過一般:
- .h文件是頭文件,內含函數(shù)聲明、宏定義、結構體定義等內容;
- .c文件是程序文件,內含函數(shù)實現(xiàn),變量定義等內容;
文件是什么后綴也沒有關系,只不過編譯器會默認對某些后綴的文件采取某些動作。你可以強制編譯器把任何后綴的文件都當作c文件來編。
在aaa.h里定義了一個函數(shù)的聲明,然后aaa.h的同一個目錄下建立aaa.c,aaa.c里定義了這個函數(shù)的實現(xiàn),然后是在main函數(shù)所在.c文件里#include這個aaa.h 然后我就可以使用這個函數(shù)了。main在運行時就會找到這個定義了這個函數(shù)的aaa.c文件。
這樣分開寫成兩個文件是一個良好的編程風格。
而且,比方說 我在aaa.h里定義了一個函數(shù)的聲明,然后我在aaa.h的同一個目錄下建立aaa.c,aaa.c里定義了這個函數(shù)的實現(xiàn),然后是在main函數(shù)所在.c文件里#include這個aaa.h 然后我就可以使用這個函數(shù)了。main在運行時就會找到這個定義了這個函數(shù)的aaa.c文件。
main函數(shù)為標準C/C++的程序入口,編譯器會先找到該函數(shù)所在的文件。
假定編譯程序編譯myproj.c(其中含main())時,發(fā)現(xiàn)它include了mylib.h(其中聲明了函數(shù)void test()),那么此時編譯器將按照事先設定的路徑(Include路徑列表及代碼文件所在的路徑)查找與之同名的實現(xiàn)文件(擴展名為.cpp或.c,此例中為mylib.c),如果找到該文件,并在其中找到該函數(shù)(此例中為void test())的實現(xiàn)代碼,則繼續(xù)編譯;如果在指定目錄找不到實現(xiàn)文件,或者在該文件及后續(xù)的各include文件中未找到實現(xiàn)代碼,則返回一個編譯錯誤.其實include的過程完全可以"看成"是一個文件拼接的過程,將聲明和實現(xiàn)分別寫在頭文件及C文件中,或者將二者同時寫在頭文件中,理論上沒有本質的區(qū)別。
以上是所謂動態(tài)方式。
對于靜態(tài)方式,基本所有的C/C++編譯器都支持一種鏈接方式被稱為Static Link,即所謂靜態(tài)鏈接。
在這種方式下,我們所要做的,就是寫出包含函數(shù),類等等聲明的頭文件(a.h,b.h,...),以及他們對應的實現(xiàn)文件(a.cpp,b.cpp,...),編譯程序會將其編譯為靜態(tài)的庫文件(a.lib,b.lib,...)。在隨后的代碼重用過程中,我們只需要提供相應的頭文件(.h)和相應的庫文件(.lib),就可以使用過去的代碼了。
相對動態(tài)方式而言,靜態(tài)方式的好處是實現(xiàn)代碼的隱蔽性,即C++中提倡的"接口對外,實現(xiàn)代碼不可見"。有利于庫文件的轉發(fā)。
//a.h
void foo();
//a.c
#include "a.h"
void foo(){
return;
}
//main.c
#include "a.h"
int main(int argc, char *argv[]) {
foo();
return 0;
}
針對上面的代碼,請回答三個問題:
- a.c 中的 #include "a.h" 這句話是不是多余的?
-從C編譯器角度看,.h和.c皆是浮云,就是改名為.txt、.doc也沒有大的分別。換句話說,就是.h和.c沒啥必然聯(lián)系。.h中一般放的是同名.c文件中定義的變量、數(shù)組、函數(shù)的聲明,需要讓.c外部使用的聲明。這個聲明有啥用?只是讓需要用這些聲明的地方方便引用。因為#include "xx.h" 這個宏其實際意思就是把當前這一行刪掉,把 xx.h中的內容原封不動的插入在當前行的位置。由于想寫這些函數(shù)聲明的地方非常多(每一個調用 xx.c 中函數(shù)的地方,都要在使用前聲明一下子),所以用#include "xx.h" 這個宏就簡化了許多行代碼--讓預處理器自己替換好了。也就是說,xx.h 其實只是讓需要寫 xx.c中函數(shù)聲明的地方調用(可以少寫幾行字),至于 include 這個 .h 文件是誰,是 .h 還是 .c,還是與這個 .h 同名的.c,都沒有任何必然關系。
-這樣你可能會說:?。磕俏移綍r只想調用 xx.c 中的某個函數(shù),卻 include了 xx.h文件,豈不是宏替換后出現(xiàn)了很多無用的聲明?沒錯,確實引入了很多垃圾,但是它卻省了你不少筆墨,并且整個版面也看起來清爽的多。魚與熊掌不可得兼,就是這個道理。反正多些聲明(.h一般只用來放聲明,而放不定義,參見拙著"過馬路,左右看")也無害處,又不會影響編譯,何樂而不為呢?
-翻回頭再看上面的3個問題,很好解答了吧?答:不一定。這個例子中顯然是多余的。但是如果.c中的函數(shù)也需要調用同個.c中的其它函數(shù),那么這個.c往往會include同名的.h,這樣就不需要為聲明和調用順序而發(fā)愁了(C語言要求使用之前必須聲明,而include同名.h一般會放在.c的開頭)。有很多工程甚至把這種寫法約定為代碼規(guī)范,以規(guī)范出清晰的代碼來。
- 為什么經常見 xx.c 里面 include 對應的 xx.h?
1)通過頭文件來調用庫功能。在很多場合,源代碼不便(或不準)向用戶公布,只要向用戶提供頭文件和二進制的庫即可。用戶只需要按照頭文件中的接口聲明來調用庫功能,而不必關心接口怎么實現(xiàn)的。編譯器會從庫中提取相應的代碼。
2)頭文件能加強類型安全檢查。如果某個接口被實現(xiàn)或被使用時,其方式與頭文件中的聲明不一致,編譯器就會指出錯誤,這一簡單的規(guī)則能大大減輕程序員調試、改錯的負擔。
頭文件用來存放函數(shù)原型。
- 如果 a.c 中不寫,那么編譯器是不是會自動把 .h 文件里面的東西跟同名的 .c 文件綁定在一起?(不會)
這個問題實際上是說,已知頭文件"a.h"聲明了一系列函數(shù)(僅有函數(shù)原型,沒有函數(shù)實現(xiàn)),"b.cpp"中實現(xiàn)了這些函數(shù),那么如果我想在"c.cpp"中使用"a.h"中聲明的這些在"b.cpp"中實現(xiàn)的函數(shù),通常都是在"c.cpp"中使用#include "a.h",那么c.cpp是怎樣找到b.cpp中的實現(xiàn)呢?
其實.cpp和.h文件名稱沒有任何直接關系,很多編譯器都可以接受其他擴展名。
譚浩強老師的《C程序設計》一書中提到,編譯器預處理時,要對#include命令進行"文件包含處理":將headfile.h的全部內容復制到#include"headfile.h"處。這也正說明了,為什么很多編譯器并不care到底這個文件的后綴名是什么----因為#include預處理就是完成了一個"復制并插入代碼"的工作。
程序編譯的時候,并不會去找b.cpp文件中的函數(shù)實現(xiàn),只有在link的時候才進行這個工作。我們在b.cpp或c.cpp中用#include "a.h"實際上是引入相關聲明,使得編譯可以通過,程序并不關心實現(xiàn)是在哪里,是怎么實現(xiàn)的。源文件編譯后成生了目標文件(.o或.obj文件),目標文件中,這些函數(shù)和變量就視作一個個符號。在link的時候,需要在makefile里面說明需要連接哪個.o或.obj文件(在這里是b.cpp生成的.o或.obj文件),此時,連接器會去這個.o或.obj文件中找在b.cpp中實現(xiàn)的函數(shù),再把他們build到makefile中指定的那個可以執(zhí)行文件中。 (非常重要)
通常,編譯器會在每個.o或.obj文件中都去找一下所需要的符號,而不是只在某個文件中找或者說找到一個就不找了。因此,如果在幾個不同文件中實現(xiàn)了同一個函數(shù),或者定義了同一個全局變量,鏈接的時候就會提示"redefined"。
