1、
动态链接库是一些独立的文件,其中包含能被可执行文件或其它动态链接库调用的函数。
静态库:扩展名.lib,程序编译链接时,链接器从库中复制这些函数和数据同其它模块组合,以生成可执行文件。程序运行时不再需要该库文件。
动态链接库:通常包含一个引入库文件.lib和DLL文件.dll,引入库文件包含DLL文件导出的函数和变量的符号名。程序编译链接时,链接器只链接引入库文件,不去复制DLL文件中的函数代码和数据,直到程序运行时才去加载所需的DLL文件,访问DLL文件中的函数和数据。故程序运行时,必须有DLL文件。
动态链接库有两种加载方式:隐式链接和显式加载。
2、Win32 DLL的创建
创建DLL工程方法:新建项目->选择Win32项目->应用程序类型选择DLL->选择空项目->为工程添加一个CPP源文件->在该源文件中编写相应函数->生成。
要想访问DLL中的函数,该函数必须是已经被导出的函数,为了让DLL导出函数,可以在定义该函数前加上_declspec(dllexport),编译后可以看到生成了一个DLL文件和一个lib文件。
eg:
/*Dll1.cpp*/
_declspec(dllexport) int add(int a, int b)
{
return a + b;
}
_declspec(dllexport) int sub(int a, int b)
{
return a - b;
}
class _declspec(dllexport) CFoo
{
public:
CFoo(int n){}
void func(){}
};
在程序中调用DLL中的函数时,应先用extern声明该函数为外部函数(或用_declspec(dllimport)声明该函数为DLL中的外部函数)。所以我们还需要添加一个头文件,这个头文件DLL本身并不需要,而是给使用DLL的人来使用:
/*Dll1.h*/
extern int add(int a, int b);
_declspec(dllimport) int sub(int a, int b);
class _declspec(dllimport) CFoo
{
public:
CFoo(int n);
void func();
};
想要查看一个DLL中有哪些导出函数,可以利用VS提供的命令行工具Dumpbin来实现。
3、Win32 DLL的使用:隐式方式加载DLL
/*DLLTestDlg.cpp*/
#include "Dll1.h"
void CDLLTestDlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
CString str;
str.Format(L"5+3=%d", add(5,3));
MessageBox(str);
}
void CDLLTestDlg::OnBnClickedButton2()
{
// TODO: 在此添加控件通知处理程序代码
CString str;
str.Format(L"5-3=%d", sub(5,3));
MessageBox(str);
}
然后将DLL文件放至可执行文件目录下或系统目录C:\WINDOWS\system32、C:\WINDOWS下或path环境变量的路径中,添加对lib文件的链接:方法一,属性->链接器->附加库目录->输入.lib所在目录,属性->连接器->输入->附加依赖项->输入lib文件名;方法二,将lib文件放至当前工程目录下,属性->链接器->命令行->附加选项->添加lib文件名;方法三,将lib文件放至当前工程目录下,在代码中添加#pragma comment预处理指令,如#pragma comment(lib, "Ws2_32.lib")。
5、从DLL中导出C++类
还可以从动态链接库中导出C++类:在DLL程序头文件中声明这个类,还需在class后添加_declspec(dllimport)关键字,在DLL程序源文件中添加类中成员函数的实现,而且此时,在源文件中需要添加对于头文件的包含,所以源文件中函数定义前的_declspec(dllimport)关键字应该去掉:
/*Dll1.h*/
_declspec(dllimport) int add(int a, int b);
_declspec(dllimport) int sub(int a, int b);
class _declspec(dllimport) CMyPoint
{
public:
void output(int x, int y);
};
/*Dll1.cpp*/
#include <stdio.h>
#include "Dll1.h"
int add(int a, int b)
{
return a+b;
}
int sub(int a, int b)
{
return a-b;
}
void CMyPoint::output(int x, int y)
{
HWND hWnd = GetForegroundWindow();
HDC hdc = GetDC(hWnd);
wchar_t buf[20];
memset(buf, 0, sizeof(buf));
swprintf(buf, L"x=%d,y=%d", x, y);
TextOut(hdc, 0, 0, buf, wcslen(buf));
ReleaseDC(hWnd, hdc);
}
GetForegroundWindow()函数的作用是返回前景窗口的句柄,前景窗口即当前用户正在使用的那个窗口。类成员函数的作用即为在当前使用窗口的(0,0)处输出x,y的坐标值。
/*DLLTestDlg.cpp*/
#include "Dll1.h"
void CDLLTestDlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
CString str;
str.Format(L"5+3=%d", add(5,3));
MessageBox(str);
}
void CDLLTestDlg::OnBnClickedButton2()
{
// TODO: 在此添加控件通知处理程序代码
CString str;
str.Format(L"5-3=%d", sub(5,3));
MessageBox(str);
}
void CDLLTestDlg::OnBnClickedButton3()
{
// TODO: 在此添加控件通知处理程序代码
CMyPoint pt;
pt.output(5, 3);
}
另外,在实现动态链接库时还可以不导出整个类,而只导出类的成员函数:在类的声明中,只在对应函数前加_declspec(dllimport)关键字。
6、使用extern "C" 解决函数名字改编问题
C++编译器在生成DLL时,会对导出的函数进行名字改编,不同的编译器使用的改编规则不同,因此改编后的函数的名字是不一样的。这样如果生成DLL的编译器和访问该DLL的程序使用的编译器不同的话,那么在访问该DLL的导出函数时就可能出现问题,后者将使用自己的改编规则生成的函数名去调用DLL中的函数。
例如,用C++编写了一个DLL,用C语言编写的程序去调用这个DLL中函数就会出现问题,因为C编译器生成的程序会使用函数的原始名称去调用DLL中函数,而C++编译器生成的DLL已经对该函数名称进行了改编。所以,如果我们希望DLL文件在编译时,导出函数的名称不要发生变化,那么在定义该函数前加上限定符:extern "C"。即在C++源文件中的语句前面加上extern "C",表明这些语句应该按照类C的编译和连接规约来编译和连接。
extern "C"只能用于导出全局函数,不能用于导出类的成员函数,且如果导出函数的调用约定发生了改变(比如DLL中方法为WINAPI或CALLBACK),那么导出函数名仍会发生改编。这种情况下,可以通过一个称为模块定义文件(DEF)的方法来解决名字改编问题。方法为右键项目选择添加“模块定义文件(.def)”,在这个文件中声明DLL名称和需要导出的方法名,如下所示,然后右键项目属性选择连接器-输入-模块定义文件,输入我们创建的.def文件名。
LIBRARY DLL_NAME
EXPORTS
func1
func2
C++中调用C生成的库的时候,在C的头文件中添加extern "C"{}。
C调用C++生成的库的时候,在C++生成库的时候在C++的定义文件中添加extern "C"{}。
7、显式(动态)加载方式加载DLL
LoadLibrary():加载指定的DLL或可执行模块,将它映射到调用进程的地址空间,函数原型:
HMODULE LoadLibrary(LPCTSTR lpFileName);
参数为DLL文件名或可执行模块exe文件名;加载可执行模块exe文件时,一般主要是为了访问该模块的一些资源,如对话框,位图,图标等。函数执行成功返回加载的那个模块的句柄,为HMODULE类型,其可以与HINSTANCE类型通用。函数执行失败返回0。
GetProcAddress():获得DLL中导出函数的地址,函数原型:
FARPROC GetProcAddress(
HMODULE hModule,
LPCSTR lpProcName
);
第一个参数为DLL模块的句柄,第二个参数指定导出函数的名字或函数的序号。函数调用成功返回导出函数的地址,应该定义一个对应的函数指针来保存该地址,通过这个函数指针来调用对应的函数;函数执行失败返回NULL。GetProcAddress()返回NULL也有可能是因为导出的函数名称改编问题,这时候可以给DLL中方法添加extern "C",调用DLL中方法的声明也要添加extern "C"。
FreeLibrary():在需要访问DLL时,调用LoadLibrary()加载DLL,当不再需要访问该DLL时,调用FreeLibrary()释放对DLL的引用。函数原型:
BOOL WINAPI FreeLibrary(HMODULE hModule);
参数为DLL模块的句柄。
#include <stdio.h>
#include<Windows.h>
#include <tchar.h>
int _tmain(int argc, _TCHAR* argv[])
{
//映射DLL到当前进程的地址空间
HINSTANCE hInst=LoadLibrary(_T("Kernel32.DLL"));
if(hInst)
{
typedef BOOL(WINAPI * MYFUNC)(LPTHREAD_START_ROUTINE, PVOID,ULONG);
MYFUNC MyQueueUserWorkItem=NULL;
//取得QueueUserWorkItem函数指针
MyQueueUserWorkItem=(MYFUNC)GetProcAddress(hInst,"QueueUserWorkItem");
if(!MyQueueUserWorkItem)
printf("函数指针获取失败");
}
//释放DLL引用
FreeLibrary(hInst);
return 0;
}
8、DllMain()函数
对于可指定模块来说,其入口函数为WinMain(),而对于DLL,其入口函数为DllMain(),但该函数是可选的,即可以提供也可以不提供该函数。DllMain的第一个参数为DLL模块的句柄, 第二个参数指明了系统调用Dll的原因,第三个参数可以用来判断是动态加载还是隐式加载。
9、MFC DLL
除了Win32 DLL,还可以编写MFC DLL,MFC DLL增加了对MFC类库的支持。
内容出处:《孙鑫VC++深入详解》
10、静态库的创建和使用
静态库的创建:新建项目-win32项目-程序类型选择“静态库”。然后分别添加头文件和源文件,内容容下所示。然后编译项目就可得到.lib静态库文件。
需要注意的一点是,静态库编译时的选项:“C/C++->代码生成->运行库” 应该与使用项目一致。
头文件:
//header.h
#ifndef __MY_LIB_HEADER__
#define __MY_LIB_HEADER__
int add(int a, int b);
int sub(int a, int b);
#endif
源文件:
//lib.cpp
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
静态库的使用有三种方式,首先都是将静态库的头文件添加到使用项目中,然后:
①、将静态库文件放置指定目录,然后将指定目录设置为项目的附加库目录:项目属性->链接器->常规->附加库目录。然后在项目属性->链接器->输入->附加依赖项(或项目属性->链接器->命令行)添加.lib静态库文件名。
②、将静态库文件放置指定目录。在文件中使用#pragma comment(lib, "path\\static_lib_name")来链接静态库。
11、DLL文件冲突解决
有时候我们想要把一些DLL文件单独放到一个目录中,或者像我们的应用使用了cef浏览器,我们的应用exe和cef浏览器是在一个目录下的,所以DLL文件也在一个目录下,然后发生了我们应用里使用的DLL和cef浏览器使用的DLL是同一种但版本不同的DLL,这种情况,如果我们不想移动cef的exe的话,就可以使用延迟加载DLL。
方法是在我们的项目属性-连接器-输入-延迟加载的DLL里输入冲突的DLL名,然后在Main()执行后调用SetDllDirectory(L"WeDlls\\"),然后把我们的DLL移动到WeDlls目录中,cef项目也是一样,在项目属性里设置延迟加载的DLL为冲突的DLL,在Main()执行后调用SetDllDirectory(L"cefDlls\\"),然后把cef的DLL移动到cefDlls目录中。
12、使用def文件生成.lib文件
有时候第三方库只提供了dll动态库和.def文件,没有.lib导入库文件,我们可以通过.def文件来生成.lib导入库,方法是进入VS命令行,执行 lib /def:libdll.def命令,其中libdll.def即为第三方库提供的def文件。