C/C++之类的前置声明
C++中將"接口與實現分離"的兩個重要目的就是"降低文件間的編譯依存關系"和"隱藏對象的實現細節"。而實現這一目的的關鍵技術就是Pimpl模式(pointer to implementation),也即是把一個類所有的實現細節都"代理"給另一個類來完成,而自己只負責提供接口。而實現"Pimpl模式"的關鍵又是"依賴對象的聲明(declaration)而非定義(definition)"。那么為什么通過依賴對象的聲明可以實現"Pimpl模式",進而實現接口與實現分離?
問題:定義一個類 class A,這個類里面使用了類B的對象b,然后定義了一個類B,里面也包含了一個類A的對象a,就成了這樣:
編譯一下A.cpp,不通過。再編譯B.cpp,還是不通過。編譯器都被搞暈了,編譯器去編譯A.h,發現包含了B.h,就去編譯B.h。編譯B.h的時候發現包含了A.h,但是A.h已經編譯過了(其實沒有編譯完成,可能編譯器做了記錄,A.h已經被編譯了,這樣可以避免陷入死循環。編譯出錯總比死循環強點),就沒有再次編譯A.h就繼續編譯。后面發現用到了A的定義,這下好了,A的定義并沒有編譯完成,所以找不到A的定義,就編譯出錯了。
兩個文件出現了相互包含的問題,C++是不允許的。解決辦法就是使用前置聲明而不是直接引用頭文件。
?
一、前置聲明的優點
(1)減小了類的體積。
(2)提高了編譯速度,因為編譯器只需知道該類已經被定義了,而無需了解定義的細節。
使用前置聲明可以減少需要包含的頭文件。而當一個頭文件被包含進來時,相當于引入了新的依賴。只要被依賴的這個頭文件有修改,代碼就會重新編譯。而且這種依賴性是會遞歸傳遞的,也就是當頭文件A發生了更改時,包含了頭文件A的頭文件B和所有包含頭文件B的頭文件都會被重新編譯。所以應該適當地減少這種情況的發生。
(3)通過"前置聲明"可以實現"接口與實現分離"。我們將需要提供給客戶的類分割為兩個classes:一個只提供接口,另一個負責實現!
二、何時使用前置聲明和#include
首先,我們為什么要包括頭文件?答案很簡單,通常是我們需要獲得某個類型的定義(definition)。那么接下來的問題就是,在什么情況下我們才需要類型的定義,在什么情況下我們只需要聲明就足夠了?答案是當我們需要知道這個類型的大小或者需要知道它的函數簽名的時候,我們就需要獲得它的定義。如下,哪些需要C的定義:
- A繼承至C
- A有一個類型為C的成員變量
- A有一個類型為C的指針的成員變量
- A有一個類型為C的引用的成員變量
- A有一個類型為std::list<C>的成員變量
- A有一個函數,它的簽名中參數和返回值都是類型C
- A有一個函數,它的簽名中參數和返回值都是類型C,它調用了C的某個函數,代碼在A的頭文件中
- A有一個函數,它的簽名中參數和返回值都是類型C(包括類型C本身,C的引用類型和C的指針類型),并且它會調用另外一個使用C的函數,代碼直接寫在A的頭文件中
- C和A在同一個名字空間里面
- C和A在不同的名字空間里面
情況一:必須要知道C的定義,因為A作為子類,必須要知道C的內容,才能繼承
情況二:必須要知道C的定義,因為需要根據C來確定A的大小,一般用Pimpl模式 改善。
情況三和情況四:不需要知道C的定義,只需要前置聲明就可以了。引用在物理上也是一個指針,效果和指針一樣。即便沒有C的定義,A也不會有任何問題。
情況五:不需要知道C的定義,有可能老式的編譯器需要。標準庫里面的容器(如:list、vector、map),在包括一個list<C>,vector<C>,map<C, C>類型的成員變量的時候,都不需要C的定義。因為它們內部其實也是使用C的指針作為成員變量,它們的大小一開始就是固定的了,不會根據模版參數的不同而改變。
情況六:不需要知道C的定義
情況七:必須要知道C的定義,需要知道調用函數的簽名。
情況八:對于引用和指針情況一樣
例如:
類C中有:C&?CdoSomething(C&);
類A中有:C&?AdoSomething (C&?c)?{ return?CdoSomething (c);};
以上情況,不需要知道C的定義,但是對上面的函數任意一個C&換成C,比如像下面的幾種示例:
類C中有:C& CdoSomething (C&);
類A中有:C& AdoSomething (C c) {return CdoSomething (c);};
類C中有:C& CdoSomething (C);
類A中有:C& AdoSomething (C& c) {return CdoSomething (c);};
類C中有:C CdoSomething (C&);
類A中有:C& AdoSomething (C& c) {return CdoSomething (c);};
類C中有:C& CdoSomething (C&);
類A中有:C AdoSomething (C& c) {return CdoSomething (c);};
那么就必須要C的定義。無論哪一種,其實都隱式包含了一個拷貝構造函數的調用,比如1中參數c由拷貝構造函數生成,3中CdoSomething的返回值是一個由拷貝構造函數生成的匿名對象。因為我們調用了C的拷貝構造函數,所以以上無論那種情形都需要知道C的定義。
情況九和情況十:不需要知道C的定義。
二、結論
(1)前置聲明只能作為指針或引用,不能定義類的對象,自然也就不能調用對象中的方法了。
(2)而且需要注意,如果將類A的成員變量B* b;改寫成B& b;的話,必須要將b在A類的構造函數中,采用初始化列表的方式初始化,否則也會出錯。
(3)盡量不要在頭文件中包含另外的頭文件。盡量在頭文件中使用類前置聲明程序下文中要用到的類,實在需要包含其它的頭文件時,可以把它放在我們的類實現文件(cpp)中。
正常結構的C++如下:
// House.h
classCBed; // 蓋房子時:現在先不買,肯定要買床的
classCHouse
{
private:
CBed &bed; // 我先給床留個位置
// CBed bed; // 編譯出錯
public:
CHouse(void);
CHouse(CBed &bedTmp);
virtual~CHouse(void);
voidGoToBed();
};
?
// House.cpp
#include"Bed.h"
#include"House.h"// 等房子開始裝修了,要買床了
?
CHouse::CHouse(void)
: bed(*newCBed()) // 這里對引用的賦值
{
CBed *bedTmp = newCBed(); // 把床放進房子
bed = *bedTmp;
}
CHouse::CHouse(CBed &bedTmp)
: bed(bedTmp)
{
}
?
CHouse::~CHouse(void)
{
delete &bed;
}
?
voidCHouse::GoToBed()
{
bed.Sleep();
}
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
總結
以上是生活随笔為你收集整理的C/C++之类的前置声明的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C/C++ 之 应用程序的编译过程
- 下一篇: C/C++之预处理命令