基于对象,通常指的是对数据的封装,以及提供一组方法对封装过的数据操作。比如 C 的 IO 库中的 FILE * 就可以看成是基于对象的。
面向对象,则在基于对象的基础上增加了多态性。所谓多态,就是可以用统一的方法对不同的对象进行同样的操作。当然,这些对象不能完全不同,而需要有一些共性,只有存在了这些共性才可能用同样的方法去操作它们。我们从 C++ 通常的实现方法的角度来看,A 和 B 在继承关系上都有共同的祖先 R ,那么我们就可以把 A 和 B 都用对待 R 的控制方法去控制它们。
为什么需要这样做?
回到一个古老的话题:程序是什么?
程序 = 算法 + 数据结构
在计算机的世界里,数据就是一个个比特的组合;代码的执行流程就是顺序、分支、循环的程序结构的组合。用计算机解决问题,就是用程序结构的组合去重新排列数据的组合,得到结果。为了从庞大的输入数据(从 bit 的角度上看,任何输入数据都可能非常的庞大),通过代码映射到结果数据。我们就必须用合理的数据结构把这些比特数据组合起来,形成数量更少的单元。
但是接口继承又有什么意义呢?以我愚见,绝大多数情况下,同样对设计没有意义。但具体到 COM 设计本身,让每个接口都继承于 IUnknown 却是有意义的。这个意义来至于基础设施的缺乏。我指的是 GC 。在没有 GC 的环境中,AddRef 和 Release 相当于让每个对象自己来实现 RC (引用计数)的自动化管理。对于非虚拟机的原生代码,考虑到 COM 不依赖具体语言,这几乎是唯一的手段。另外 COM 还支持 apartment 的概念,甚至允许 COM 对象处于不同的机器间,这也使得 GC 实现困难。
QueryInterface 存在于每个 COM 接口中却有那么一点格格不入。它之所以存在,是因为 COM 接口指针承担了双重责任,既指出了一个抽象概念,又引用了对象的实体。但从一个具体算法来看,它只需要对一组相同的抽象概念做操作即可。但它做完操作后,很可能(但不是必须)需要把对象放入另一个不同的集合中,供其它算法操作。这个时候,就需要 QueryInterface 将其转换为另外一个接口。
C++ 提供了对面向对象的支持,但 C++ 所用的方法(虚表、继承、多重继承、虚继承、等等)只是一种在 C 已有的模型上,追加的一种高效的实现方式而已。它不一定是最高效的方式(虽然很少能做到更高效),也不是最灵活的方式(可以考察 Ruby )。我想,只用 C++ 写程序的人最容易犯的错误就是认为 C++ 对面向对象的支持的实现本身就是面向对象的本质。如果真的理解了面向对象,在特定需求下可以做出特定的结构来实现它。语言就已经是次要的东西了。