在C++中封裝的概念是把一個對象的外觀接口同實際工作方式(實現(xiàn))分離開來,但是C++的封裝是不完全的,編譯器必須知道一個對象的所有部分的聲明,以便創(chuàng)建和管理它。我們可以想象一種只需聲明一個對象的公共接口部分的編程語言,而將私有的實現(xiàn)部分隱藏起來。C + +在編譯期間要盡可能多地做靜態(tài)類型檢查。這意味著盡早捕獲錯誤,也意味著程序具有更高的效率。然而這對私有的實現(xiàn)部分來說帶來兩個影響:一是即使程序員不能輕易地訪問實現(xiàn)部分,但他可以看到它;二是造成一些不必要的重復(fù)編譯。
然而C++并沒有將這個原則應(yīng)用到二進制層次上,這是因為C++的類既是描述了一個接口同時也描述了實現(xiàn)的過程,示例如下:
class CMyString


{
private:
const int m_cch;
char *m_psz;
public:
CMyString(const char *psz);
~CMyString();
int Length() const;
int Index(const char *psz) const;
}

CMyStirng對外過多的暴露了內(nèi)存布局實現(xiàn)的細節(jié),這些信息過度的依賴于這些成員變量的大小和順序,從而導(dǎo)致了客戶過度依賴于可執(zhí)行代碼之間的二進制耦合關(guān)系,這樣的接口不利于跨語言跨平臺的軟件開發(fā)和移植。
1.1.1 Handle-Body模式
解決這個問題的技術(shù)有一種叫句柄類( handle classes)。有關(guān)實現(xiàn)的任何東西都消失了,只剩一個單一的指針“m_pThis”。該指針指向一個結(jié)構(gòu),該結(jié)構(gòu)的定義與其所有的成員函數(shù)的定義都出現(xiàn)在實現(xiàn)文件中。這樣,只要接口部分不改變,頭文件就不需變動。而實現(xiàn)部分可以按需要任意更動,完成后只要對實現(xiàn)文件進行重新編譯,然后再連接到項目中。
下面是這項技術(shù)的簡單例子。頭文件中只包含公共的接口和一個簡單的沒有完全指定的類指針。
class CMyStringHandle


{
private:
class CMyString;
CMyString *m_pThis;
public:
CMyStringHandle (const char *psz);
~ CMyStringHandle ();
int Length() const;
int Index(const char *psz) const;
};

CMyStringHandle:: CMyStringHandle(const char *psz)
:m_pThis(new CMyString(psz));


{
}

CMyStringHandle::~ CMyStringHandle()


{
delete m_pThis;
}

int CMyStringHandle::Length()


{
return m_pThis->Length();
}

int CMyStringHandle::Index(const char *psz)


{
return m_pThis->Index(psz);
}

