最近在看COM彙總技術時遇到一個關於QueryInterface的問題。在《COM技術內幕》和《COM原理與應用》中都是寥寥數句帶過,看起來很易理解,我卻看了許久才有所領悟。
先說明一下,為了節省篇幅,對於一些約定俗成的代碼和變數,下文不再進行說明,如內部組件指向外部組件的m_pUnknownOuter和外部組件指向內部組件的m_pUnknownInner等,這些內容在相關書籍都有描述。
問題描述:
在外部組件CB彙總內部組件CA時,內部組件的非委託未知介面示意如下:
struct INondelegatingUnknown{ virtual HRESULT __stdcall NondelegatingQueryInterface(const IID&, void**) = 0; virtual ULONG __stdcall NondelegatingAddRef() = 0; virtual ULONG __stdcall NondelegatingRelease() = 0;};
這裡,查詢函數為NondelegatingQueryInterface。
NondelegatingQueryInterface的實現示意代碼如下:
HRESULT __stdcall CA::NondelegatingQueryInterface(const IID& iid, void** ppv){ if (iid == IID_IUnknown) { *ppv = static_cast<INondelegatingUnknown*>(this); } else if (iid == IID_IY) { *ppv = static_cast<IY*>(this); } .....}
同時,CA還要實現內部組件的委託未知介面:
ULONG __stdcall CA::QueryInterface(const IID& iid, void** ppv){ return m_pUnknownOuter->QueryInterface(iid, ppv);}
現在假設外部組件CB實現了介面IX, 內部組件CA實現了組件IY,那麼根據上述兩本書中的描述,在CB查詢IY介面時使用如下代碼:
m_pUnknownInner->QueryInterface(IID_IY, ppv);
那麼問題來了,通過上述代碼來看,理論上應該會調用CA::QueryInterface(),而根據QueryInterface的實現,又會調用回外部組件的查詢函數CB::QueryInterface(),從而形成了一個死迴圈!而實際運行當然不會出現這種情況,在查詢IY介面時,會調用NondelegatingQueryInterface而非QueryInterface!原因何在?
書中對於這個問題的解釋很簡單,在外部組件CB建立CA時,擷取m_pUnknownInner即內部組件的IUnknown介面時,使用NondelegatingQueryInterface進行了查詢,注意該函數的實現,查詢IUnknown介面時對CA的this指標進行了強制轉換,轉換成了非委託未知介面。書中特意強調“通過這一轉換,我們可以保證返回的是一個非委託的未知介面指標,當向委託介面指標查詢IID_IUnknown時,他返回的將總是一個指向其自身的指標”。我不是很明白這段話的意思,但是從現象上看,正是由於這個強制轉換使得外部組件在查詢內部組件的介面時能夠正確運行。
其實這個問題涉及了一些很基礎的知識,在學習C++的時候我自以為理解了這些基礎,可是當遇到問題時甚至不知道原來和這些基礎的內容有關!
首先我在這裡推薦幾篇文章,對於理解這個問題很有協助:
http://blog.csdn.net/haoel/article/details/3081328
http://blog.csdn.net/haoel/article/details/3081385
這兩篇講解C++記憶體物件版面配置很好。如果讀者對這些內容瞭解並且對上述問題也很清楚那麼就不必看我獻醜了...謝謝...
好,繼續說問題。
簡單來說,問題是明明調用了QueryInterface函數,結果卻是調用了NondelegatingQueryInterface。那麼再看一下內部組件CA的資料結構:
class CA : public IY, public INondelegatingUnknown{public: virtual HRESULT __stdcall QueryInterface(); virtual ULONG __stdcall AddRef(); virtual ULONG __stdcall Release(); virtual HRESULT __stdcall NondelegatingQueryInterface(const IID&, void**); virtual ULONG __stdcall NondelegatingAddRef(); virtual ULONG __stdcall NondelegatingRelease();};
看到這段資料結構,再聯想之前的強制轉換,會不會有什麼想法呢?
如果沒有,那麼再看下IUnknown的資料結構:(注意這不是系統中的定義,而是對IUnknown的示意,不過也差不多就是了)
interface IUnknown{ virtual HRESULT __stdcall QueryInterface() = 0; virtual ULONG __stdcall AddRef() = 0; virtual ULONG __stdcall Release() = 0;};
比較NondelegatingQueryInterface的結構,可以發現二者在結構上一致的。
在《COM技術內幕》中還有這樣一段話“COM並不關心介面的名字是什麼,而只關心vtbl的結構。”這回是不是突然感覺好像明白了什嗎?
是的,因為IUnknown的結構和NondelegatingQueryInterface一致,因此在強制轉換時,將this強制轉換成了NondelegatingQueryInterface,而此時外部組件獲得的m_pUnknownInner指標的值並不是內部組件CA的地址,而是CA中NondelegatingQueryInterface結構的地址!讀者可能會疑惑,在CA的實現中並沒有NondelegatingQueryInterface結構啊?如果你看了我推薦了那兩篇部落格文章,此時你就可能已經完全明白了。不過,我們還是來一步步分析吧。
首先,我們要驗證的第一個問題是,對於多重繼承,將衍生類別的指標強制轉換成基類類型之後,是否就會出現和上述問題一些樣的現象?
範例程式碼如下:
#include <iostream>using namespace std;class Base1{public: virtual void func1() = 0; virtual void func2() = 0;};class Base2{public: virtual void anotherFunc1() = 0; virtual void anotherFunc2() = 0;};class Derived : public Base1, public Base2{public: virtual void func1() { cout << "Base1::func1" << endl; } virtual void func2() { cout << "Base1::func2" << endl; } virtual void anotherFunc1() { cout << "Base2::anotherFunc1" << endl; } virtual void anotherFunc2() { cout << "Base2::anotherFunc2" << endl; }};int main(){ Derived d; cout << "------------------------" << endl; d.func1(); d.func2(); d.anotherFunc1(); d.anotherFunc2(); cout << "------------------------" << endl; Base1* pB1 = (Base1*)&d; pB1->func1(); pB1->func2(); cout << "-------Caution----------" << endl; pB1 = (Base1*)(Base2*)&d; pB1->func1(); pB1->func2(); system("pause"); return 0;}
代碼中,最後的一個測試,我們將d先強轉成了Base2,然後又強轉成了Base1,那麼運行結果:
------------------------Base1::func1Base1::func2Base2::anotherFunc1Base2::anotherFunc2------------------------Base1::func1Base1::func2-------Caution----------Base2::anotherFunc1Base2::anotherFunc2
在調用Base1的函數時,實際啟動並執行確實是Base2的函數!
可以分析得出,在由&d轉換成Base2*時,指標值發生了變化,也就是說,新的指標pB1和&d的值已經不同了:
cout << "-------Pointer----------" << endl; cout << (int*)&d << endl; cout << (int*)pB1 << endl;
結果如下:
-------Pointer----------0012FE5C0012FE60
指標的值確實發生了變化,轉換後的指標位移了4 Byte,那麼為什麼會有這樣的結果?答案就是C++類的虛函數表。
在C++的類中,如果使用了繼承關係,類的結構中就會有一個虛函數表,讀者可以自己測試一下,如果是一個沒有任何內容的空類,其大小為1 Byte,這個是系統自動填滿的內容。如果是其中包含了一個虛函數,那麼其大小就為4 Byte,而這個4Byte就是虛函數表指標的大小。多重繼承的情況下,在類的結構中會有多個基類的虛函數表,比如上例,Derived類繼承了Base1和Base2,那麼其中就有2個虛函數表,在我們調用虛函數時,會從對應的虛函數表中進行查詢:
在多重繼承中,衍生類別中對於基類中虛函數表和各成員的排列順序與繼承的順序一致,最後才是衍生類別自己的成員:
由於這樣的資料結構,在進行強制轉換時,實際上是將虛函數表的指標傳出,故轉換後指標的值發生了變化。至於為什麼是傳的虛函數表的指標而不是某個成員的指標呢?因為在記憶體結構中虛函數表是位於最上部的,虛函數表類似於header。
好了,現在對於最開始的問題基本已經明白了。外部組件CB建立CA時需要擷取內部組件CA的IUnknown指標,建立過程中使用NondelegatingQueryInterface進行IUnknown的擷取,該函數中將指向CA組件自己的指標強制轉換成了非委託未知介面的指標,根據CA的繼承關係,轉換後的指標發生了變化,該指標實際上是NondelegatingUnknown的虛函數表的指標,因此,外部組件CB使用m_pUnknownInner查詢時,實際上使用的是NondelegatingUnknown其中的函數。
還有一個遺留的小問題:雖然我們擷取了NondelegatingUnknown的指標,可是函數名不同為什麼依然可以調用?還記得書中那句話麼:“COM並不關心介面的名字是什麼,而只關心vtbl的結構。”NondelegatingUnknown和Unknown在結構上是相同的,在傳遞給m_pUnknownInner時,發生了隱式轉換,所以根據函數在記憶體中的位置,可以找到對應函數,而且,虛函數的調用是運行時確定,運行時的程式不過是一堆0和1,函數名什麼的對於這些機器碼沒有什麼意義,那些不過是進階語言給我們看的罷了。
以上是我個人的分析和總結,並不一定是真實的實現,因為我也在網上看到了一些不同的分析。歡迎大家一起討論。