頭文件和Include: Why and How
簡(jiǎn)介
這篇文件介紹了一個(gè)常見的新手問(wèn)題:如何理解#include, 頭文件和源文件的關(guān)系。
為什么需要頭文件
如果你剛寫C++, 你可能會(huì)問(wèn)為什么需要#include文件综慎,為什么需要多個(gè).cpp文件聚蝶?原因很簡(jiǎn)單:
-
可以提升編譯速度候齿。當(dāng)你的程序和代碼越來(lái)越大靶草,如果所有的東西都放到一個(gè)源文件中,即使你只做了一個(gè)小小的修改诊霹,所有的東西都要重新編譯幢泼。對(duì)規(guī)模比較小的程序這可能不是個(gè)問(wèn)題,但是對(duì)規(guī)模較大的程序洁仗,編譯一次可能會(huì)耗費(fèi)好幾分鐘层皱。你能想象到每次小的修改都要等一段很長(zhǎng)時(shí)間的情景嗎?
編譯 -> 等8分鐘 -> "我去赠潦,忘了個(gè)分號(hào)" -> 編譯 -> 等8分鐘 -> 調(diào)試 -> 編譯 -> 等8分鐘
可以讓你的代碼組織得更合理叫胖。它把不同的概念放到不同的文件中,當(dāng)你要做修改時(shí)很容易找到對(duì)應(yīng)的代碼她奥。
允許你將接口和實(shí)現(xiàn)相分離瓮增。沒(méi)看懂沒(méi)關(guān)系,后面會(huì)講哩俭。
C++程序的構(gòu)建分為2個(gè)階段绷跑。第一,每個(gè)源文件被分別獨(dú)立編譯凡资。編譯器為每個(gè)源文件產(chǎn)生中間結(jié)果砸捏,這些中間結(jié)果叫做目標(biāo)文件. 所有這些源文件被分別編譯完成后,最終被鏈接到一起,產(chǎn)生最終的二進(jìn)制文件(可執(zhí)行程序).
這意味著每個(gè)文件都是和其他文件獨(dú)立分開編譯的. 結(jié)果就是編譯的時(shí)候a.cpp
對(duì)b.cpp
中的內(nèi)容一無(wú)所知带膜,下面是個(gè)例子:
// in myclass.cpp
class MyClass
{
public:
void foo();
int bar;
};
void MyClass::foo()
{
// do stuff
}
// in main.cpp
int main()
{
MyClass a; // Compiler error: 'MyClass' is unidentified
return 0;
}
雖然MyClass
在myclass.cpp
中聲明了吩谦,但沒(méi)在main.cpp
中聲明,編譯main.cpp
會(huì)發(fā)生錯(cuò)誤.
這時(shí)候頭文件就有用了膝藕。頭文件允許你將接口(這里的MyClass
)對(duì)其他源文件可見式廷,但將實(shí)現(xiàn)(這里的MyClass
成員函數(shù)體)放到你自己的.cpp
文件中,如下:
// in myclass.h
class MyClass
{
public:
void foo();
int bar;
};
// in myclass.cpp
#include "myclass.h"
void MyClass::foo()
{
}
//in main.cpp
#include "myclass.h" // defines MyClass
int main()
{
MyClass a; // no longer produces an error, because MyClass is defined
return 0;
}
#include
語(yǔ)句就像做拷貝/粘貼動(dòng)作芭挽。編譯器在編譯文件時(shí)會(huì)將#include這一行替換成所包含文件的內(nèi)容滑废。
.h/.cpp/.hpp/.cc等的區(qū)別
所有這些文件本質(zhì)上都是文本文件,但是不同類型的文件應(yīng)該有不同的擴(kuò)展后綴:
- 頭文件應(yīng)該有.h類的擴(kuò)展后綴(.h / .hpp / .hxx);
- C++源文件應(yīng)該使用.c類的擴(kuò)展后綴(.cpp / .cxx / .cc)袜爪;
- C源文件只應(yīng)該用.c類型.
C++和C文件做區(qū)分是因?yàn)閷?duì)一些編譯器這兩種是不同的蠕趁。
那頭文件和源文件的區(qū)別是什么?一般來(lái)說(shuō)辛馆,頭文件是被包含的俺陋,但不會(huì)被編譯;源文件會(huì)被編譯昙篙,但不會(huì)被包含腊状。
有時(shí)候(但很少很少發(fā)生)也會(huì)包含源文件,比如實(shí)例化模板苔可〗赏冢總之記住:不要包含源文件焚辅!
頭文件防護(hù)
如果你把一個(gè)文件包含了不止一次映屋,會(huì)出現(xiàn)讓人抓狂的錯(cuò)誤:
// myclass.h
class MyClass
{
void DoSomething() { }
};
// main.cpp
#include "myclass.h" // define MyClass
#include "myclass.h" // Compiler error - MyClass already defined
你可能會(huì)說(shuō),“我怎么可能把同一個(gè)文件包含2次呢同蜻?”棚点。像上面的情形可能不太會(huì)發(fā)生,但下面的情形可能經(jīng)常出現(xiàn):
// x.h
class X { };
// a.h
#include "x.h"
class A { X x; }
// b.h
#include "x.h"
class B { X x; };
// main.cpp
#include "a.h" // also includes "x.h"
#include "b.h" // includes x.h again! ERROR
有些人可能會(huì)告訴你別在頭文件中放#include語(yǔ)句湾蔓,被聽他們的瘫析。在頭文件中放#include語(yǔ)句沒(méi)什么問(wèn)題,只要你處理好如下兩個(gè)問(wèn)題:
- 只#include你真正需要包含的東西(下一節(jié)會(huì)講)
- 在多次包含時(shí)添加頭文件防護(hù)卵蛉。
頭文件防護(hù)是在文件頭部通過(guò)#define定義一個(gè)唯一標(biāo)識(shí)符的技巧,如下:
//x.h
#ifndef __X_H_INCLUDED__ // if x.h hasn't been included yet...
#define __X_H_INCLUDED__ // #define this so the compiler knows it has been included
class X { };
#endif
在x.h第一次被包含時(shí)么库,定義了__X_H_INCLUDED__
這個(gè)宏傻丝;當(dāng)x.h再次被包含時(shí),會(huì)檢查失敗诉儒,x.h就不會(huì)被重復(fù)包含了葡缰。
記住,總是要對(duì)頭文件添加防護(hù)!
為什么不防護(hù)你的.cpp文件呢泛释?因?yàn)槟憔筒粫?huì)包含.cpp文件滤愕。
正確的包含方式
你創(chuàng)造的類經(jīng)常會(huì)依賴其他類。比如怜校,子類總是依賴它的父類间影,因?yàn)橐粋€(gè)類要從父類繼承的話,在編譯期間就要了解其父類茄茁。
有兩種依賴你需要了解:
- 可以被前向聲明的依賴
- 需要被#include的依賴
比如魂贬,類A使用類B,那么類B就是類A的一個(gè)依賴裙顽。是否可以前向聲明付燥,或需要被包含,取決于類A如何使用類B:
- 什么都不做:A和B沒(méi)有任何關(guān)系愈犹;
- 什么都不做:對(duì)B的引用是在一個(gè)友元聲明里键科;
- 前向聲明B:A包含了一個(gè)B的指針或引用,B* myb漩怎;
- 前向聲明B:一個(gè)或多個(gè)函數(shù)有一個(gè)B的對(duì)象/指針/引用作為參數(shù)或返回值勋颖, B MyFunction(B myb);
-
#include "b.h"
: B是A的父類 -
#include "b.h"
: A包含B的對(duì)象,B myb
要盡量選簡(jiǎn)單的選擇扬卷,優(yōu)先什么也不做牙言,其此是前向聲明,最后再#include頭文件怪得。
理想情況下咱枉,類的依賴應(yīng)該放到頭文件中,下面是一個(gè)“正確”的頭文件例子:
//=================================
// include guard
#ifndef __MYCLASS_H_INCLUDED__
#define __MYCLASS_H_INCLUDED__
//=================================
// forward declared dependencies
class Foo;
class Bar;
//=================================
// included dependencies
#include <vector>
#include "parent.h"
//=================================
// the actual class
class MyClass : public Parent // Parent object, so #include "parent.h"
{
public:
std::vector<int> avector; // vector object, so #include <vector>
Foo* foo; // Foo pointer, so forward declare Foo
void Func(Bar& bar); // Bar reference, so forward declare Bar
friend class MyFriend; // friend declaration is not a dependency
// don't do anything about MyFriend
};
#endif // __MYCLASS_H_INCLUDED__
上面的例子展示了兩類不同的依賴以及如何處理它們徒恋。因?yàn)镸yClass只使用了Foo的指針而沒(méi)有使用Foo對(duì)象蚕断,所有我們可以前向聲明Foo, 而不需要#include "foo.h". 盡量使用前向聲明,不需要時(shí)就不要#include.多余的#include會(huì)引入問(wèn)題入挣。
為什么這是正確的包含方法
總的觀點(diǎn)就是使"myclass.h"自包含亿乳,不需要其他程序了解MyClass內(nèi)部的工作。如果其他類要使用MyClass, 它直接#include "myclass.h"就夠了径筏。
另外的某某方法會(huì)要求你在#include "myclass.h"之前先#include MyClass所有的依賴葛假,因?yàn)閙yclass.h不能自己包含它的全部依賴。這讓人頭疼滋恬,因?yàn)槭褂眠@個(gè)類很不直觀聊训。
這個(gè)例子展示了一個(gè)好的方法:
//example.cpp
// I want to use MyClass
#include "myclass.h" // will always work, no matter what MyClass looks like.
// You're done
// (provided myclass.h follows my outline above and does
// not make unnecessary #includes)
這是另外一個(gè)不好的某某方法:
//example.cpp
// I want to use MyClass
#include "myclass.h"
// ERROR 'Parent' undefined
出錯(cuò)了,再包含parent.h:
#include "parent.h"
#include "myclass.h"
// ERROR 'std::vector' undefined
#include "parent.h"
#include <vector>
#include "myclass.h"
// ERROR 'Support' undefined
為什么盎致取带斑?我的類沒(méi)用到Support肮乃隆?好吧勋磕,繼續(xù)包含吧妈候。。挂滓。
#include "parent.h"
#include <vector>
#include "support.h"
#include "myclass.h"
// ERROR 'Support' undefined
present.h使用了Support苦银,所以你必須在#include "parent.h"之前先包含"suport.h".
那support.h要是再依賴其他頭文件呢?按這種某某方法杂彭,我們不僅要記住每個(gè)類的依賴墓毒,還要記住它們的#include順序。這很快就會(huì)成為一個(gè)噩夢(mèng)亲怠。
如果你要對(duì)MyClass做小的修改會(huì)發(fā)生什么呢所计?比如你要用std::list替換std::vector。用某某方法团秽,你必須修改每個(gè)#include “myclasss.h”的文件主胧,把<vector>替換成<list>;而采用我的方法习勤,只需要修改"myclass.h"或"myclass.cpp".
我上面展示的“正確”的方法事關(guān)封裝踪栋。所有使用MyClass的文件不需要指定MyClass使用了什么,也不需要#include MyClass的依賴图毕。要使用MyClass夷都,唯一要做的就是#include "MyClass.h"。頭文件是自包含的予颤,是面向?qū)ο笥押玫亩诠伲子谑褂煤途S護(hù)。
循環(huán)依賴
循環(huán)依賴就是兩個(gè)類互相依賴蛤虐。比如党饮,類A依賴B,同時(shí)類B又依賴類A驳庭。如果你堅(jiān)持上面說(shuō)的“正確”的包含方法刑顺,盡量使用前向聲明,通常不會(huì)碰到這個(gè)問(wèn)題饲常。
下面這個(gè)例子說(shuō)明了為什么只包含需要的頭文件:
// a.h -- assume it's guarded
#include "b.h"
class A { B* b; };
// b.h -- assume it's guarded
#include "a.h"
class B { A* a };
一眼看上去似乎沒(méi)有什么錯(cuò)蹲堂。B依賴A,所以包含它贝淤;A依賴B柒竞,也包含它。
這是個(gè)循環(huán)包含(也就無(wú)限包含)的問(wèn)題霹娄。比如你要編譯“a.cpp”:
// a.cpp
#include "a.h"
編譯器會(huì)這樣做:
#include "a.h"
// start compiling a.h
#include "b.h"
// start compiling b.h
#include "a.h"
// compilation of a.h skipped because it's guarded
// resume compiling b.h
class B { A* a }; // <--- ERROR, A is undeclared
盡管你已經(jīng)包含了“a.h”, 編譯器在B類被編譯之前不會(huì)看到A類能犯。這就是循環(huán)包含問(wèn)題。這也是為什么在使用指針或引用時(shí)犬耻,你應(yīng)該盡量使用前向聲明的原因踩晶。這里,"a.h"不該#include "b.h"枕磁,使用前向聲明來(lái)聲明B就行渡蜻;同樣的,b.h也應(yīng)該通過(guò)前向聲明來(lái)聲明A计济。
當(dāng)存在兩個(gè)互相依賴時(shí)茸苇,也會(huì)發(fā)生循環(huán)包含問(wèn)題(比如不能使用前向聲明):
// a.h (guarded)
#include "b.h"
class A
{
B b; // B is an object, can't be forward declared
};
// b.h (guarded)
#include "a.h"
class B
{
A a; // A is an object, can't be forward declared
};
然而這種情況在概念上時(shí)不可能的。這是一個(gè)設(shè)計(jì)缺陷沦寂。如果A包含了B對(duì)象学密,B又包含了A對(duì)象,然后A又包含了B對(duì)象... 產(chǎn)生了無(wú)限遞歸传藏,兩個(gè)類都不能被實(shí)例化腻暮。解決辦法時(shí)一個(gè)類或兩個(gè)類都包含另一個(gè)類的指針或引用,然后前向聲明它即可毯侦。
函數(shù)內(nèi)聯(lián)
內(nèi)聯(lián)函數(shù)就是函數(shù)體需要在每個(gè)cpp文件中存在哭靖,否則會(huì)發(fā)生鏈接錯(cuò)誤(因?yàn)樗鼈儾荒茉阪溄悠陂g被鏈接,它們需要在編譯期間被編譯到代碼中)侈离。
這有可能發(fā)生循環(huán)引用:
class B
{
public:
void Func(const A& a) // parameter, so forward declare is okay
{
a.DoSomething(); // but now that we've dereferenced it, it
// becomes an #include dependency
// = we now have a potential circular inclusion
}
};
關(guān)鍵點(diǎn)是當(dāng)內(nèi)聯(lián)函數(shù)需要存在于頭文件中時(shí)试幽,它們不需要存在于類定義中。我們利用一下循環(huán)漏洞:
// b.h (assume its guarded)
//------------------
class A; // forward declared dependency
//------------------
class B
{
public:
void Func(const A& a); // okay, A is forward declared
};
//------------------
#include "a.h" // A is now an include dependency
inline void B::Func(const A& a)
{
a.DoSomething(); // okay! a.h has been included
}
這么做是絕對(duì)安全的卦碾。完全避免了循環(huán)依賴問(wèn)題铺坞,即使a.h包含了b.h。這是因?yàn)锽類在被完全定義之前蔗坯,#include并沒(méi)有出現(xiàn)康震。
可是把#include放到頭文件的末尾比較丑陋,有其他辦法嗎宾濒?有的腿短,可以把函數(shù)體放到另一個(gè)頭文件中:
// b.h
// blah blah
class B { /* blah blah */ };
#include "b_inline.h" // or I sometimes use "b.hpp"
// b_inline.h (or b.hpp -- whatever)
#include "a.h"
#include "b.h" // not necessary, but harmless
// you can do this to make this "feel" like a source
// file, even though it isn't
inline void B::Func(const A& a)
{
a.DoSomething();
}
這樣做將接口和實(shí)現(xiàn)相分離,并允許實(shí)現(xiàn)被內(nèi)聯(lián)绘梦。
前向聲明模板
前向聲明對(duì)簡(jiǎn)單的類是很直觀的方法橘忱,但對(duì)模板類就不那么直觀了⌒斗睿考慮下面的場(chǎng)景:
// a.h
// included dependencies
#include "b.h"
// the class template
template <typename T>
class Tem
{
/*...*/
B b;
};
// class most commonly used with 'int'
typedef Tem<int> A; // typedef'd as 'A'
// b.h
// forward declared dependencies
class A; // error!
// the class
class B
{
/* ... */
A* ptr;
};
看上去符合邏輯钝诚,但代碼不工作!因?yàn)锳不是一個(gè)真正的類榄棵,而是一個(gè)typedef凝颇。同時(shí)注意我們不能#include “a.h"潘拱,因?yàn)榇嬖谘h(huán)依賴問(wèn)題。
為了前向聲明A拧略,我們需要typedef它芦岂。這意味著我們需要前向聲明typedef。像這樣做:
template <typename T> class Tem; // forward declare our template
typedef Tem<int> A; // then typedef 'A'
這比前向聲明class A
要丑陋垫蛆。并且禽最,這樣使模板類不易封裝,它把模板類的內(nèi)部布局完全暴露了出來(lái)袱饭。如果要做修改川无,會(huì)是大麻煩。
一個(gè)辦法是創(chuàng)建一個(gè)頭文件來(lái)包含模板類的前向聲明虑乖,如下:
//a.h
#include "b.h"
template <typename T>
class Tem
{
/*...*/
B b;
};
//a_fwd.h
template <typename T> class Tem;
typedef Tem<int> A;
//b.h
#include "a_fwd.h"
class B
{
/*...*/
A* ptr;
};