}
intm_Count;
};
main()
{
IA*pIa;
IB*pIb;
Ca*pCa=newCa
(2);
pIa=pCa;
pIa->func1();
pIa->func2();
pIb->func3();
pIb->func4();
deletepCa;
}
//////--test.cend--//////
上例中,定义了IA,IB两个接口,你可以注意到他们所有的成员函数都
被声明为virtual,并且在函数末尾用=0做了结束。
类似这样的函数
我们在C++中称之为纯虚函数,如果整个的类都由纯虚函数组成,那么
这个类就叫做抽象基类。
抽象基类本身由于没有实体函数与变量,所以
并不分配内存。
一般它的用途是为派生类指定内存结构。
打个比方来说,
就好像把房子分割成很多小间,规定以后哪些小间应该放什么(函数的
实体)但具体的东西则要等派生类来填放。
这里有一个概念需要说明一下:
组件并不是类,上面我们用一个类就实
现了两组接口,同样我们也可以用它来实现更多接口。
组件本身其实只
是一个接口集及其实现的集合。
一个组件可能包含了多个接口,每一个
接口都有各自的实现。
同时,接口并非总是继承的,COM规范没有要求实现某个接口的类必
须从那个接口继承。
这是因为客户并不了解COM组件的继承关系。
对接口
的继承只不过是一种实现细节而已。
下面将介绍QueryInterface函数。
这个函数被用来查询其他接口。
客户
于组件之间的通讯是通过接口完成的。
哪怕是客户查询其他一个组件时,
也需要通过一个接口(换而言之,如果一个组件不支持这个接口,那他
一定不是一个COM组件)这个接口的名字叫IUnknown,它有三个函数,如
下所示:
interfaceIUnknown
{
virtualHRESULT__stdcall
QueryInterface(constIID&iid,void**ppv)=0;
virtualULONG__stdcallAddRef()=0;
virtualULONG__stdcallRelease()=0;
};
COM组件的所有接口都继承了IUnknown,这样一来,每一个接口的前三个函数都
是QueryInterface,这就是的所有的COM接口都可以被当成是IUnknown来处理。
客户只要通过一个CoCreateInstance函数就可以创建该组件的实例并且获取其
IUnknown*。
HRESULT__stdcallCoCreateInstance(
constCLSID&clsid,
IUnknown*pIUnknownOuter,
DWORDdwClsContext,
constIID&iid,
void**ppv
);
下面的CODE演示创建一个组件:
extern"C"constGUIDCLSID_COM1=(
0x32bb8230,0xb41b10x11cf,
0xa6,0xbb,0x0,0x80,0xc7,0xb2,0xd6,0x82);
extern"C"constGUIDIID_IX=(
0x32bb8230,0xb591c0x11ff,
0xc1,0xb0,0xc7,0xf8,0x21,0x35,0x1c,0x2f);
CoInitialize();
Ia*pIx=NULL;
HRESULThr=:
:
CoCreateInstance(
CLSID_COM1,
NULL,
CLSCTX_INPROC_SERVER,
IID_IX,
(void**)&pIx);
if(SUCCEEDED(hr))
{
pIx->Fx();
pIx->Release();
}
extern"C"constGUID其实是所谓的"全局唯一标示符"(Globally
UniqueIdentifier)。
我们规定用它来表示不同的接口。
换而言之,如
果你发现有两个GUID完全相同,你完全有理由相信他们标示的是同一个
接口。
(有专门的算法来产生该结构,确保它在时间和空间上都是唯一的。
)
接下来的CoInitialize函数初始化COM库。
这一步是非常重要的,如果
没有初始化,以后进行的操作都将失败。
下面我们来看看HRESULT值。
这是一个32位的返回值,其最高位表示函
数调用是否成功。
第十六位包含的就是函数的返回值,其余的15位包含的
是此类型以及返回值起源的更详细信息。
为了确定函数调用是否成功,需
要使用SUCCEED和FAILED宏。
CoCreateInstance函数是由COM函数库提供的,它的作用就是按照查询的
组件和接口到系统中去寻找其所在的文件(一般总是EXE或者DLL文件)然
后创建该组件并查询其接口。
一般来说,这个函数的具体实现是与系统相
关的,以后将会提到,在windows系统中,将查询注册表已确定某个特定
的组件在哪个文件中。
上例查询的是CLSID_COM1这个组件,由于一个组件可能包含有多个接口,
所以我们使用IID_IX来制定所需要的接口,CLSCTX_INPROC_SERVER是一个
常数,其指定组件所在的是一个DLL(由于DLL运行在客户的内存空间,所
以可以称为是进程内组件)。
最后一个参数传入的是接口指针,它将返回
查询到的接口指针。
可以想见,一个组件指针可能同时被几个客户所使用,所以需要一种手
段来让组件实例知道自己正在被几个客户所使用,这样他才能再合适的时
候销毁自己以让出内存空间。
如果销毁的实际不当,比如还有个指针正在
使用中,那么以后对该指针的调用就将失败并且用户程序将崩溃。
COM采用
相当简单的一种手段来进行所谓的引用计数:
维护一个组件或接口的全局
变量,但该变量的值为零时,销毁自己的时间就到了。
CoCreateInstance
实际上产生了该组件的实例,并在内部已经调用IUnknown的AddRef()函数
来将引用计数置1了。
正因为如此,例子最后调用的Release()函数就是做
了清理工作:
这个接口指针已经完成了它的工作,所以调用Release()告
诉它:
把你的引用计数减一。
如果不这样做,组件将永远保留在内存中,
直到应用程序结束的时候才从栈中被清除。
对AddRef和Release函数的调用是为了更好的控制组件的生命期,当然如
果处理得当,可以适当的减少AddRef/Release对以提高性能。
一种特殊的
情况就是当一个组件的生命期完全被包含在另一个组件内时,我们对被包含
的那个组件可以不予计数。
我不准备详细讨论优化问题,因为对于一般应
用来说,保证程序的强壮和稳定才是最重要的。
这里还得介绍一下ProgID。
ProgID其实程序员给某个CLSID指定的易记
名。
某些语言如visualbasic使用ProgID而非CLSID来表示组件。
这里请注
意,程序员对ProgID的命名只不过是遵循一个约定俗成的规定,并没有对
具体的实现有任何的强制标准,所以其名字发生冲突也是有可能的。
一般
来说,ProgID具有如下格式:
..
以我的注册表为例:
INSHandler.INSHandler.1
ImgUtil.CoSniffStream.1
StaticMetafile
Netscape.Help.1
不过由于ProgID没有专门的命名规则,所以出现不同于上述格式的名字
也是完全有可能的。
有时候客户并不关心它所连接的组件版本,换而言之,
客户只需要知道该组件存在就心满意足了。
所以,组件经常会有一个与版
本无关的ProgID,此ProgID被映射成是所安装的最新版本的组件。
完成从ProgID到CLSID的转换非常简单,只需要利用COM库中提供的两个函
数CLSIDFromProgID和ProgIDFromCLSID就可以了。
CLSIDclsid;
CLSIDFromProgID(L"Netscape.Help.1",&clsid);
上面的L""是一个扩展宏,用来转换普通的ANSI字符串成为Unicode串。
下面需要讨论的问题是:
假设现在我已经写好了一个组件,怎么才能在
注册表中登记它的接口呢?
非常简单,我们只需要在组件中实现下面两个
函数就可以了。
__declspec(dllexport)DllRegisterServer();
__declspec(dllexport)DllUnregisterServer();
具体而言,DllRegisterServer的实现实际上是通过直接调用注册表函数来
实现的。
为了注册或者取消某个组件的注册,需要用的函数一般有:
RegOpenKeyEx
RegCreateKeyEx
RegSetValueEx
RegEnumKeyEx
RegDeleteKey
RegCloseKey
使用这些函数是需要#include或者,并在additional
librarys里加上advapi32.lib。
现在的一个问题是:
客户怎样选择自己所需要的组件呢?
开发人员需
要的是一种无需创建组件实例就能知道它是否能提供所需接口的方法。
轮询系统中的所有组件和接口不失为一种解决的方法,但这样做的系统
开销相当大。
为此引进了称为组件类别的方案。
一个组件类别实际上就是一个接口集合。
我们分配给该集合一个GUID
以唯一的标示它,它被称作CATID。
对于任何一个组件,如果它实现了某
个组件类别的所有接口,那么它就可以把自己注册成是该组件类别的一
个成员。
这样一来,客户只需要选择合适的组件类别并查询其下所有列
出的组件就可以了。
对组件而言,并不限制它只能属于一个组件类别。
反过来,属于某个组件类别的组件并不限于只实现改组件类别中的接口。
如果乐意,你可以写一个组件支持实现所有组件级别并且还有额外的接
口。
组件类别是怎样被实现的?
使用ComponentCategoryManager(由
windows提供),它是一个实现了ICatRegister和ICatInformation接口的
组件。
ICatRegister可以完成新组件类别的登记或取消,也可以将某个组
件登记入某个组件类别,或取消之。
ICatInformation则可以用来获取系
统中某个组件类别的数据。
组件中分配了一块内存,然后建起通过一个参数(可能是一个返回的指
针)传递给了客户,这是一种非常常见的做法。
问题是:
谁来释放这块
内存?
这主要是由于组建和客户可能是有不同的程序员实现的,他们之
间没有办法建立一种分配和释放内存的标准办法。
COM解决中各问题的办
法是提供一个接口(IMalloc),它可以有CoGetMalloc返回。
为了分配
内存,只需要调用IMalloc:
:
Alloc,而调用改函数所分配的内存可以有
IMalloc:
:
Free负责释放。
为了更加简单的实现,COM库提供了两个更加
简单的函数:
void
CoTaskMemAlloc(ULONGcb
/*sizeinbytesofblocktobeallocated*/);
和
voidCoTaskMemFree(void*pv);
如果你认真看了我的文章,到现在为止你大体上已经有了一个概念:
COM究竟是一种什么概念,它在哪些程度上需要程序员来实现,哪些则是
由操作系统所提供的COM库完成的。
不十分严格的说,COM的目的是把各
种各样的函数分类,然后封装成一个个物件,这些物件在windows系统中
以DLL或者EXE的形式具体存在,并且通过注册表,window随时随地可以
知道某个特定组件的代码是在那个对应得DLL或者EXE里。
这里提一下,
怎么告诉windows你需要哪个组件呢?
我们使用GUID,其复杂的算法保证
了世界上没有两个个接口的ID标示号码是完全一样的!
从而可以唯一的
确定组件,包括内含的接口,在客户需要该组件的时候windows也就可以
正确的装载它了。
同样也是因为这个唯一性,客户在任何时候都可以直
截了当的,明确无误的询问windows,我要的就是这个组件里的这个接
口!
告诉我你有吗?
这时候,通过一个CoCreateInstance函数,windows
将返回接口指针,或者干脆的告诉你,没有找到!
那么,windows内部在执行这个函数的时候具体做了些什么呢?
首先它
查询了注册表,找寻你所要的组件(组件也就是接口集,而所谓接口也
就是一组函数所组成的集合的代名词,这么说你明白了吗?
)如果没有
找到该组件,查询自然失败,函数返回,如果找到了,那么进一步的,
内核将向windows返回该组件的IUnknown*指针,windows随后利用
IUnknown:
:
QueryInterface函数查询你所指定的那个接口是不是被该组件
所实现(或者说支持)说到这里你一定可以发现,凡是接口,一般来说总
是要由你的代码去实现,IUnknown这个所有COM组件都必须实现的接口,
其目的之一就是让Windows知道如何查询你的组件。
直到组件里实现了哪
些接口的只有你自己--写这个组件的人,所以你有责任妥当的好好些
QueryInterface函数以便返回正确的指针,windows随后将该指针转给
CoCreateInstance的调用处,整件事情也就结束了。
现在你了解了吗?
写到这里,不知不觉的已经过了一个星期,不知道有没有朋友看了拙
文之后有质疑?
我学习COM的时间也不长,欢迎大家一起来讨论!
明天打算讨论类厂,欢迎感兴趣的朋友继续关注!