h文件和c文件的區別include本身只是一個簡單的文件包含預處理命令,即爲把include的後面文件放到這條命令這裏,除此之外,沒有其它的用處(至少我也樣認爲).

好久沒看C了,本來就忘得一乾二淨的,一臉懵逼的看着zend。

關於.c 和 .h 的區別

  • 子程序不要定義在.h中。函數定義要放在.c中,而.h只做聲明.否則多引用幾次,就會發生函數重複定義的錯誤。
  • .h只做聲明,編譯後不產生代碼
  • 這樣做目的是爲了實現軟件的模塊化,使軟件結構清晰,而且也便於別人使用你寫的程序,純粹用 C 語言語法的角度,你當然可以在 .h 中放任何東西,因爲 #include 完全等價 於把 .h 文件 Ctrl-C Ctrl-V 到 .c 中,.h 中應該都是一些宏定義和變量、函數聲明,告訴別人你的程序“能幹什麼、該怎麼用”..c 中是所有變量和函數的定義,告訴計算機你的程序“該怎麼實現”
  • 當然,如果一個 .h 被多個 .c 包含,而且 .h 中有對象(變量或函數)的定義,就會發生重複定義的錯誤了.聲明可以無窮多次,定義只能一次
  • 一般來說,一個C文件應該是一個模塊,如果你的程序僅僅有一個模塊(僅僅一個C文件),就可以不用建立H文件了。否則你的模塊肯定不是獨立的,你的模塊裏面的實現要被別的模塊調用。這個時候你最好生成一個頭文件(H文件),在頭文件裏面可以聲明你的那些函數是公共的。當別的模塊包含你的頭文件後,就可以使用你的公共聲明瞭。
  • 一個C對應一個H,這樣管理起來方便,比如你有一個"feed_dog.c",那麼就再添加一個"feed_dog.h":
#ifndef _feed_dog_h#define _feed_dog_h
extern void feed_dog(void);
#endif

其實在H文件裏寫函數也無所謂,只是不符合習慣而已。只要按照以上的格式寫,一個H文件添加多少次都無所謂,

  • 只是一種約定,在編譯器裏面,.c和.h是沒有區別的,.c和.h如何使用完全取決於程序員,不過爲了你的程序以後還能看懂而且別人也能看懂,請遵守普遍的約定,這些約定前面的大蝦們已經講了很多了.這個就象汽車在馬路上要靠右行使一樣,是人爲約定,汽車(編譯器)本身並不知道自己是在靠左還是靠右行使.如果你喜歡,還可以用任意後綴命名源文件和頭文件,但這樣幹可能會導致集成編譯和調試環境罷工,你只好自己寫makefile文件了.
  • 爲了生成一個最終的可執行文件,就需要一些目標文件,也就是需要C文件,而這些C文件中又需要一個main.函數作爲可執行程序的入口,那麼我們就從一個C文件入手,假定這個C文件內容如下:
