第 1 章 COM 是一個(gè)更好的 C++
1.1 軟件分發(fā)和 C++
80 年代后期吏垮,C++ 庫以源代碼的形式分發(fā)镣奋。書中給出了一個(gè) FastString 類作為例子:
// faststring.h ///////////////////////////////
// 快速字符串查找類
class FastString
{
char* m_psz;
public:
FastString(const char* psz);
~FastString(void);
int Length(void) const; // 返回字符數(shù)目
int Find(const char* psz) const; // 返回子串偏移量
}
// faststring.cpp /////////////////////////////
#include "faststring.h"
#include <string.h>
FastString::FastString(const char* psz)
{
m_psz = new char[strlen(psz)+1];
strcpy(m_psz, psz);
}
FastString::~FastString()
{
delete[] m_psz;
}
int FastString::Length(void) const
{
return strlen(m_psz);
}
int FastString::Find(const char* psz) const
{
// 子串查找算法,從略
return 0;
}
直接分發(fā)源代碼的方式存在兩個(gè)問題:
- 占用內(nèi)存和磁盤吁恍。源代碼直接集成到客戶程序中客蹋,多個(gè)客戶程序運(yùn)行盐捷,也會(huì)有多個(gè) FastString.obj 被加載到內(nèi)存中;
- 難以替換辜腺。源代碼一旦鏈接到客戶程序休建,就必須重新編譯客戶程序才有可能替換。
1.2 動(dòng)態(tài)鏈接和 C++
解決上面問題的一種技術(shù)方案是把 FastString 類以動(dòng)態(tài)鏈接庫(DLL)的形式包裝起來:
class __declspec(dllexport) FastString
{
char* m_psz;
public:
FastString(const char* psz);
~FastString();
int Length(void) const;
int Find(const char* psz) const;
}
把 FastString 庫放到 DLL 中评疗,這是從原始的 C++ 類走向可替換的测砂、有效的可重用組件的重要一步。
1.3 C++ 和可移植性
直接將 C++ 類從 DLL 中導(dǎo)出百匆,會(huì)存在兩個(gè)問題:
- 鏈接器:由于 名稱改編 機(jī)制砌些,當(dāng)客戶程序與 FastString 的編譯器不同時(shí),很可能無法鏈接成功;
- 編譯器:不同編譯器有自己實(shí)現(xiàn)語言機(jī)制的專有方式存璃,有可能客戶編譯器無法使用 DLL 導(dǎo)出的代碼仑荐。比如 Microsoft 編譯器產(chǎn)生的函數(shù),它拋出的異常無法被 Watcom 生成的客戶程序捕捉到纵东。
這兩個(gè)問題產(chǎn)生的根本原因在于粘招, C++ 缺少二進(jìn)制一級(jí)的標(biāo)準(zhǔn),各廠商按照自己的方式實(shí)現(xiàn) C++ 編譯器偎球,使得創(chuàng)建 “廠商獨(dú)立的組建軟件” 比較困難洒扎。
1.4 封裝性和 C++
除了上述問題外,另一個(gè)問題則與封裝性有關(guān)衰絮。 C++ 的 public, private 關(guān)鍵字支持了語法上的關(guān)鍵字袍冷,但 C++ 標(biāo)準(zhǔn)并沒有定義 二進(jìn)制層次 上的封裝性。
換句話說岂傲,修改 C++ private 成員难裆,在語法上來說對(duì)其使用者是透明的;但在二進(jìn)制角度看來镊掖,對(duì)類成員的修改將導(dǎo)致二進(jìn)制結(jié)構(gòu)的改變乃戈。
書中舉了一個(gè)例子,給 FastString 增加一個(gè)成員 m_cch 用于記錄字符串長(zhǎng)度:
// faststring.h 2.0 版
class __declspec(dllexport) FastString
{
const int m_cch; // 記錄字符串長(zhǎng)度亩进,提高 Length 函數(shù)效率
char* m_psz;
public:
FastString(const char* psz);
~FastString();
int Length(void) const;
int Find(const char* psz) const;
}
在客戶看來症虑,上述改動(dòng)在語法上是沒有問題的, FastString 的 public 接口沒有變归薛。但在二進(jìn)制層次上谍憔, FastString 對(duì)象的大小從 4 byte 變成了 8 byte. 如果客戶程序還是像之前那樣為 FastString 對(duì)象分配 4 byte 的內(nèi)存,就極有可能引發(fā)異常主籍。
上述問題的根本原因在于 C++ 的編譯模型习贫,它要求客戶程序必須知道對(duì)象的布局結(jié)構(gòu),從而導(dǎo)致了客戶和庫代碼之間的二進(jìn)制耦合關(guān)系千元。
這導(dǎo)致 FastString 類的實(shí)現(xiàn)難以被替換苫昌。無法修復(fù)原有組件的 Bug, 也無法為組件添加新的功能。
1.5 把接口從實(shí)現(xiàn)中分離出來
封裝(encapsulation)的概念以 “把一個(gè)對(duì)象的外觀(接口)同其實(shí)際工作方式(實(shí)現(xiàn))分離開來” 為基礎(chǔ)幸海。 C++ 的問題在于這條原則沒有被應(yīng)用到二進(jìn)制層次上祟身,因?yàn)?C++ 類既是接口也是實(shí)現(xiàn)。
解決這一問題的思路是物独,把接口和實(shí)現(xiàn)做成兩個(gè)類袜硫。一個(gè) C++ 類代表指向一定數(shù)據(jù)類型的接口,另一個(gè) C++ 類作為數(shù)據(jù)類型的實(shí)際實(shí)現(xiàn)挡篓。這樣一來婉陷,實(shí)現(xiàn)類可以被修改,而接口類保持不變。我們還需要一種方法把 接口類 和 實(shí)現(xiàn)類 聯(lián)系起來憨攒。
書中給出了以下示例代碼:
// faststringitf.h 接口類頭文件
class __declspec(dllexport) FastStringItf
{
class FastString; // 導(dǎo)入 實(shí)現(xiàn)類 的名稱
FastString* m_pThis; // 實(shí)現(xiàn)類指針世杀,大小保持不變,客戶不需要知道實(shí)現(xiàn)
public:
FastStringItf(const char* psz);
~FastStringItf();
int Length(void) const;
int Find(const char* psz) const;
}
// faststringitf.cpp
FastStringItf::FastStringItf(const char* psz)
{
m_pThis = new FastString(psz);
}
FastStringItf::~FastStringItf()
{
delete m_pThis;
}
int FastStringItf::Length(void) const
{
return m_pThis->Length();
}
int FastStringItf::Find(const char* psz) const
{
return m_pThis->Find(psz);
}
接口類 FastStringItf 和 實(shí)現(xiàn)類 FastString 之間通過 m_pThis 來聯(lián)系肝集。書里把它叫做句柄類瞻坝。它只是個(gè)句柄,所以大小不會(huì)變杏瞻,并且客戶不會(huì)知道 FastString 的實(shí)現(xiàn)細(xì)節(jié)所刀。如此一來上一節(jié)所講的封裝性問題就得到了解決。即 FastString 的實(shí)現(xiàn)被更改時(shí)捞挥,客戶程序不會(huì)受到影響浮创。
但句柄類這種方式也存在問題:
- 每次都通過句柄類來轉(zhuǎn)發(fā)函數(shù)調(diào)用,有一定開銷且比較繁瑣砌函,容易寫錯(cuò)斩披;
- 編譯器\鏈接器問題仍然沒有得到解決。
1.6 抽象基類作為二進(jìn)制接口
“接口與實(shí)現(xiàn)分離” 技術(shù)確實(shí)可以解決封裝性的問題讹俊,但它的形式需要更改一下垦沉,以解決 編譯器\鏈接器 的兼容性問題。
解決這一問題的關(guān)鍵思路在于 虛函數(shù)調(diào)用機(jī)制 仍劈。它的運(yùn)行原理如下:
- 編譯器在后臺(tái)為每個(gè)包含虛函數(shù)的類產(chǎn)生一個(gè)靜態(tài)函數(shù)指針數(shù)組厕倍。這個(gè)數(shù)組被稱為虛函數(shù)表(vtbl);
- 虛函數(shù)表中包含了這個(gè)類的所有虛函數(shù)的函數(shù)指針;
- 該類的每個(gè)實(shí)例對(duì)象都包含一個(gè)不可見的數(shù)據(jù)成員,被稱為虛函數(shù)指針(vptr)贩疙,它指向虛函數(shù)表;
- 客戶調(diào)用虛函數(shù)的時(shí)候讹弯,先根據(jù) vptr 找到 vtbl, 然后根據(jù)偏移量找到函數(shù)指針,進(jìn)行調(diào)用;
虛函數(shù)調(diào)用機(jī)制有兩個(gè)特點(diǎn):
- 幾乎所有的編譯器廠商都采用了上述 vtbl, vptr 的方式來實(shí)現(xiàn)該機(jī)制这溅,這使得它在不同編譯器上是等價(jià)的组民。也就是說它具備 編譯器 兼容性;
- 這一機(jī)制的實(shí)現(xiàn)中不涉及任何符號(hào)悲靴,也就不涉及 名稱改編 問題臭胜,所以它具備 鏈接器 兼容性;
因此对竣,我們可以把 虛函數(shù)表 作為 客戶程序與庫 之間的 二進(jìn)制協(xié)議:
- 客戶程序編譯器根據(jù) 接口類 生成虛表庇楞;
- 實(shí)現(xiàn)類 繼承自 接口類榜配,以保證兩者的虛表結(jié)構(gòu)一致否纬;
- 庫將 實(shí)現(xiàn)類對(duì)象的指針 轉(zhuǎn)換為 接口類對(duì)象的指針,然后傳給 客戶程序蛋褥,因?yàn)樘摫斫Y(jié)構(gòu)一致临燃,客戶程序可以安全地訪問實(shí)現(xiàn)類函數(shù),而無須了解任何實(shí)現(xiàn)細(xì)節(jié)。
書中給出了以下示例代碼:
// ifaststring.h 接口類
class IFastString
{
public:
virtual int Length(void) const = 0;
virtual int Find(const char* psz) = 0;
}
// faststring.h 實(shí)現(xiàn)類
class FastString : public IFastString
{
const int m_cch;
char* m_psz;
public:
FastString(const char* psz);
~FastString();
int Length() const;
int Find(const char* psz) const;
}
接口類作為二進(jìn)制協(xié)議膜廊,應(yīng)該只定義“調(diào)用這些方法的可能性”乏沸,不需要定義實(shí)現(xiàn),把它的函數(shù)定義為純虛函數(shù)更好一些爪瓜。
為了在不暴露 FastString 的前提下讓用戶創(chuàng)建出 FastString 實(shí)例蹬跃,需要額外導(dǎo)出一個(gè)函數(shù):
// ifaststring.h
extern "C"
{
IFastString* CreateFastString(const char* psz);
}
// faststring.cpp
IFastString* CreateFastString(const char* psz)
{
return new FastString(psz);
}
如此一來,客戶程序就可以通過抽象基類 IFastString 所定義的二進(jìn)制協(xié)議來訪問 實(shí)現(xiàn)類 FastString 的方法铆铆,而不會(huì)引起編譯器蝶缀、鏈接器兼容性問題。
實(shí)際上薄货,庫只是把實(shí)現(xiàn)類的 vtbl 地址傳給了 客戶程序翁都,沒有傳遞任何數(shù)據(jù),因此也不會(huì)有 封裝性 的問題谅猾。
另外柄慰,還有一個(gè)容易忽視的問題需要注意一下,假如客戶執(zhí)行了如下代碼:
int f(void)
{
IFastString* pfs = CreateFastString("Deface me");
int n = pfs->Find("me");
delete pfs; // 內(nèi)存泄漏
return n
}
注意 delete pfs;
這句代碼税娜,此刻客戶程序拿到的是 IFastString 指針坐搔,而 IFastString 的析構(gòu)函數(shù)并不是虛函數(shù),則 FastString 對(duì)象的析構(gòu)函數(shù)無法被執(zhí)行到巧涧。
假如把 IFastString 的析構(gòu)函數(shù)定義為虛函數(shù)的話薯蝎,析構(gòu)函數(shù)在 vtbl 中的位置隨著編譯器不同而不同,這會(huì)引起之前說的編譯器兼容性問題谤绳。
合適的解決方案是為 IFastString 增加一個(gè) Delete()
純虛函數(shù)占锯。并且讓實(shí)現(xiàn)類在函數(shù)實(shí)現(xiàn)時(shí)刪除自身:
// ifaststring.h
class IFastString
{
public:
virtual void Delete(void) = 0;
virtual int Length(void) const = 0;
virtual int Find(const char* psz) const = 0;
}
// faststring.h
class FastString : public IFastString
{
public:
void Delete(void)
{
delete this;
}
// 從略 ...
}
// 客戶程序
int f(void)
{
IFastString* pfs = CreateFastString("Deface me");
int n = pfs->Find("me");
pfs->Delete();
return n;
}
借助于上述技術(shù),我們可以安全地在一個(gè) C++ 環(huán)境中暴露 DLL 中的類缩筛,并且在另一個(gè) C++ 開發(fā)環(huán)境中訪問這個(gè)類消略。對(duì)于建立一個(gè)與編譯器廠商無關(guān)的可重用組件來說,這種能力是非常重要瞎抛、非常關(guān)鍵的艺演。
1.7 運(yùn)行時(shí)多態(tài)性
FastString DLL 只引出了一個(gè)符號(hào) CreateFastString
. 這使得客戶可以很方便地按需動(dòng)態(tài)裝入 DLL(使用 LoadLibrary), 并且使用 GetProcAddress 得到唯一的入口函數(shù):
IFastString* CallCreateFastString(const char* psz)
{
Static IFastString* (*pfn)(const char*) = 0;
if (!pfn)
{
const TCHAR szDll[] = __TEXT("FastString.dll");
const char szFn[] = "CreateFastString";
HINSTANSE hInstance = ::LoadLibrary(szDll);
if (hInstance)
{
*(FARPROC*)&pfn = GetProcAddress(h, szFn);
}
}
return pfn ? pfn(psz) : 0;
}
這項(xiàng)技術(shù)有幾個(gè)可能的應(yīng)用:
- 如果客戶沒有安裝 FastString.dll, 它可以避免產(chǎn)生異常;
- 減少進(jìn)程地址空間初始化的工作桐臊, DLL 只在需要的時(shí)候裝載胎撤;
- 允許客戶在同一接口的不同實(shí)現(xiàn)之間做出動(dòng)態(tài)的選擇(加載實(shí)現(xiàn)了同一接口的不同 DLL),下述代碼說明了這一點(diǎn):
IFastString* CallCreateFastString(const char* psz, bool bLeftToRight = true)
{
static IFastString* (*pfnlr)(const char*) = 0;
static IFastString* (*pfnrl)(const char*) = 0;
// 默認(rèn)使用 FastString.dll
IFastString* *(**ppfn)(const char*) = &pfnlr;
const TCHAR* pszDll = __TEXT("FastString.dll");
// bLeftToRight == false 時(shí)使用 FastStringRL.dll
if (!bLeftToRight)
{
pszDll = __TEXT("FastStringRL.dll");
ppfn = &pfnrl;
}
if (!(*ppfn))
{
const char szFn[] = "CreateFastString";
HINSTANSE hInstance = ::LoadLibrary(pszDll);
if (hInstance)
{
*(FARPROC*)ppfn = ::GetProcAddress(hInstance, szFn);
}
}
return (*ppfn) ? (*ppfn)(psz) : 0;
}
FastString.dll 和 FastStringRL.dll 都實(shí)現(xiàn)了 IFastString 接口断凶, CallCreateFastString 可以根據(jù)參數(shù)在運(yùn)行時(shí)選擇加載合適的 dll. 這就是這節(jié)題目中說的 運(yùn)行時(shí)多態(tài)性
這種運(yùn)行時(shí)多態(tài)性對(duì)于“利用二進(jìn)制組件建立動(dòng)態(tài)的復(fù)合系統(tǒng)”來說伤提,非常有幫助。
1.8 對(duì)象擴(kuò)展性
到現(xiàn)在為止所展現(xiàn)的技術(shù)使得客戶可以動(dòng)態(tài)地選擇并裝載二進(jìn)制文件认烁,從而可以隨著時(shí)間的推移不斷升級(jí)它們的實(shí)現(xiàn)肿男,而客戶無需重新編譯介汹。
然而,對(duì)象的接口卻不能隨著時(shí)間不斷進(jìn)化舶沛。一旦增刪或修改接口的函數(shù)嘹承,虛表就會(huì)發(fā)生改變,客戶就必須重新編譯來適應(yīng)這種改變如庭。更糟糕的是叹卷,改變接口的定義完全違背了接口的封裝性。
這意味著已經(jīng)接口是絕對(duì)不能改變的坪它,它即是語義的約定豪娜,也是二進(jìn)制結(jié)構(gòu)的約定。
盡管如此哟楷,但我們通常都會(huì)希望能夠在接口已經(jīng)設(shè)計(jì)好之后瘤载,為接口加入原先沒有預(yù)料到的新功能。下面的例子在接口的尾部增加了一個(gè)新方法卖擅,我們看看直接修改接口會(huì)造成哪些問題:
class IFastString
{
public:
virtual void Delete() = 0;
virtual int Length() = 0;
virtual int Find(const char* psz) = 0;
// 新增一個(gè)方法
virtual int FindN(const char* psz, int n) = 0;
}
上述修改得到的 vtbl 是原先 vtbl 的超集鸣奔,新增的方法將添加到 vtbl 的尾部。
下面看一下新老客戶和新老接口之間調(diào)用時(shí)是否會(huì)存在問題:
- 老客戶調(diào)用新接口:沒有問題惩阶,因?yàn)樾?vtbl 是老 vtbl 的超集挎狸;
- 新客戶調(diào)用老接口:有問題,因?yàn)槔?vtbl 中沒有 FindN 方法的函數(shù)指針断楷,新客戶試圖調(diào)用時(shí)會(huì)引起異常锨匆;
直接修改接口定義,這種技術(shù)的問題在于它打破了接口的封裝性冬筒。這個(gè)問題的解決方法是 “允許實(shí)現(xiàn)類暴露多個(gè)接口”(這樣一來新方法可以添加在新接口中恐锣,不會(huì)影響舊接口)。這可以用兩種途徑來實(shí)現(xiàn):
-
新增一個(gè)接口讓它繼承自原接口舞痰,再讓實(shí)現(xiàn)類繼承它土榴,這種做法適合于 新接口 is-a 老接口 的情形;
// ifaststring.h class IFastString2 : public IFastString { public: virtual int FindN(const char* psz, int n) = 0; } // faststring.h class FastString : public IFastString2 { // 從略 }
-
新增一個(gè)獨(dú)立的接口响牛,讓實(shí)現(xiàn)類同時(shí)繼承它和原接口玷禽,這種做法適合于 新接口和原接口 無關(guān)的情形;
// ipersistent.h class IPersistentObject { public: virtual void Delete() = 0; virtual bool Load(const char* pszFileName) = 0; virtual bool Save(const char* pszFileName) = 0; } // faststring.h class FastString : public IFastString, public IPersistentObject { // 從略 }
無論使用哪種方式呀打,客戶都可以通過 C++ 的運(yùn)行時(shí)類型識(shí)別(RTTI, Runtime Type Identification) 功能矢赁,在運(yùn)行時(shí)詢問實(shí)現(xiàn)類對(duì)象,來獲取需要的接口:
bool SaveString(IFastString* pfs, const char* pszFileName)
{
bool bResult = false;
// RTTI 詢問 pfs 指向的對(duì)象是否能導(dǎo)出為 IPersistentObject*
IPersistentObject* ppo = dynamic_cast<IPersistentObject*>(pfs);
if (ppo)
{
bResult = ppo->Save(pszFileName);
}
return bResult;
}
然而贬丛, RTTI 是一個(gè)編譯器極為相關(guān)的特征撩银。讓客戶去執(zhí)行 dynamic_cast 會(huì)引起之前討論的編譯器兼容性問題。一個(gè)簡(jiǎn)單的辦法是瘫寝,讓每個(gè)接口都暴露出一個(gè)跟 dynamic_cast 等價(jià)的方法:
// 基接口蜒蕾,定義所有接口都必須有的方法
class IExtensibleObject
{
public:
virtual void* Dynamic_Cast(const char* pszType) = 0;
virtual void Delete(void) = 0;
}
class IPersistentObject : public IExtensibleObject
{
public:
virtual bool Load(const char* pszFileName) = 0;
virtual bool Save(const char* pszFileName) = 0;
}
class IFastString : public IExtensibleObject
{
public:
virtual int Length() = 0;
virtual int Find(const char* psz) = 0;
}
實(shí)現(xiàn)類 FastString 利用 static_cast 來實(shí)現(xiàn) Dynamic_Cast:
void* FastString::Dynamic_Cast(const char* pszType)
{
if (strcmp(pszType, "IFastString") == 0)
{
return static_cast<IFastString*>(this);
}
else if (strcmp(pszType, "IPersistentObject") == 0)
{
return static_cast<IPersistentObject*>(this);
}
else if (strcmp(pszType, "IExtensibleObject") == 0)
{
return static_cast<IFastString*>(this);
}
return 0;
}
注意 Dynamic_Cast 的第三個(gè)判斷條件,當(dāng)客戶請(qǐng)求 IExtensibleObject 接口時(shí)焕阿,轉(zhuǎn)換的目標(biāo)是 IFastString* 而不是 IExtensibleObject*, 這是因?yàn)?IFastString 和 IPersistentObject 都繼承了 IExtensibleObject, static_cast<IExtensibleObject> 不能處理這種二義性咪啡。
要解決二義性的問題可以讓 IFastString 和 IPersistentObject 通過 虛繼承 的方式來繼承 IExtensibleObject. 然而,引入虛基類將導(dǎo)致在結(jié)果對(duì)象中引入不必要的運(yùn)行時(shí)復(fù)雜性暮屡,并且也會(huì)帶來編譯器兼容性問題撤摸。所以采用 虛基類 這種做法并不合適。
1.9 資源管理
上一節(jié)討論的使實(shí)現(xiàn)類暴露多個(gè)接口的技術(shù)褒纲,還有一個(gè)問題需要解決准夷。考慮如下客戶的代碼:
void f(void)
{
IFastString* pfs = 0;
IPersistentObject* ppo = 0;
pfs = CreateFastString("Feed Bob");
if (pfs)
{
ppo = (IPersistentObject*)pfs->Dynamic_Cast("IPersistentObject");
if (!ppo)
{
pfs->Delete();
}
else
{
ppo->Save("C:\\autoexec.bat");
ppo->Delete();
}
}
}
上述代碼不會(huì)出問題莺掠,但對(duì)客戶來說卻會(huì)造成困擾:對(duì)象最初是通過其 IFastString 接口暴露給用戶的衫嵌,最后卻通過 IPersistentObject 接口來銷毀。在簡(jiǎn)單的代碼中彻秆,這不會(huì)有什么問題楔绞,只要客戶能夠意識(shí)到 pfs, ppo 指向的是同一個(gè)對(duì)象就可以了。但在復(fù)雜的項(xiàng)目中唇兑,客戶不得不記住每個(gè)接口指針對(duì)應(yīng)的對(duì)象是哪個(gè)酒朵,并且對(duì)每個(gè)對(duì)象只能調(diào)用一次 Delete 方法。
簡(jiǎn)化這一問題的思路是扎附,將管理對(duì)象生命周期的責(zé)任推給對(duì)象實(shí)現(xiàn)部分蔫耽。一個(gè)簡(jiǎn)單的方法是讓每個(gè)對(duì)象都維護(hù)一個(gè)引用計(jì)數(shù),當(dāng)接口指針被復(fù)制的時(shí)候留夜,引用計(jì)數(shù)增加匙铡;當(dāng)接口指針被銷毀的時(shí)候,引用計(jì)數(shù)減少碍粥。我們修改一下 IExtensibleObject 接口:
class IExtensibleObject
{
public:
virtual void* Dynamic_Cast(const char* pszType) = 0;
virtual void DuplicatePointer(void) = 0; // 接口指針被復(fù)制時(shí)慰枕,調(diào)用此方法
virtual void DestroyPointer(void) = 0; // 接口指針不再有用時(shí),調(diào)用此方法
}
// faststring.h
class FastString : public IFastString
public IPersistentObject
{
int m_cPtrs;
public:
FastString (const char* psz) : m_cPtrs(0) {}
void DuplicatePointer(void)
{
m_cPtrs ++;
}
void DestroyPointer(void)
{
// 引用計(jì)數(shù)為零時(shí)即纲,銷毀自己
if (--m_cPtrs == 0) {
delete this;
}
}
}
此外具帮,F(xiàn)astString 中原有的函數(shù) CreateFastString, Dynamic_Cast 都進(jìn)行了復(fù)制指針的操作,所以要修改一下調(diào)用 DuplicatePointer :
IFastString* CreateFastString(const char* pszType)
{
IFastString* pfs = new FastString(psz);
if (pfs)
{
pfs->DuplicatePointer();
}
return pfs;
}
void* FastString::Dynamic_Cast(const char* pszType)
{
void* pvResult = 0;
if (strcmp(pszType == "IFastString")) {
pvResult = static_cast<IFastString*> this;
}
else if (strcmp(pszType == "IPersistentObject")) {
pvResult = static_cast<IPersistentObject*> this;
}
else if (strcmp(pszType == "IExtensibleObject")) {
pvResult = static_cast<IFastString*> this;
}
else
{
return 0;
}
// 復(fù)制出了新的指針低斋,增加引用計(jì)數(shù)
((IExtensibleObject*)pvResult)->DuplicatePointer();
return pvResult;
}
我們看看修改后的 客戶代碼:
void f(void)
{
IFastString* pfs = 0;
IPersistentObject* ppo = 0;
pfs = CreateFastString("Feed Bob");
if (pfs) {
ppo = (IPersistentObject*) pfs->Dynamic_Cast("IPersistentObject");
if (ppo) {
ppo->Save("C:\\autoexec.bat");
// ppo 用完了蜂厅,釋放
ppo->DestroyPointer();
}
// pfs 用完了,釋放
pfs->DestroyPointer();
}
}
修改后的客戶代碼里膊畴,每個(gè)指針都被看做了一個(gè)具有獨(dú)立生命周期的實(shí)體掘猿,客戶無需把接口指針與具體的對(duì)象聯(lián)系起來。
利用引用計(jì)數(shù)的方案使得一個(gè) 對(duì)象可以以非常一致的方式暴露多個(gè)接口唇跨。有了這種在運(yùn)行時(shí)進(jìn)行接口協(xié)商的機(jī)制稠通,這為創(chuàng)建一個(gè)"能夠隨時(shí)間不斷進(jìn)化"的動(dòng)態(tài)組建系統(tǒng)奠定了基礎(chǔ)衬衬。
1.10 我們走到哪兒了?
這一章從一個(gè)簡(jiǎn)單的 C++ 類開始改橘,然后一步一步討論如何把這個(gè)類做成可重用的二進(jìn)制組件滋尉。
- 以動(dòng)態(tài)鏈接庫的形式發(fā)布這個(gè)類,從物理上將這個(gè)類與客戶分離開來飞主;
- (封裝性)用接口和實(shí)現(xiàn)的概念狮惜,把實(shí)現(xiàn)細(xì)節(jié)封裝到二進(jìn)制協(xié)議后面,使得對(duì)象的布局能夠隨時(shí)間進(jìn)化碌识;
- (編譯器兼容)采用抽象基類作為定義接口的方法碾篡,使得防火墻以編譯器無關(guān)的 vtbl, vptr 形式出現(xiàn);
- (運(yùn)行時(shí)多態(tài))使用 LoadLibrary 和 GetProcAddress 在運(yùn)行時(shí)動(dòng)態(tài)選擇同一接口的不同實(shí)現(xiàn)筏餐,呈現(xiàn)運(yùn)行時(shí)的多態(tài)性开泽;
- (可擴(kuò)展性)使用類似 RTTI 的形式,在運(yùn)行時(shí)詢問對(duì)象是否實(shí)現(xiàn)了指定的接口魁瞪。這種結(jié)構(gòu)使得我們能夠擴(kuò)充接口的現(xiàn)有版本眼姐,并且也可以從單個(gè)對(duì)象暴露多個(gè)不相關(guān)的接口。
總而言之佩番,我們剛剛設(shè)計(jì)了組件對(duì)象模型(COM, Component Object Model).