源檔案如何根據#include來關聯頭檔案
1、系統自帶的頭檔案用尖括号括起來,這樣編譯器會在系統檔案目錄下查找。
2、使用者自定義的檔案用雙引号括起來,編譯器首先會在使用者目錄下查找,然後在到C++安裝目錄(比如VC中可以指定和修改庫檔案查找路徑,Unix和Linux中可以通過環境變量來設定)中查找,最後在系統檔案中查找。
編譯器的工作過程
第一個階段是預處理階段,在正式的編譯階段之前進行。預處理階段将根據已放置在檔案中的預處理指令來修改源檔案的内容。
把你所有引用的頭檔案打開來插入到我們本身的程式中。
預處理階段會把我們程式中的所有宏進行替換掉,我們經常在程式開頭定義一個宏定義,那宏定義的替換就是在預處理階段完成的。
我們在編寫程式的時候常常會寫入一些注釋,對程式并沒有作用,是以在預處理階段程式會把我們寫入的編譯删除掉,機器是看不到我們寫的注釋的。
我們的條件編譯我們常常會寫#ifdef這時候我們不符合條件的那一部分我們機器也是不會看到的,他不會進入到編譯階段。
第二個階段主要完成的任務就是由編譯器來檢查一下你的程式是不是有問題是不是有一些文法錯誤,在vs下編寫程式的時候也會編譯一下看看我們的程式有沒有error或者warning再去執行。當你的程式沒有問題的時候編譯還會把c程式程式設計彙編語言。
第三階段是彙編階段,這一階段就是把我們第二階段生成的彙編代碼變成我們的可執行檔案,也就是把我們的彙編語言變成機器語言
第四個階段是連結,例如,某個源檔案中的函數可能引用了另一個源檔案中定義的某個符号(如變量或者函數調用等);在程式中可能調用了某個庫檔案中的函數,等等。所有的這些問題,都需要經連結程式的處理方能得以解決。
頭檔案和源檔案的差別
頭檔案和源檔案在本質上沒有任何差別。 隻不過一般:字尾為 .h 的檔案是頭檔案,内含函數聲明、宏定義、結構體定義等内容。字尾為 .c 的檔案是源檔案,内含函數實作,變量定義等内容。而且是什麼字尾也沒有關系,隻不過編譯器會預設對某些字尾的檔案采取某些動作。這樣分開寫成兩個檔案是一個良好的程式設計風格。
簡單的說其實要了解C檔案與頭檔案(即.h)有什麼不同之處,首先需要弄明白編譯器的工作過程,一般說來編譯器會做以下幾個過程:
①預處理階段
②詞法與文法分析階段
③ 編譯階段,首先編譯成純彙編語句,再将之彙編成跟CPU相關的二進制機器碼,生成各個目标檔案 (.obj檔案)
④連接配接階段,将各個目标檔案中的各段代碼進行絕對位址定位,生成跟特定平台相關 的可執行檔案,當然,最後還可以用objcopy生成純二進制碼,也就是去掉了檔案格 式信 息。(生成.exe檔案)
編譯器在編譯時是以C檔案為機關進行的,也就是說如果你的項目中一個C檔案都沒有,那麼你的項目将無法編譯,連接配接器是以目标檔案為機關,它将一個或多個目标檔案進行函數與變量的重定位,生成最終的可執行檔案,在PC上的程式開發,一般都有一個main函數,這是各個編譯器的約定。
(main .c檔案 目标檔案obj 可執行檔案exe )
有了這些基礎知識,再言歸正傳,為了生成一個最終的可執行檔案,就需要一些目标檔案,也就是需要C檔案,而這些C檔案中又需要一個main函數作為可執行程式的入口,那麼我們就從一個C檔案入手,假定這個C檔案内容如下:
#include "mytest.h"
int main(int argc,char **argv)
{
test = 25;
printf("test.................%d/n",test);
}
頭檔案内容如下:
int test;
現在以這個例子來講解編譯器的工作:
1.預處理階段:編譯器以C檔案作為一 個單元,首先讀這個C檔案,發現第一句是包含一個頭檔案,就會在所有搜尋路徑中尋找這個檔案,找到之後,就會将相應頭檔案中再去處理宏,變量, 函數聲明,嵌套的頭檔案包含等,檢測依賴關系,進行宏替換,看是否有重複定義與聲明的情況發生,最後将h檔案中所有的内容全部掃描進這個目前的C檔案中,形成一個中間“C檔案”
2.編譯階段,在上一步中相當于将那個頭檔案中的test變量掃描進了一個中間C檔案,那麼test變量就變成了這個檔案中的一個全局變量,此時就将所有這個中間C檔案的所有變量,函數配置設定空間,将各個函數編譯成二進制碼,按照特定目标檔案格式生成目标檔案,在這種格式的目标檔案中進行各個全局變量,函數的符号描述,将這些二進制碼按照一定的标準組織成一個目标檔案。
3.連接配接階段,将上一步成生的各個目标檔案,根據一些參數,連接配接生成最終的可執行檔案,主要的工作就是重定位各個目标檔案的函數,變量等,相當于将個目标檔案中的二進制碼按一定的規範合到一個檔案中。
回到C檔案與頭檔案各寫什麼内容的話題上:
理論上來說C檔案與頭檔案裡的内容,隻要是C語言所支援的,無論寫什麼都可以的,比如你在頭檔案中寫函數體,隻要在任何一個C檔案包含此頭檔案就可以将這個函數編譯成目标檔案的一部分(編譯是以C檔案為機關的,如果不在任何C檔案中包含此頭檔案的話,這段代碼就形同虛設),你可以在C檔案中進行函數聲明,變量聲明,結構體聲明,這也不成問題!!!
那為何一定要分成頭檔案與C檔案呢?為何一般都在頭件中進行函數,變量聲明,宏聲明,結構體聲明 呢?而在C檔案中去進行變量定義,函數實作呢?
那為何一定要分成頭檔案與C檔案呢?為何一般都在頭件中進行函數,變量聲明,宏聲明,結構體聲明 呢?而在C檔案中去進行變量定義,函數實作呢?
原因如下:
1.如果在h檔案中實作一個函數體,那麼如果在多個C檔案中引用它,而且又同時編譯多個C檔案,将其生成的目标檔案連接配接成一個可執行檔案,在每個引用此頭檔案的C檔案所生成的目标檔案中,都有一份這個函數的代碼,如果這段函數又沒有定義成局部函數,那麼在連接配接時,就會發現多個相同的函數,就會報錯。
2.如果在h檔案中定義全局變量,并且将此全局變量賦初值,那麼在多個引用此頭檔案的C檔案中同樣存在相同變量名的拷貝,關鍵是此變量被賦了初值,是以編譯器就會将此變量放入DATA段,最終在連接配接階段,會在DATA段中存在多個相同的變量,它無法将這些變量統一成一個變量,也就是僅為此變量配置設定一個空間,而不是多份空間,假定這個變量在頭檔案沒有賦初值,編譯器就會将之放入 BSS段,連接配接器會對BSS段的多個同名變量僅配置設定一個存儲空間 。
3.如果在C檔案中聲明宏,結構體,函數等,那麼我要在另一個C檔案中引用相應的宏,結構體,就必須再做一次重複的工作,如果我改了一個C檔案中的一個聲明,那麼又忘了改其它C檔案中的聲明,這不就出了大問題了,如果把這些公共的東東放在一個頭檔案中,想用它的C檔案就隻需要引用一個就OK了!!!這樣豈不友善,要改某個聲明的時候,隻需要動一 下頭檔案就行了
4.在頭檔案中聲明結構體,函數等,當你需要将你的代碼封裝成一個庫,讓别人來用你的代碼,你又不想公布源碼,那麼人家如何利用你的庫中的各個函數呢??一種方法是公布源碼,别人想怎麼用就怎麼用,另一種是提供頭檔案,别人從頭檔案中看你的函數原型,這樣人家才知道如何調用你寫的函數,就如同你調用printf函數一樣,裡面的參數是怎樣的??你是怎麼知道的??還不是看人家的頭檔案中的相關聲明 啊!!!
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的過程完全可以“看成”是一個檔案拼接的過程,将聲明和實作分别寫在h檔案及C檔案中,或者将二者同時寫在頭檔案中,理論上沒有本質的差別。以上是所謂動态方式。對于靜态方式,基本所有的C/C++編譯器都支援一種連結方式被稱為Static Link,即所謂靜态連結。在這種方式下,我們所要做的,就是寫出包含函數,類等等聲明的頭檔案(a.h,b.h,…),以及他們對應的實作檔案(a.cpp,b.cpp,…),編譯程式會将其編譯為靜态的庫檔案(a.lib,b.lib,…)。在随後的代碼重用過程中,我們隻需要提供相應的頭檔案(.h)和相應的庫檔案(.lib),就可以使用過去的代碼了。相對動态方式而言,靜态方式的好處是實作代碼的隐蔽性,即C++中提倡的“接口對外,實作代碼不可見”。有利于庫檔案的轉發。
.c檔案和.h檔案的概念與聯系
如果說難題最難的部分是基本概念,程式設計也是如此,如果概念很清晰,那基本上沒什麼難題(會難在數學上,比如算法的選擇、時間空間與效率的取舍、穩定與資源的平衡上)。但是,要掌握清晰的概念也沒那麼容易。比如下面這個例子,看看你有沒有很清晰透徹的認識。
//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 檔案綁定在一起?
正确的概念是:從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中的實作呢?
編譯器預處理時,要對#include指令進行“檔案包含處理”:将頭檔案headfile.h的全部内容複制到#include “headfile.h”處。這也正說明了,為什麼很多編譯器并不care到底這個檔案的字尾名是什麼----因為#include預處理就是完成了一個“複制并插入代碼”的工作。
程式編譯的時候,并不會去找b.cpp檔案中的函數實作,隻有在link的時候才進行這個工作。我們在b.cpp或c.cpp中用#include “a.h”實際上是引入相關聲明,使得編譯可以通過,程式并不關心實作是在哪裡,是怎麼實作的。源檔案編譯後成生成目标檔案(obj檔案),目标檔案中,這些函數和變量就視作一個個符号。在link的時候,需要在makefile裡面說明需要連接配接哪個obj檔案(在這裡是b.cpp生成的.obj檔案),此時,連接配接器會去.obj檔案中找在b.cpp中實作的函數,再把他們build到makefile中指定的那個可以執行檔案中。
在VC中,一般情況下不需要自己寫makefile,隻需要将需要的檔案都包括在project中,VC會自動幫你把makefile寫好。
通常,編譯器會在每個.o或.obj檔案中都去找一下所需要的符号,而不是隻在某個檔案中找或者說找到一個就不找了。是以,如果在幾個不同檔案中實作了同一個函數,或者定義了同一個全局變量,連結的時候就會提示“redefined”.
原文連結:https://blog.csdn.net/qq_30815237/article/details/88948632