《COM本質(zhì)論》學(xué)習(xí)第一章

第 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è)問題:

  1. 占用內(nèi)存和磁盤吁恍。源代碼直接集成到客戶程序中客蹋,多個(gè)客戶程序運(yùn)行盐捷,也會(huì)有多個(gè) FastString.obj 被加載到內(nèi)存中;
  2. 難以替換辜腺。源代碼一旦鏈接到客戶程序休建,就必須重新編譯客戶程序才有可能替換。

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è)問題:

  1. 鏈接器:由于 名稱改編 機(jī)制砌些,當(dāng)客戶程序與 FastString 的編譯器不同時(shí),很可能無法鏈接成功;
  2. 編譯器:不同編譯器有自己實(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ì)受到影響浮创。

但句柄類這種方式也存在問題:

  1. 每次都通過句柄類來轉(zhuǎn)發(fā)函數(shù)調(diào)用,有一定開銷且比較繁瑣砌函,容易寫錯(cuò)斩披;
  2. 編譯器\鏈接器問題仍然沒有得到解決。

1.6 抽象基類作為二進(jìn)制接口

“接口與實(shí)現(xiàn)分離” 技術(shù)確實(shí)可以解決封裝性的問題讹俊,但它的形式需要更改一下垦沉,以解決 編譯器\鏈接器 的兼容性問題。

解決這一問題的關(guān)鍵思路在于 虛函數(shù)調(diào)用機(jī)制 仍劈。它的運(yùn)行原理如下:

  1. 編譯器在后臺(tái)為每個(gè)包含虛函數(shù)的類產(chǎn)生一個(gè)靜態(tài)函數(shù)指針數(shù)組厕倍。這個(gè)數(shù)組被稱為虛函數(shù)表(vtbl);
  2. 虛函數(shù)表中包含了這個(gè)類的所有虛函數(shù)的函數(shù)指針;
  3. 該類的每個(gè)實(shí)例對(duì)象都包含一個(gè)不可見的數(shù)據(jù)成員,被稱為虛函數(shù)指針(vptr)贩疙,它指向虛函數(shù)表;
  4. 客戶調(diào)用虛函數(shù)的時(shí)候讹弯,先根據(jù) vptr 找到 vtbl, 然后根據(jù)偏移量找到函數(shù)指針,進(jìn)行調(diào)用;

虛函數(shù)調(diào)用機(jī)制有兩個(gè)特點(diǎn):

  1. 幾乎所有的編譯器廠商都采用了上述 vtbl, vptr 的方式來實(shí)現(xiàn)該機(jī)制这溅,這使得它在不同編譯器上是等價(jià)的组民。也就是說它具備 編譯器 兼容性;
  2. 這一機(jī)制的實(shí)現(xiàn)中不涉及任何符號(hào)悲靴,也就不涉及 名稱改編 問題臭胜,所以它具備 鏈接器 兼容性;

因此对竣,我們可以把 虛函數(shù)表 作為 客戶程序與庫 之間的 二進(jìn)制協(xié)議:

  1. 客戶程序編譯器根據(jù) 接口類 生成虛表庇楞;
  2. 實(shí)現(xiàn)類 繼承自 接口類榜配,以保證兩者的虛表結(jié)構(gòu)一致否纬;
  3. 庫將 實(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)用:

  1. 如果客戶沒有安裝 FastString.dll, 它可以避免產(chǎn)生異常;
  2. 減少進(jìn)程地址空間初始化的工作桐臊, DLL 只在需要的時(shí)候裝載胎撤;
  3. 允許客戶在同一接口的不同實(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ì)存在問題:

  1. 老客戶調(diào)用新接口:沒有問題惩阶,因?yàn)樾?vtbl 是老 vtbl 的超集挎狸;
  2. 新客戶調(diào)用老接口:有問題,因?yàn)槔?vtbl 中沒有 FindN 方法的函數(shù)指針断楷,新客戶試圖調(diào)用時(shí)會(huì)引起異常锨匆;

直接修改接口定義,這種技術(shù)的問題在于它打破了接口的封裝性冬筒。這個(gè)問題的解決方法是 “允許實(shí)現(xiàn)類暴露多個(gè)接口”(這樣一來新方法可以添加在新接口中恐锣,不會(huì)影響舊接口)。這可以用兩種途徑來實(shí)現(xiàn):

  1. 新增一個(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
    {
        // 從略
    }
    
  2. 新增一個(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).

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末众旗,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子趟畏,更是在濱河造成了極大的恐慌贡歧,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赋秀,死亡現(xiàn)場(chǎng)離奇詭異利朵,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)猎莲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門绍弟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人著洼,你說我怎么就攤上這事樟遣。” “怎么了身笤?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵豹悬,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我液荸,道長(zhǎng)瞻佛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任娇钱,我火速辦了婚禮伤柄,結(jié)果婚禮上绊困,老公的妹妹穿的比我還像新娘。我一直安慰自己适刀,他們只是感情好秤朗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蔗彤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪疯兼。 梳的紋絲不亂的頭發(fā)上然遏,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音吧彪,去河邊找鬼待侵。 笑死,一個(gè)胖子當(dāng)著我的面吹牛姨裸,可吹牛的內(nèi)容都是我干的秧倾。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼傀缩,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼那先!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赡艰,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤售淡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后慷垮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體揖闸,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年料身,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了汤纸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡芹血,死狀恐怖贮泞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情幔烛,我是刑警寧澤隙畜,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站说贝,受9級(jí)特大地震影響议惰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜乡恕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一言询、第九天 我趴在偏房一處隱蔽的房頂上張望俯萎。 院中可真熱鬧,春花似錦运杭、人聲如沸夫啊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撇眯。三九已至,卻和暖如春虱咧,著一層夾襖步出監(jiān)牢的瞬間熊榛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工腕巡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留想幻,地道東北人籽御。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓元旬,卻偏偏與公主長(zhǎng)得像岂昭,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子车伞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

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

  • 一择懂、溫故而知新 1. 內(nèi)存不夠怎么辦 內(nèi)存簡(jiǎn)單分配策略的問題地址空間不隔離內(nèi)存使用效率低程序運(yùn)行的地址不確定 關(guān)于...
    SeanCST閱讀 7,806評(píng)論 0 27
  • 動(dòng)態(tài)調(diào)用動(dòng)態(tài)庫方法c/c++linuxwindows 關(guān)于動(dòng)態(tài)調(diào)用動(dòng)態(tài)庫方法說明 一、 動(dòng)態(tài)庫概述 1另玖、 動(dòng)態(tài)庫的...
    KINGZ1993閱讀 13,911評(píng)論 0 10
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理休蟹,服務(wù)發(fā)現(xiàn),斷路器日矫,智...
    卡卡羅2017閱讀 134,652評(píng)論 18 139
  • 這篇文章是我之前翻閱了不少的書籍以及從網(wǎng)絡(luò)上收集的一些資料的整理赂弓,因此不免有一些不準(zhǔn)確的地方,同時(shí)不同JDK版本的...
    高廣超閱讀 15,599評(píng)論 3 83
  • 一哪轿、創(chuàng)建Workspace 1盈魁、創(chuàng)建Workspace文件夾,文件夾的名字可以直接使用項(xiàng)目名窃诉。 2杨耙、打開Xcode...
    灬暮Se丶毛蟲閱讀 460評(píng)論 0 2