轉-C語言中.h和.c檔案解析

來源:互聯網
上載者:User

標籤:color   exe   fine   是什麼   事先   關聯   動作   不為   介面   

C語言中.h和.c檔案解析(很精彩)

    簡單的說其實要理解C檔案與標頭檔(即.h)有什麼不同之處,首先需要弄明白編譯器的工作過程,一般說來編譯器會做以下幾個過程:

      1.預先處理階段

  2.詞法與文法分析階段

  3.編譯階段,首先編譯成純彙編語句,再將之彙編成跟CPU相關的二進位碼,產生各個目標檔案 (.obj檔案)

  4.串連階段,將各個目標檔案中的各段代碼進行絕對位址定位,產生跟特定平台相關的可執行檔,當然,最後還可以用objcopy產生純二進位碼,也就是去掉了檔案格式資訊。(產生.exe檔案)

 

  編譯器在編譯時間是以C檔案為單位進行的,也就是說如果你的項目中一個C檔案都沒有,那麼你的項目將無法編譯,連接器是以目標檔案為單位,它將一個或多個目標檔案進行函數與變數的重定位,產生最終的可執行檔,在PC上的程式開發,一般都有一個main函數,這是各個編譯器的約定,當然,你如果自己寫連接器指令碼的話,可以不用main函數作為程式入口!!!!

  (main .c檔案 目標檔案 可執行檔)

  有了這些基礎知識,再言歸正傳,為了產生一個最終的可執行檔,就需要一些目標檔案,也就是需要C檔案,而這些C檔案中又需要一個main函數作為可執行程式的入口,那麼我們就從一個C檔案入手,假定這個C檔案內容如下:

 

  #include <stdio.h>

  #include "mytest.h"

  int main(int argc,char **argv)

  { 

    test = 25;

    printf("test.................%d\n",test);

  }

  mytest.h標頭檔內容如下:

  int test;

 

  現在以這個例子來講解編譯器的工作:

  1.預先處理階段:編譯器以C檔案作為一個單元,首先讀這個C檔案,發現第一句與第二句是包含一個標頭檔,就會在所有搜尋路徑中尋找這兩個檔案,找到之後,就會將相應標頭檔中再去處理宏,變數,函式宣告,嵌套的標頭檔包含等,檢測依賴關係,進行宏替換,看是否有重複定義與聲明的情況發生,最後將那些檔案中所有的東東全部掃描進這個當前的C檔案中,形成一個中間"C檔案"

  2.編譯階段,在上一步中相當於將那個標頭檔中的test變數掃描進了一個中間C檔案,那麼test變數就變成了這個檔案中的一個全域變數,此時就將所有這個中間C檔案的所有變數,函數分配空間,將各個函數編譯成二進位碼,按照特定目標檔案格式產生目標檔案,在這種格式的目標檔案中進行各個全域變數,函數的符號描述,將這些二進位碼按照一定的標準組織成一個目標檔案

  3.串連階段,將上一步成生的各個目標檔案,根據一些參數,串連產生最終的可執行檔,主要的工作就是重定位各個目標檔案的函數,變數等,相當於將個目標檔案中的二進位碼按一定的規範合到一個檔案中再回到C檔案與標頭檔各寫什麼內容的話題上:理論上來說C檔案與標頭檔裡的內容,只要是C語言所支援的,無論寫什麼都可以的,比如你在標頭檔中寫函數體,只要在任何一個C檔案包含此標頭檔就可以將這個函數編譯成目標檔案的一部分(編譯是以C檔案為單位的,如果不在任何C檔案中包含此標頭檔的話,這段代碼就形同虛設),你可以在C檔案中進行函式宣告,變數聲明,結構體聲明,這也不成問題!!!那為何一定要分成標頭檔與C檔案呢?又為何一般都在頭件中進行函數,變數聲明,宏聲明,結構體聲明呢?而在C檔案中去進行變數定義,函數實現呢??原因如下

  1.如果在標頭檔中實現一個函數體,那麼如果在多個C檔案中引用它,而且又同時編譯多個C檔案,將其產生的目標檔案串連成一個可執行檔,在每個引用此標頭檔的C檔案所產生的目標檔案中,都有一份這個函數的代碼,如果這段函數又沒有定義成局部函數,那麼在串連時,就會發現多個相同的函數,就會報錯 

  2.如果在標頭檔中定義全域變數,並且將此全域變數賦初值,那麼在多個引用此標頭檔的C檔案中同樣存在相同變數名的拷貝,關鍵是此變數被賦了初值,所以編譯器就會將此變數放入DATA段,最終在串連階段,會在DATA段中存在多個相同的變數,它無法將這些變數統一成一個變數,也就是僅為此變數分配一個空間,而不是多份空間,假定這個變數在標頭檔沒有賦初值,編譯器就會將之放入 BSS段,連接器會對BSS段的多個同名變數僅分配一個儲存空間

  3.如果在C檔案中聲明宏,結構體,函數等,那麼我要在另一個C檔案中引用相應的宏,結構體,就必須再做一次重複的工作,如果我改了一個C檔案中的一個聲明,那麼又忘了改其它C檔案中的聲明,這不就出了大問題了,程式的邏輯就變成了你不可想象的了,如果把這些公用的東東放在一個標頭檔中,想用它的C檔案就只需要引用一個就OK了!!!這樣豈不方便,要改某個聲明的時候,只需要動一下標頭檔就行了

  4.在標頭檔中聲明結構體,函數等,當你需要將你的代碼封裝成一個庫,讓別人來用你的代碼,你又不想公布源碼,那麼人家如何利用你的庫呢?也就是如何利用你的庫中的各個函數呢??一種方法是公布源碼,別人想怎麼用就怎麼用,另一種是提供標頭檔,別人從標頭檔中看你的函數原型,這樣人家才知道如何調用你寫的函數,就如同你調用printf函數一樣,裡面的參數是怎樣的??你是怎麼知道的??還不是看人家的標頭檔中的相關聲明啊!!!當然這些東東都成了C標準,就算不看人家的標頭檔,你一樣可以知道怎麼使用

 

  c語言中.c和.h檔案的困惑

  本質上沒有任何區別。 只不過一般:.h檔案是標頭檔,內含函式宣告、宏定義、結構體定義等內容

  .c檔案是程式檔案,內含函數實現,變數定義等內容。而且是什麼尾碼也沒有關係,只不過編譯器會預設對某些尾碼的檔案採取某些動作。你可以強制編譯器把任何尾碼的檔案都當作c檔案來編。

  這樣分開寫成兩個檔案是一個良好的編程風格。

  而且,比方說 我在aaa.h裡定義了一個函數的聲明,然後我在aaa.h的同一個目錄下建立aaa.c ,aaa.c裡定義了這個函數的實現,然後是在main函數所在.c檔案裡#include這個aaa.h 然後我就可以使用這個函數了。 main在運行時就會找到這個定義了這個函數的aaa.c檔案。

  這是因為:

  main函數為標準C/C++的程式入口,編譯器會先找到該函數所在的檔案

  假定編譯器編譯myproj.c(其中含main())時,發現它include了mylib.h(其中聲明了函數void test()),那麼此時編譯器將按照事先設定的路徑(Include路徑列表及代碼檔案所在的路徑)尋找與之同名的實現檔案(副檔名為.cpp或.c,此例中為mylib.c),如果找到該檔案,並在其中找到該函數(此例中為void test())的實現代碼,則繼續編譯;如果在指定目錄找不到實現檔案,或者在該檔案及後續的各include檔案中未找到實現代碼,則返回一個編譯錯誤.其實include的過程完全可以"看成"是一個檔案拼接的過程,將聲明和實現分別寫在標頭檔及C檔案中,或者將二者同時寫在標頭檔中,理論上沒有本質的區別。

 

  以上是所謂動態方式。

  對於靜態方式,基本所有的C/C++編譯器都支援一種連結方式被稱為Static Link,即所謂靜態連結。

  在這種方式下,我們所要做的,就是寫出包含函數,類等等聲明的標頭檔(a.h,b.h,...),以及他們對應的實現檔案(a.cpp,b.cpp,...),編譯器會將其編譯為靜態庫檔案(a.lib,b.lib,...)。在隨後的代碼重用過程中,我們只需要提供相應的標頭檔(.h)和相應的庫檔案(.lib),就可以使用過去的代碼了。

  相對動態方式而言,靜態方式的好處是實現代碼的隱蔽性,即C++中提倡的"介面對外,實現代碼不可見"。有利於庫檔案的轉寄.

 

  如果說難題最難的部分是基本概念,可能很多人都會持反對意見,但實際上也確實如此。我高中的時候學物理,老師抓的重點就是概念--概念一定要搞清,於是難題也成了容易題。如果你能分析清楚一道物理難題存在著幾個物理過程,每一個過程都遵守那一條物理定律(比如動量守恒、牛II定律、能量守恒),那麼就很輕鬆的根據定律列出這個過程的方程,N個過程必定是N個N元方程,難題也就迎刃而解。即便是高中的物理競賽難題,最難之處也不過在於:

  (1)、混淆你的概念,讓你無法分析出幾個物理過程,或某個物理過程遵循的那條物理定律;

  (2)、存在高次方程,列出方程也解不出。而後者已經是數學的範疇了,所以說,最難之處還在於掌握清晰的概念;

  程式設計也是如此,如果概念很清晰,那基本上沒什麼難題(會難在數學上,比如演算法的選擇、時間空間與效率的取捨、穩定與資源的平衡上)。但是,要掌握清晰的概念也沒那麼容易。比如下面這個例子,看看你有沒有很清晰透徹的認識。 //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" 這句話是不是多餘的?

  為什麼經常見 xx.c 裡面 include 對應的 xx.h?

  如果 a.c 中不寫,那麼編譯器是不是會自動把 .h 檔案裡面的東西跟同名的 .c 檔案綁定在一起?(不會)

  (請針對上面3道題仔細考慮10分鐘,莫要著急看下面的解釋。:) 考慮的越多,下面理解的就越深。)

  好了,時間到!請忘掉上面的3道題,以及對這三道題引發出的你的想法,然後再聽我慢慢道來。正確的概念是:從C編譯器角度看,.h和.c皆是浮雲,就是改名為.txt、.doc也沒有大的分別。換句話說,就是.h和.c沒啥必然聯絡。.h中一般放的是同名.c檔案中定義的變數、數組、函數的聲明,需要讓.c外部使用的聲明。這個聲明有啥用?只是讓需要用這些聲明的地方方便引用。因為 #include "xx.h" 這個宏其實際意思就是把當前這一行刪掉,把 xx.h 中的內容原封不動的插入在當前行的位置。由於想寫這些函式宣告的地方非常多(每一個調用 xx.c 中函數的地方,都要在使用前聲明一下子),所以用 #include "xx.h" 這個宏就簡化了許多行代碼--讓前置處理器自己替換好了。也就是說,xx.h 其實只是讓需要寫 xx.c 中函式宣告的地方調用(可以少寫幾行字),至於 include 這個 .h 檔案是誰,是 .h 還是 .c,還是與這個 .h 同名的 .c,都沒有任何必然關係。

  這樣你可能會說:啊?那我平時只想調用 xx.c 中的某個函數,卻 include了 xx.h 檔案,豈不是宏替換後出現了很多無用的聲明?沒錯,確實引入了很多垃圾,但是它卻省了你不少筆墨,並且整個版面也看起來清爽的多。魚與熊掌不可得兼,就是這個道理。反正多些聲明(.h一般只用來放聲明,而放不定義,參見拙著"過馬路,左右看")也無害處,又不會影響編譯,何樂而不為呢?

 

  翻回頭再看上面的3個問題,很好解答了吧?答:不一定。這個例子中顯然是多餘的。但是如果.c中的函數也需要調用同個.c中的其它函數,那麼這個.c往往會include同名的.h,這樣就不需要為聲明和調用順序而發愁了(C語言要求使用之前必須聲明,而include同名.h一般會放在.c的開頭)。有很多工程甚至把這種寫法約定為代碼規範,以規範出清晰的代碼來。

 

  答:1中已經回答過了。

  答:不會。問這個問題的人絕對是概念不清,要不就是想混水摸魚。非常討厭的是中國的很多考試出的都是這種爛題,生怕別人有個清楚的概念了,絕對要把考生搞暈。

  搞清楚文法和概念說易也易,說難也難。竅門有三點: 不要暈著頭工作,要抽空多思考思考,多看看書;

  看書要看好書,問人要問強人。爛書和爛人都會給你一個錯誤的概念,誤導你;

  勤能補拙是良訓,一分辛苦一分才;

 

  (1)通過標頭檔來調用庫功能。在很多場合,原始碼不便(或不準)向使用者公布,只要向使用者提供標頭檔和二進位的庫即可。使用者只需要按照標頭檔中的介面聲明來調用庫功能,而不必關心介面怎麼實現的。編譯器會從庫中提取相應的代碼。

  (2)標頭檔能加強型別安全檢查。如果某個介面被實現或被使用時,其方式與標頭檔中的聲明不一致,編譯器就會指出錯誤,這一簡單的規則能大大減輕程式員調試、改錯的負擔。

  標頭檔用來存放函數原型。

  標頭檔如何來關聯源檔案?

 

  這個問題實際上是說,已知標頭檔"a.h"聲明了一系列函數(僅有函數原型,沒有函數實現),"b.cpp"中實現了這些函數,那麼如果我想在"c.cpp"中使用"a.h"中聲明的這些在"b.cpp"中實現的函數,通常都是在"c.cpp"中使用#include "a.h",那麼c.cpp是怎樣找到b.cpp中的實現呢?

  其實.cpp和.h檔案名稱沒有任何直接關係,很多編譯器都可以接受其他副檔名。

 

  譚浩強老師的《C程式設計》一書中提到,編譯器預先處理時,要對#include命令進行"檔案包含處理":將headfile.h的全部內容複寫到#include "headfile.h"處。這也正說明了,為什麼很多編譯器並不care到底這個檔案的尾碼名是什麼----因為#include預先處理就是完成了一個"複製並插入代碼"的工作。

  程式編譯的時候,並不會去找b.cpp檔案中的函數實現,只有在link的時候才進行這個工作。我們在b.cpp或c.cpp中用#include "a.h"實際上是引入相關聲明,使得編譯可以通過,程式並不關心實現是在哪裡,是怎麼實現的。源檔案編譯後成生了目標檔案(.o或.obj檔案),目標檔案中,這些函數和變數就視作一個個符號。在link的時候,需要在makefile裡面說明需要串連哪個.o或.obj檔案(在這裡是b.cpp產生的.o或.obj檔案),此時,連接器會去這個.o或.obj檔案中找在b.cpp中實現的函數,再把他們build到makefile中指定的那個可以執行檔案中。

 (非常重要)

  在VC中,一幫情況下不需要自己寫makefile,只需要將需要的檔案都包括在project中,VC會自動幫你把makefile寫好。

 

  通常,編譯器會在每個.o或.obj檔案中都去找一下所需要的符號,而不是只在某個檔案中找或者說找到一個就不找了。因此,如果在幾個不同檔案中實現了同一個函數,或者定義了同一個全域變數,連結的時候就會提示"redefined"

轉-C語言中.h和.c檔案解析

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.