頭文件和Include: Why and How

頭文件和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;
}

雖然MyClassmyclass.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;
};
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末懦趋,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子疹味,更是在濱河造成了極大的恐慌愕够,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佛猛,死亡現(xiàn)場(chǎng)離奇詭異惑芭,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)继找,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門遂跟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人婴渡,你說(shuō)我怎么就攤上這事幻锁。” “怎么了边臼?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵哄尔,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我柠并,道長(zhǎng)岭接,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任臼予,我火速辦了婚禮鸣戴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘粘拾。我一直安慰自己窄锅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布缰雇。 她就那樣靜靜地躺著入偷,像睡著了一般追驴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上疏之,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天氯檐,我揣著相機(jī)與錄音,去河邊找鬼体捏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛糯崎,可吹牛的內(nèi)容都是我干的几缭。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼沃呢,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼年栓!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起薄霜,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤某抓,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后惰瓜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體否副,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年崎坊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了备禀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡奈揍,死狀恐怖曲尸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情男翰,我是刑警寧澤另患,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站蛾绎,受9級(jí)特大地震影響昆箕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜租冠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一为严、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧肺稀,春花似錦第股、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)诲锹。三九已至,卻和暖如春涉馅,著一層夾襖步出監(jiān)牢的瞬間归园,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工稚矿, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留庸诱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓晤揣,卻偏偏與公主長(zhǎng)得像桥爽,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子昧识,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容