為什么我們要使用c++的前置聲明

轉(zhuǎn)載聲明:(http://blog.csdn.net/fjb2080/archive/2010/04/27/5533514.aspx) 鹅搪。
感謝原作者的辛勤付出。

一骚灸、正文

定義一個類 class A辅甥,這個類里面使用了類B的對象b洗做,然后定義了一個類B,里面也包含了一個類A的對象a址芯,就成了這樣:

//a.h  
#include "b.h"  
class A  
{  
....  
private:  
    B b;  
};  
//b.h  
#include "a.h"  
class B  
{  
....  
private:  
    A a;  
};

一編譯灾茁,就出現(xiàn)了一個互包含的問題了,這時就有人跳出來說谷炸,這個問題的解決辦法可以這樣北专,在a.h文件中聲明類B,然后使用B的指針旬陡。

//a.h   
//#include "b.h"  
class B;   
class A   
{  
 ....   
private:  
 B b;   
};   
//b.h   
#include "a.h"   
class B  
{  
 ....   
private:  
 A a;   
};

然后拓颓,問題就解決了。
但是描孟,有人知道問題是為什么就被解決的嗎驶睦,也就是說,加了個前置聲明為什么就解決了這樣的問題匿醒。下面啥繁,讓我來探討一下這個前置聲明。
類的前置聲明是有許多的好處的青抛。
我們使用前置聲明的一個好處是旗闽,從上面看到,當我們在類A使用類B的前置聲明時蜜另,我們修改類B時适室,只需要重新編譯類B,而不需要重新編譯a.h的(當然举瑰,在真正使用類B時捣辆,必須包含b.h)。
另外一個好處是減小類A的大小此迅,上面的代碼沒有體現(xiàn)汽畴,那么我們來看下:

class B;  
class A  
{  
    ....  
private:  
    B *b;  
....  
};  
//b.h  
class B  
{  
....  
private:  
    int a;  
    int b;  
    int c;  
}; 

我們看上面的代碼旧巾,類B的大小是12(在32位機子上)。

如果我們在類A中包含的是B的對象忍些,那么類A的大小就是12(假設沒有其它成員變量和虛函數(shù))鲁猩。如果包含的是類B的指針*b變量,那么類A的大小就是4罢坝,所以這樣是可以減少類A的大小的廓握,特別是對于在STL的容器里包含的是類的對象而不是指針的時候,這個就特別有用了嘁酿。
在前置聲明時隙券,我們只能使用的就是類的指針和引用(因為引用也是居于指針的實現(xiàn)的)。

總結(jié)來說:
1)降低模塊的耦合闹司。因為隱藏了類的實現(xiàn)娱仔,被隱藏的類相當于原類不可見,對隱藏的類進行修改游桩,不需要重新編譯原類拟枚。
2)降低編譯依賴,提高編譯速度众弓。指針的大小為(32位)或8(64位)恩溅,X發(fā)生變化,指針大小卻不會改變谓娃,文件c.h也不需要重編譯脚乡。
3)接口與實現(xiàn)分離,提高接口的穩(wěn)定性滨达。
1奶稠、通過指針封裝,當定義“new C”或"C c1"時 ,編譯器生成的代碼中不會摻雜X的任何信息捡遍。
2锌订、當使用C時,使用的是C的接口(C接口里面操作的類其實是pImpl成員指向的X對象)画株,與X無關(guān)辆飘,X被通過指針封裝徹底的與實現(xiàn)分離。

那么谓传,我問你一個問題蜈项,為什么我們前置聲明時,只能使用類型的指針和引用呢续挟?

如果你回答到:那是因為指針是固定大小紧卒,并且可以表示任意的類型,那么可以給你80分了诗祸。為什么只有80分跑芳,因為還沒有完全回答到轴总。

想要更詳細的答案,我們看下下面這個類:

class A  
{  
public:  
    A(int a):_a(a),_b(_a){} // _b is new add  
      
    int get_a() const {return _a;}  
    int get_b() const {return _b;} // new add  
private:  
    int _b; // new add  
    int _a;  
}; 

我們看下上面定義的這個類A博个,其中_b變量和get_b()函數(shù)是新增加進這個類的怀樟。
那么我問你,在增加進_b變量和get_b()成員函數(shù)后這個類發(fā)生了什么改變坡倔,思考一下再回答漂佩。

好了脖含,我們來列舉這些改變:

1.第一個改變當然是增加了_b變量和get_b()成員函數(shù)罪塔;
2.第二個改變是這個類的大小改變了,原來是4养葵,現(xiàn)在是8征堪。
3.第三個改變是成員_a的偏移地址改變了,原來相對于類的偏移是0关拒,現(xiàn)在是4了佃蚜。
4.上面的改變都是我們顯式的、看得到的改變着绊。還有一個隱藏的改變谐算,想想是什么。归露。洲脂。

