通过将C++对象封装到DLL中,可以很好地把对象的实现代码有效地保护起来。但是,还存在两个主要问题:第一,因为DLL方式的重用要使用包含声明的头文件,所以对象所有的数据成员仍然被客户看得一清二楚,即使客户不需要访问它们;而且,如果DLL中对象的数据成员发生变化(比如DLL升级成新版本),那么使用其头文件的所有客户程序必须重新编译。第二,DLL中导出函数的命名因C++编译器不同而不尽相同,如果以显式连接方式使用DLL,不同的C++编译器编译的DLL就不能通用(具体细节读者参见本书“动态链接库”一章),严重地削弱了DLL的重用性。另外,C++对象的导出函数往往很多,显式连接DLL时使用它们是很麻烦的事情。要解决这两个问题,可以采用C++的抽象基类技术。
实际上,客户关心的重点在对象提供的功能上面。在对象中,功能的载体是成员函数(方法),况且客户对数据成员的操作可以通过成员函数来完成,因此,可以建立一个只包含所有导出成员函数的“表”,在表中包含函数的地址。这样,当客户程序调用函数时,只需要简单的查找这个表获得相应函数的地址,然后跳到该地址执行即可。由于客户没有包含对象的数据成员,就算对象改变了数据成员的大小,客户程序也不必重新编译。
使用C++的抽象基类的特性,可以很容易的实现这样一个表,因为编译器会为抽象基类创建一个包含所有虚函数地址的vtable表(虚函数表)。C++的抽象基类是这样的类:包含若干只有声明没有实现的纯虚函数。抽象基类不能实例化,只能作为其它类的基类,并且它的派生类必须实现抽象基类的所有虚函数。抽象基类的这种“强制派生类实现虚函数”的特性有如下的好处:可以通过基类的抽象操作来动态地调用派生类的实际操作——将一个抽象基类的指针指向派生类对象,从而调用派生类的函数。所有派生类共享基类的抽象操作,抽象基类就像一个说明书,指导着派生类必须实现哪些功能。
如果使用抽象基类改造上面的char_set类,就可以达到隐藏数据成员和具体实现代码的目的。下面就按步骤来使用抽象基类技术改进DLL的重用。
(1)建立一个使用抽象基类DLL。
新建一个类型为“Win32 Dynamic-Link Library” 的空项目(Empty Project),项目名称DLL_VTBL,添加一个新的头文件,文件名为Ichar_set.h,然后在这个文件中定义抽象基类(如程序清单8-6所示)。因为Ichar_set.h含有用户所要使用的虚函数(功能函数),所以又称为接口文件。一般,Visual C++环境下和接口有关的标识符都有个字符“I”,表示接口Interface。
程序清单8-6 接口文件IChar_Set.h
定义了抽象基类之后,含有抽象类的头文件就是接口文件,将会像说明书一样发布给客户,它列出了客户程序可以使用的函数(即接口),这些函数在上一个例子中是DLL的导出函数,这里都变成了抽象类的纯虚函数。下面,要用抽象基类对char_set类进行改造,首先,把上例的两个文件char_set.h和char_set.cpp拷贝并添加到本项目中,然后分别作如程序清单8-7所示的修改(代码中需要修改的地方用黑体字书写,其余未加修改的地方我们做了些省略)。
程序清单8-7 修改后char_set.h和char_set.cpp文件
最后,Build程序,生成新的Lib和DLL文件(DLL_VTBL.lib与DLL_VTBL.dll)。(www.xing528.com)
(2)建立客户程序。
参照上列创建控制台类型的工程(CLIENT2),并新建一个客户程序Client2.cpp见程序清单8-8。
程序清单8-8 客户程序Client2.cpp
然后,拷贝DLL_VTBL项目中的Ichar_set.h、DLL_VTBL.lib与DLL_VTBL.dll三个文件到CLIENT2目录下,修改Project Settings的Link中选项,最后,Build和Execute本例程,读者会发现,虽然是由编译器通过vtable进行函数调用,但一切功能都和以前一样。
在上面的例程中,派生类char_set继承了抽象基类IChar_Set的vtable表,派生类的每一个实例都有指向vtable表的指针,存放在数据成员的前面。比如语句:
char_set *pObject = new char_set;
它会产生如图8-3所示的内存结构。
图8-3 char_set类对象的内存结构
到这里为止,存在于DLL中的C++对象已经非常像COM了。但它还不是真正的COM对象,真正的COM对象能够被COM库所使用,也就是说,目前这个C++对象还要满足一些标准(规范),以便为另外一些遵守标准的函数(COM库中的函数)所使用。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。