一个应用程序通常是由单个的二进制文件组成的,这样的话,每次更新其中的一部分都需要将整个应用程序重新编译。而 COM 的基本思想就是:将单个的应用程序分隔成多个独立的部分,也即组件。这并不是说将应用程序分割成文件、模板或类,因为这在本质上并没有什么变化,仍然是将它们编译并链接成一个铁板一块状的应用程序。在组件架构中,一个组件类似于一个微型应用程序(即:已经编译、链接好并可以使用),而应用程序是由这样的多个组件打包而成的(各定制的组件在运行时同其他组件连接起来以构成某个应用程序)。这样的架构思想实现了修改应用程序只需替换其中一个组件,而无须陷入整体重新编译的窘态。
COM 有几个特性——
- 它并不是类似 Win32 API 那样的函数集,它只是一个规范,提供了编写组件的一个标准方法,而遵循 COM 标准的组件可以被组合起来以形成应用程序;
- 它不是一种计算机语言,恰恰相反,因为组件是以二进制形式发布的,它具有与语言的无关性,可用任何语言来编写组件,且任一客户能够使用任一组件;
- COM 组件是以 Win32 动态链接库(DLLs)或可执行文件(EXEs)的形式发布的可执行代码组成的,但它本身与 DLL 并没有很大关系,只不过 COM 利用了 DLL 极佳的动态链接能力。
COM 是与语言无关的,所以为了将客户与 COM 组件连接起来,COM 组件通过接口与外界打交道。而对于 COM 组件这样的二进制文件来说,什么是接口呢?它同样有着一个二进制的标准——表示一个接口的内存块必须具有一定的结构。
以下将用 C++ 语言来描述这些细节。
首先介绍 COM 组件中接口的概念,以下贴一段描述接口实现以及使用的代码,但注意:这并不是真正的 COM 接口,COM 接口还需支持一些额外的属性。
#include <iostream> #include <objbase.h> //此头文件中定义有 #define interface struct using namespace std; //定义 IX,IY 接口 interface IX //纯抽象基类 { virtual void _stdcall Fx1() = 0; //纯虚函数 virtual void _stdcall Fx2() = 0; }; interface IY { virtual void _stdcall Fy1() = 0; virtual void _stdcall Fy2() = 0; }; //实现组件 class CA :public IX, public IY { public: //实现 IX 接口 virtual void __stdcall Fx1() { cout << "CA::Fx1" << endl; } virtual void __stdcall Fx2() { cout << "CA::Fx2" << endl; } //实现 IY 接口 virtual void __stdcall Fy1() { cout << "CA::Fy1" << endl; } virtual void __stdcall Fy2() { cout << "CA::Fy2" << endl; } }; void trace(const char* pMsg) { cout << pMsg << endl; } //客户 int main() { trace("客户:创建该组件的实例"); CA* pA = new CA; //得到指向 IX 的指针 IX* pIX = pA; trace("客户:调用 IX 接口"); pIX->Fx1(); pIX->Fx2(); //得到指向 IY 的指针 IY* pIY = pA; trace("客户:调用 IY 接口"); pIY->Fy1(); pIY->Fy2(); trace("客户:删除组件"); delete pA; cin.get(); return 0; }
如代码所见,在 C++ 中用纯抽象基类来定义 COM 接口,这是因为纯抽象基类所定义的内存结构可以满足 COM 对接口的要求。
那么纯抽象基类所定义的内存结构是什么样的呢?这就不得不提到虚函数表了。
我们知道,在 C++ 中,因为虚函数的存在,导致了隐式向上强制转换的问题(即:基类指针或引用可以指向基类对象或派生类对象),为了能够在程序运行时选择正确的虚方法(究竟是基类的还是派生类的,该过程又被称为动态联编),编译器采用了虚函数表的方法——在每个类中添加一个隐藏的指针成员,其指向一个函数地址数组,而这个数组中存储了所有定义的虚函数的地址(对于派生类来说,如果其没有重新定义虚函数,数组中将继承基类中对应虚函数原始版本的地址),这个数组就被称为虚函数表(virtual function table, vtbl)。
那么,纯抽象基类的内存结构也就可想而知了——一块连续的内存中存储了所有的虚函数的地址,而当派生类继承一个抽象基类时,它也将继承此内存结构。而“巧合”的是,COM 接口的内存结构同 C++ 编译器为抽象基类所生成的内存结构是相同的。
今天将《Inside COM》读到了第二章,做一个阶段性的笔记总结,我这么懒,可能会过几天再往后看吧QAQ
PS:我对于 C++ 的编译器实现并不清楚,在虚函数表的解释那儿可能有点儿问题,欢迎友好交流=。=