int main(int argc,char **argv) {  test = 25;  printf("test.................%d\n",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標準,就算不看人家的頭文件,你一樣可以知道怎麼使用

程序源碼中".h"文件與".c"文件有什麼區別呀??

在一個程序源碼中,看到了udp.h文件又看到了udp.c文件,不知道這兩者是什麼關係呀?又有何區別呢?

  • .c就是C語言系列的源文件,以文本形式存在,而.h系列則是頭文件,即C系列中存放函數和全局變量的文件,因爲C中的函數是被封裝起來的,即無法看到其代碼.

頭文件與之實現文件的的關係 關於兩者以前的關係,要從N年以前說起了~ long long ago,once aupon a time .......那是一個被遺忘的年代,在編譯器只認識.c(.cpp))文件,而不知道.h是何物的年代。那時的人們寫了很多的.c(.cpp)文件,漸漸地,人們發現在很多.c(.cpp)文件中的聲明語句就是相同的,但他們卻不得不一個字一個字地重複地將這些內容敲入每個.c(.cpp)文件。但更爲恐怖的是,當其中一個聲明有變更時,就需要檢查所有的.c(.cpp)文件,並修改其中的聲明,啊~簡直是世界末日降臨!

終於,有人(或許是一些人)再不能忍受這樣的折磨,他(們)將重複的部分提取出來,放在一個新文件裏,然後在需要的.c(.cpp)文件中敲入#include XXXX這樣的語句。這樣即使某個聲明發生了變更,也再不需要到處尋找與修改了---世界還是那麼美好! 因爲這個新文件,經常被放在.c(.cpp)文件的頭部,所以就給它起名叫做“頭文件”,擴展名是.h. 從此,編譯器(其實是預處理器)就知道世上除了.c(.cpp)文件,還有個.h的文件,以及一個叫做#include命令。

雖然後來又發生很多的變化,但是這樣的用法一直延續至今,只是時日久遠了,人們便淡忘了當年的緣由罷了。

提到了頭文件,就說說它的作用吧~林銳GG寫的高質量C/C++編程上頭文件的作用的簡短描述:

(1)通過頭文件來調用庫功能。在很多場合,源代碼不便(或不準)向用戶公佈,只要向用戶提供頭文件和二進制的庫即可。用戶只需要按照頭文件中的接口聲明來調用庫功能,而不必關心接口怎麼實現的。編譯器會從庫中提取相應的代碼。 (2)頭文件能加強類型安全檢查。如果某個接口被實現或被使用時,其方式與頭文件中的聲明不一致,編譯器就會指出錯誤,這一簡單的規則能大大減輕程序員調試、改錯的負擔。 預處理是編譯器的前驅,作用是把存儲在不同文件裏的程序模塊集成爲一個完整的源程序.

include本身只是一個簡單的文件包含預處理命令,即爲把include的後面文件放到這條命令這裏,除此之外,沒有其它的用處(至少我也樣認爲).

我對乾坤一笑兄的觀點,十分贊同,基礎的東東一定要弄明白.我下面就乾坤一笑兄的例子做講,完備他的一些讓人迷惑不解的時候~ 例子:

//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" 這句話是不是多餘的? 1.爲什麼經常見 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 中的某個函數,卻 include了 xx.h 文件,豈不是宏替換後出現了很多無用的聲明?沒錯,確實引入了很多垃圾 ,但是它卻省了你不少筆墨,並且整個版面也看起來清爽的多。魚與熊掌不可得兼,就是這個道理。反正多些聲明(.h一般只用來放聲明,而放不定義,參見拙著“過馬路,左右看”)也無害處,又不會影響編譯,何樂而不爲呢? 翻回頭再看上面的3個問題,很好解答了吧?

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

在此裏要明確一點,編譯器是按照編譯單元進行編譯的,所謂的編譯單元,是指一個.c文件以及它所include的所有.h文件.最直觀的理解就是一個文件,一個工程中可以包含很多文件,其中有一個程序的入口點,即我們通常所說的main()函數(當然也可以沒有這個函數,程序照樣能啓動,詳細見我的blog中).在沒有這個程序入口點的情況下,編譯單元只生成目標文件object file(.o文件,windows下叫做.obj).

這個例子中總共包含了二個編譯單元,分別是a.c,main.c,按照我所說的,在編譯階段只是生成各自的.o文件.這個階段不和其它的文件發生任何的關係.而include這個預處理指令發生在預處理階段(早先編譯階段,只是編譯器的一個前驅處理程序). .h .c不見得是浮雲,脫離了編譯器談這些沒有任何的意義,拋開更深層次的這些,比如說,OS如何啓動這個文件,PE結構(linux 下爲elf)等等 編譯器首先要識別這個文件纔可能去編譯它,這是前提.如果你改了它的擴展名那麼你的編譯器還能認識它嗎上升到一個更高的層次上看待這個問題,XX兄說的也不錯我想XX兄說的意思就是兩者不可因爲名字相同就認爲兩者有什麼關係,名字是可以隨便的~

兩者之間的聯繫

(拿我舉個例子,一個數據表如果多於30個字段,我就覺得頭大了,現在弄的表有的多達上百個字段,真希望那位高人研究出什麼好的方法來,也讓我們的世界美好一些) 乾坤一笑的第三個問題很有代表性,多次在網上看到,現在的編譯器絕對沒有那麼智能,而且也沒有必須那麼做.下面我們主要聊聊編譯器的處理過程.(我想初學者有疑問的正在於此,即是對於編譯過程.h .c(.cpp)的變化不太瞭解,) 下面我說舉個簡單的例子來聊聊~例子如下:

//a.cpp#include   "a.h"int   A::f(int   t){    return   t;}
//main.cpp#include   "a.h"void   main(){      A   a;      a.f(3);}

在預處理階段,預處理器看到#include”文件名"就把這個文件讀進來,比如它編譯main.cpp,看到#include "a.h",它就把a.h的內容讀進來,它知道了,有一類A,包含一個成員函數f,這個函數接受一個int型的參數,返回一個int型的值。 再往下編譯很容易就把A a這行讀懂了,它知道是要拿A這個類在棧上生成一個對象。 再往下,它知道了下面要調用A的成員函數f了,參數是3,由於它知道這個函數要一個整形數用參數,這個3正好匹配,那就正好把它放到棧上,生成一條調用f(int)函數的指令(一般可能是一句call),至於這個f(int)函數到底在哪裏,它不知道,它留着空,鏈接時再解決。它還知道f(int)函數要返回一個int,所以也許它也爲這一點做好了準備(在例子中,我們沒用這個返回值,也許它就不處理)。 再往下到文件末尾了main.cpp編譯好了,生成了main.obj。整個編譯過程中根本就不需要知道a.cpp的內容。同理,編譯器再編譯a.cpp,把f()函數編譯好,編譯a.cpp時,它也不用管別的,把f()編譯好就行了。生成了a.obj。最後一步就是鏈接的階段了,鏈接器把項目中所有.cpp生成的所有.obj鏈接起來,在這一步中,它就明確了f(int)函數的實現所在的地址,把main.obj中空着的這個地址位置填上正確的地址。最終生成了可執行文件main.exe。

明白了嗎?不明白那就多說幾句了,我們在學編譯原理的時候都知道,編譯器是分階段進行的,每一個階段將源程序從一種表示轉換成另一種表示,一般情況下都進行如下順序:源程序->詞法分器->語法分析器->語義分析器->中間代碼生成器->代碼優化器->代碼生成器->目標程序. 其中這中間6項活動都要涉及的兩項主要活動是:符號管理器與錯誤處理器. 歸根原因,這裏有一個叫做符號表的東東在裏面讓你着魔一樣不明白,其實符號表是一個數據結構.編譯器的基本一項功能就是要記錄源程序中使用的標識符並收集與每個標識符相關的各種屬性信息.屬性信息表明了該標識符的存儲位置/類型/作用域(在那個階段有效)等信息,通俗的說一下就是,當編譯器看到一個符號聲明時,例如你的函數名它就會把它放到這個符號表中去登記一下符號表裏存放着你的函數的入口地址,參數個數,返回信息等等一堆東西而在聯接階段主要是處理工程中的符號表與調用對應處理關係,即我們通常所說的解引用.經過前面的,不知明白與否?

最後引用一下XXX兄的結尾三點:搞清楚語法和概念說易也易,說難也難。竅門有三點:1.不要暈着頭工作,要抽空多思考思考,多看看書; 2.看書要看好書,問人要問強人。爛書和爛人都會給你一個錯誤的概念,誤導你; 3.勤能補拙是良訓,一分辛苦一分才;

如果認爲.c和.h文件是僅僅名字不一樣難免理解得膚淺了點.有op的歷史看來,語言的發展是趨向與oop..h文件的出現.有點類的性質在裏邊..h文件的隱蔽性好.這個道理不難發現.只要大開c自己的.h文件看看,就很明顯了.所以,我同意XXX兄認爲乾坤一笑的膚淺. 但是,從另外一個方面看.: (至於編譯器的實現.我還沒了解.不過.我相信.象)

```這樣的程序不會出現吧....呵呵.所以現在的人要理解.h和.c簡單化.也有點歷史和時代的影響.
 現在總結一下:
1.頭文件可以預先告訴編譯器一些必要的聲明,讓編譯器順利進行下去,在連接實現以前.未必出現實際的定義. 頭文件的意義在 a.使得程序簡明,清晰. b.避免了重複編寫相同的聲明代碼. 2.**.c和**.h文件沒有必然的聯繫.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章