這個隱藏的改變是類A的默認構(gòu)造函數(shù)和默認拷貝構(gòu)造函數(shù)發(fā)生了改變。
由上面的改變可以看到剧包,任何調(diào)用類A的成員變量或成員函數(shù)的行為都需要改變恐锦,因此,我們的a.h需要重新編譯疆液。
如果我們的b.h是這樣的:

//b.h  
#include "a.h"  
class B  
{  
...  
private:  
    A a;  
}; 

那么我們的b.h也需要重新編譯一铅。
如果是這樣的:

class A;  
class B  
{  
...  
private:  
    A *a;  
}; 

那么我們的b.h就不需要重新編譯。
像我們這樣前置聲明類A:
class A;
是一種不完整的聲明堕油,只要類B中沒有執(zhí)行需要了解類A的大小或者成員的操作潘飘,則這樣的不完整聲明允許聲明指向A的指針和引用。
而在前一個代碼中的語句
A a;
是需要了解A的大小的掉缺,不然是不可能知道如果給類B分配內(nèi)存大小的福也,因此不完整的前置聲明就不行,必須要包含a.h來獲得類A的大小攀圈,同時也要重新編譯類B暴凑。
再回到前面的問題,使用前置聲明只允許的聲明是指針或引用的一個原因是只要這個聲明沒有執(zhí)行需要了解類A的大小或者成員的操作就可以了赘来,所以聲明成指針或引用是沒有執(zhí)行需要了解類A的大小或者成員的操作的现喳。

二凯傲、使用總結(jié)

這篇文章很大程度是受到Exceptional C++ (Hurb99)書中第四章 Compiler Firewalls and the Pimpl Idiom (編譯器防火墻和Pimpl慣用法) 的啟發(fā),這一章講述了減少編譯時依賴的意義和一些慣用法嗦篱,其實最為常用又無任何副作用的是使用前置聲明來取代包括頭文件冰单。

Item 26 的Guideline - "Never #include a header when a forward declaration will suffice"

在這里,我自己總結(jié)了可以使用前置聲明來取代包括頭文件的各種情況和給出一些示例代碼灸促。

首先诫欠,我們?yōu)槭裁匆^文件?問題的回答很簡單浴栽,通常是我們需要獲得某個類型的定義(definition)荒叼。那么接下來的問題就是,在什么情況下我們才需要類型的定義典鸡,在什么情況下我們只需要聲明就足夠了被廓?問題的回答是當我們需要知道這個類型的大小或者需要知道它的函數(shù)簽名的時候,我們就需要獲得它的定義萝玷。

假設我們有類型A和類型C嫁乘,在哪些情況下在A需要C的定義:

1.A繼承至C
2.A有一個類型為C的成員變量
3.A有一個類型為C的指針的成員變量
4.A有一個類型為C的引用的成員變量
5.A有一個類型為std::list<C>的成員變量
6.A有一個函數(shù),它的簽名中參數(shù)和返回值都是類型C
7.A有一個函數(shù)球碉,它的簽名中參數(shù)和返回值都是類型C蜓斧,它調(diào)用了C的某個函數(shù),代碼在頭文件中
8.A有一個函數(shù)睁冬,它的簽名中參數(shù)和返回值都是類型C(包括類型C本身挎春,C的引用類型和C的指針類型),并且它會調(diào)用另外一個使用C的函數(shù)痴突,代碼直接寫在A的頭文件中
9.C和A在同一個名字空間里面
10.C和A在不同的名字空間里面

1.沒有任何辦法搂蜓,必須要獲得C的定義,因為我們必須要知道C的成員變量辽装,成員函數(shù)帮碰。
2.需要C的定義,因為我們要知道C的大小來確定A的大小拾积,但是可以使用Pimpl慣用法來改善這一點殉挽,詳情請
看Hurb的Exceptional C++。
3.4.不需要拓巧,前置聲明就可以了斯碌,其實3和4是一樣的,引用在物理上也是一個指針肛度,它的大小根據(jù)平臺不同傻唾,可能是32位也可能是64位,反正我們不需要知道C的定義就可以確定這個成員變量的大小。
5.不需要冠骄,有可能老式的編譯器需要伪煤。標準庫里面的容器像list, vector凛辣,map抱既,
在包括一個list<C>,vector<C>扁誓,map<C, C>類型的成員變量的時候防泵,都不需要C的定義。因為它們內(nèi)部其實也是使用C的指針作為成員變量蝗敢,它們的大小一開始就是固定的了捷泞,不會根據(jù)模版參數(shù)的不同而改變。
6前普,不需要肚邢,只要我們沒有使用到C壹堰。7拭卿,需要,我們需要知道調(diào)用函數(shù)的簽名贱纠。8峻厚,8的情況比較復雜,直接看代碼會比較清楚一些谆焊。

      C& doToC(C&);        
      C& doToC2(C& c) {return doToC(c);};

