动态链接库通常不能直接运行,也不能接收消息。它们是一些独立的文件,其中包含能被可执行程序或其他DLL调用来完成某项工作的函数。只有在其他模块调用动态链接库中的函数时,它才发挥作用。实际编程时,可把完成某种功能的函数放在一个动态链接库中,提供给其他程序调用。
Windows API中所有函数都包含在DLL中,比较重要的有3个,分别为:
1、Kernel32.dll
包含用于管理内存、进程和线程的函数。
2、User32.dll
包含用于执行用户界面任务(如窗口创建和消息传递的函数等)的函数。
3、GDI32.dll
包含用于画图和显示文本的函数。
静态库和动态库
1、静态库
函数和数据被编译成一个二进制文件,扩展名为.LIB。使用静态库时,编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其他模块组合起来创建最终的可执行文件。发布产品时,只需要发布这个可执行文件,并不需要发布被使用的静态库。
2、动态库
需要提供2个文件:引入库.lib文件和DLL文件。动态库的引入库.lib文件与静态库虽然后缀相同,但含义完全不同。引入库文件.lib包含DLL导出的函数和变量的符号名,.dll文件包含DLL实际的函数和数据。编译链接时,只需要链接DLL的引入库文件,DLL中的函数代码和数据并不复制到可执行文件中,直到可执行程序运行时,才去加载需要的DLL,将该DLL映射到进程的地址空间,然后访问DLL中的导出函数。发布产品时,除了发布可执行文件,还要同时发布动态链接库。
如果多个应用程序使用同一个DLL,该DLL的页面只需要放入内存一次,所有的应用程序都可以共享它的页面。
有两种加载动态链接库的方式:隐式链接和显式加载。
应用程序如果想访问某个DLL中的函数,该函数必须是已经被导出的函数。为了让DLL导出一些函数,需要在每一个将要被导出的函数前面添加标志符_declspec(dllexport)。
客户调用DLL的导出函数时,必须先申明该函数是外部的extern。除了使用extern关键字表明函数是外部定义的之外,还可以使用_declspec(dllimport)来表明函数是从动态链接库中引入的。使用_declspec(dllimport)声明外部函数,能够明确告诉编译器函数是从动态链接库中引入的,编译器可以生成运行效率更高的代码。
通常在编写动态链接库时,都会提供一个头文件,在此文件中提供DLL导出函数原型的声明,以及函数的有关注释文档。程序编译时,头文件不参与编译,源文件单独编译。
dll_1.h文件
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);
dll_1.cpp文件
#define DLL1_API _declspec(dllexport)
#include "dll_1.h" DLL1_API int add(int a, int b)
{
return a + b;
} DLL1_API int subtract(int a, int b)
{
return a - b;
}
动态链接库还可以导出C++类。
dll_1.h文件
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b); class DLL1_API point {
public:
void Output(int x, int y);
};
dll_1.cpp文件
#define DLL1_API _declspec(dllexport)
#include "dll_1.h" DLL1_API int add(int a, int b)
{
return a + b;
} DLL1_API int subtract(int a, int b)
{
return a - b;
} void point::Output(int x, int y)
{
HWND hwnd = GetForegroundWindow();
HDC hdc = GetDC(hwnd);
TCHAR buf[] = { '\0' };
swprintf(buf, , _T("x = %d, y = %d"), x, y);
TextOut(hdc, , , buf, wcslen(buf));
ReleaseDC(hwnd, hdc);
}
如果隐式链接加载DLL,.dll更新后,需要将新的.dll和.lib文件复制到相应工程目录。
动态链接库还可以不导出整个C++类,而只导出C++类的某些函数。
dll_1.h文件
class/* DLL1_API*/ point {
public:
void DLL1_API Output(int x, int y);
void test();
};
如果声明类时,指定了导出标志,该类中的所有函数都将被导出,否则只有那些声明时指定了导出标志的类成员函数才被导出。
名字改编问题
C++编译器生成DLL时,会对导出函数的名字进行改编,不同编译器使用的改编规则不同,改编后的名字是不一样的。如果利用不同的编译器分别生成DLL和访问DLL的客户端程序,客户端程序访问DLL的导出函数时就会出现问题。因此,希望动态链接库文件编译时,导出函数的名称不要发生改变。做法是,定义导出函数时,加上限定符extern “C”,字母C大写。
dll_1.h文件
#ifdef DLL1_API
#else
#define DLL1_API extern "C" _declspec(dllimport)
#endif DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);
dll_1.cpp文件
#define DLL1_API extern "C" _declspec(dllexport)
#include "dll_1.h" DLL1_API int add(int a, int b)
{
return a + b;
} DLL1_API int subtract(int a, int b)
{
return a - b;
}
extern “C”可以解决C++和C语言之间相互调用时函数命名的问题,但该方法不能用于导出一个类的成员函数,只能用于导出全局函数。如果导出函数的调用约定发生改变,即使使用限定符extern “C”,函数的名字仍会发生改变。
显示加载方式加载DLL
LoadLibrary的作用是将指定的可执行模块映射到调用进程的地址空间,返回值是加载模块的句柄。GetProcAddress函数用来获得导出函数的地址。动态加载DLL时,客户端不再需要包含导出函数声明的头文件和引入库文件,只需要.dll文件即可。
void CMFCApplication1Dlg::OnBnClickedBtnSub()
{
HMODULE HInst = LoadLibrary(_T("ConsoleApplication1.dll"));
if (HInst == NULL)
return;
typedef int (*ADDPROC)(int a, int b); ADDPROC Add = (ADDPROC)GetProcAddress(HInst, "add");
if (!Add) {
MessageBox(_T("获取函数地址失败!"));
return;
}
CString str;
str.Format(_T("5 + 3 = %d."), Add(, ));
MessageBox(str);
}
实际上采用隐式链接方式访问DLL时,程序启动时也是通过调用LoadLibrary函数加载该进程的动态链接库的。
如果采用动态加载方式使用DLL,访问DLL的客户端程序,如果对DLL的访问已经完成,不再需要访问该DLL时,应调用FreeLibrary函数释放该DLL。FreeLibrary主要减少被加载的DLL的引用计数。当计数变为0时,该DLL模块将从调用进程的地址空间卸载。
DllMain函数
DLL的入口函数是DllMain,该函数是可选的。编写DLL程序时,可以提供也可以不提供DllMain函数。如果提供了DllMain函数,系统加载该DLL时,就会调用该函数。
不应该在DllMain函数中进行复杂的调用,因为加载该动态链接库时,可能还有一些核心动态链接库没有被加载。如果自己编写的DllMain函数需要调用核心动态链接库中的某些函数,就会导致程序终止。
参考资料:
1、《VC++深入详解》