我们上节讨论的结果是.DLLs对于任何的程序员都是非常实用的工具.然而,使用他们却有很多限制,任何人在作的时候都要意识到这点.
MFC Issues
在上一节已经提到了这个,但是很有再一次提的价值.MFC扩展的.DLL只能在和客户端的程序用相同的MFC和正确的MFC的代码库的情况下才好使.正规的.DLL也是如此.
Compiler Incompatibility Issues
一个很重要的问题就是在以C++为基础的.DLLs,当它们建立在某一个编译器上,而调用它们的客户端却建立在另一个编译器上,通常情况下,再多的努力,它也不会工作.:(
ANSI协会制定了C和C++的语言的标准.也就是说,它指定了C和C++的函数和数据类型必须由一个编译器来支持.但是它并没有提供一个完整的基于二进制级的关于如何用函数和数据类型的实现.结果,编译器厂商就根据自己的方式来自由的实现其语言功能.
很多的C/C++程序员知道不同的编译器操作的数据类型是不同的.一个编译器为int型变量分配2bytes,但另一个也许会分配4bytes.一个会用4bytes的double,另一个可能用8bytes的.在函数实现和操作符重载方面就有更大的差别了.不同的编译器的差别比你想的还要多,所以,这些不同使你的.DLL不能运行于某些的程序上.
编译器的不兼容问题可以通过插入pragmas和其他的重编译说明到你的代码来解决,但是很难,而且不易读.但是用到不兼容的编译器确实是不可避避免的.解决编译器不兼容问题的最好的方法是让你的.DLL导出一个简单的接口类,让它指回你的.DLL,我们将在下面讨论.
Recompiling
让我们假设你建立了内含名为CMyClass的类的.DLL.当一个客户程序连到你的.DLL,.DLL创建这个类的实例并导出这个实例.假设你的导出实例为30字节.
现在假设你对CMyClass做了一些改动,加了一个int型变量,这样你的导出的CMyClass的实例就由原来的30字节变成了34字节,你将新的.DLL给用户,让它替代原来的.现在,错误来了,客户程序期待一个30字节的实例,但是你的新的.DLL却送来了34字节的实例,客户程序将抛出异常.
客户程序并不需要改变代码,所有要做的就是导入一个CMyClass类的实例.在代码的某处,加上这行:
_declspec(dllimport) CMyClass myObject;
客户程序的代码不需任何改变,只需要重编译客户程序来解决此问题,重编译后,客户程序将等待34字节的实例.
这是很严重的问题,不重新编译客户端而只在改变.DLL后将此.DLL重置是我们的目标.然而,你的.DLL要导出整个类或一个类的实例,那么此目标是不可能实现的.你必须重编译客户端.如果你没有客户端的源代码,你将使用不了新的.DLL.
Solutions
如果对上面的问题有一个很好的解决,那么我们不会用COM了.这里有一些建议:
尽可能的用MFC的扩展的.DLLs.虽然这样限制了你的客户程序的类型,但是解决了编译器不兼容的问题.
如果你的.DLL导出类或者类的实例,你不得不在修改了.DLL后重新编译你的客户端.为了避免这样,你必须做到分解你要导出的类,实现导出类的一个接口.最好的方法是,创建一个作为第一个类的接口的类,这样你改变了导出类的话,接口类不变,客户程序无需重新编译.
这里有一个例子.假设你要导出CMyClass类.CMyClass有两个公有函数,int FunctionA(int)和int FunctionB(int).替代导出CMyClass,我将创建一个导出接口CMyInterface.CMyInterface将含有一个指向CMyClass的实例.这里给出它的头文件:
class AFX_EXT_CLASS CMyInterface
{
class CMyClass; //forward declaration of CMyClass
CMyClass *m_pMyClass;
public:
CMyInterface( );
~CMyInterface( );
int FunctionA(int);
int FunctionB(int);
};
这份头文件将用在.DLL和客户端程序.注意,前面的声明意味着没有CMyClass的备份也可以编译.
在.DLL内部,这样实现CMyInterface:
CMyInterface::CMyInterface( )
{
m_pMyClass = new CMyClass;
}
~CMyInterface::~CMyInterface( )
{
delete m_pMyClass;
}
CMyInterface::FunctionA( )
{
return m_pMyClass->FunctionA( );
}
CMyInterface::FunctionB( )
{
return m_pMyClass->FunctionB( );
}
因此,CMyClass的每一个函数,CMyInterface将提供相应的函数.客户程序将和客户程序没有联系.如果它想调用CMyClass::FunctionA,只需调用CMyInterface::FunctionA.接口类会用指针调用CMyClass.用这种布局你可以改变CMyClass了----不用担心CMyClass的大小变了.CMyInterface的接口的大小不变.即使你给CMyClass加了一个私有变量,CMyInterface的大小也不会变.要是你加了公有成员,就在CMyInterface里边直接加上对应新变量的"getter" 和 "setter" 函数,不用担心,加入新的函数,CMyInterface接口类的大小不会改变.
建立一个单独的接口可以避免编译器不兼容,客户端重编译的问题.只要接口类不变,就不需重编译.但仍然有两个小问题,一:对于每一个CMyClass的公有的成员变量,你必须在CMyInterface里创建实际的对应的函数或变量.这个例子中只有两个函数,所以很简单.如果CMyClass有成千上万的函数和变量,这将变得很困难,而且易错.二:你将增大进程的开销.客户程序不再直接访问CMyClass,替代的通过访问CMyInterface来访问CMyClass.如果一个函数要被调用成千次,那此进程将会耗用很长时间.