--------------------------------------Chapter1 COM是一个更好的C++----------------------------------------------
C++的编译和链接模型:
C++编译模型要求客户的编译器必须能够访问与对象的内存布局有关的所有信息,这样才能构造类的实例,或调用类的非虚成员函数。这些信息包括对象的私有和公有成员的大小和顺序。
影响c++组件成为客户独立的组件软件的因素:
1、符号名字(名字改编)(编译链接阶段)
2、语言的特征(如异常、虚基类、RTTI)(运行阶段)
3、
每个编译器厂商对RTTI的实现也不相同。?
可重用的二进制组件结构:
客户和组件之间的二进制耦合关系,指客户必须知道组件中对象的内存结构。
二进制接口:
前提:某个给定平台上,所有的c++编译器都实现了同样的虚函数调用机制。
要求:类定义没有数据成员、并且至多只有一个基类。
使用dll是为了保持c++类库的模块化特征。
如果没有二进制标准(比如com),当老版本客户使用新版本dll时,因为老版本客户以老版本lib的信息申请内存给新版本dll的构造函数来创建对象,会造成因为内存情况(对象大小、结构等)的差异而出现运行时错误。
dll中有其导出表(引出表,export list)。
dll的lib是导入库,lib不包含实际代码,只包含dll文件名和导出符号。在链接导入库(引入库)时,会在可执行程序中调用导入库符号的地方安放存根,在运行时由装载器动态装载dll,把导入符号解析到对应内存位置。
存根:存在于应用程序中的,在运行时用来定位其他模块(如dll)某个内存地址的记号。
不同的c++编译器厂商对重载函数可能会采用不同的名字改编方案,以适应现有的C链接器。
由于dll的导入库lib和dll的导出符号表使用生成dll的编译器的名字改编方案,其他使用不同名字改编方案的客户程序将不能成功链接lib。
extern "C"用来修饰全局函数(或变量),不能消除成员函数的名字改编问题。
DEF(Module Definition File)文件可以把dll的导出符号名在lib(导入库)中定义不同的导入符号名。这样就可以实现为不同的客户编译器订制导入符号。从而可使任何一个厂商编译器都能获得与dll在链接层次上的兼容性。
采用句柄类实现接口和实现的分离。即在接口类中包含一个实现类的指针,接口类随实现类一起编译(如在一个dll中),所以不会导致new操作出现错误。
而接口的内存结构不会改变。
接口类相当于客户和实现类之间的二进制防火墙。
句柄类没有解决编译器、链接器兼容性问题。
虚函数表:
编译器为每一个包含虚函数的类生成的静态函数指针数组。
使用虚析构函数会破坏接口类的编译器独立性,因为虚析构函数在vtbl中的位置随编译器的不同而不同。所以,解决对象析构的一个可行办法是在接口类中增加一个虚函数Delete(),以便可以调用到派生类的实现,并可以删除掉对象的完整结构。
使用dll导出抽象基类的方法,通常增加一个创建实现类的全局函数,用来导出创建实现类对象的方法。
*(FARPROC *)&pfnCreate = GetProcAddress(h, szFnName);//FARPROC?
修改接口类(增加方法):
通过在接口类最后增加新函数的方法,虽然可以解决老客户对新接口的兼容性。但新客户对老版本dll(老接口)的使用,依然会导致崩溃。修改方法有两种:
1、定义新接口扩展原接口,并让实现类实现(继承)新接口。并通过RTTI(dynamic_cast<INewInterface*>pInterface)使客户调用时可以分辨是非支持新接口。
2、增加新接口,让实现类通过多继承实现多接口。仍然使用RTTI(同上)获取到新接口。
com实现了组件内对象的二进制结构和外部客户的无关性。
让单个对象暴露多个接口,通过RTTI实现运行时对象的接口类型识别。
使用引用计数的方式,在组件内部管理对象的生命期,从而避免客户调用对象的析构函数,(虽然有通过增加普通虚函数接口来避免直接调用析构函数的方法)。也有利于组件对对象实现细节的封装。