Windows静态链接库与动态链接库的创建和显式与隐式调用
一、链接库简介
库是写好的现有的,成熟的,可以复用的代码。链接库主要可分为静态库与动态库两种,动态库调用又可分为隐式调用与显式调用两种,下面给出两者间的主要区别。如果理解有困难可以先继续往下看,实践一遍后再回味。
1.1 静态库与动态库区别
- 静态链接库的后缀名为lib,动态链接库的导入库的后缀名也为lib。不同的是,静态库中包含了函数的实际执行代码,而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息;
- 由于静态库是在编译期间直接将代码合到可执行程序中,而动态库是在执行期时调用DLL中的函数体,所以执行速度比动态库要快一点;
- 静态库链接生成的可执行文件体积较大,且包含相同的公共代码,造成内存浪费;
- 使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。而使用运行时动态链接,系统不会终止,但由于DLL中的导出函数不可用,程序会加载失败;
- DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性,适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。
1.2 动态库隐式与显式调用的区别
- 隐式调用需要调用者写的代码量少,调用起来和使用当前项目下的函数一样直接;而显式调用则要求程序员在调用时,指明要加载的动态库的名称和要调用的函数名称;
- 隐式调用由系统加载完成,对程序员透明;显式调用由程序员在需要使用时自己加载,不再使用时,自己负责卸载;
- 由于显式调用由程序员负责加载和卸载,好比动态申请内存空间,需要时就申请,不用时立即释放,因此显式调用对内存的使用更加合理, 大型项目中应使用显式调用;
- 当动态链接库中只提供函数接口,而该函数没有封装到类里面时,如果使用显式调用的方式,调用方甚至不需要包含动态链接库的头文件(需要调用的函数名是通过GetProcAddress()函数的参数指明的),而使用隐式调用时,则调用方不可避免要加上动态库中的头文件和导入库文件;
- 显式调用更加灵活,可以模拟多态效果(具体见后文)。
二、Windows静态库的创建和使用
2.1 Windows静态库的创建
如果是使用VS命令行生成静态库,也是分两个步骤来生成程序:
- 首先,通过使用带编译器选项 /c 的 Cl.exe 编译代码 (cl /c mystatic.cpp),创建名为“mystatic.obj”的目标文件。
- 然后,使用库管理器 Lib.exe 链接代码 (lib mystatic.obj),创建静态库mystatic.lib。
我们一般不这么用,使用Visual Studio工程设置更方便。创建win32控制台程序时,勾选静态库类型;或者打开工程“属性面板”–>”配置属性”–>”常规”–>配置类型–>选择静态库(.lib)也可以生成静态库。
示例代码使用跟Linux篇的一致,方便对比,下面仅列出头文件代码:
// mylib.h
#ifndef _TEST_H
#define _TEST_H
extern "C" float add(float a, float b);
extern "C" float sub(float a, float b);
extern "C" float mul(float a, float b);
#endif
编译生成静态库文件在目录.\staticlib\Debug\staticlib.lib下。
2.2 Windows静态库的调用
2.2.1菜单链接包含静态库文件和头文件
- “属性面板”–>”配置属性”–>“链接器”–>”常规”–>“附加库目录”–>输入静态库所在目录;
- “属性面板”–>”配置属性”–>“链接器”–>”输入”–>“附加依赖库”–>输入静态库名staticlib.lib;
- “属性面板”–>”配置属性”–>“C/C++”–>” 常规”–>“附加包含目录”–>键入mylib.h 头文件所在目录的路径或浏览至该目录;
- 编译生成可执行文件在目录.\test\Debug\test.exe,已链接静态库,可独立运行。
2.2.2命令链接包含静态库文件和头文件
先将前面生成的staticlib.lib静态库文件和头文件mylib.h拷贝到测试案例test.cpp所在目录(也可以包含全路径),在test工程添加现有头文件mylib.h,给出test.cpp示例代码如下:
#include "stdafx.h"
#include "mylib.h"
#include <cstdlib>
#include <iostream>
#pragma comment(lib, "staticlib.lib")
using namespace std;
int main(int argc, char *argv[])
{
float a = 3.7, b = 2.9;
for(int i = 0; i < argc; ++i){
printf("argv[%d]: %s\n", i, argv[i]);
if(i == 1)
a = atof(argv[1]);
if(i == 2)
b = atof(argv[2]);
}
cout << "a + b = " << add(a, b) << endl;
cout << "a - b = " << sub(a, b) << endl;
cout << "a * b = " << mul(a, b) << endl;
return 0;
}
生成可执行文件,Ctrl+F5执行结果如下图示:
也可以通过VS给main函数传递参数运行调试,具体方法如下:
- “属性面板”–>”配置属性”–>”调试”–>”命令参数“–>输入参数,比如”345.21 123.89“
三、Windows动态库的创建和使用
3.1 Windows动态库的创建
创建win32控制台程序时,勾选DLL类型;或者打开工程“属性面板”–>”配置属性”–>”常规”–>配置类型–>选择动态库(.dll)也可以生成静态库。
创建好的工程默认提供了一个dllmain.cpp里面有一个DllMain函数是动态链接库的入口函数,VS已经帮我们创建好了,不需要改动该文件,示例代码如下:
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "stdafx.h"
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;
}
// mydll.h头文件声明函数以extern "C" __declspec(dllexport)导出为dll
#pragma once
#ifdef DYNAMIC_H_ //通过该宏是否定义,自动判断要导出还是导入dll的函数或变量符号
#define DLL_EXPORT_IMPORT extern "C" __declspec(dllexport)
#else
#define DLL_EXPORT_IMPORT extern "C" __declspec(dllimport)
#endif
DLL_EXPORT_IMPORT float add(float a, float b);
DLL_EXPORT_IMPORT float sub(float a, float b);
DLL_EXPORT_IMPORT float mul(float a, float b);
包含库函数定义的源文件与静态链接库一致,只是多了如下的宏定义,编译时弹出警告,建议宏定义放到预编译头文件内,把该宏定义放到"stdafx.h"头文件内再次编译无警告,编译生成静态库文件在目录.DLL\dynamic\Debug\dynamic.dll下。
#define DYNAMIC_H_
补充一点关于"mydll.h"宏定义的说明,查微软MSDN有下面一段话。本文示例并没有导出变量,所以没有__declspec(dllimport)直接使用extern “C” __declspec(dllexport)也是可以的,但如果要导出变量,就要使用上面"mydll.h"宏定义的形式了。
不使用 __declspec(dllimport) 也能正确编译代码,但使用 __declspec(dllimport) 使编译器可以生成更好的代码。编译器之所以能够生成更好的代码,是因为它可以确定函数是否存在于 DLL 中,这使得编译器可以生成跳过间接寻址级别的代码,而这些代码通常会出现在跨 DLL 边界的函数调用中。但是,必须使用 __declspec(dllimport) 才能导入 DLL 中使用的变量。
3.2 Windows动态库的隐式调用
动态库的隐式调用与前面提到的静态库调用很相似,同样需要包含一个头文件mydll.h和一个动态导入库文件dynamic.lib,同样支持菜单操作和命令包含两种方式,这里不再详细赘述,只给出测试文件test.cpp包含的头文件如下:
//test.cpp包含的头文件,包括mydll.h和dynamic.lib
#include "stdafx.h"
#include "mydll.h"
#include <cstdlib>
#include <iostream>
#pragma comment(lib, "dynamic.lib")
这里可能大家有个疑问,动态库怎么还有一个dynamic.lib文件?即无论是静态链接库还是动态链接库,最后都有lib文件,那么两者区别是什么呢?其实,两个是完全不一样的东西。
staticlib.lib的大小为18KB,dynamic.lib的大小为2KB,静态库对应的lib文件叫静态库,动态库对应的lib文件叫导入库。实际上静态库本身就包含了实际执行代码、符号表等等,而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。
测试程序执行结果及传参过程跟静态库方法一致,只有一点区别,可执行程序必须能找到其对应的依赖动态库dynamic.dll,否则执行报错。
四、Windows动态库的显式调用
Windows显式调用动态库,#include <windows.h>头文件中提供了下面几个接口:
- HMODULE LoadLibraryA(LPCSTR lpLibFileName):加载 DLL 和获取模块句柄;
- FARPROC GetProcAddress(HMODULE hModule,LPCSTR lpProcName):获取指向应用程序要调用的每个导出函数的函数指针。由于应用程序是通过指针调用 DLL 的函数,编译器不生成外部引用,故无需与导入库链接;
- BOOL FreeLibrary(HMODULE hLibModule):使用完 后释放DLL 。
下面给出示例代码,生成动态链接库(libdynamicmath.so)的代码跟前面一致,这里只列出测试代码explicit.cpp如下:
// explicit.cpp不再包含"mydll.h"头文件和"dynamic.lib"导入库文件,通过程序内部命令显式加载和释放
#include "stdafx.h"
#include <cstdlib>
#include <iostream>
#include <Windows.h>
using namespace std;
int main(int argc, char *argv[])
{
if(argc < 2){
cout << "Argument error." << endl;
exit(1);
}
float a = 3.7, b = 2.9;
char *libname = nullptr;
for(int i = 0; i < argc; ++i){
printf("argv[%d]: %s\n", i, argv[i]);
if(i == 1)
libname = argv[1];
if(i == 2)
a = atof(argv[2]);
if(i == 3)
b = atof(argv[3]);
}
//open the lib
HMODULE hMod = LoadLibraryA(libname);
if(hMod == NULL){
cout << "Load " << libname << " failed." << endl;
exit(1);
}else{
cout << "Load " << libname << " successfully." << endl;
}
//get function pointer
typedef float (*pf_t)(float, float);
pf_t add = (pf_t)GetProcAddress(hMod, "add");
pf_t sub = (pf_t)GetProcAddress(hMod, "sub");
pf_t mul = (pf_t)GetProcAddress(hMod, "mul");
if(!(add && sub && mul)){
cout << "Can't find symbol function." << endl;
FreeLibrary(hMod);
exit(1);
}else{
cout << "Find this symbol function." << endl;
}
//call library function
cout << "a + b = " << add(a, b) << endl;
cout << "a - b = " << sub(a, b) << endl;
cout << "a * b = " << mul(a, b) << endl;
//close the lib
if(!FreeLibrary(hMod)){
cout << "Close " << libname << " failed." << endl;
exit(1);
}
return 0;
}
通过VS给main函数传递参数(“dynamic.dll 876.456 345.897”)运行调试,结果如下图示:
把动态链接库以参数形式传递给可执行程序进行选择调用,如果多个dll中有同一函数的不同实现,便可以通过这种方式实现多态调用效果。读者也可以稍加改动,把函数名也通过参数传递给可执行程序实现选择调用。可以通过dumpbin /EXPORTS dynamic.dll命令查看符号表,从中找到库文件里面的函数名。如果dumpbin显示不识别或不是内部命令,需要找到其所在路径并添加到环境变量。
本文的源代码可以到GitHub下载:https://github.com/StreamAI/LinkLibrary (不熟悉GitHub使用的可以参考文章:GitHub社会化编程)。
如果想了解Linux环境动态库与静态库的创建和使用,可以查看我的另一个博文:Linux静态链接库与动态链接库的创建和显式与隐式调用
如果想深入了解动态库与静态库的链接与调用过程,推荐一本书《程序员的自我修养—链接、装载与库》。