1. 程式人生 > >C語言中 .h檔案和.c檔案的區別 (轉)

C語言中 .h檔案和.c檔案的區別 (轉)

要理解.c檔案與.h檔案有什麼不同之處,首先需要弄明白編譯器的工作過程,一般說來編譯器會做以下幾個過程:
1.預處理階段
2.詞法與語法分析階段
3.編譯階段,首先編譯成純彙編語句,再將之彙編成跟CPU相關的二進位制碼,生成各個目標檔案
4.連線階段,將各個目標檔案中的各段程式碼進行絕對地址定位,生成跟特定平臺相關的可執行檔案,當然,最後還可以用objcopy生成純二進位制碼,也就是去掉了檔案格式資訊

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

有了這些基礎知識,再言歸正傳,為了生成一個最終的可執行檔案,就需要一些目標檔案,也就是需要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檔案,發現第一句與第二句是包含.h檔案,就會在所有搜尋路徑中尋找這兩個檔案,找到之後,就 會將相應.h檔案中再去處理巨集,變數,函式宣告,巢狀的.h檔案包含等,檢測依賴關係,進行巨集替換,看是否有重複定義與宣告的情況發生,最後將那些檔案中 所有的東東全部掃描進這個當前的C檔案中,形成一箇中間“C檔案”


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


3.連線階段,將上一步成生的各個目標檔案,根據一些引數,連線生成最終的可執行檔案,主要的工作就是重定位各個目標檔案的函式,變數等,相當於將個目標檔案中的二進位制碼按一定的規範合到一個檔案中

再回到c檔案與h檔案各寫什麼內容的話題上:
理論上來說C檔案與h檔案裡的內容,只要是C語言所支援的,無論寫什麼都可以的,比如你在h檔案中寫函式體,只要在任何一個C檔案包含此.h檔案就可以將 這個函式編譯成目標檔案的一部分(編譯是以C檔案為單位的,如果不在任何C檔案中包含此.h檔案的話,這段程式碼就形同虛設),你可以在C檔案中進行函式聲 明,變數宣告,結構體宣告,這也不成問題!!!那為何一定要分成h檔案與C檔案呢?又為何一般都在h檔案中進行函式,變數宣告,巨集宣告,結構體宣告呢?而 在C檔案中去進行變數定義,函式實現呢??原因如下:


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


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


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


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

例子:
//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;

    針對上面的程式碼,請回答三個問題:
1.a.c 
中的#include "a.h" 這句話是不是多餘的?為什麼經常見xx.c 裡面include 對應的xx.h
2.
如果a.c 中不寫,那麼編譯器是不是會自動把.h 檔案裡面的東西跟同名的.c 檔案繫結在一起?
3.
第三個問題我給他改了一下:如果a.c 中不寫include<>,那麼編譯器是不是會自動把.h 檔案裡面的東西跟同名的.c檔案繫結在一起?

下面是一位牛人的原話:

    從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 中的某個函式,卻includexx.h 檔案,豈不是巨集替換後出現了很多無用的宣告?沒錯,確實引入了很多垃圾,但是它卻省了你不少筆墨,並且整個版面也看起來清爽的多。魚與熊掌不可得兼,就是這個道理。反正多些宣告(.h一般只用來放宣告,而放不定義,參見拙著過馬路,左右看)也無害處,又不會影響編譯,何樂而不為呢?翻回頭再看上面的3個問題,很好解答了吧?

它的解答如下:
1.不一定。這個例子中顯然是多餘的。但是如果.c中的函式也需要呼叫同個.c中的其它函式,那麼這個.c往往會include同名的.h,這樣就不需要為宣告和呼叫順序而發愁了(C語言要求使用之前必須宣告,而include同名.h一般會放在.c的開頭)。有很多工程甚至把這種寫法約定為程式碼規範,以規範出清晰的程式碼來。
2.
答:1中已經回答過了。
3.
答:不會。問這個問題的人絕對是概念不清,要不就是想混水摸魚。非常討厭的是中國的很多考試出的都是這種爛題,生怕別人有個清楚的概念了,絕對要把考生搞暈。
 

補充一下:

      一個程式僅有一個H檔案,沒有C檔案,可以嗎?

      答案是肯定的,不可以!在VC6.0下可以看到編譯按鈕的顏色!變灰,表示無法編譯。

bf096b63f6246b600997c499ebf81a4c500fa245.jpg