1.1 软件的分发和C++
把C++作为组件的基础底层结构所带来的问题:以前,C++一直以分发源代码的形式来发布库,这样做是可行的,但存在问题:如果多个程序都使用一个代码库,则各个程序都将在自己程序中编译同样的代码,并生成可执行文件,导致内存的浪费。同时,更新困难。如:假设一段代码变成可执行的代码大小为16MB,如果一个用户安装了3个程序,这3个程序都使用了这段代码,则将占用48MB的空间,都知道计算机上可能有很多程序都使用了基础类库,如果基础类库一味的拷贝,则所占的空间将可想而知。如下图可知。FastString类库编译了FastingString.obj。
动态链接和C++:解决共享代码的问题的一种技术是把类库代码以动态链接库的形式包装起来。使用__declspec(dllexport)关键字。使用该方法声明的方法,将被加到dll的引出表中,允许在运行时把每个方法的名字解析到内存中对应的地址。并且引入库暴露了方法的符号。注意:引入库很小(引出符号的文本的两倍),当类库从DLL中引出时,它的机器码在用户的硬盘上只保留一份,当多个客户访问代码时,操作系统的装载器可以很灵活地让所有的客户程序共享一份dll的只读可执行代码的物理内存页。并且厂商可以统一地进行更新DLL文件。
C++和可移植性:一旦确定使用DLL的形式发布C++类,就将面临C++的根本弱点:C++缺少二进制级的统一标准。许多编译器厂商有自己独特的链接和编译方式,使得编译出来的DLL不能互用。对于创建多个二进制的、基于组件的可执行代码来说,这是个严重问题,因为每个组件很可能是用不同的编译器和连接器建立起来的。C++缺少二进制标准,这限制了语言特征在跨越DLL边界时的应用。
封装性和C++:在C++中建立二进制组件的另一障碍是与封装有关。考虑情形:一个组织应用中使用了FastString,而他们想在产品发货前两个月完成开发测试,或者在发布FastString的DLL之后想改变其中的个别函数。但是虽然C++通过private和public关键字可以支持语法上的封装,但是C++并没有定义二进制层面上的封装,因为编译器需要知道内存的布局,这样才能构造实例。如下图:1.0版本FastString实例大小为4字节,因为只有一个char*,而2.0版本增长到8字节,因为多了一个int类型。针对1.0用户,类定义编写的客户分配4个字节的内存,并传递给构造函数。而2.0版本的构造函数和方法都假定客户为每个实例分配了8个字节的内存,并且毫无保留的写入这8个字节,而对于原先1.0客户来说,这完全是粗鲁的,因为后4个字节是别的代码的并不是FastString的。如下图。
把接口从实现中分离出来:封装以把一个对象的外观(接口)同其实际工作方式(实现)分离开为基础,而C++没有把这条规则运用二进制层面上,因为C++的类既是接口优势实现。这个弱点可以通过下面这种方法解决:构造一个模型,把接口和实现做成两个分离的实体,即C++类。定义一个C++类代表指向一定数据类型的接口;定义另一个C++类,作为这个数据类型的实现,于是从理论上讲,对象的实现可以修改实现的细节而接口保持不变。注意:这个接口类的二进制布局结构并不会随着实现类数据成员的增加或删除而改变。并且FastString类的声明不需要被包含在这个头文件中就可进行编译了,可以方便的把实现隐藏起来。虽然有以上优点,但是,接口必须把每个方法调用显示地传递给实现类,这种工作对于非常大的类库无疑是一种巨大的负担。
抽象基类作为二进制接口:如前所述,兼容性问题起源于2个方面:1、运行时表现的语言特性;2、在链接时刻如何表达符号的名字。为了与解决编译器无关的特性,为了实现独立性我们必须确定语言的那些放免具有统一的实现形式。
假设一:任何基于C的系统,都必须遵守:复合类型(C风格的struct)在运行时的表现形式对于不同的编译器往往保持不变
假设二:所有的编译器都强制使用同样的顺序传递参数(从右至左/从左至右)
假设三:某个给定的平台上的所有C++编译器都实现了同样的虚函数调用机制(只要不定义数据成员即可?)
根据前面的假设,可以解决编译器依赖性问题。假设三成立,定义接口类IFastString,表明所有的编译器将为IFastString产生等价的机器码,这个等价的条件是:接口没有数据成员(因为接口没有数据成员,所以任何人不能用任何实在的方式来实现这些方法?);接口不能从其他接口派生。为了进一步加强思想,把方法设计为纯虚函数,这样子避免了用接口的实现。对于实现类从接口重载每个纯虚函数,实现这些方法。因为FastString从IFastString继承而来,所以FastString是IFastString的一个超集。这意味着FastString将包含一个vptr,指向一个与IFastString兼容的vtbl。
把实现类的定义暴露给客户等于绕过了接口的二进制封装,从而破坏接口的基本意图。为了使客户能够构造fastString对象,让DLL引出一个全局函数,由它代表客户的new操作,这个函数必须以extern “C”的方式引出,这样任何一个C++编译器都可以访问。如同句柄方法一样,new操作符仅仅在FastString DLL内部调用,这意味着对象的大小和布局结构将使用与编译实现类的所有方法相同的编译器建立起来。
对象的析构,
并不会从外层向基类型递归地销毁。因为FastString没有被调用到。解决方法:增加delete方法。
注意:在FastStringDLL中,除了一个入口函数之外,其他的所有入口函数都虚函数,接口类的虚函数总是通过保存在vtbl中的函数指针被间接调用,客户不需要再开发时链接这些函数的符号名。