COM 接口基础






4.81/5 (71投票s)
2003年8月10日
9分钟阅读

338917
本文简要介绍了 COM 接口背后实际发生的事情。
目的
本文面向从未接触过 COM 技术的软件程序员。文章不涵盖 COM 基础知识,但涵盖了接口的基本概念。它使用强大的 C++ 语言,解释了一个类最终如何成为一个接口,以及形成这种结构的原因。本文只是简要介绍接口背后实际发生的事情。
目录
引言
组件历史
使用面向对象方法 (oo)
C++ 的局限性
使用 C++ 的接口基本思想
使用 C++ 的基本 COM 接口
接口继承
摘要
引言
程序员经常将代码功能分解成小的、更简单的部分。每个部分称为一个组件。其理念是将这些组件存储在库中,并通过 API 服务(应用程序编程接口)来访问它们(即组件)。这些库的重用非常简单强大,并且易于访问。
组件历史
许多程序员在开发组件时遇到了几个问题。这些问题与组件本身以及使用组件的客户端都相关。以下是开发人员遇到的一些主要问题:
- 范围 - 组件的范围对组件的开发人员和用户都造成了问题。组件作者的任何更改都可能影响使用该组件的应用程序,并导致整个客户端代码重新编译。
- 版本 - 如何强制程序员检查接口版本?维护和版本发布可能会成为一个问题。
- 通信 - 组件通信也是一个问题,尤其是在有多人参与编写过程时。
- 语言 - 如果组件是用 C++ 编写的,如何使用 Visual Basic 或 C 等语言来访问它?
解决组件开发问题的方案有几种,但其中一种强大的方法是使用面向对象的方法。
使用面向对象方法 (OO)
编写组件最常用的技术之一是使用面向对象的方法。面向对象方法的使用允许程序员以更抽象的方式来处理应用程序:将应用程序视为一组对象,每个对象都有其独特的属性,并且一个对象可以与另一个对象通信。使用面向对象的方法可以使程序员更容易理解应用程序的复杂性,并找到简化问题的合适解决方案。设计一个对象比阅读过程式算法要容易得多。C++ 是使用面向对象概念的语言之一。在本文中,示例是用 C++ 编写的(需要具备 C++ 基本知识)。
C++ 的局限性
实现面向对象的编程语言之一是 C++。从组件访问的角度来看,C++ 是有限的。
- 大小很重要 I - 假设我们有两个类,即类 A 和类 B,并且类 B 继承类 A。现在假设我们要向类 A(父类)添加一个新的数据成员。类 B 的大小会自动改变(或其虚拟表的大小),我们需要重新编译类 B 的代码。
- 区分 - 没有明确的方法来区分类的实现究竟在哪里(在上一个部分是类 A 和类 B)。两个类中方法的实现可以在父类(类 A)或子类(类 B)中。C++ 没有提供简单的区分方法。
- 大小很重要 II - 有时,使用组件(上一个部分中的类 A 和类 B)的客户端需要提前知道组件的大小。不知道大小可能会导致客户端和组件之间出现同步问题。
- 大小很重要 III - 假设我们向父类(上一个部分中的类 A)添加了一个虚函数,现在该类将有一个指向虚拟表的指针,并且每个继承的类也将包含一个指向虚拟表的指针,这意味着每个继承的类现在将包含额外的 4 个字节(指向虚拟表的指针),如果使用该类的客户端依赖于类的大小,通信可能会中断。
为了解决上述问题,我们使用接口。接口究竟是什么?我们如何编写代码来定义接口?我们如何与接口通信?这些问题的答案以及更多内容将在下一节中找到。
使用 C++ 的接口基本思想
考虑以下类
class CExampleArray { public: int getLength() { return m_iLength; } private: int m_iLength; int m_ArrVec[100]; };
如果我们创建上述类的实例,由于 `m_ArrVec` 被预定义为大小为 100,它将浪费内存空间。如果我们需要的整数数组大于 100 怎么办?该类不足以定义通用数组。
我们可以通过将固定大小更改为指针,并将数组的大小存储在另一个数据成员中来解决问题,如下面的类所示。
class CExampleArray { public: int getLength() { return m_iLength; } private: int m_iLength; int* m_ArrVec; short m_iArrSize; };
由于上述类中更改和添加了数据成员,因此应重新编译它,并且任何使用该类的客户端也需要重新编译才能与新类一起工作。现在在新版本的类中,类的实例不会占用浪费的内存空间,但任何依赖于数组大小的客户端都会有问题(因为一个客户端可以定义一个包含 20 个整数元素的数组,而另一个客户端可以定义一个包含 100 个整数元素的数组)。
除了改变类在内存中大小的成员之外,我们还可以添加改变大小的虚函数。
class CExampleArray { public: virtual void ReverseArray(); int getLength() { return m_iLength; } private: int m_iLength; int* m_ArrVec; short m_iArrSize; };
由于虚方法 `ReverseArray`,将为此类创建一个虚拟表,并且类的每个实例将包含指向虚拟表的指针 (VPTR),这将为总类大小增加 4 个字节(指针大小)。再次,客户端与类的通信可能会中断。
假设我们不想让客户端修改类的成员数据。我们可以限制客户端对我们类的任何数据修改,并将数据处理转移到另一个类。
class CExampleArray { public: virtual void ReverseArray(); int getLength() { return m_pDataImpl->getLength(); } private: CExampleArrayDataImpl* m_pDataImpl; }; class CExampleArrayDataImpl { // the data implementation here }
现在所有数据成员都处理在类 `CExampleArrayDataImpl` 中。要检索或修改数据成员,我们通过调用 `m_pDataImpl` 指针指向的实现类的方法来访问它。实现类的大小可以改变(即添加新的数据成员,添加虚函数),但这不会导致基类大小改变,因为它只保存指向实现类的指针。
下图显示了两个类(`CExampleArray` 和 `CExampleArrayDataImple`)如何在内存中表示。
类 `CExampleArray` 仍然包含一个数据成员(指向 `CExampleArrayDataImple` 的指针)。我们的目标是创建一个没有任何数据成员的类,一个仅通过其方法公开的类,并允许继承类实现公开的方法。
为了做到这一点,我们需要将 `CExampleArray` 类中的所有方法头更改为纯虚函数,并且还需要从类中删除所有数据成员。
class CExampleArray { public: virtual void ReverseArray() = 0; virtual int getLength() = 0; };
这是一个抽象类,它只包含函数的纯虚声明(“= 0”表示纯虚)。所有数据成员都已删除。
这个抽象类被称为接口。接口基本上是一个类,只包含纯虚函数,没有数据成员。
使用此接口的客户端将只使用指向接口的指针,而无需依赖接口大小。接口的所有数据都对客户端隐藏,并在接口的继承类中实现。语法上,接口类通常以前缀 'I' 开头。从现在开始,我将把接口类 `CExampleArray` 称为 `IExampleArray`。
到目前为止,我们已经了解了接口的样子。现在让我们看看一个基本的 COM 接口是什么样的。
使用 C++ 的基本 COM 接口
让我们看看如何使用 C++ 创建 COM 接口。C++ 不允许我们创建抽象类的实例,即以下代码行无效。
IExampleArray* pNewInstance = new IExampleArray;
接口类只能通过使用指向虚拟表的指针来访问,该虚拟表公开接口中的方法。接口不是独立存在的,它通常与一个继承类一起出现,该类实现了接口中公开的方法。这种实现接口公开方法的类通常称为 co-class。下面是一个 co-class 的例子。
class CExampleArrayImpl : public IExampleArray { public: virtual void ReverseArray() { // implementation here } virtual int getLength() { // implementation here } };
由于实现类 `CExampleArrayImpl` 没有纯虚函数,我们可以轻松创建它的实例。我们现在将编写一个方法,对实现类(co-class)执行新操作,并返回一个指向接口的有效指针(由于 C++ 的强大技术——多态性,这可以做到)。我们可以使用全局方法来创建 co-class 的实例,也可以使用静态方法。使用创建 co-class 实例并返回指向其接口的指针的技术通常称为类工厂。下面是全局创建实例的方法。
IExampleArray*CreateArrInstance() { return new CExampleArrayImpl ; }
现在,如果客户端想使用该接口,它所要做的就是通过调用 `CreateArrInstance` 方法获取指向接口对象的指针,然后调用接口的方法。
int UseTheInterfaceMethod() { // obtain a pointer to the interface IExampleArray* pArr = CreateArrInstance(); // invoke the interface method int iLength = pArr->getLength(); // exit successfully return 0; }
客户端看不到 `CExampleArrayImpl` 类,也不会直接使用 new 运算符。它只知道 `IExampleArray` 接口的虚拟表。接口可以像 C++ 中的类一样继承。接口继承。
我们可以创建另一个接口,它将继承自 `IExampleArray` 并成为 `IExampleArray` 的扩展(将具有与 `IExampleArray` 相同的 2 个方法,并添加自己的新方法)。
class IAnOtherExampleArray : public IExampleArray { public: // the 2 methods from the interface IExampleArray must appear here as well virtual void ReverseArray() = 0; virtual int getLength() = 0; // new exposed method added by this interface virtual BOOL Find(int iKey) = 0; };
请注意,接口之间允许继承,因为它们毕竟是类。我们现在可以创建一个新的 co-class(`IAnOtherExampleArray` 接口的实现类)。
class CExampleArrayImpl : public IAnOtherExampleArray { public: virtual void ReverseArray() { // implementation here } virtual int getLength() { // implementation here } // the new method implementation virtual BOOL Find(int iKey) { // implementation here } };
使用上述接口的客户端将看到 `IExampleArray` 和 `IAnOtherExampleArray`,但不会看到 `CExampleArrayImpl` 和 `CAnOtherExampleArrayImpl` co-class。为了获取指向新接口 `IAnOtherExampleArray` 的指针,客户端必须使用动态转换,如下所示。
int UseTheInterfaceMethod() { // obtain a pointer to the interface IExampleArray* pArr = CreateArrInstance(); // invoke the interface method int iLength = pArr->getLength(); // obtain a pointer to the new interface IAnOtherExampleArray *pNewArr = dynamic_cast<IAnOtherExampleArray *> (pArr); // invoke new interface method pNewArr->Find(1); // exit successfully return 0; }
注意:为了使上述代码在 Visual Studio 环境下正常工作,请确保使用选项 '/GR' 进行编译,该选项启用运行时类型信息,否则您将收到编译警告(C4541)。
摘要
现在,希望您能更好地理解接口概念背后的含义。我没有在文章中涵盖 COM 问题,因为 MSDN 和开发人员网站上都有关于该技术的优秀文章。