MFC .DLL指南(摘)

来源:互联网 发布:ubuntu 开启图形界面 编辑:程序博客网 时间:2024/06/05 09:04

MFC .DLL指南(1)
(eastvc发表于2001-10-29 0:48:30)

  
  这里要提到的一点是,在用COM和ATL前,程序员一般用.DLLs来代替它们. 你可以用.DLL做很多事.如果你有几个程序要用到相同的函数或资源,你可以将代码放到一个.DLL中.将多个程序要共同用到的代码放到一个简单的.DLL中可以节省维护时间,因为代码就在一个地方.:)修理和其他的改动最多做一次就够了.如果你有一个在不同时间用不同程序的程序,你应该把这些程序做成.DLLs,根据需要的导入相应的.DLL.有很多理由要用到.DLLS.
  虽然用.DLL能做的COM全能做,但是仍有很多好原因使得我们要用.DLLs,所以它们没有消失.但是.DLLs还是有很多严重缺点的,这些严重的缺点就是我们为什么会首先想到用COM.但.DLLs仍然是很实用的工具.对照COM和ATL,.DLLs是非常简单的来实现的.学习COM和ATL需要投入大量的时间和努力.实现.DLL却相对简单,修改起来也不难.如果你会一些C++和MFC,你现在就可以实现.DLLs.
  这篇文章回顾一下用MFC实现.DLLs的几种形式,包括何时用和怎么用各种形式.在下一篇将讨论.DLLs的局限性(这就是为何会出现COM和ATL)

  

Different types of .DLLs


  可以用MFC来实现两种.DLLs:一个是MFC扩展.DLL,一个是正规的.DLLs.正规的.DLLs有两种实现方式:dynamically linked 或者 statically linked.Visual C++也允许你用generic Win32 .DLL,但本文我们只讨论以MFC为基础的.DLL形式.

  MFC extension .DLLs
  每一个.DLL都有一些接口.接口是一套变量,指针,函数或者是类,可以通过客户程序来访问.MFC的扩展.DLL有一个C++形式的接口,也就是说它提供给客户程序("export")C++函数或者整个类.导出函数可以用C++或者MFC的数据形式作为参数或返回值.当导出整个类,客户程序可以创建此类的对象或者派生于此类.在.DLL中,你也可以用MFC和C++.
  Visual C++用的MFC的类代码库也存在于.DLL中.一个扩展.DLL是动态连接到MFC的代码库的.DLL的.客户程序也必须动态的连接到MFC的代码库.随着时间的推移,MFC的库也在增长.结果,就有了几个不同的MFC的代码库的版本.客户端和扩展的.DLL必须建立在相同版本的MFC上.因此,一个MFC扩展的.DLL要运行,客户端和此扩展的.DLL必须动态的连到相同的MFC代码库的.DLL上,并且,此库还得在该程序运行的机器上好使.
  注意:如果你的程序静态的连接到MFC,但是你希望改变它便于从一个扩展的.DLL中访问函数,这时,你要改此应用程序为动态连接到MFC.在Visual C++,在菜单中选"Project | Settings",在"General"设置标签中可以把你的程序改成动态连接到MFC.
  MFC扩展.DLLs非常小.你可以建立一个导出一些函数或者类的大约10-15KB的.DLL.显然的,你的.DLL的大小要以你要存多少代码到你的.DLL中为准.但是通常MFC扩展的.DLLs是相对小和快捷的.
  
  Regular .DLLs
  MFC的扩展.DLL只能工作在用MFC编的客户程序上.如果你的.DLL可以被大多数的Win32程序导入和运行,你应改选择正规的.DLL.但你只能导出C-style函数.你不能导出类.你不能导出C++函数或overloaded函数.不能用MFC的数据类型作为参数和返回值.但可以在你的.DLL中用C++和MFC,但是你的接口必须全是C-style.  
  当然正规的.DLL也要访问MFC的代码库的.DLL.可以动态,可以静态连接.如果动态连接,意味着你的.DLL函数所需的MFC代码不用建立在你的.DLL中,你的.DLL会从你的客户端的机器上MFC的代码库的.DLL中取得所需要的代码.如果正确的MFC代码库的.DLL版本没找到,你的.DLL将不能工作.像MFC扩展的.DLL一样,正规的.DLL也非常小,只能在客户端所在的机器有MFC代码库的.DLL情况下工作.
  如果你静态连接到MFC代码库,你的.DLL包括它自己的所有的所需的MFC代码.那么,它将非常庞大,但是它不依赖于客户端的电脑配置.如果你不知道主机的机器配置情况,这是一个很好的方法.如果你的客户在你的公司范围内,你可以知道他们的MFC .DLL的配置情况,或者你的安装程序带了正确版本的MFC .DLL,那么静态连接就不是一个好方法了.

  

