依然是补充内容, 还是跟继承相关, 不过这次还有模板(template).

直接看代码吧:

这是一个数组模板类, 提供了数据类型无关的一系列比较操作, 子类需要重载的就是Compare() 这个纯虚函数了. 比如有一个字符串的类:

这样的写法似乎是理所应当的, 但是考虑一下之前笔记1.1 中关于vptr, vbtl 的内容, 使用虚函数首先会层加4 个byte 的内存开销, 而且later binding 的操作过程相对于其它运算以及函数调用操作是非常慢的. 于是M$ 的童鞋们想出了不用虚函数, 也能实现动态绑定的方法, 还是继续看代码:

在模板参数中, 我们加入了子类的类型, 所以在父类中, 就可以用static_cast 来转成特定的子类类型来进行操作, 而不是到vptr 那边兜个圈子了. 而接下来只要保证String 类继承Array 类, 就不会有类型不对之类的发满出现了:

这样的做法, 既节省了vptr 和vbtl 的内存开销, 又大大提高了performance.

可能会有这样的疑问: a) 定义类的时候, 把自己的类型作为模板参数可以么? 答案是可以, template 的标准并没有说不可以. b) 定义Array 类的时候, 并不知道有Compare() 这个函数, 也可以调用? 丸子告诉你, 你一定是java写多了, C++ 就是这样的, 的确有些类型不安全.

以上. 刚才说了这是M$ 的人想出来的, 而这也是ATL(Active Template Library) 的基础.

首先解释一下, COM 这个东西真的非常的难入门啊. 1.1 只是为了说明一些补充内容, 不算正式笔记嗯.

本文主要说一下C++ 中多重继承的一些问题.

要自己写一个COM 组件, 最最最简单的大概是这样的一个结构: 写2 个interface 继承IUnknown 接口, 然后再写一个实现类实现这两个interface.

这里假设的是我们用的是C++ 来实现COM 组件. 因为C++ 中没有语言级别的interface 的支持, 所以SDK 中interface 实际上被typedef 成了struct, 以保证所有申明的接口都默认就是public 的访问权限. 于是这就涉及到了C++ 中多继承的概念. 先不说COM, 假设已经有如下的类定义:

可以把ClassA 想像成IUnknown 接口, ClassB 和ClassC 想像成接口类, ClassD 就是实现类. COM 的标准是不允许使用虚拟基类, 即被注释掉的virtual 关键字, 理由是各厂家的编译器在实现这个feature 的时候, 二进制的格式不统一. 这里牵涉到一个vptr(virtual pointer), vtbl(virtual table) 的概念. 简单来说, C++ 中任何带有虚函数的类都会存在vptr 和vtbl, 它们的作用就是负责动态绑定. vptr 和vtbl 都是编译器生成而程序员是不能手动控制的(有些编译器指令大概可以). vptr 是一个指针指向vtbl, vtbl 是一个类似数组的东西, 它保存了当前类中所有虚函数的实际绑定地址.

更详细的内容可以参考: <<Inside the C++ Object Model>> 第4.3 节. 然后有一段代码可以帮助更好的理解vptr 和vtbl: http://www.go4expert.com/forums/showthread.php?t=8403

知道了以上这些之后, 再来看如下代码:

以下是我机器上的执行结果:

惊讶吧? 作为父类实例的pC 指针跟作为子类实例的pD 指针居然不相等. 为什么呢? 就是因为vptr 的影响. 注意: 只有多重继承的时候才会这样. 单继承的时候指针的转换还是相等的, 比如pB 和pD 指针. 而相对的, 因为包含ClassA 的两重数据, pA1 和pA2 也是不相等的.

让我们把virtual 关键字加上再试试结果看会怎么样吧?

我们看到pB 和pC 之间差了4 个byte, 而之前是1 个byte. 看来虚拟基类果然把C++ 的vptr 结构改了呢. 具体怎么改的我也暂时没能力探明=v=. 说这个只是为了证明: C++ 中的虚拟基类是会破坏COM 的二进制兼容性的.

以上.

今天开始重新拜读Don Box 的经典著作<<Essential COM>>.

之所以是重看, 是因为去年看的时候完全都不得要领, 光前两章就看了一个星期, 还没完全懂. 一年的修炼之后, 两个小时看完了第一章, 理解度应该在90%以上了.

言归正传, 记录下第一章的内容.

COM, Component Object Model, 看名字, 应该知道这是一个基于组件的对象模型. COM 其实只是一套规范, 定义了用C++来实现组件的重用必须要怎么做的规范. COM 是OOP的, 因为要作为组件对象嘛, 呵呵. 我们知道, 要做到OOP, 那么封装(encapsulation), 继承(inheritance), 多态(polymorphism) 一个都不能少. COM 在这三个要素中, 封装的作用尤其的重要. COM 是通过纯虚函数来把接口(interface) 和实现(implementation) 相分离的, 这让我们在写COM 类的时候, 要做到KISS(Keep It Simple& Stupid), 即接口要尽可能简单.

以上对于COM 说了个大概的思路, 下面说一下为什么要用COM 这个东西.

