本次讲COM的另一大可重用特性——聚合,前文讲了包容其实相当于客户程序的角色,而本文说的聚合则完全体现了可重用性。
1.聚合原理
如下图(借用《COM原理与应用》)
对象B只实现了接口IOtherInterface,但是当你向对象B请求ISomeInterface时,对象B直接把对象A的ISomeInterface暴露给客户程序,调用ISomeInterface工作时和对象B没有任何关系,这就是聚合的过程,顾名思义可以把各种接口简单的聚合到此处使用。
但是并不是所有对象都能被聚合,聚合过程必须对应的对象支持才行。
以此处对象A和对象B为例,虽然对象B聚合了对象A的ISomInterface,然而在客户程序看来,并不知道从对象B处拿到的ISomeInterface实际上是对象A的接口,这一过程对于客户程序是不可见的。这就涉及到一个问题,用户认为ISomeInterface是对象B的,自然想从ISomeInterface查询对象B的其他接口,如IOtherInterface,而此时ISomeInterface是对象A的,显然就无法直接完成这一过程了,这就是矛盾所在。
那要怎么办呢,好说!我们在请求对象A的ISomeInterface接口时,告诉它这个接口是给外部聚合用的,这样从ISomeInterface查询对象B的接口时,直接调用对象B来完成即可。
具体CoCreateInstance原型如下
STDAPI CoCreateInstance( REFCLSID rclsid,LPUNKNOWN pUnkOuter,DWORD dwClsContext,REFIID riid,LPVOID * ppv );
可以看到,这里可以传入一个pUnkOuter参数,这个参数表明了是哪个外部对象在聚合本对象,为NULL时表明没有被聚合。
2.实现原理
如下图
实现对象A时指明一个委托IUnknown和一个非委托IUnknown,非委托IUnknown完成之前IUnknown接口工作,委托IUnknown则根据传入的pUnkOuter参数判断是调用非委托IUnknown接口还是pUnkOuter接口完成具体工作。
3.虚函数和多重继承
在具体实现聚合程序前,需要先搞懂虚函数和多重继承的特性,如下程序
class IInterfaceA { public: virtual void A_PrintAge(int age) = 0; }; class IInterfaceAA : public IInterfaceA { }; class IInterfaceB { public: virtual void B_PrintAge(int age) = 0; }; class IInterfaceBB : public IInterfaceB { }; class TestPrint: public IInterfaceAA,public IInterfaceBB { public: virtual void A_PrintAge(int age) { cout << __FUNCTION__ << " My Age is: " << age << endl; } virtual void B_PrintAge(int age) { cout << __FUNCTION__ << " My Age is: " << age << endl; } };具体的类继承结构如下:
IInterfaceA IInterfaceB | | InterfaceAA InterfaceBB |_______________| | TestPrint
TestPrint中存在两个虚表,具体 使用接口指针接对应对象指针时会命中其中一个分支,调用对应接口功能。
如下两个分别命中IInterfaceA和IInterfaceB功能
IInterfaceA* pA = (IInterfaceA*)p; //命中IInterfaceA分支 pA->A_PrintAge(10); IInterfaceAA* pAA = (IInterfaceAA*)p; //命中IInterfaceA分支 pAA->A_PrintAge(10); cout << endl; IInterfaceB* pB1 = (IInterfaceB*)p; //命中IInterfaceB分支 pB1->B_PrintAge(10); IInterfaceBB* pBB1 = (IInterfaceBB*)p; //命中IInterfaceB分支 pBB1->B_PrintAge(10);
但是这里还存在一个重要概念, 相同的接口结构,只要虚表结构(参数类型、个数、返回值)相同,就可以互换,记住这个特性,这是实现 聚合的关键,如下
TestPrint *p = new TestPrint; IInterfaceA* pA = (IInterfaceA*)p; //命中IInterfaceA分支 pA->A_PrintAge(10); IInterfaceB* pB = (IInterfaceB*)pA; //IInterfaceA虚表转成IInterfaceB虚表 pB->B_PrintAge(10); IInterfaceAA* pAA = (IInterfaceAA*)p; //命中IInterfaceA分支 pAA->A_PrintAge(10); IInterfaceBB* pBB = (IInterfaceBB*)pAA; //IInterfaceAA虚表转成IInterfaceBB虚表 pBB->B_PrintAge(10); pB = (IInterfaceB*)pAA; //IInterfaceAA虚表转成IInterfaceB虚表 pB->B_PrintAge(10); cout << endl; IInterfaceB* pB1 = (IInterfaceB*)p; //命中IInterfaceB分支 pB1->B_PrintAge(10); IInterfaceA* pA1 = (IInterfaceA*)pB1; //IInterfaceB虚表转成IInterfaceA虚表 pA1->A_PrintAge(10); IInterfaceBB* pBB1 = (IInterfaceBB*)p; //命中IInterfaceB分支 pBB1->B_PrintAge(10); IInterfaceAA* pAA1 = (IInterfaceAA*)pBB1; //IInterfaceBB虚表转成IInterfaceAA虚表 pAA1->A_PrintAge(10); pA1 = (IInterfaceA*)pBB1; //IInterfaceBB虚表转成IInterfaceA虚表 pA1->A_PrintAge(10);
IInterfaceA和IInterfaceB的*_PrintAge函数有相同的接口,因此 可以用IInterfaceB的接口去承接IInterfaceA的内容,此时调用B_PrintAge其实就是调用IInterfaceA->A_PrintAge。
4.聚合的实现
还是用之前的例子,这里对象B只实现IName接口,当客户程序查询IAge接口时,直接返回对象A的IAge接口。
先改造对象A支持聚合
1.定义非委托IUnknown接口
如下
//非委托IUnknown接口 class INondelegationUnknown { public: virtual HRESULT STDMETHODCALLTYPE NondelegationQueryInterface( _In_ REFIID riid,_Out_ void **ppvObject) = 0; virtual ULONG STDMETHODCALLTYPE NondelegationAddRef( void) = 0; virtual ULONG STDMETHODCALLTYPE NondelegationRelease( void) = 0; };它的结构和IUnknown结构一样,因此可以用IUnknown指针来接收它的指针。
2.委托IUnknown接口实现
创建对象A时记下外部IUnknown接口对象,
CPeople::CPeople(IUnknown* pUnknownOuter) { m_pUnknownOuter = pUnknownOuter; g_EasyComNumber++;//组件引用计数 this->m_nRef = 0; }
在委托接口中进行判断选择,m_pUnknownOuter为NULL则调用非委托接口,不为NULL则调用m_pUnknownOuter外部接口
//IUnknown 委托接口 HRESULT STDMETHODCALLTYPE CPeople::QueryInterface( _In_ REFIID riid,_Out_ void **ppvObject ) { if (NULL != m_pUnknownOuter) { return m_pUnknownOuter->QueryInterface(riid,ppvObject); } else { return NondelegationQueryInterface(riid,ppvObject); } } ULONG STDMETHODCALLTYPE CPeople::AddRef( void ) { if (NULL != m_pUnknownOuter) { return m_pUnknownOuter->AddRef(); } else { return NondelegationAddRef(); } } ULONG STDMETHODCALLTYPE CPeople::Release( void ) { if (NULL != m_pUnknownOuter) { return m_pUnknownOuter->Release(); } else { return NondelegationRelease(); } }
3.非委托IUnknown接口实现
HRESULT STDMETHODCALLTYPE CPeople::NondelegationQueryInterface( _In_ REFIID riid,_Out_ void **ppvObject ) { if (riid == IID_IUnknown) { *ppvObject = (INondelegationUnknown*)this;//强制返回的是非委托IUnknown接口,注意虚函数和多重继承的使用 ((INondelegationUnknown*)this)->NondelegationAddRef(); } else if (riid == IID_IAge) { *ppvObject = (IAge*)this;//委托IUnknown接口 ((IAge*)this)->AddRef(); } else if (riid == IID_IName) { *ppvObject = (IName*)this;//委托IUnknown接口 ((IName*)this)->AddRef(); } else { *ppvObject = NULL; return E_NOINTERFACE; } return S_OK; } ULONG STDMETHODCALLTYPE CPeople::NondelegationAddRef( void ) { m_nRef++; return (ULONG)m_nRef; } ULONG STDMETHODCALLTYPE CPeople::NondelegationRelease( void ) { m_nRef--; if (m_nRef == 0) { delete this; return 0; } return (ULONG)m_nRef; }
这里AddRef和Release是正常IUnknown实现, 最关键的在于这里的非委托QueryInterface的实现,注意如下几点:
1.查询IUnknown接口时强制返回的是非委托IUnknown接口,增加的引用计数也是自己的
2.查询其它接口时返回的对象支持的接口都是委托IUnknown接口,增加的引用计数也是调用委托AddRef进行的
4.创建函数
//IClassFactory HRESULT STDMETHODCALLTYPE CPeopleFactory::CreateInstance( _In_ IUnknown *pUnkOuter,_In_ REFIID riid,_Out_ void **ppvObject ) { if (NULL!=pUnkOuter && IID_IUnknown!=riid) { return CLASS_E_NOAGGREGATION; } CPeople *pObj = NULL; HRESULT hr = S_FALSE; *ppvObject = NULL; //创建组件对象 pObj = new CPeople(pUnkOuter); if (pObj == NULL) { return hr; } //获得非托管第一个接口指针 hr = pObj->NondelegationQueryInterface(riid,ppvObject); if (S_OK != hr) { delete pObj; } return hr; }这里注意两点:
1.外部聚合本对象时,只能传入pUnkOuter的同时查询IUnknown接口,这样做是为了实现方便
2.此处使用的非委托NondelegationQueryInterface查询非委托IUnknown接口,外部仍然可以用IUnknown接口接收,这是前面讲的虚函数的特性,不不再赘述
然后,对象B中实现对象A的初始化等工作如下
1.对象A的初始化
HRESULT CPeople::Init() { IUnknown* pUnknownOuter = (IUnknown*)this; HRESULT hr = CoCreateInstance(CLSID_EasyComPeople,pUnknownOuter,CLSCTX_INPROC_SERVER,IID_IUnknown,(LPVOID*)&m_pUnknownInner); if (Failed(hr)) { std::cout << "Create EasyCom Fail In EasyComEx2" << endl; return E_FAIL; } //本次聚合IAge接口,因此尝试查询IAge接口是否存在 IAge* pAge = NULL; hr = m_pUnknownInner->QueryInterface(IID_IAge,(LPVOID*)&pAge); if (Failed(hr)) { m_pUnknownInner->Release(); return E_FAIL; } pAge->Release(); //此处查询IID_IAge接口,调用的是[CLSID_EasyComPeople]委托IUnknown接口, //判断传入的pUnknownOuer非NULL,故调用外部AddRef,这里pAge Release也是调用的外部Release接口 //在潘爱民的《COM原理与应用》类似实现此处直接使用pUnknownOuter->Release,容易造成误解 return S_OK; }这里得到对象A的IUnknown指针, pAge接口AddRef和Release调用的都是外部对象的。
2.对象B查询IAge接口时
if (riid == IID_IAge) { return m_pUnknownInner->QueryInterface(IID_IAge,ppvObject); }直接查询返回对象A的IAge接口,而且由于此时IAge接口位于委托IUnknown分支上,查询其他接口时判断pUnknownOuter非NULL,会再调用对象B即pUnknownOuter查询接口。
下面看看何时调用初始化和析构
3.工厂对象中调用初始化
//IClassFactory HRESULT STDMETHODCALLTYPE CPeopleFactory::CreateInstance( _In_ IUnknown *pUnkOuter,_Out_ void **ppvObject ) { CPeople *pObj = NULL; HRESULT hr = S_FALSE; *ppvObject = NULL; //创建组件对象 pObj = new CPeople; if (pObj == NULL) { return hr; } //Init调用前引用计数为0,Init中先AddRef后Release会导致对象被销毁,所以此处先AddRef再Init最后再Release //这是COM引用计数的常用方法 pObj->AddRef(); //初始化包容对象 hr = pObj->Init(); if (Failed(hr)) { std::cout << "EasyComEx2 Init Fail" << std::endl; delete pObj; return hr; } //获得非托管第一个接口指针 hr = pObj->QueryInterface(riid,ppvObject); if (S_OK != hr) { delete pObj; } pObj->Release(); return hr; }
4.释放对象A接口
CPeople::~CPeople() { g_EasyComNumber--;//组件引用计数 //同样遵循COM使用规则,因为m_pAge->Release会调用外部Release,这里先AddRef,然后再Release //然而此处计数已经为0了,AddRef后再m_pAge->Release会导致引用计数为0,再次delete,造成递归 //所以只好先将引用计数强制指定为1,反正马上对象就要销毁了,设成多少也无所谓了,也不用Release了 //这里是《COM原理与应用》书上用到代码,其实m_pAge压根就用不到,因为直接通过m_pUnknownInner查询了,因此此段代码实属多余 /*m_nRef = 1; IUnknown* pUnknownOuter = this; pUnknownOuter->AddRef(); if (m_pAge!=NULL) { m_pAge->Release(); }*/ if (m_pUnknownInner != NULL) { m_pUnknownInner->Release(); } }析构函数中只需简单的Release对象A接口即可,这里《COM原理与应用》将此处复杂化了,详细看注释。
原创,转载请注明来自http://blog.csdn.net/wenzhou1219