Building a .DLL


  可以用App Wizard来实现以MFC为基础的.DLL.选择"File | New",在"Projects"标签上,选择"MFC AppWizard (.DLL).",为你的工程选一个名字,然后单击"OK".在下一个屏幕,选择建立一个MFC扩展的.DLL,或者正规的.DLL"using shared MFC .DLL"(就是动态连接到MFC),或者正规的.DLL(静态的连接到MFC).选择其中一个,按"Finish".
  App Wizard新建立的.DLL没做任何事.编译新的.DLL,但是它不导出任何类和函数,本质上来说,没有任何用,你现在有两个工作:
  1.增加函数.
  2.修改客户端来调用你的.DLL.
  
  Export a Class
  上面提到,只有MFC的扩展.DLL能导出MFC/C++类.假设你建立了一个扩展的.DLL,你可以通过从另一个工程加入.cpp和.h文件来创建一个类,也可以在你的工程中创建新类.要导出这个新类,你必须在类的声明前加一个宏"AFX_EXT_CLASS",像这样:

     class AFX_EXT_CLASS CMyClass
     {
          //class declaration goes here
     };
  
  还用一种方法来导出一个类,很简单而且很好,我会在下面讨论客户端如何做才能用上你的导出类提到.
  
  Export objects and variables
  代替导出整个类的方法,你可以导出该类在.DLL中的对象.客户端程序可以调用所有的该导出对象的公有函数和访问它的公有成员变量.
  首先,创建一个.h文件,定义了你的新类.然后创建实现你的新类的.cpp文件.在.cpp文件底部,在你所有的公有的,私有的类函数后,创建一个这样的类的实例:

     _declspec(dllexport) CMyClass myObject;

  这行的作用是创建一个CMyClass类的实例,当客户端导入.DLL时,使得客户端可以访问该实例.通过该实例,访问它的公有函数和成员编量.注意,每个客户端导入.DLL将获得该实例的一个拷贝,也就是说,如果不同的程序访问相同的.DLL,如果一个程序改变了该实例,不会影响另一个程序.
  除了导出实例,你可以用同样的方法导出变量.如果你加上此行:
  
     _declspec(dllexport) int x;

  就可以导出变量,为客户端应用.下面说的很重要:你只能导出全局的实例或者变量.局部的实例或者变量当它们跑出作用域,就会停止生存.如果用下面的方法,将不会正常工作:

     MyFunction( )
     {
          _declspec(dllexport) CMyClass myObject;
          _declspec(dllexport) int x;
     }

  一旦实例或者变量跑出作用域,它们将停止生存.

  Export a function
  导出函数和导出变量是很相似的.你可以简单的在函数前面加上"_declspec(dllexport)"

     _declspec(dllexport) int MyExportedFunction(int);

  这就是导出的全部.记住,只有MFC扩展的.DLL能导出C++函数或者以MFC的数据类型为参数或者返回值.正规的.DLLs只能导出C-style函数.

  

Using the .DLL in a client application


  一个.DLL不能运行它自己.它需要客户端导入它,调用它的接口.
  当你编译你的.DLL时,编译器创建两个很重要的文件: .DLL文件和.lib文件.你的客户端需要这两个文件.你必须拷贝它们到客户端的工程文件夹.

  除了.DLL和.lib文件,你的客户端还需要要导出的类,函数,实例和变量所在的头文件.要导出函数时要加"_declspec(dllexport)"声明.现在要导入了,就要加入"_declspec(dllimport)"声明.如下:

     _declspec(dllimport) CMyClass myObject;
     _declspec(dllimport) int x;
     _declspec(dllimport) int MyExportedFunction(int);

  为了可读性,我们可以这样写:

     #define DLLIMPORT _declspec(dllimport)
    
     DLLIMPORT CMyClass myObject;
     DLLIMPORT int x;
     DLLIMPORT int MyExportedFunction(int);

  现在你声明了你的实例,变量和函数,可以用了.:)
  要导出整个类,你必须将整个.h头文件拷过来..DLL和客户端要有唯一的关于此导出类的头文件.记住,类的声明要加上: AFX_EXT_CLASS 宏.
  一旦你建立了客户端,你已经准备给客户用了,你应该给他们你的Release可执行文件和Release的.DLL.不用给用户.lib文件..DLL可以放在客户程序的目录,或者系统目录.还有上面提到的,你要提供正确的MFC代码库的.DLL.这个.DLL是你的机器装Visual C++时候用的.

  

A Word of Caution


  本文已经提供了足够的信息来创建你自己的.DLLs.但有句警告,上面提到过的,.DLLs有一些很严重的缺点.这些缺点是我们为什么要用COM和ATL的原因.主要有两点:
  第一,要是客户程序用的编译器和.DLL的编译器不一样的话,将不兼容.
  第二,当你修改了.DLL,你必须重编译客户端程序,即使你的客户端一点都没改变.你仍然要重新拷贝一份新的.DLL和.lib到客户端,然后重编译.
  有一些方法可以避免上面的一些情况,我会在后面的章节提到.:P

MFC .DLL指南(2)
(eastvc发表于2001-10-29 10:47:44)

  
  我们上节讨论的结果是.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.如果一个函数要被调用成千次,那此进程将会耗用很长时间.

  Conclusion
  这篇文章没有涉及到所有的有关的问题的解释,它也不可能覆盖所有的问题.更好的讨论请见Dale Rogerson写的Inside COM和Don Box写的Essential COM.本文旨在帮你意识到一些问题,这样你应该明确下一步做什么了.:)


原创粉丝点击