要实现组件的重用, 最容易想到的方法自然是dll. 没错, 把所有可以重用的组件统统包到一个dll 里. 但这样是有问题的: 由于各个编译器(compiler) 对于C++ 输出符号(export symbol)的命名(name mangling) 并没有统一, 所以用Visual C++编译的dll 在gcc 或者C++ Builder 里很可能是不能用的, 会找不到符号. 也就是说, 我们要确保的是二进制(binary)上的一致性. 有一种方法可以保证输出符号一致, 那就是加一个*.def文件. 好了, 假设这个问题我们也解决了, 问题又来了, 有一些C++的语言特性依然是编译器相关的(C++标准并没有规定语言的特征在运行时是怎么样的): 典型的就是exception 的处理. 好的, 你又可以说不用exception 就行了, 但是还有其它不能跨越dll 边界的语言特性呢? 比如多继承的时候, 纯虚基类的实现就是编译器相关的.

假设已经解决了上面的所有问题, 接下来的问题跟封装有关. 我就沿用书上的例子了: 假设有一个FastString 类已经实现了功能, 且已经发布. 现在我们要发布v2.0 的FastString 类, 为此我们加了很多的新功能, 公共(public) 接口保持不变的基础上, 添加新的函数, 为此我们也添加了成员变量, 当然它们都是私有(private) 的. 但是依然有这样一个问题: C++虽然在代码层保持了封装, 但是二进制层却不行. 客户端代码必须明确了解类库的二进制结构. 所以当我没用sizeof 之类的, 跟布局(alignment) 相关的操作时, 就会有问题. 当然这种二进制的耦合性是为了提高C++ 的performance. 一种解决方法是如mfc 的做法, 把版本号加在dll 名之中, 比如mfc80.dll, mfc90.dll 等.

总之, 我们现在要做的是设计这样一种方法: 可以屏蔽C++在二进制层面的不兼容性, 也就是说, 把接口(interface)和实现(implementation)分离. 能想到方法有两种, 包含(composition)和继承(inheritance). 这也是一般OOA/D 中能想到的方法, 我们分别来看一下这两种方法. 依然书上的例子了: 假设已有一个FastString 类已经实现了功能, 现在的目的是屏蔽二进制层面的不兼容性.

先来看包含. 我们在定义一个FastStringItf 类, 里面包含一个FastString 类的指针作为成员变量, 这其实用的是proxy 模式. 于是对于一个操作, 我们调用的是FastStringItf 接口, 实际操作的则是FastString 的实例. 但是这样有会有什么问题呢? 就是当类的操作很多的时候, 我们每次都要新加2 个函数的调用开销, 书上说对于performance 而言会不太好, 可是我并没有觉得想对于继承来说, 到底会不好到哪里.

然后就是继承了, 这也是COM 所用的方法. 首先声明一个IFastString 的接口, 然后是具体的 FastString 类. 并export 出一个全局函数来创建和返回FastString 的实例. 这个函数给dll 外的客户端代码调用, 是对外的唯一需要link 的接口. 由于我们得到了IFastString 的一个实例, 且有它的头文件(即所有操作声明), 所以我们就可以调用所有IFastString 的操作而不用link 到dll 中具体的操作实现. 好像很难懂, 看代码吧:

解决了以上问题之后, 书中又提出了另外一个问题: 当我们要为FastString 类加入新功能, 比如持久化(persistence) 的时候, 应该怎么做, 到底是修改IFastString 接口, 还是让FastString 类实现一个新的IPersistentObject 接口. 答案是后者, 因为对于接口来讲, 尽量不要修改它, 这样会破环封装性.

于是我们继续coding, 首先创建了一个IFastSting 的实例, 然后转成IPersistentObject 的实例使用持久化的功能. 但是用完之后, 我们要销毁实例的时候, 到底应该如何delete? 因为多重继承的问题, 我们到底销毁 IFastString 的实例, 还是IPersistentObject 的实例, 还是都要销毁? 这个问题对于简单的代码来说, 很容易解决, 只需要销毁一个就可以了, 但是当FastString 继承了很多的接口的时候, 可能就会搞不清. 针对这个问题, COM 引入了引用计数(reference count) 这个东西. 每当用接口来返回创建的FastString 对象的时候, 引用计数加一, 包括转换接口对象的情况. 而销毁接口对象的时候, 简单的把引用计数减一, 计数为零的时候实际销毁对象. 这两个操作对应的是COM 中AddRef() 和Release() 操作. 而对于转换对象接口, 不要依赖于C++ 的RTTI 实现, 因为这也是编译器相关的. 在最后附的例子代码中, 我们自己写两个一个DynamicCast() 函数来负责这项工作, 这个函数在COM 里的对象函数是 QueryInterface().

好了, 第一章就此结束, 初看会比较复杂, 但是当你把design pattern啊, windows 系统的其它部件的设计都有所了解的时候, 就会很容易理解. 侯捷童鞋有这样一篇文章: From Cpp to COM, 建议看一下, 里面还推荐了两本书<<Inside C++ Object Model>> 和<<Inside COM>>.