轉(zhuǎn)載文章:C++中的頭文件和源文件
一、C++編譯模式
通常咱圆,在一個C++程序中,只包含兩類文件——.cpp文件和.h文件功氨。其中序苏,.cpp文件被稱作C++源文件,里面放的都是C++的源代碼捷凄;而.h文件則被稱作C++頭文件忱详,里面放的也是C++的源代碼。
C+ +語言支持“分別編譯”(separate compilation)跺涤。也就是說匈睁,一個程序所有的內(nèi)容,可以分成不同的部分分別放在不同的.cpp文件里桶错。.cpp文件里的東西都是相對獨立的航唆,在編 譯(compile)時不需要與其他文件互通,只需要在編譯成目標文件后再與其他的目標文件做一次鏈接(link)就行了院刁。比如糯钙,在文件a.cpp中定義 了一個全局函數(shù)“void a() {}”,而在文件b.cpp中需要調(diào)用這個函數(shù)退腥。即使這樣任岸,文件a.cpp和文件b.cpp并不需要相互知道對方的存在,而是可以分別地對它們進行編譯狡刘, 編譯成目標文件之后再鏈接享潜,整個程序就可以運行了。
這是怎么實現(xiàn)的呢颓帝?從寫程序的角度來講米碰,很簡單窝革。在文件b.cpp中,在調(diào)用 “void a()”函數(shù)之前吕座,先聲明一下這個函數(shù)“void a();”虐译,就可以了。這是因為編譯器在編譯b.cpp的時候會生成一個符號表(symbol table)吴趴,像“void a()”這樣的看不到定義的符號漆诽,就會被存放在這個表中。再進行鏈接的時候锣枝,編譯器就會在別的目標文件中去尋找這個符號的定義厢拭。一旦找到了,程序也就可以 順利地生成了撇叁。
注意這里提到了兩個概念供鸠,一個是“定義”,一個是“聲明”陨闹。簡單地說楞捂,“定義”就是把一個符號完完整整地描述出來:它是變 量還是函數(shù),返回什么類型趋厉,需要什么參數(shù)等等寨闹。而“聲明”則只是聲明這個符號的存在,即告訴編譯器君账,這個符號是在其他文件中定義的繁堡,我這里先用著,你鏈接 的時候再到別的地方去找找看它到底是什么吧乡数。定義的時候要按C++語法完整地定義一個符號(變量或者函數(shù))椭蹄,而聲明的時候就只需要寫出這個符號的原型了。 需要注意的是瞳脓,一個符號塑娇,在整個程序中可以被聲明多次,但卻要且僅要被定義一次劫侧。試想,如果一個符號出現(xiàn)了兩種不同的定義哨啃,編譯器該聽誰的烧栋?
這 種機制給C++程序員們帶來了很多好處,同時也引出了一種編寫程序的方法拳球∩笮眨考慮一下,如果有一個很常用的函數(shù)“void f() {}”祝峻,在整個程序中的許多.cpp文件中都會被調(diào)用魔吐,那么扎筒,我們就只需要在一個文件中定義這個函數(shù),而在其他的文件中聲明這個函數(shù)就可以了酬姆。一個函數(shù)還 好對付嗜桌,聲明起來也就一句話。但是辞色,如果函數(shù)多了骨宠,比如是一大堆的數(shù)學(xué)函數(shù),有好幾百個相满,那怎么辦层亿?能保證每個程序員都可以完完全全地把所有函數(shù)的形式都 準確地記下來并寫出來嗎?
二立美、什么是頭文件
很顯然匿又,答案是不可能。但是有一個很簡單地辦法建蹄,可以幫助程序員們省去記住那么多函數(shù)原型的麻煩:我們可以把那幾百個函數(shù)的聲明語句全都先寫好琳省,放在一個文件里,等到程序員需要它們的時候躲撰,就把這些東西全部copy進他的源代碼中针贬。
這 個方法固然可行,但還是太麻煩拢蛋,而且還顯得很笨拙桦他。于是,頭文件便可以發(fā)揮它的作用了谆棱。所謂的頭文件快压,其實它的內(nèi)容跟.cpp文件中的內(nèi)容是一樣的,都是 C++的源代碼垃瞧。但頭文件不用被編譯蔫劣。我們把所有的函數(shù)聲明全部放進一個頭文件中,當某一個.cpp源文件需要它們時个从,它們就可以通過一個宏命令 “#include”包含進這個.cpp文件中脉幢,從而把它們的內(nèi)容合并到.cpp文件中去。當.cpp文件被編譯時嗦锐,這些被包含進去的.h文件的作用便發(fā) 揮了嫌松。
舉一個例子吧,假設(shè)所有的數(shù)學(xué)函數(shù)只有兩個:f1和f2奕污,那么我們把它們的定義放在math.cpp里:
/* math.cpp */
double f1()
{
//do something here....
return;
}
double f2(double a)
{
//do something here...
return a * a;
}
/* end of math.cpp */
并把“這些”函數(shù)的聲明放在一個頭文件math.h中:
/* math.h */
double f1();
double f2(double);
/* end of math.h */
在另一個文件main.cpp中萎羔,我要調(diào)用這兩個函數(shù),那么就只需要把頭文件包含進來:
/* main.cpp */
#include "math.h"
main()
{
int number1 = f1();
int number2 = f2(number1);
}
/* end of main.cpp */
這 樣碳默,便是一個完整的程序了贾陷。需要注意的是缘眶,.h文件不用寫在編譯器的命令之后,但它必須要在編譯器找得到的地方(比如跟main.cpp在一個目錄下)髓废。 main.cpp和math.cpp都可以分別通過編譯巷懈,生成main.o和math.o,然后再把這兩個目標文件進行鏈接瓦哎,程序就可以運行了砸喻。
三、include
#include
是一個來自C語言的宏命令蒋譬,它在編譯器進行編譯之前割岛,即在預(yù)編譯的時候就會起作用。#include的作用是把它后面所寫的那個文件的內(nèi)容犯助,完完整整地癣漆、 一字不改地包含到當前的文件中來。值得一提的是剂买,它本身是沒有其它任何作用與副功能的惠爽,它的作用就是把每一個它出現(xiàn)的地方,替換成它后面所寫的那個文件的 內(nèi)容瞬哼。簡單的文本替換婚肆,別無其他。因此坐慰,main.cpp文件中的第一句(#include "math.h")较性,在編譯之前就會被替換成math.h文件的內(nèi)容。即在編譯過程將要開始的時候结胀,main.cpp的內(nèi)容已經(jīng)發(fā)生了改變:
/* ~main.cpp */
double f1();
double f2(double);
main()
{
int number1 = f1();
int number2 = f2(number1);
}
/* end of ~main.cpp */
不多不少赞咙,剛剛好。同理可知糟港,如果我們除了main.cpp以外攀操,還有其他的很多.cpp文件也用到了f1和f2函數(shù)的話,那么它們也通通只需要在使用這兩個函數(shù)前寫上一句#include "math.h"就行了秸抚。
四速和、頭文件中應(yīng)該寫什么
通 過上面的討論,我們可以了解到耸别,頭文件的作用就是被其他的.cpp包含進去的健芭。它們本身并不參與編譯,但實際上秀姐,它們的內(nèi)容卻在多個.cpp文件中得到了 編譯。通過“定義只能有一次”的規(guī)則若贮,我們很容易可以得出省有,頭文件中應(yīng)該只放變量和函數(shù)的聲明痒留,而不能放它們的定義。因為一個頭文件的內(nèi)容實際上是會被引 入到多個不同的.cpp文件中的蠢沿,并且它們都會被編譯伸头。放聲明當然沒事,如果放了定義舷蟀,那么也就相當于在多個文件中出現(xiàn)了對于一個符號(變量或函數(shù))的定 義恤磷,縱然這些定義都是相同的,但對于編譯器來說野宜,這樣做不合法扫步。
所以,應(yīng)該記住的一點就是匈子,.h頭文件中河胎,只能存在變量或者函數(shù)的聲明, 而不要放定義虎敦。即游岳,只能在頭文件中寫形如:extern int a;和void f();的句子。這些才是聲明其徙。如果寫上int a;或者void f() {}這樣的句子胚迫,那么一旦這個頭文件被兩個或兩個以上的.cpp文件包含的話,編譯器會立馬報錯唾那。(關(guān)于extern访锻,前面有討論過,這里不再討論定義跟 聲明的區(qū)別了通贞。)
但是朗若,這個規(guī)則是有三個例外的。
一昌罩,頭文件中可以寫const對象的定義哭懈。因為全局的const對象默 認是沒有extern的聲明的,所以它只在當前文件中有效茎用。把這樣的對象寫進頭文件中遣总,即使它被包含到其他多個.cpp文件中,這個對象也都只在包含它的 那個文件中有效轨功,對其他文件來說是不可見的旭斥,所以便不會導(dǎo)致多重定義。同時古涧,因為這些.cpp文件中的該對象都是從一個頭文件中包含進去的垂券,這樣也就保證 了這些.cpp文件中的這個const對象的值是相同的,可謂一舉兩得羡滑。同理菇爪,static對象的定義也可以放進頭文件算芯。
二,頭文件中可 以寫內(nèi)聯(lián)函數(shù)(inline)的定義凳宙。因為inline函數(shù)是需要編譯器在遇到它的地方根據(jù)它的定義把它內(nèi)聯(lián)展開的熙揍,而并非是普通函數(shù)那樣可以先聲明再鏈 接的(內(nèi)聯(lián)函數(shù)不會鏈接),所以編譯器就需要在編譯時看到內(nèi)聯(lián)函數(shù)的完整定義才行氏涩。如果內(nèi)聯(lián)函數(shù)像普通函數(shù)一樣只能定義一次的話届囚,這事兒就難辦了。因為在 一個文件中還好是尖,我可以把內(nèi)聯(lián)函數(shù)的定義寫在最開始意系,這樣可以保證后面使用的時候都可以見到定義;但是析砸,如果我在其他的文件中還使用到了這個函數(shù)那怎么辦 呢昔字?這幾乎沒什么太好的解決辦法,因此C++規(guī)定首繁,內(nèi)聯(lián)函數(shù)可以在程序中定義多次作郭,只要內(nèi)聯(lián)函數(shù)在一個.cpp文件中只出現(xiàn)一次,并且在所有的.cpp文 件中弦疮,這個內(nèi)聯(lián)函數(shù)的定義是一樣的夹攒,就能通過編譯。那么顯然胁塞,把內(nèi)聯(lián)函數(shù)的定義放進一個頭文件中是非常明智的做法咏尝。
三,頭文件中可以寫類 (class)的定義啸罢。因為在程序中創(chuàng)建一個類的對象時编检,編譯器只有在這個類的定義完全可見的情況下,才能知道這個類的對象應(yīng)該如何布局扰才,所以允懂,關(guān)于類的 定義的要求,跟內(nèi)聯(lián)函數(shù)是基本一樣的衩匣。所以把類的定義放進頭文件蕾总,在使用到這個類的.cpp文件中去包含這個頭文件,是一個很好的做法琅捏。在這里生百,值得一提 的是,類的定義中包含著數(shù)據(jù)成員和函數(shù)成員柄延。數(shù)據(jù)成員是要等到具體的對象被創(chuàng)建時才會被定義(分配空間)蚀浆,但函數(shù)成員卻是需要在一開始就被定義的,這也就 是我們通常所說的類的實現(xiàn)。一般蜡坊,我們的做法是杠输,把類的定義放在頭文件中赎败,而把函數(shù)成員的實現(xiàn)代碼放在一個.cpp文件中秕衙。這是可以的,也是很好的辦法僵刮。 不過据忘,還有另一種辦法。那就是直接把函數(shù)成員的實現(xiàn)代碼也寫進類定義里面搞糕。在C++的類中勇吊,如果函數(shù)成員在類的定義體中被定義,那么編譯器會視這個函數(shù)為 內(nèi)聯(lián)的窍仰。因此汉规,把函數(shù)成員的定義寫進類定義體,一起放進頭文件中驹吮,是合法的针史。注意一下,如果把函數(shù)成員的定義寫在類定義的頭文件中碟狞,而沒有寫進類定義中啄枕, 這是不合法的,因為這個函數(shù)成員此時就不是內(nèi)聯(lián)的了族沃。一旦頭文件被兩個或兩個以上的.cpp文件包含频祝,這個函數(shù)成員就被重定義了。
五脆淹、頭文件中的保護措施
考 慮一下常空,如果頭文件中只包含聲明語句的話,它被同一個.cpp文件包含再多次都沒問題——因為聲明語句的出現(xiàn)是不受限制的盖溺。然而漓糙,上面討論到的頭文件中的 三個例外也是頭文件很常用的一個用處。那么咐柜,一旦一個頭文件中出現(xiàn)了上面三個例外中的任何一個兼蜈,它再被一個.cpp包含多次的話,問題就大了拙友。因為這三個 例外中的語法元素雖然“可以定義在多個源文件中”为狸,但是“在一個源文件中只能出現(xiàn)一次”。設(shè)想一下遗契,如果a.h中含有類A的定義辐棒,b.h中含有類B的定 義,由于類B的定義依賴了類A,所以b.h中也#include了a.h⊙現(xiàn)在有一個源文件泰涂,它同時用到了類A和類B,于是程序員在這個源文件中既把 a.h包含進來了辐怕,也把b.h包含進來了逼蒙。這時,問題就來了:類A的定義在這個源文件中出現(xiàn)了兩次寄疏!于是整個程序就不能通過編譯了是牢。你也許會認為這是程序 員的失誤——他應(yīng)該知道b.h包含了a.h——但事實上他不應(yīng)該知道。
使用"#define"配合條件編譯可以很好地解決這個問題陕截。在一 個頭文件中驳棱,通過#define定義一個名字,并且通過條件編譯#ifndef...#endif使得編譯器可以根據(jù)這個名字是否被定義农曲,再決定要不要繼 續(xù)編譯該頭文中后續(xù)的內(nèi)容社搅。這個方法雖然簡單,但是寫頭文件時一定記得寫進去乳规。