標籤:

在開發大C++工程的時候如何判斷和避免循環include?

比如說我正在修改一個大型項目的代碼。比如裡面的類之間有複雜的include關係。如果我增加了新的類A,想讓它在某個已有的類B中被調用。那麼自然是B的頭文件中include A的頭文件。但是A的頭文件中可能要include C類的頭文件,而且很有可能C類 include了B的頭文件。B類和C類之間可能有其他類構成include的鏈條。所以在寫類A的時候如何判斷這種循環include並且用前向聲明forward declaration呢?


C++頭文件是歷史遺留問題,掌握好規律還是能避免陷阱:

0. 頭文件做好 include guard,例如 pragma once。

1. 盡量使用 forward declaration,namespace 中的 class 都可以前向聲明,模板也可以前向聲明,例如 class Foo; typedef std::shared_ptr& FooPtr。但是 nested class 無法前向聲明。

2. 保證每個構成interface的頭文件都獨立可用,例如 class A 的 cpp 文件第一個包含的頭文件應該是 class A 的頭文件,以此類推。盡量 cpp/頭文件 配對:盡量每一個 class 有一個頭文件和一個 cpp 文件,文件名與類名相同,僅擴展名不同。(類模板除外,沒辦法。)

3. include 頭文件使用絕對路徑,從VCS的根開始。

4. cpp 文件和頭文件中寫 include 時按一定的原則分組(例如本項目、本公司、第三方C++庫、C++標準庫、第三方C庫,libc庫),每組以內按字母順序排列頭文件。

5. 做到第3點之後,用簡單的腳本就能生成頭文件的包含關係圖(Doxygen也行),然後就很容易看出循環依賴。也不難自動檢測。

6. 頭文件里不要埋地雷,比如修改 struct 的默認對齊方式,修改編譯器的優化級別或警告級別等等。


遇到這種情況基本上說明你的文件組織不好,通常我選擇的方法是把其中一個文件拆成兩個。因為你A-&>B的時候,B-&>A基本上只是為了A裡面的少數幾個類。這通常證明,AB裡面的那麼多類,按照現在的做法放在AB兩個文件,是不對的。於是你要重新思考一下文件的布局。你可以稍微調整一下。

本來關係密切的類,放在一起是應該的。


我回答完了。才發現題目是如何避免循環include頭文件,而不是如何避免重複include多餘頭文件。審題不清,不過回答還是保留著。

過度include不必要的頭文件,會增加編譯時間。對於C++來說特別嚴重。

極端的解決方法

頭文件中不能出現include, 所有必要的包含放到 cpp 文件裡面。

不過這方法用起來不方便。說一個簡單的,容易操作的。

頭文件第一位包含

比如寫類A,有文件 A.h, 和A.cpp

那麼在A.cpp中,將A.h的包含寫在第一位。在A.cpp中寫成

// 前面沒有別的頭文件包含
#include "A.h"
#include &
#include .......
.... 包含其它頭文件

之後可以嘗試在 A.h 中去掉多餘的頭文件。當A.cpp可以順利編譯通過的時候,A.h包含的頭文件就是過多或者剛剛好的。而不會是包含不夠的。

再來看如何減少A.h不必要的包含。

前置聲明

首先,只在頭文件中使用引用或者指針,而不是使用值的,可以前置聲明。而不是直接包含它的頭文件。

比如

class Test : public Base
{
public:
void funA(const A a);
void funB(const B* b);
void funC(const space::C c);

private:
D _d;
};

這裡,我牽涉到幾個其它類,Base, A, B, space::C(C 在命名空間space裡面), D。Base和D需要知道值,A, B, space::C只是引用和指針。所以Base, C的頭文件需要包含。A, B,space::C只需要前置聲明。

#include "Base.h"
#include "D.h"

namespace space
{
class C;
}

class A;
class B;
class Test : public Base
{
public:
void funA(const A a);
void funB(const B* b);
void funC(const space::C c);

private:
D _d;
};

注意命名空間裡面的寫法。

對於C++裡面的智能指針,比如 std::shared_ptr&。這時候,A還是只需要前置聲明。而智能指針的頭文件需要包含。

impl手法

簡單說來,就是類裡面包含實現類的指針。在cpp裡面實現。

代碼結構

從更大範圍的代碼組織上面來說。

提倡拆成多個小類。分散到多個文件。而不是全部塞在一個文件中。這點說起來簡單,現實中總會或多或少違反。

比如 工程需要用到一些常量字元串(或者消息定義,或者enum值,有多個變種)。一個看起來清晰的結構,是將字元串都放到同一個頭文件中,這樣就容易比較。不過這樣一來,這個字元串文件,就幾乎會被包含。當以後新加一個字元串時候,就算只加一行,工程幾乎被全部編譯。工程一大,夠看一章書了。以前我想偷懶的時候,就是這樣隨便改一改,跟著等編譯。

更好的做法,是按照字元串的用途來分拆開。

又比如,有些支持庫。有些不注意的,就會寫一個GlobalUtils.h之類的頭文件,包含所有支持庫,因為這樣可以不關心到底應該包含哪個,反正包含GlobalUtils.h就行,這樣多省事。不過這樣一來,需要加一個支持的函數,比如就只是角度轉弧度的小函數,也會發生連鎖編譯。

當寫個小函數只需要1分鐘,編譯需要30分鐘。我就寧願將小函數複製粘貼一次。這樣就容易引起代碼重複。