這是所有客戶程序員都能看到的。
class CMyString;
是一個沒有完全指定的類型說明或類聲明(一個類的定義包含類的主體)。它告訴編譯器,CMyString是一個結(jié)構(gòu)的名字,但沒有提供有關(guān)該結(jié)構(gòu)的任何東西。這對產(chǎn)生一個指向結(jié)構(gòu)的指針來說已經(jīng)足夠了。但我們在提供一個結(jié)構(gòu)的主體部分之前不能創(chuàng)建一個對象。在這種技術(shù)里,包含具體實現(xiàn)的結(jié)構(gòu)主體被隱藏在實現(xiàn)文件中。
在設(shè)計模式中,這就叫做Handle-Body 模式,Handle-Body只含有一個實體指針,服務(wù)的數(shù)據(jù)成員永遠被封閉在服務(wù)系統(tǒng)中。
Handle-Body的布局結(jié)構(gòu)永遠不會隨著實現(xiàn)類數(shù)據(jù)成員的加入或者刪除或者修改而導(dǎo)致Handle-Body的修改,即Handle-Body協(xié)議不依賴于C++實現(xiàn)類的任何細節(jié)。這就有效的對用戶的編譯器隱藏了這些細節(jié),用戶在使用對這項技術(shù)時候,Handle-Body 接口成了它唯一的入口。
然而Handle-Body模式也有自己的弱點:
1、接口類必須把每一個方法調(diào)用顯示的傳遞給實現(xiàn)類,這在一個只有一個構(gòu)造和一個析構(gòu)的類來說顯然不構(gòu)成負擔(dān),但是如果一個龐大的類庫,它有上百上千個方法時候,光是編寫這些方法傳遞就有可能非常冗長,這也增加了出錯的可能性。
2、對于關(guān)注于性能的應(yīng)用每一個方法都得有兩層的函數(shù)調(diào)用,嵌套的開銷也不理想
3、由于句柄的存在,依然存在編譯連接器兼容性問題。
1.1.2 抽象接口
使用了“接口與實現(xiàn)的分離”技術(shù)的 Handle-Body 解決了編譯器/鏈接器的大部分問題,而C++面向?qū)ο缶幊讨械某橄蠼涌谕瑯邮沁\用了“接口與實現(xiàn)分離”的思想,而采用抽象接口對于解決這類問題是一個極其完美的解決方案。
1、抽象接口的語言描述:
class IMyString
{
virtual int Length() const = 0; //這表示是一個純虛函數(shù),具有純虛函數(shù)的接口
virtual int Index(const char *psz) const = 0;
};
2、抽象接口的內(nèi)存結(jié)構(gòu):
抽象接口采用虛函數(shù)表來調(diào)用成員方法。
3、 抽象接口的實現(xiàn)代碼:
接口:
class IMyString
{
virtual int Length() const = 0; //這表示是一個純虛函數(shù),具有純虛函數(shù)的接口
virtual int Index(const char *psz) const = 0;
};
實現(xiàn):
class CMyString:public IMyString
{
private:
const int m_cch;
char *m_psz;
public:
CMyString(const char *psz);
virtual ~CMyString();
int Length() const;
int Index(const char *psz) const;
}
從上面采用抽象接口的實例來看,抽象接口解決了Handle-Body所遺留下來的全部缺陷。
抽象接口的一個典型應(yīng)用:
抽象工廠(AbstractFactroy)
1.2 多繼承與菱形缺陷、this跳轉(zhuǎn)等
多重繼承是C++語言獨有的繼承方式,其它幾乎所有語言都秉承了單一繼承的思想。這是因為多重繼承致命的缺陷導(dǎo)致的:
1.2.1 菱形缺陷
當(dāng)繼承基類時,在派生類中就獲得了基類所有的數(shù)據(jù)成員副本。假如類B 從A1和A2兩個類多重繼承而來,這樣B類就包含A1、A2類的數(shù)據(jù)成員副本。
考慮如果A1、A2都從某基類派生,該基類稱為Base,現(xiàn)在繼承關(guān)系將出現(xiàn)菱形繼承關(guān)系。
我們C++語言來描述這種繼承關(guān)系:
class Base{… … };
class A1 :public Base {… … };
class A2 :public Base {… … };
class B :public A1,public A2 {… … };
那么A1、A2都具有Base的副本。這樣B就包含了Base的兩個副本,副本發(fā)生了重疊,不但增加了存儲空間,同時也引入了二義性。這就是菱形缺陷,菱形缺陷的兩個缺陷:
1、子對象重疊
2、向上映射的二義性。
菱形缺陷的其中一種解決辦法是使用虛擬繼承。
在C++世界里最廣泛的使用虛擬繼承解決菱形缺陷的應(yīng)用便是標(biāo)準(zhǔn)C++的輸入/輸出iostream;
1.2.2 多重接口與方法名沖突問題(Siamese twins)
對繼承而來的虛函數(shù)改寫很容易,但是如果是在改寫一個“在兩個基類都有相同原型”的虛函數(shù)情況就不那么容易了。
提出問題:
假設(shè)汽車最大速度的接口為ICar,潛艇最大速度的接口為 IBoat,有一個兩棲類的交通工具它可以奔跑在馬路上,也可以航行在大海中,那么它就同時擁有ICar、IBoat兩種交通工具的最大速度特性,我們定義它的接口為ICarBoat;
class ICar
{
virtual int GetMaxSpeed()= 0;
};
class IBoat
{
virtual int GetMaxSpeed()= 0;
};
我們先對ICarBoat的接口做一個嘗試:
class CCarBoat
{
virtual int GetMaxSpeed();//既完成ICar的GetMaxSpeed()接口方法又 //完成IBoat的接口方法?顯然不能夠
};
解決問題:
顯然上面這個嘗試根本就無法成功,只用一個實現(xiàn)方法,怎么能夠求出這個ICarBoat交通工具奔跑在馬路上的最高時速,同時也能夠求出航行在大海上的最大航行速度呢。
上面這一問題矛盾就在一一個方法,卻需要兩個答案。看來ICarBoat要返回兩個答案就必須有兩個方法了,我們假設(shè)一個方法是求在陸地上奔跑的速度,名稱為GetCarMaxSpeed();另一個方法是求在大海上航行的最大速度,名稱為GetBoatMaxSpeed();那這兩個方法又怎么和GetMaxSpeed()接口方法聯(lián)系起來呢;
幸運的是,我們找到了解決辦法,而且解決辦法有很多種,下面介紹一下繼承法。
class IXCar :public ICar
{
virtual int GetMaxSpeed()
{
GetCarMaxSpeed();
}
virtual int GetCarMaxSpeed() = 0;
};
class IXBoat:public IBoat
{
virtual int GetMaxSpeed()
{
GetBoatMaxSpeed();
}
virtual int GetBoatMaxSpeed() = 0;
};
classCCarBoat: public IXCar , public IXBoat
{
virtual int GetCarMaxSpeed()
{
… …
}
virtual int GetBoatMaxSpeed()
{
… …
}
};
1.2.3 this跳轉(zhuǎn)
this跳轉(zhuǎn)是指的“對象同一性”問題。
在單一繼承的世界內(nèi),無論繼承關(guān)系怎么復(fù)雜,針對于同一對象,無論它的子類或者父類的this指針永遠相等。即如果 B從A繼承,那么 對于一個已經(jīng)實例化B類的對象 bObject,永遠有(B*)&bObject ==(A*)&bObject 成立。
但是在多繼承的世界內(nèi),上面的等式就不能恒成立,對象的同一性受到了挑戰(zhàn)。
特別的是,在多繼承世界內(nèi)如果菱形關(guān)系存在情況下,如果對于已經(jīng)實例化B類的對象bObject; (Base*)(A1*)&bObject != (Base*)(A2*)&bObject 成立,當(dāng)這種事情發(fā)生的時候我們就只能特殊處理了。這種情況在COM應(yīng)用中處處都會發(fā)生。
1.3 C++多態(tài)的兩種多態(tài)形式和區(qū)別
C++有兩種多態(tài)多態(tài)形式:
1、編譯時刻多態(tài),編譯時刻多態(tài)依靠函數(shù)重載或者模板實現(xiàn)
2、運行時刻多態(tài)。運行時刻多態(tài)依靠需函數(shù)虛接口實現(xiàn)