实现编译器和供应商无关的接口设计与组件选择
立即解锁
发布时间: 2025-08-20 01:59:29 阅读量: 1 订阅数: 3 


COM+编程实战:使用Visual C++和ATL构建企业级应用
### 实现编译器和供应商无关的接口设计与组件选择
#### 1. 接口作为抽象基类
在实现编译器独立性时,一个主要问题是不同编译器使用的非标准名称修饰方案。不过,如果客户端调用类的虚方法,客户端生成的机器语言代码不会通过符号名称引用该方法,而是使用虚表(vtbl)机制来调用方法。所以,若客户端仅调用虚方法,名称修饰就不再是问题。
但采用虚方法机制会带来三个新问题:
- **数据成员问题**:若接口类包含数据成员,虚指针(vptr)在内存布局中的位置由供应商决定,可能导致不同供应商的实现不同。解决办法是在接口定义中不声明任何数据成员,这也符合向客户端隐藏实现细节的理念。
- **多重继承问题**:接口类的多重继承会使类的内存布局包含多个vptr,且vptr的顺序没有统一标准。可通过将派生限制为仅一个基类来解决。
- **虚表条目顺序问题**:客户端编译器假设虚表中的第一个条目是接口类中声明的第一个虚函数的地址,第二个条目是第二个虚函数的地址,以此类推。但对于重载的虚函数,这种假设在所有编译器中并不都成立。因此,接口类中不应允许重载函数声明。
为确保所有编译器为接口的客户端方法调用生成等效的机器语言代码,接口类定义应满足以下条件:
- 仅包含纯虚方法
- 不包含任何重载的虚方法
- 不包含任何成员变量
- 最多从一个基类派生,且基类也需遵守相同的限制
按照这些规则定义接口类,就能实现接口类定义的编译器独立性。
这种定义的接口类称为抽象基类,对应的C++实现类必须从接口类派生,并使用有意义的实现覆盖每个纯虚方法。以下是将`IVideo`类重新定义为抽象基类以及`CVcr`类的对应实现代码:
```cpp
// Video.h - Definition of interface IVideo
class IVideo
{
public:
virtual long _stdcall GetSignalValue() = 0;
};
// File vcr.h
#include "Video.h"
class CVcr : public IVideo
{
public:
CVcr(void);
long _stdcall GetSignalValue();
private:
long m_lCurValue;
int m_nCurCount;
};
```
这种机制不仅解决了编译器依赖问题,还解决了旧的不透明指针技术的一些弱点:
- **性能损失**:旧技术每次方法调用会产生两次函数调用的成本,一次调用接口,一次嵌套调用实现。
- **易出错**:对于包含数百个方法的大型类库,编写前向调用不仅繁琐,还容易出现人为错误。
从抽象基类派生实现类可以解决这两个弱点。但新问题是,C++编译器不允许实例化抽象基类,只能实例化具体类(如实现类)。若向客户端透露实现类定义,会绕过接口类的二进制封装。
客户端实例化基类对象的合理方法是从DLL导出一个全局函数,该函数返回基类的新实例。只要该函数声明为`extern "C"`,客户端代码就不会遇到名称修饰问题。
```cpp
// Video.h
extern "C" IVideo* _stdcall CreateVcr();
// Vcr.cpp (implementation)
IVideo* _stdcall CreateVcr(void)
{
return new CVcr;
}
```
以下是使用该“工厂”机制的TV客户端代码:
```cpp
#include "Video.h"
#include <iostream.h>
int main(int argc, char* argv[])
{
int i;
IVideo* pVideo = CreateVcr();
for(i=0; i<10; i++) {
long val = pVideo->GetSignalValue();
cout << "Round: " << i << " - Value: " << val << endl;
}
delete pVideo; // we are done with it
return 0;
}
```
不过,这段代码存在一个细微的缺陷:`pVideo`的`new`操作在VCR可执行文件中进行,而`delete`操作在TV可执行文件中进行。结果是`CVcr`类的析构函数永远不会被调用,若客户端和服务器使用不同的C++编译器,客户端的内存释放结果将不可预测。
一个可行的解决方案是在接口类中添加一个显式的`Delete`方法作为另一个纯虚函数,并让派生类在该方法的实现中删除自身。以下是修订后的`IVideo`接口类和`CVcr`实现类的定义:
```cpp
// Video.h - Definition of interface IVideo
class IVideo
{
public:
virtual long _stdcall GetSignalValue() = 0;
virtual void _stdcall Delete() = 0;
};
// File vcr.h
#include "Video.h"
class CVcr : public IVideo
{
public:
CVcr(void);
long _stdcall GetSignalValue();
void _stdcall Delete();
private:
long m_lCurValue;
int m_nCurCount;
};
// File vcr.cpp
void CVcr::Delete()
{
delete this;
}
```
修订后的客户端代码如下:
```cpp
int main(int argc, char* argv[])
{
int i;
IVideo* pVideo = CreateVcr();
for(i=0; i<10; i++) {
long val = pVideo->GetSignalValue();
cout << "Round: " << i << " - Value: " << val << endl;
}
pVideo->Delete();
return 0;
}
```
综上所述,通过使用抽象基类定义接口、将`Delete`功能作为接口规范的一部分以及使用“工厂”方法获取接口,最终实现了编译器独立性。
#### 2. 组件的动态选择
回顾硬件电视和VCR的连接,只要VCR有符合预定义“视频”规格的视频输出插孔,任何品牌的VCR都能与电视配合使用。这是因为电视关注的是“视频”规格,而非VCR本身。实际上,也可以使用DVD播放器,只要其视频输出插孔能输出符合相同规格的视频信号,电视就能显示输出。
但现有的TV代码总是与特定供应商提供的VCR.dll链接,即使声称支持动态链接。为何不扩展模型以使用其他供应商提供的DLL呢?毕竟已经将接口与实现分离,如果另一个供应商为相同接口提供了更好的实现,理应能够使用新的实现。
然而,操作系统加载器会在执行时自动加载DLL,我们没有机会选择想要使用的DLL,这是因为客户端程序与导入库链接,该导入库包含指示操作系统加载器加载特定DLL的指令。
若要在运行时选择特定供应商的DLL,显然不能使用导入库机制。回顾使用导入库的原因是为了解决客户端程序外部的符号。在最新的接口定义中,程序外部的唯一符号是工厂方法`CreateVcr`。所以,若在客户端代码中自己加载DLL并解析该符号,就无需将程序与导入库链接。
微软提供了两个Win32 API来解决这个问题:`LoadLibrary`可用于动态加载DLL,`GetProcAddress`可用于解析过程入口点。可以编写一个客户端函数`CreateInstance`来实现以下功能:
- 加载指定的DLL
- 解析外部符号`CreateVcr`
- 调用`CreateVcr`方法并返回接口指针
客户端代码如下:
```cpp
IVideo* CreateInstance(char* pszDll)
{
// Define a pointer to the prototype of CreateVcr function
typedef IVideo* (_stdcall *CREATEVCRPROC)(void);
// Load the specified library
HINSTANCE h = LoadLibrary(pszDll);
// Obtain the procedure entry point for CreateVcr
CREATEVCRPROC proc =
reinterpret_cast<CREATEVCRPROC> (GetProcAddress(h, "CreateVcr"));
// Execute "CreateVcr" indirectly
return (*proc)();
}
int main(int argc, char* argv[])
{
int i;
IVideo* pVideo = CreateInstance("vcr.dll");
for(i=0; i<10; i++) {
long val = pVideo->GetSignalValue();
cou
```
0
0
复制全文
相关推荐