從上面的代碼來看惠桃,A的一個成員函數(shù)doToC2調(diào)用了另外一個成員函數(shù)doToC,但是無論是doToC2辖试,還是doToC辜王,它們的的參數(shù)和返回類型其實都是C的引用(換成指針,情況也一樣)罐孝,引用的賦值跟指針的賦值都是一樣呐馆,無非就是整形的賦值,所以這里即不需要知道C的大小也沒有調(diào)用C的任何函數(shù)莲兢,實際上這里并不需要C的定義汹来。

但是,我們隨便把其中一個C&換成C改艇,比如像下面的幾種示例:

1.C& doToC(C&);           
   C& doToC2(C c) {return doToC(c);};                                
2.C& doToC(C);               
   C& doToC2(C& c) {return doToC(c);};                
3.C doToC(C&);                
   C& doToC2(C& c) {return doToC(c);};                
4.C& doToC(C&);                
   C doToC2(C& c) {return doToC(c);};

無論哪一種收班,其實都隱式包含了一個拷貝構(gòu)造函數(shù)的調(diào)用,比如1中參數(shù)c由拷貝構(gòu)造函數(shù)生成谒兄,3中doToC的返回值是一個由拷貝構(gòu)造函數(shù)生成的匿名對象摔桦。因為我們調(diào)用了C的拷貝構(gòu)造函數(shù),所以以上無論那種情形都需要知道C的定義承疲。

9和10都一樣邻耕,我們都不需要知道C的定義瘦穆,只是10的情況下,前置聲明的語法會稍微復雜一些赊豌。
最后給出一個完整的例子扛或,我們可以看到在兩個不同名字空間的類型A和C,A是如何使用前置聲明來取代直接包括C的頭文件的:

#pragma once  
  
#include <list>  
#include <vector>  
#include <map>  
#include <utility>  
  
    //不同名字空間的前置聲明方式  
namespace test1  
{  
          class C;  
}  
  
  
  
namespace test2  
{     
       //用using避免使用完全限定名  
    using test1::C;  
      
    class A   
    {  
    public:  
                C   useC(C);  
            C& doToC(C&);  
            C& doToC2(C& c) {return doToC(c);};  
                           
    private:  
            std::list<C>    _list;  
            std::vector<C>  _vector;  
            std::map<C, C>  _map;  
            C*              _pc;  
            C&              _rc;  
      
    };  
} 
#ifndef C_H  
#define C_H  
#include <iostream>  
  
namespace test1  
{  
            
    class C  
    {  
    public:  
           void print() {std::cout<<"Class C"<<std::endl;}  
    };  
  
}  
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末碘饼,一起剝皮案震驚了整個濱河市熙兔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌艾恼,老刑警劉巖住涉,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異钠绍,居然都是意外死亡舆声,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門柳爽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來媳握,“玉大人,你說我怎么就攤上這事磷脯《暾遥” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵赵誓,是天一觀的道長打毛。 經(jīng)常有香客問我,道長俩功,這世上最難降的妖魔是什么幻枉? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮诡蜓,結(jié)果婚禮上熬甫,老公的妹妹穿的比我還像新娘。我一直安慰自己万牺,他們只是感情好罗珍,可當我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著脚粟,像睡著了一般覆旱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上核无,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天扣唱,我揣著相機與錄音,去河邊找鬼。 笑死噪沙,一個胖子當著我的面吹牛炼彪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播正歼,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼辐马,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了局义?” 一聲冷哼從身側(cè)響起喜爷,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎萄唇,沒想到半個月后檩帐,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡另萤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年湃密,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片四敞。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡泛源,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出目养,到底是詐尸還是另有隱情俩由,我是刑警寧澤毒嫡,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布癌蚁,位于F島的核電站,受9級特大地震影響兜畸,放射性物質(zhì)發(fā)生泄漏努释。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一咬摇、第九天 我趴在偏房一處隱蔽的房頂上張望伐蒂。 院中可真熱鬧,春花似錦肛鹏、人聲如沸逸邦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缕减。三九已至,卻和暖如春芒珠,著一層夾襖步出監(jiān)牢的瞬間桥狡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留裹芝,地道東北人部逮。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像嫂易,于是被迫代替她去往敵國和親兄朋。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,864評論 2 354

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