大家都懂的,DLL就是动态链接库,LIB是静态链接库。DLL其实就是EXE,只不过没main。
动态链接是相对于静态链接而言的。所谓静态链接就是把函数或过程直接链接到可执行文件中,成为可执行程序中的一部分,当多个程序调用同样的函数时,内存里就会有这个函数的多个拷贝,浪费内存资源。而动态链接则是提供了一个函数的描述信息给可执行文件(并没有内存拷贝),当程序被夹在到内存里开始运行的时候,系统会在底层创建DLL和应用程序之间的连接关系,当执行期间需要调用DLL函数时,系统才会真正根据链接的定位信息去执行DLL中的函数代码。
在WINDOWS32系统底下,每个进程有自己的32位的线性地址空间,若一个DLL被进程使用,则该DLL首先会被调入WIN32系统的全局堆栈,然后通过内存映射文件方式映射到这个DLL的进程地址空间。若一个DLL被多个进程调用,则每个进程都会接收到该DLL的一个映像,而非多份的拷贝。但,在WIN16系统下,每个进程需要拥有自己的一份DLL空间,可以理解为何静态链接没啥区别。
2:DLL和LIB区别和联系。
DLL是程序在运行阶段才需要的文件。
LIB是程序编译时需要链接的文件。
DLL只有一种,其中一定是函数和过程的实现。
LIB是有两种。若只生成LIB的话,则这个LIB是静态编译出来的,它内部包含了函数索引以及实现,这个LIB会比较大。若生成DLL的话,则也会生成一个LIB,这个LIB和刚才那个LIB不同,它是只有函数索引,没有实现的,它很小。但是这俩LIB依然遵循上个原则,是在编译时候是需要被链接的。若不链接第一个LIB的话,在程序运行时会无法找到函数实现,当掉。若不链接第二个LIB的话,在程序运行时依然会无法找到函数实现。但第二种LIB有一种替代方式,就是在程序里,使用LoadLibrary,GetProcAddress替代第二个LIB的功能。第一种LIB生成的EXE文件会很大,因为LIB所有信息被静态链接进EXE里了。第二种LIB生成的EXE文件会比较小,因为函数过程实现依旧在DLL内。
(啰嗦了一堆,某志希望大家能够明白两个LIB的区别。要再不行的话,我们可以将静态编译的LIB称为 静态链接库。但动态编译的LIB称为 引入库。可能会比较好一些。)
静态链接LIB的优点是免除挂接动态链接库,缺点是EXE大,版本控制麻烦些。
动态链接DLL的优点是文件小,版本更换时换DLL就好了,缺点是多了点文件。动态链接若是被多个进程使用,会更加方便和节省内存。
3:为什么编译DLL时总会同时生成一个LIB?这个LIB有用吗?
若我们不是用静态链接,而使用DLL,那么我们也需要一个LIB,这个LIB的作用是被链接到程序里,在程序运行时告诉系统你需要什么DLL文件。这个LIB里保存的是DLL的名字和输出函数入口的顺序表。它是有意义的。
当然,若我们的应用程序里不链接这个LIB,则可以使用LoadLibrary,GetProcAddress来告诉系统我们在运行时需要怎么着DLL以及其内的函数。
4:DLL意义。
1:DLL真正实现了跨语言。各种语言都可以生成DLL,而对系统以及应用程序来说,哪种语言生成的DLL是没有区别的。
2:DLL有足够的封装性,对于版本更新有很大好处。因为DLL是运行期间才会使用,所以,即使DLL内函数实现有变化(只要参数和返回值不发生变化),程序是不需要进行编译的。大大提高了软件开发和维护的效率。
3:DLL被多个进程使用,因为有内存映射机制,无需占用更多内存。
5:创建DLL。(注意:某志就不再讲解使用MFC AppWizard[dll] 方式创建DLL了。有兴趣的自己去百度。这里创建DLL只指使用Win32 Dynamic-link Library创建Non-MFC DLL。呃,DLL的三种类型就不解释了,依旧那句话:百度一下你就知道。)
每个应用程序必须有一个main或者winmain函数作为入口,DLL一样,有自己的缺省的入口函数,就是DllMain。函数如下
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: // 进程被调用
case DLL_THREAD_ATTACH: // 线程被调用
case DLL_THREAD_DETACH: // 线程被停止
case DLL_PROCESS_DETACH: // 进程被停止
break;
}
return TRUE;
}
一般情况下,我们不需要对这个缺省的入口函数进行什么修改,它就会使动态链接库得到正确的初始化。但是,当我们的DLL需要额外分配内存或者资源的时候,或者,DLL希望对调用自己的进程或线程进行初始化或清除的额外操作时,可以在上述代码case中加一些自己感冒的东东。(懒……不想细写了- -Orz,现在是晚上2点了,明天还一堆的事情)
DLL对于导出类和导出函数没啥不同。只要加上 __declspec( dllexport ) 修饰函数或者类就好了。
但是有查看过DLL代码的人员都会经常见到这么一段代码
#ifdef FK_DLL_EXPORTS
#define FK_DLL __declspec( dllexport )
#else
#define FK_DLL __declspec( dllimport )
#endif
意义很明显,但是,问题是 FK_DLL_EXPORTS 这个宏是应该在哪儿定义呢?在DLL项目内,还是在使用DLL的应用程序内?
这点某志曾迷糊很久,呵呵~其实后来想想,还是蛮明确的。export是导出。import是导入。对于DLL来说,是要导出这些函数给其他应用程序使用的,所以应当定义 FK_DLL_EXPORTS 宏。对于使用DLL的应用程序来说,是导入,是无需定义的。
使用时候也很简单。
class FK_DLL CMyDllClass{} ;
则整个类被导出。
FK_DLL void MyTestFun( int a );
则该函数被导出。
但是有时我们可以见到这样的代码
extern "C" FK_DLL void MyTestFun2( float b );
其中extern "C"的原理就是标示该函数要求以C形式去进行编译,不要以C++形式去编译。具体的编译原理就不罗嗦了,简而言之,被extern "C"定义函数,可以被C以及其他语言进行DLL调用,而未被extern "C"定义的函数,C是无法访问DLL中这个函数的。
在VS中开发DLL还有一种方式,使用.def文件。
新建个文本文档,改后缀为FKDll.def,加入到工程里。
FKDll.def里加入以下代码
LIBRARY FKDll
EXPORTS
就可以了。其中,LIBRARY语句是说明.def文件是属于FKDll这个Dll的。EXPORTS下面是我们需要导出的函数名。后面加的@+数字,是表示导出函数的顺序编号。这样就足够了。(详细的自己百度,好困,zzzZZZ)
6:使用DLL
使用DLL有两种方式。显式链接和隐式链接。
隐式链接很容易。直接#progam comment(lib, "FKDll.lib") 就可以。当然,也可以在项目工程->属性->链接库里加上库和路径(相对路径和绝对路径都可以)。
显式链接则麻烦些。在程序中使用LoadLibrary加载DLL,再GetProcAddress获取函数实现,在程序退出之前,调用FreeLibrary来动态释放掉链接库。
例如:
void Main()
{
typedef void (*FKDllFun1)(int a);
FKDllFun1 pFun1;
HINSTANCE hDLL = LoadLibrary("FKDll.dll"); // 若hDll为空则读取Dll失败。
pFun1 = (pFun1)GetProcAddress(hDll, "MyTestFun1" ); // 从应用程序中的DLL镜像中获取名为 MyTestFun1 的函数指针
pFun1( 100 );
FreeLibrary(hDll);
}
当然,我们刚才.def里面还指定了导出函数的导出顺序,那么我们可以修改里面获取函数指针那一段为
pFun1 = (pFun1)GetProcAddress(hDll, MAKEINTERSOURCE(1) ); // 1 是刚才指定的MyTestFun1函数导出顺序编号。
这样可以更快,但是别将编号记混了,会导致诡异的错误。
7:比较显式链接和隐式链接。
可能的话,尽量使用显式链接。
显式链接可以在程序执行时动态的加载DLL和卸载DLL文件,隐式链接是做不到的。
显式链接LoadLibrary,GetProcAddress时能获知是否加载失败,我们可以对其进行检查错误处理。而显式链接可能是一个很恶劣的提示或是程序崩溃的结果。
对于有些Ex类型的加强函数,显式链接可以允许我们找到替代方案。也包括选择D3d9.dll和OpenGL.dll时也可采用同样处理。
例如:
if( GetProcAddress( hDll, "FKDllFunEx") == NULL )
{
pFun = GetProcAddress( hDll, "FKDllFun"); // 然后使用pFun进行处理
}
8:导出类和导出函数
类和函数的导出方式上面给出了说明,原本极其类似的。
我们说下使用导出类。
若我们隐式的使用了一个导出类,则我们在应用程序里继承它的时候,就如同该类就在应用程序代码里一样,无需任何处理。
例如:
class FK_DLL CMyDllClass{} ; // Dll文件内的代码
-----------------------
class CAppClass : public CMyDllClass // 应用程序内代码,无需做任何处理。
{
....
}
也可以直接使用DLL导出类
void main
{
CMyDllClass* pClass = new CMyDllClass ();
}
但是,若应用程序声明或者分类一个DLL中导出类的对象时会存在一个很讨厌的问题:这个操作会使内存跟踪系统失效,使其错误的报告内存分配和释放情况。
为解决这个问题,我们可以给出两个接口函数对DLL导出类进行创建销毁支持,就可以使内存跟踪系统正常了。例如
class FK_DLL CMyDllClass{} ;
额外增加俩函数
FK_DLL CMyDllClass* CreateMyDllClass(){ return new CMyDllClass(); }
FK_DLL void DestoryMyDllClass( CMyDllClass* p_pClass ){ delete p_pClass; }
-----------------------------------------------
上面的方法可以正确进行内存跟踪了,但是,因为DLL导出类CMyDllClass依旧是导出的状态,用户同样可以跳过我们提供的接口直接使用。那么怎么办呢。方法是不再对类进行DLL导出,而对类内的函数全部进行DLL导出即可,
-----------------------------------------------
但是若仅仅提供上面两个接口函数以及类内全部函数,的确功能可以实现,却无法进行类继承了。若这个类继承很重要,必须开放,那么就需要使用新的内存跟踪程序替换应用程序内的原有内存跟踪程序。或者使用下面的一个方法。(见模块9:复杂问题)
-----------------------------------------------
同样,我们也可以发现,在不导出DLL类本身,而只导出DLL类内函数也有一些好处,一些我们不希望外界知道的函数可以不设置导出标记,这进一步保护了DLL内函数的安全性。
9:复杂问题。
若我们使用LoadLibrary显式加载一个DLL,并尝试在应用程序中调用一个类内成员函数的话,无论该函数是否在头文件中有声明,VS会给出一个"unresolved external symbol(未解析的外部符号)"的错误。我们此时可以将项目属性中的内联函数扩展选项修改为"Only __inline"或"Any Suitable"即可。但,我们可能在调试连编的时候期望关闭内联函数扩展,那么另一种解决方案是,将希望导出的函数声明为虚函数,例如
class CMyDllClass
{
FK_DLL virtual void MyTestFun( int a ){ dosth(); }
// 用上面代码替换 FK_DLL void MyTestFun( int a ){ dosth(); }
}
这样做还有一个额外的好处。将导出的类成员函数设置为虚函数之后,该虚函数所在的类在应用程序中也如同被声明一样,可以接受继承。
例如若是上面的做法,应用程序就可以进行顺利继承,而不必要求CMyDllClass 被标示为导出。(原理不知,希望精通底层的高手协助解释。)
class CAppClass : public CMyDllClass // 应用程序内代码,无需做任何处理。
{
....
}