最近在看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);
书中对于这个问题的解释很简单,在外部组件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::func1 Base1::func2 Base2::anotherFunc1 Base2::anotherFunc2 ------------------------ Base1::func1 Base1::func2 -------Caution---------- Base2::anotherFunc1 Base2::anotherFunc2在调用Base1的函数时,实际运行的确实是Base2的函数!
可以分析得出,在由&d转换成Base2*时,指针值发生了变化,也就是说,新的指针pB1和&d的值已经不同了:
cout << "-------Pointer----------" << endl; cout << (int*)&d << endl; cout << (int*)pB1 << endl;
结果如下:
-------Pointer---------- 0012FE5C 0012FE60指针的值确实发生了变化,转换后的指针偏移了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,函数名什么的对于这些机器码没有什么意义,那些不过是高级语言给我们看的罢了。
以上是我个人的分析和总结,并不一定是真实的实现,因为我也在网上看到了一些不同的分析。欢迎大家一起讨论。