小結

  1. 要減少頭文件重複包含。需要團隊的人有這個意識,認識到這是不好的。我相信還是有很多人,對這問題認識不夠,你規定要這樣做,還會被當成小題大作。
  2. 不要貪方便。直接包含一個大的頭文件,短期是很方便,長期會有麻煩。
  3. 了解一些C++常用的手法。我相信很多人,連前置聲明,也是不知道的。


《大規模C++程序設計》里講得比較清楚


上邊的人說了很多,有些麻煩,沒有那麼多的幾個經驗的人要搞清楚還挺不容易。我只強調一句,有一種東西叫做不完全類型,組織數據結構時就要想到如果不是繼承關係,就一定是這麼用。


不是通常都ifdef或者pragma once嗎


能用申明的就不用include. 比如你一個頭文件裡面定義了class A{...}; 另一個頭文件裡面需要A的引用或者A的指針,但是不需要在頭文件里訪問A的函數或成員變數,那麼就不用include "A.h", 之需要在你的頭文件前申明一下就可以。 class A; 這樣可以避免很多不必要的include.


1。保證每個頭文件都有#pragma_once或者#ifndef #define ... #endif (這是cpp的情況 oc可以略過)

2。在B類的.h頭文件裡面盡量只用class A;這樣先聲明一下A類 然後就可以在B的聲明裡面使用A類

3。在B類的實現文件裡面再#include"A.cpp"包含頭文件(cpp情況 如果是oc請務必使用import oc的import關鍵字能保證只包含一次 也即實現1中的功能)

4。對A類的頭文件和實現也是類似

這樣就能解決循環包含的情況 這只是方法論 其他一些原則性的東西請參考排名第一的回答


為什麼相當一批回答(接近一半)不知道ifdef和pragma once?

哪怕是幾個文件的小工程也會遇到這種問題吧?難道不知道上面方法的人以往都是手工改代碼解決的循環問題?太可怕了。

這個問題真的很好,是面照妖鏡,一下子很多人就現了原形。

補充:是我理解錯了,沒看清問題的內容只看到了問題的標題。建議這個問題的標題需要修改。


不知道有沒有人是像我這樣處理的,網上似乎沒找到像我這樣的思路:

(我個人覺得挺好用的啊,為什麼沒人用呢。。。)

#pragma once
class A;//劃重點
#include"HeaderB.h"

class A{
B *b;
};

#pragma once
class B;//劃重點
#include "HeaderA.h"

class B {
A *a;
};

即:在類的頭文件中,額外在【#pragma once之下】、【需要用到該類頭文件之上】額外加一行該類的聲明。

常規處理class A和class B互相包含,是通過在class A的頭文件里加上class B的前置聲明。但是一旦class B改了個名字,就要去各個調用了class B的頭文件里到處找,然後一個個改名,非常麻煩。而這種方法相當於把class B的前置聲明統一交給class B自己的頭文件管理,想改名也只要直接右鍵重命名即可(指的是Visual Studio。。。)

如果【不想考慮/不想知道】哪些頭文件包含了自己這個類,只要直接把該類的前置聲明加在【#pragma once】下面即可。

或者養成習慣,每寫一個類的頭文件,都在【#pragma once】下面加一行自己的前置聲明,一勞永逸,麻麻再也不用擔心循環include。。。

附一張效果圖:

丑是丑了點但是好用啊。。。至於為什麼我把前置聲明插在一堆頭文件中間,是因為前面是UE4自帶的頭文件,肯定不會用到我自己寫的這個類啊。而下面是我自己寫的頭文件,才可能會用到我這個類。

正好用前置聲明隔開。。。

(其實我也不大確定這個是不是不規範的用法,因為網上真沒找到和我一樣這麼用的。。。。)


建議用cpplint檢查一下代碼規範,可以檢查include相關的規範的。

http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml?showone=Names_and_Order_of_Includes#Names_and_Order_of_Includes

http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml?showone=The__define_Guard#The__define_Guard


把工具層和業務層分開,建立分層不可逆樹。


使用doxygen生成代碼的頭文件包含圖, 此乃分析頭文件循環包含的利器!

當然, 如何解決循環包含, 還是要看C++功底了.


陳碩已經回答的很詳細,我想避免循環include應該還有一種方式,就是依賴倒置。雙方或一方不再直接依賴對方,而是依賴於對方的抽象,運用得當的話這應該也是一種不錯的方式(回答完才發現好幾個同學已經提出了這種方案……)。


好吧c++里沒有#import,這一定是個悲劇


。。。。。。舊的慣例不是

#ifndef _FOO_H_
#define _FOO_H_
/* declarations go here */
#endif

這樣么?還有 #pragma once更方便,好像主流的編譯器也都支持了,到底是用了多小眾的編譯器才會不相容啊。

吐個槽,管理大型項目竟然不知道這些。。。

====================================================================

以上全部口胡,好吧,我沒看完題。。。去看 @陳碩 的答案吧,懶得寫了


推薦閱讀:

為什麼C++使用sizeof關鍵字的時候不需要include <cstddef>頭文件就可以使用?
如何在C++中拋出一個編譯錯誤?
C++的RAND函數生成的值為什麼存在嚴重的不隨機性?
在c語言中,使用函數指針是否可以提高函數的調用速度 ?

TAG:C | CC |