QueryInterface的本质初探

2022-09-15 11:25:03 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

转载请注明出处,版权归作者所有

lyzaily@126.com

yanzhong.lee

本文写给COM的初学者!

QueryInterface接口对COM的重要性不言而喻,该接口的实现有个规则——由QueryInterface返回的IUnknow接口指针必须相同,我的疑问是微软是如何使用C 实现这一COM规则的呢?请读者注意,我只探讨微软使用C 实现的COM,而不是其他公司使用其他语言实现的COM组件,当然无论什么公司使用十分语言,都必须遵循COM给出的规则。

为了实现该接口,COM给出了5个规则,具体的可以参考《COM技术内幕》一书。

为了得到该问题的答应,我们首先要分析一下C 类中的虚函数,以及这些虚函数如何在子类中被继承的。

最有说服力的莫过于真实的例子了,我还是给出在VS2005中写的例子吧,我先给出一个例子, 就回答了我上面提出的问题。

//给出一个纯虚基类

class CBase{ public: virtual int func(int param) = 0; private: };

//给出基类的第一个子类

class CTestA:public CBase{ public: CTestA(); ~CTestA(); virtual int func(int param); private: int m_value; }; int CTestA::func(int param) { m_value = param; this; //为了查看this值而添加 return 0; } CTestA::CTestA() { m_value = 0; } CTestA::~CTestA() { }

//给出基类的第二个子类

class CTestB:public CBase{ public: CTestB(); ~CTestB(); virtual int func(int param); private: int m_valueb; };

int CTestB::func(int param) { m_valueb = param; this; //为了查看this值而添加 return 0; } CTestB::CTestB() { m_valueb = 0; } CTestB::~CTestB() { }

//给出CTestA和CTestB的公共子类 class CTestSub:public CTestA,public CTestB { public: virtual int func(int param); private: };

int CTestSub::func(int param) { void *temPtr = NULL; temPtr = static_cast<CTestA*>(this); temPtr = static_cast<CTestB*>(this); return 0; }

//测试主函数

int _tmain(int argc, _TCHAR* argv[]) { CTestSub sub; CBase* ptr = static_cast<CTestA*>(&sub); //(1) CTestA* aPtr = static_cast<CTestA*>(&sub); //(2) CTestB* bPtr = static_cast<CTestB*>(&sub); //(3) ptr->func(1); //(4) aPtr->func(2); //(5) bPtr->func(3); //(6)

return 0; } 如果在页节点的子类中(如:CTestSub 类)实现了基类(如:CBase类)中声明的虚函数,则在(4)-(6)的函数调用中,我们进入的是叶节点类(CTestSub类)实现的func函数,而没有调用其父类CTestA或CTestB类中的func函数,这一点符合C 的规则的——C 中的规则是这样的,如果某个方法在类中被声明为virtual的,并在子类中已经重新实现了,我们在用指向父类的指针(该指针被赋值成子类对象的地址)调用该虚函数时,调用的是子类中实现的函数,这个子类不是其他的子类,而是其地址被赋给了父类指针的子类(这里就是sub对象对应的类);这种现象出现的原因是——子类中维护的虚函数表中有关func函数的地址已经被替换成子类中实现的func函数地址,所以真正调用的是CTestSub类实现的func函数;如果CTestSub不重新实现func函数,那么CTestSub虚函数表中的func处的地址仍然是父类中func的地址,在这样的情况下真正被调用的函数体当然是父类中实现的func函数了,如果调用的是父类中的函数func,那么func中使用的this指针当然是指向父类的实例了,这点规则和我们实验的结果一致的。

在CTestSub类中实现func时得出如下试验结果:

在该测试程序中我们三次进入func函数,其中的this值都是一样的为:0x12ff48,这个也是对象sub的地址。

那么如果我们没有实现CTestSub 类中的func函数时,会有什么结果呢?????这点疑问非常关键!

经过测试,如果不实现CTestSub 中的func函数,这上面(4)-(6)调用函数时,进入的是不同函数而且this指针也不一样,(4)和(5)进入的是CTestA实现的func函数,this指针指向的是sub对象中包含的CTestA对象的内存地址;(6)进入的是CTestB实现的func函数,this指针指向的是sub对象中包含的CTestB对象的内存地址。

这个实验的事实,说明了每个类对象都维护一个虚函数表(vtbl)CTestA、CTestB以及CTestSub 维护的是不同的vtbl,每个类按类声明时虚函数的顺序将本类中实现的虚函数指针填写到自己的虚表中,所以如果CTestSub 中重新实现了CTestA或CTestB中实现的虚函数,则在子类的虚函数表中将用子类实现的虚函数地址来覆盖父类中实现的函数地址;如果子类没有实现父类中实现的虚函数,则虚表中填充的仍然是父类中实现的虚函数地址;所有如果CTestSub 中不实现虚函数func,则CTestSub的实例sub中的虚表中保存的还是父类中的func函数地址,因此最终调用的就是CTestA或CTestB的func函数了。

讲到这里,大家对QueryInterface为什么会返回相同的IUnkown接口指针有所了解了吧!

这个问题的回答归总如下:

实现组件的类实现了IUnkown中的虚函数QueryInterface,这一点保证实现组件的类维护的虚表VTBL中存储的是该类中实现的QueryInterface函数地址,而不是父类中的QueryInterface函数地址。这样在QueryInterface使用的this指针就是组件的类的实例地址,而不是组件父类的实例地址了。要是的返回的IUnknow地址一致,则this指针指向组件类的实例是必需的。

其实这篇文章名字也可以称为《QueryInterface中使用的this指针究竟指向谁?》。呵呵,当然指向组件类的实例啊!从上面的例子可以知道,调用func时,如果真正调用的是父类的func实现,这时func中使用的this指针就是指向sub中的父类实例空间;如果真正调用的是CTestSub类实现的func函数,则此时func中使用的this指针就是指向CTestSub 的实例sub。COM的QueryInterface函数也就是使用了C 虚函数的这一技术规则;只要COM组件类实现了QueryInterface接口函数,无论怎么调用该函数,该函数中使用的this指针始终指向组件类的实例。总之一句话,调用哪个类实现的func函数,那么func中使用的this指针就指向哪个类的实例。

以上解释可以能比较啰嗦,但是看官要定下神来慢慢缕缕就能理解QueryInterface的本质了,要通晓COM本质,这点必须弄明白,因为QueryInterface对COM来说实在是太重要了。

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/162932.html原文链接:https://javaforall.cn

0 人点赞