虛擬函數表和C++抽象基類

一.虛擬函數表

      爲什麼用C++的純抽象基類來實現COM接口是可行的呢?這主要是由於純抽象類說定義的內存結構可以滿足COM對接口的需求。

當定義一個純抽象基類時,實際上定義的是一個內存塊的結構,且其所有的實現都是一些具有相同的基本結構的內存塊。但是,此內存只有在派生類中實現此抽象基類時纔會被分配。繼承抽象基類的派生類,也將繼承此內存結構。

interface IX
{
   virtual void __stdcall fx1() = 0;
   virtual void __stdcall fx2() = 0;
   virtual void __stdcall fx3() = 0;
   virtual void __stdcall fx4() = 0;
};


           由圖1可以看出,一個純抽象基類所定義的內存結構包含兩部分:虛擬函數表vTable和指向vTable的指針vTable Pointer。其中,指向抽象基類的指針指向此vTable指針,而vTable指針則指向虛擬函數表vTablevTable中包含一組指向虛擬函數實現的指針,例如圖1的第一項爲派生類中所實現的fx1函數的地址,第二項則是fx2的地址....

            COM接口的內存結構同C++編譯器爲抽象基類所生成的內存結構是相同的。因此,可以合用抽象基類來定義COM接口。所上面的COM抽象基類IX是一個COM接口,是由於其內存結構符合COM規範的要求。但是,對於一個COM接口還有其他的一些需求。例如,所有的COM接口都必須繼承一個名爲IUnkown的接口。這意味着,所有COM接口的前三個項都是相同的,其中保存的是IUnkown中三個成員函數的實現的地址。

二.vTable指針及實例數據

    vTable指針有什麼用途呢?

vTable指針在由抽象基類函數指針到函數的過程中增加了一個額外的級別,正是這一額外的級別給接口的實現帶來了極大的靈活性。

C++編譯器生成代碼時,實現抽象基類的類可能會將特定於實例的信息同vTable一塊保存。例如,CA實現了抽象基類IX,代碼如下:

class CA: public IX
{
public:

    //implement interface IX
    virtual void __stdcall fx1() {cout << “CA::fx1” << endl;}
    virtual void __stdcall fx2() {cout << “CA::fx2” << endl;}
    virtual void __stdcall fx3() {cout << “CA::fx3” << endl;}
    virtual void __stdcall fx4() {cout << “CA::fx4” << endl;}
     //constructor
     CA(int i):m_ix1(i), m_ix2(i*i), m_ix3(i*i*i)
     {}

     //interface data
     int m_ix1;
     int m_ix2;
     int m_ix3;
};

   對於上面我們說說的編譯器,vTableCA的類數據將如圖2所示。需要注意的是,實例數據理應是可以通過指向類的指針pA來訪問的,但是由於客戶通常並不知道實例數據是如何保存的,因此客戶也就無法訪問它們了。


雖然C++可以直接操作和使用實例數據,但COM組件絕不會訪問任何實例數據。在COM中,對一個組件的訪問只能通過函數完成,而絕不能直接通過變量來訪問。此外,純抽象基類只有虛擬函數,沒有任何實例數據

三:多重實例

         vTable的作用決不僅僅是給實例數據的保存提供一個方便的位置,實際上,同一個類的不同實例還可以共享同一vTable。如果我們們建立了CA的兩個不同的實例,那麼將會有兩組不同的實例數據,但不同的實例可以共享同一vTable及相同的實現(圖3)。如:

int main()
{

//first instance of CA.
   CA* pA1 = new CA(2);
//second instance of CA
  CA* pA2 = new CA(3);
}

可是,雖然COM組件可以使用vTable指針來共享vTable,但這一點並不是必須的。COM組件的每一個實例都有一個不同的vTable

四:不同的類,相同的vTable 

           接口的真正威力在於繼承此接口的所有類均可以被客戶按同一方式進行處理。例如,類CB也繼承了IX

class CB: public IX
{
 public:

     //implement interface IX
    virtual void __stdcall fx1() {cout << “CB::fx1” << endl;}
    virtual void __stdcall fx2() {cout << “CB::fx2” << endl;}
    virtual void __stdcall fx3() {cout << “CB::fx3” << endl;}
    virtual void __stdcall fx4() {cout << “CB::fx4” << endl;}
};

這樣一來,客戶就可以通過同一個IX指針來訪問CA和CB。

void f(IX* pIX)
{
    pIX->fx1();
    pIX->fx2();

}

int main()

{
   //create instance of CA
   CA* pA = new CA(2);

   //create instance of CB
   CB* pB = new CB;

//get IX pointer to CA

 IX* pIX = pA;
 f(pIX);

//get IX pointer to CB
  pIX = pB;
  f(pIX);
}

在這裏,我們將CACB都當作IX接口來使用,這是多態的一個例子。此例子的內存結構如圖4所示。兩個類CACB分別具有不用的實例數據、vTable以及實現。但因它們的vTable具有相同的格式而可以按相同的方式來訪問。


4的這種格式是編譯器根據抽象基類的定義而生成的。在某個類實現抽象基類時,此時他將被強制使用此種格式。對於組件也將是這樣的。當組件返回一個IX接口指針時,它必須保證此指針指向正確的結構。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章