欢迎转载。唯一要求是请注明出处和作者。共同努力吧。
作者:苦孩子
blog地址:http://blog.csdn.net/kuhaizi
QueryInterface的实现
它需要完成的是根据某个给定的返回指向相应接口的指针。若组件支持客户指定的接口,那么应返回S_OK已经湘阴的指针。若不支持。返回值应是E_NOINTERFACE并将相应的指针返回值置成NULL。下面是一个例子:
interface IX: IUnknown{/*…..*/};
interface IY: IUnknown{/*…..*/};
class CA: public IX, public IY{/*….*/};
注意IUnknown并不是虚拟基类。IX和IY并不能按虚拟方式继承IUnknown这是由于导致与com不兼容的vtbl。若IX和IY按虚拟方式继承IUnknown,那么IX和IY的vtbl中的头三个函数指向的将不是IUnknown的三成员函数。
下面是CA中的QueryInterface。此处的QueryInterface的实现可以返回三种不同接口的指针。IUnknown、IX、IY。不过要注意的是返回的IUnknown指针都是同一个,即使CA继承了两个IUnknown接口(其中的一个来自于IX,另一个来自于IY)。
HRESULT __stdcall CA::QueryInterface(const IID & iid,void **ppv)
{
if(iid == IID_IUnknown)
{
//the client wants the IUnknown interface
*ppv = static_cast<IX*>(this);
}
else if(iid == IID_IX)
{
//the client wants the IX interface
*ppv = static_cast<IX*>(this);
}
else if(iid == IID_IY)
{
//The client wants the IY interface
*ppv = stactic_cast<IY*>(this);
}
else
{
//we don’t support the interface the client
//wants. Be sure to set the resulting pointer
//to NULL
*ppv = NULL;
return E_NOINTERFACE;
}
}
在QueryInterface中不能使用case语句。因为接口标识符是一个结构而不是一个数。
在客户所查询的接口不被支持时,QueryInterface将*ppv置为NULL。这一点不但时COM规范所要求的。而且从其本身而言也时一种好的作法。
上面代码中将this指针保存在ppv中时。我们对之进行了类型的转换。保存在ppv中的值将会根据所用的转换不同而不同。例如将this指针转换成一个IX指针所得到的地址与将其转换成一个IY指针所得的地址时不同的。
static_cast<IX*>(this)!=static_cast<IY*>(this);
static_cast<void*>(this)!=static_cast<IY*>(this);
旧式的类型转换为:
(IX*)this !=(IY*)this
(void*)this != (IY*)this
将this指针进行类型转换会导致其值的改变。这一点主要是由于C++实现方式。
多重继承及类型的转换。
通常将一种类型的指针转换成另外一种类型并不会改变它的值。但为了支持多重继承,在某些情况下,C++必须改变类指针的值。许多C++程序员并不清楚多重继承的此种负面效果。
COM的需求之一:QueryInterface对所有的IUnknown接口查询请求都必须返回相同的指针。
QueryInterface的规则:
QueryInterface返回的总是同一IUnknown指针。
若客户曾经获取过某个接口,那么它将总能获取此接口。
客户可以再次获取已经拥有的接口。
客户可以返回到起始接口。
若能够从某个接口获取某特定接口,那么可以从任意接口都将可以获取此接口。
同一IUnkown接口。
为了确定两个接口是否指向同一组件,可以通过查询IUnknown接口,然后将返回值进行比较。这个决定两个接口是否指向同一组件。
客户可以再次获取已经拥有的接口。
所有的接口都继承了IUnknown,而许多函数都需要一个IUnknown指针作为参数。它们应该能够使用任何IUnknown指针来获取任何接口。
客户可以返回到起始接口。
若客户拥有一个IX接口指针并成功地使用它来查询了一个IY接口,那么它将可以使用此IY接口来查询一个IX接口。不论客户所拥有的接口是什么。它都可以返回起始时所用的接口。
QueryInterface是COM最为重要的部分。这主要是因为一个组件实际上就是由QueryInterface定义了。组件所支持的接口集就是QueryInterface能够为之返回接口指针的那些接口。
客户了解组件所支持接口的唯一方法是进行查询。这一点同C++有着很大的不同。在那里,某个类的客户可以知道该类的所有成员。这是因为它拥有此类的头文件。从某种意义上,COM更类似于在某次社交聚会上同某人会面。而与对他们进行工作面试有很大的不同,在进行工作面试时,被试者将提交一份介绍他们情况的个人简历。这份个人简历试类似于C++类的定义。而当在社会聚会上会面时,没有人会给对方提供个人简历。为了解对方的情况,必须向他们提问。这一点时类似于COM组件的。
各组件声明它是否属于某个特定的组件类别。客户可以在不创建组件的情况下获取此种信息。
QueryMnltipleInterface
分布式COM(DCOM)定义了一个新接口IMultiQI。此接口有一个新的 成员函数。QueryMultipleInterface。使用此函数,客户可以通过一次调用而查询组件的多个接口。这主要时为了减少数据在网络上来回传输的次数,以提高应用程序的效率。
引用计数
但客户从组件取得一个接口时,此引用计数值将增1。让客户使用完某个接口后,组件的引用计数值将减1。但引用计数值为0时,组件即可将自己从内存种删除。当创建某个已有接口的另外一个引用时,客户也将会增大相应组件的引用计数值。
AddRef可增大引用计数值,Release将减少这一值。
为正确地使用引用计数,需要了解以下三条简单的规则:
1.在返回之前调用AddRef。对于那些返回接口指针的函数 ,在返回之前应用相应的指针调用AddRef。这些函数包括QueryInterface及CreateInstance。这样当客户从这种函数得到一个接口后,它将无需调用AddRef。
2.使用完接口之后调用Release。在使用完某个接口之后应调用此接口的Release函数。
3.在赋值之后调用AddRef。在将一个接口指针赋给另外一个接口指针时,应调用AddRef。换句话说,在建议接口的另外一个应用之后应增加相应组件的引用计数。
一般而言。在给某个接口指定一个别名时,都应调用AddRef。在实际程序种,很难决定某些没有加上的AddRef和Release调用到底是优化寒士程序种的错误。计数引起的问题并不是那么容易就能够找出来的。
调用QueryInterface时,实际上时告诉相应的组件它想要使用某个接口。QueryInterface将调用所请求接口上的AddRef。当客户用完某个接口之后,它将调用此接口上的Release。只要组件的引用计数值不为零。那么它将一直出于内存种。当引用计数为零时,组件将把它从内存种删除。
为什么要选择为每一个接口单独维护一个引用计数而不是针对整个组件维护引用计数呢?这主要有两个原因,一是使用调试更为方便,另一个原因是支持资源的按需获取。
一、程度调试
假定在程序中忘记了对某个接口调用Release,就要检查使用了此组件所提供的所有接口的代码。但若组件支持对每一个接口分别维护一个引用计数,那么可以把查找的范围限制在 某个特定的接口上。在某些情况下这可以节省大量的时间。
二、资源的按需获取
在实现某个按接口时可能需要大量的内存或其他资源。对于此情况,可以在QueryInterface的实现中,在客户请求此接口时完成资源的分配。但若只对整个组件维护一个引用计数,组件将无法决定何时可以安全地将同此接口相关联地内存释放。但若对每一个接口分别维护一个引用计数,那么决定合适可以将此内存释放将会容易很多。
另外一种作法时在另外一个单独的组件中实现此种需要大量资源的接口。然后将指向此组件的接口传给客户。
AddRef和Release的实现实际上时非常简单的。实际上可以通过增大和减少某个数值而实现。例如下面的代码给出了一个实现的例子。
ULONG __stdcall AddRef()
{
return ++m_cRef;
}
ULONG __stdcall Release()
{
if(--m_cRef == 0)
{
delete this;
return 0;
}
return m_cRef;
}
AddRef将记录有组件引用计数值地成员变量m_cRef增大。而Release则将此值减少。并在此值为0时,将组件删除。在许多情况下,许多程序员可能会使用Win32的函数InterlockedIncrement和InterlockedDecrement来实现AddRef和Release。这两个函数可以确保在同一时刻只会有同一个线程来访问成员变量。根据Com对象所使用的线程模型的不同,多线程可能会引起一些问题。
ULONG __stdcall AddRef()
{
return InterlockedIncrement(&m_cRef);
}
ULONG __stdcall Release()
{
if(InterlockedDecrement(&m_cRef) == 0)
{
delete this;
return 0;
}
return m_cRef;
}
另外要注意的时AddRef和Release的返回值没有什么意义。只是在程序调试中才可能用的上。客户不应将此值当成时组件或其接口的精确引用数。
在QueryInterface和CreateInstance中都使用了AddRef。
HRESULT __stdcall CA::QueryInterface(const IID&iid,void **ppv)
{
if(iid == IID_IUnknown)
{
*ppv = static_cast<IX*>(this);
}
else if(iid == IID_IX)
{
*ppv = static_cast<IX*>(this);
}
else if(iid == IID_IY)
{
*ppv = static_cast<IY*>(this);
}
else
{
*ppv = NULL;
return E-NOINTERFACE;
}
static_cast<IUnknown*>(*ppv)->AddRef();
}
IUnknown * CreateInstance()
{
IUnknown * pI = *ppv = static_cast<IX*>(new CA);
pI->AddRef();
return pI;
}
这样当建立一个新组件时,实际上也将建立一个对此组件的引用。因此创建组件时,在将指针返回给客户之前,应该增大组件的引用计数值。这使得程序员可以不必在调用CreateInstance或QueryInterface之后记着去调用AddRef。
在某些情况下可以省去对AddRef和Release的调用。
引用计数的优化规则
应记住客户必须像每一个接口具有一个单独的引用计数值那样来处理各接口。因此,客户必须对不同的接口分别引用计数,即使他们的生命期是嵌套的。
一.输出参数规则
输出参数指的是给函数的调用者一个值的函数参数。在函数体中将设置此输出参数的值而不会使用调用者传进来的值。例如QueryInterface的第二个参数就是一个输入参数。
HRESULT QueryInterface(const IID&,void **);
任何在输出参数中或作为返回值返回一个新的接口指针的函数必须对此接口指针调用AddRef。这一规则同开头给出的“返回之前调用AddRef”规则是一样的。只不过是换了一种提法。QueryInterface函数的实现就遵循了这一规则。在返回之前对接口指针调用了AddRef。我们所实现的组件的CreateInstance函数也遵循了这一规则。
二.输入参数规则
输入参数指的是给函数传递某个值的参数。在函数体中将会使用这个值但却不会修改它或将其返回给调用者。在C++中,输入参数实际上就是那些按值传递的参数或常量。
对传入函数的接口指针。无需调用AddRef和Release,这是因为函数的生命期嵌套在调用者的生命期内。为记住这一点,可以想象将函数的代码以内联方式展开的情形。
三.输入-输出参数规则
在函数中,对于用输入-输出参数传递进来的接口指针。必须在给它赋另外一个接口指针值之前调用其Release。在函数返回之前,还必须对输出参数中所保存的接口指针调用AddRef。
四.局部变量规则
对局部复制的接口指针。由于它们只是在函数的生命期内才存在。因此无需调用AddRef和Release。这条规则实际是输入参数规则的直接结果。
五.全局变量规则
对于保存在全局变量中的接口指针,在将其传递给另外一个函数之前,必须调用其AddRef。由于此变量是全局性的,因此任何函数都可以通过调用其Release来终止其生命期。对于保存在成员变量中的接口指针。也应按此种方式进行处理。因为类中的任何成员函数都可以改变此种接口指针的状态。
六.不能确定时的规则
对于任何不能确定的情况,都应调用AddRef和Release对。