簡單來說,要了解C文件和頭文件(即.h)的區別,首先需要了解編譯器的工作過程。 一般來說,編譯器會做以下幾個過程:
1.預處理階段
2. 詞法分析階段
3、編譯階段,先編譯成純匯編語句,再編譯成與CPU相關的二進制代碼,生成各個目標文件(.obj文件)
4、在連接階段,對每個目標文件中的每段代碼進行絕對地址定位,生成特定平臺相關的可執行文件。 當然也可以用于最后生成純二進制代碼,即去掉文件格式信息。 (生成.exe文件)
編譯器在編譯的時候是以C文件為單位的,也就是說,如果你的工程中沒有C文件,那么你的工程是不會被編譯的,而鏈接器是以目標文件為單位的,它會每個目標文件重新定位函數和變量以生成最終的可執行文件。 PC端的程序開發一般都有一個main函數。 這是每個編譯器的約定。 當然,如果你自己寫鏈接描述文件,你可以使用main函數作為程序入口! ! ! !
(main.c文件目標文件可執行)
有了這些基礎知識,我們言歸正傳,為了生成最終的可執行文件,我們需要一些目標文件,也就是C文件,而這些C文件需要一個main函數作為可執行程序的入口,那么我們將從一個C文件開始,假設這個C文件的內容如下:
#
# “。H”
int main(int argc, char **argv)
{
測試 = 25;
("測試............%d\n",測試);
}
.h頭文件內容如下:
內部測試;
現在以這個例子來說明編譯器的工作:
1.在預處理階段,編譯器以C文件為單位,首先讀取C文件,發現第一句和第二句包含頭文件,就會在所有搜索路徑中搜索這兩個文件,并找到它們,它會處理對應頭文件中的宏、變量、函數聲明、嵌套頭文件包含等,檢測依賴關系,進行宏替換,看是否有重復的定義和聲明,最后將那些文件放入All這些東西被掃描到當前的 C 文件中,形成一個中間“C 文件”
2.在編譯階段,在上一步中,相當于把頭文件中的test變量掃描到一個中間C文件中,那么test變量就變成了這個文件中的全局變量,此時所有的中間C文件的所有變量和函數分配空間,將每個函數編譯成二進制代碼,并根據特定的目標文件格式生成目標文件。 在這種格式的目標文件中,對各個全局變量和函數進行符號描述,并將這些二進制代碼按照一定的編譯標準組織成一個目標文件
3. 在連接階段,根據一些參數將上一步生成的目標文件連接起來,生成最終的可執行文件。 主要任務是將各個目標文件的函數、變量等重新定位,相當于將二進制代碼按照一定的規范組合成一個文件,再回到C文件寫什么的話題頭文件:理論上來說,只要C文件和頭文件里的內容是C語言支持的,不管寫什么都可以,比如你把函數體寫在頭文件里,只要這個頭文件包含在任何C文件中,函數就可以編譯成目標文件的一部分(編譯是基于C文件的,如果不在任何C文件中如果這個頭文件包含在,這段代碼沒用),你可以在C文件中進行函數聲明、變量聲明和結構聲明,這不是問題! ! !
那為什么一定要分頭文件和C文件呢? 為什么函數、變量聲明、宏聲明和結構聲明一般都在頭文件中進行? 而在C文件中定義變量,函數實現呢? ? 原因如下:
1.如果一個函數體是在頭文件中實現的,如果在多個C文件中被引用,同時編譯多個C文件,生成的目標文件會鏈接成一個可執行文件,每次引用this在頭文件的C文件生成的目標文件中,有一段這個函數的代碼。 如果這個函數沒有定義為局部函數,那么在鏈接的時候,會發現多個相同的函數,就會報錯。
2、如果在頭文件中定義了一個全局變量,并為該全局變量賦了初值,那么在引用這個頭文件的多個C文件中,存在同名變量名的副本。 關鍵是給變量賦初值。 因此,編譯器會將這個變量放入DATA段。 最后,在連接階段,DATA段中會出現多個相同的變量。 它不能將這些變量統一為一個變量,即只為這個變量分配一個空間。 而不是多個空格,如果這個變量在頭文件中沒有賦初值,編譯器會把它放在
在BSS段中,鏈接器只會為BSS段中的多個同名變量分配一個存儲空間。
3、如果在一個C文件中聲明了宏、結構體、函數等,那么如果我要在另一個C文件中引用相應的宏和結構體,就必須再做一次重復的工作。 如果我改了一個C文件而忘記改其他C文件中的語句,那問題就大了,程序的邏輯會變得你無法想象。 如果你把這些的東西放在一個頭文件里,你要用它的C文件,只需要引用一個就OK了! ! !這不是很方便嗎? 當你想改變一個語句時,你只需要移動頭文件。
4.在頭文件中聲明結構、函數等。 當你需要把你的代碼打包成一個庫讓別人使用你的代碼,而你又不想把源碼公布出來,別人怎么用你的庫呢? 也就是如何使用你庫中的每一個函數? ? 一種方法是將源代碼公開,其他人可以隨意使用。 另一種是提供頭文件,別人可以從頭文件中讀到你的函數原型,從而知道如何調用你寫的函數,就像你調用函數一樣。 ,里面的參數是什么? ? 你怎么知道? ? 不是看別人頭文件中的相關語句! ! ! 當然,這些東西都成了C標準,即使不看別人的頭文件,也能知道怎么用。
C語言中.c和.h文件的混淆
本質上沒有什么區別。只是籠統地說:.h文件是一個頭文件,里面包含了函數聲明、宏定義、結構體定義等。
.c文件是程序文件,包含函數實現、變量定義等內容。 并且后綴是什么并不重要,但是編譯器會默認對具有某些后綴的文件采取某些操作。 您可以強制編譯器將任何后綴的文件編譯為c文件。
將這兩個文件分開編寫是一種很好的編程風格。
而且,比如我在aaa.h中定義了一個函數聲明,然后我在aaa.h的同級目錄下創建了aaa.c。 這個函數的實現定義在aaa.c中,然后main函數位于.c文件中的#this aaa.h,然后我就可以使用這個函數了。 Main 會在運行時找到這個定義這個函數的 aaa.c 文件。
這是因為:
main函數是標準C/C++程序的入口,編譯器會先找到該函數所在的文件。
假設編譯器在編譯.c(包括main())時,找到mylib.h(里面聲明了函數void test()),那么編譯器就會按照預設的路徑(路徑列表和代碼文件所在的路徑) )尋找同名的實現文件(擴展名.cpp或.c,本例為mylib.c),如果找到,找到其中的函數(本例為void test()),然后繼續編譯; 如果在指定目錄下找不到實現文件,或者在本文件及后續文件中找不到實現代碼,則返回編譯錯誤。 其實這個過程可以“看”成一個文件拼接過程,聲明和實現分別寫在頭文件和C文件中,或者兩者同時寫在頭文件中,理論上是沒有的本質區別。
以上就是所謂的動態方法。
對于靜態方法,基本上所有的C/C++編譯器都支持一種叫做Link的鏈接方法,也就是所謂的靜態鏈接。
這樣,我們所要做的就是編寫包含函數、類等聲明的頭文件(ah、bh、...)及其對應的實現文件(a.cpp、b.cpp、...)。 .), 編譯器會把它編譯成靜態庫文件(a.lib,b.lib,...)。 在后續的代碼復用過程中,我們只需要提供相應的頭文件(.h)和相應的庫文件(.lib),就可以使用過去的代碼了。
與動態方法相比,靜態方法的優勢在于實現代碼的隱蔽性,即C++所提倡的“接口對外,實現代碼不可見”。 有利于庫文件的轉發。
許多人會反對說難題中最難的部分是基本概念,但事實確實如此。 高中學物理的時候,老師講的是概念,概念要搞清楚,難的就變簡單了。 如果你能分析清楚一個物理問題有幾個物理過程,每個過程都遵守那個物理定律(比如動量守恒、牛二定律、能量守恒定律),那么很容易把這個過程的方程按照定律 ,N個過程一定是N個N元方程,問題就很容易解決了。 就算是高中物理競賽題,最難的也無非是:
(一)混淆概念,導致無法分析幾個物理過程,或者某個物理過程所遵循的物理規律;
(2) 有高階方程,方程即使列出也解不出來。 后者已經屬于數學的范疇,所以最難的是要把握清楚的概念;
編程也是如此。 概念清楚的話,基本沒有問題(數學上會比較難,比如算法的選擇,時間空間和效率的權衡,穩定性和資源的平衡)。 然而,要掌握一個清晰的概念卻不是那么容易。 看看下面的例子,看看你是否有一個清晰透徹的理解。
//啊無效 foo(); //ac # "ah" //我的問題出來了:這句話到底有沒有必要?
無效的 foo()
{ ; } //main.c # "ah" int main(int argc, char *argv[]) {foo(); 0; }
對于上面的代碼,請回答三個問題:
. (1)ac中#“啊”這句是多余的嗎?
(2)為什么在xx.c中經常看到對應的xx.h?
(3)如果不是用ac寫的,編譯器會自動把.h文件的內容綁定到同名的.c文件中嗎? (慣于)
(以上3個問題請仔細思考10分鐘,不要急著看下面的解釋。:) 想的越多理解的越深。 )
好了,是時候了! 請忘掉以上3個問題以及你對這三個問題的想法,然后慢慢聽我說。 正確的觀念是:在C編譯器看來,.h和.c都是浮云,改名為.txt或.doc也沒有太大區別。 換句話說,.h和.c之間沒有必然聯系。 一般.h包含定義在同名.c文件中的變量、數組、函數的聲明,以及需要在.c之外使用的聲明。 這個語句有什么用? 只是為了便于參考需要這些聲明的地方。 因為宏#“xx.h”的實際意思是刪除當前行,在當前行位置原封不動的插入xx.h中的內容。 由于我要寫這些函數聲明的地方很多(xx.c中每一個調用函數的地方都必須在使用前聲明),所以使用#"xx.h"宏可以簡化很多行代碼——讓預處理器替換自身。 也就是說,xx.h其實只是調用了xx.c中需要寫函數聲明的地方(可以少寫幾行)。 至于這個.h文件是誰的,是.h還是.c,或者跟這個.h有關系嗎?同名的.c沒有必然關系。
所以你可能會說:嗯? 然后我平時就是想調用xx.c中的某個函數,但是把xx.h文件刪掉了。 宏替換后不是有很多無用的語句嗎? 是的,確實引入了很多垃圾,但是卻省了很多筆墨,整個布局看起來也干凈多了。 這就是魚和熊掌不可兼得的原因。 反正多聲明(.h一般只用來放聲明,不放定義,看我的書《過馬路,左看右看》)無傷大雅,不會影響編譯,何樂而不為呢?
回過頭來看以上三個問題,是不是很容易回答呢? 答:不一定。 這個例子顯然是多余的。 但是如果.c中的函數還需要調用同一個.c中的其他函數,那么這個.c往往會和.h同名,所以不用擔心聲明和調用的順序(C 要求在使用前必須聲明,同名的.h一般會放在.c)的開頭。 許多項目甚至同意將這種寫法作為代碼規范來規范清晰的代碼。
答:1已經回答了。
答:沒有。問這個問題的人肯定是不清楚,或者就是想渾水摸魚。 很煩人的是中國很多考試都有這樣的爛題。 生怕別人概念清楚,肯定會把考生搞糊涂。
搞清楚語法和概念說起來容易做起來難。 有三招:工作不要發呆,抽空多思考,多看書;
讀書要讀好書,問人要問強人。 壞書壞人會給你錯誤的觀念,誤導你;
勤能補拙是一種很好的修養,一分耕耘一分收獲;
(1) 通過頭文件調用庫函數。 在很多場合,源代碼是不方便(或不允許)發布給用戶的,只要提供頭文件和二進制庫給用戶即可。 用戶只需要根據頭文件中的接口聲明調用庫函數即可,無需關心接口是如何實現的。 編譯器從庫中提取相應的代碼。
(2) 頭文件可以加強類型安全檢查。 如果接口的實現或使用方式與頭文件中的聲明不一致,編譯器會指出錯誤。 這個簡單的規則可以大大減輕程序員調試和改錯的負擔。
頭文件用于存儲函數原型。
頭文件與源文件有什么關系?
這道題其實是說已知的頭文件“啊”聲明了一系列函數(只有函數原型,沒有函數實現),而這些函數是在“b.cpp”中實現的,那么如果我要在“c.cpp”中在"ah"中聲明的"b.cpp"中實現的函數通常在"c.cpp"#"ah"中使用,那么c.cpp如何找到b.cpp中的實現呢?
事實上,.cpp 和.h 文件名沒有任何直接關系,許多編譯器可以接受其他擴展名。
譚浩強先生在《C程序設計》一書中提到,編譯器在進行預處理時,需要對#命令進行“文件包含處理”:將.h的全部內容復制到#
“。H”。 這也解釋了為什么很多編譯器不關心這個文件的后綴是什么——因為#是為了完成一個“復制和插入代碼”的工作。
程序編譯時不會在b.cpp文件中尋找函數實現,只有在鏈接時才做這個工作。 我們在b.cpp或c.cpp中使用#"ah"來實際引入相關聲明,這樣編譯就可以通過,程序不關心它是在哪里實現的,是如何實現的。 源文件編譯后,生成目標文件(.o 或.obj 文件)。 在目標文件中,這些函數和變量被視為符號。 鏈接的時候需要指定需要連接哪個.o或者.obj文件(這里是b.cpp生成的.o或者.obj文件),這時候鏈接器會去這個.o或者.obj file 查找b.cpp中實現的函數,并構建到.cpp中指定的可執行文件中。 (很重要)
在VC中,一堆情況不需要你自己寫,你只需要包含所有需要的文件,VC會自動幫你寫。
通常,編譯器會在每個 .o 或 .obj 文件中查找所需的符號,而不是只查找某個文件或找到一個文件就不找。 因此,如果在幾個不同的文件中實現了同一個功能,或者定義了同一個全局變量,鏈接時會提示“”。
最后,無論你是在職場成長還是在大學的入門階段,C/C++都是一門編程語言,不僅可以增強你的思維能力,還可以為編程打下堅實的基礎。 如果你想做軟件開發,成為核心程序員, C/C++是更好的選擇。 作者有一個千人C/C++程序員的Q群(Q船有線:C語言編程學習聚集地(默默建立))如果覺得自學C/C++語言有難度,有興趣的小伙伴學習或者學習C/C++編程的可以進來交流。 下面給大家分享一下C/C++的學習路線圖: