应用背景
在程序开发时,无论项目怎么变化,有些功能是必不可少同时又是不变的,比如数据存储,通讯,或者某个第三发组件。这个时候,我们无需重新造轮子,应该用想办法使之标准化。使用静态库,动态库是重用代码的一种绝佳方式。
具体应用
一 静态库:
编译静态库时,只会产生.lib文件。所有数据都在lib文件中。静态库的使用方式只有一种,即静态载入,在程序编译链接阶段,会将静态库中的所有数据都链接合并到最终生成的exe文件中,链接完成后就不再需要静态库文件,这样方便程序移植,但是也带来程序臃肿过大的弊端。同时,如果静态库中函数有所变化,所有引用它的工程需要重新编译来生成新的exe文件,过程较为麻烦。
二 动态库:
编译动态库时,会产生.lib文件和.dll文件。lib文件只包含函数或者变量的地址信息,而其具体实现都在dll文件中。编译链接阶段只需要.lib文件中的数据,与dll文件无任何关系,程序运行时才会用到dll文件,与lib文件无任何关系。动态库的使用方式包括静态载入和动态载入,静态载入在程序启动时就载入dll文件,动态载入在需要时使用函数LoadLibrary手动载入dll文件,载入dll文件的时机更加随机而不会都在程序启动时一起载入,对于程序中大量使用dll文件时尤为有效,减少了程序启动的时间,在特定条件下载入特定的dll文件,分散dll文件的载入时间。同时,如果动态库中函数实现有所变化,只需要用新的dll文件替换掉旧的dll文件即可,exe文件无需重新编译生成,非常适合库中函数需要经常更新的场合。
三 解决方案TestLibrary
下面结合一个具体的工程来做解释。解决方案为TestLibrary,TestLibary目录中内容如下:
文件夹TestDll、TestLib、UseDll、UseLib
是四个工程的主目录,分别用来生成动态库、生成静态库、使用动态库、使用静态库,各个工程的输出文件路径都设置为Output
。
1 工程TestLib
有两个文件TestLib.h
与TestLib.cpp
。
TestLib.h
内容:
// 注意:静态库导出函数无需使用extern "C" __declspec(dllexport)修饰
void TestLibFunc1();
void TestLibFunc2();
TestLib.cpp
内容:
#include "TestLib.h"
#include <iostream>
void TestLibFunc1()
{
std::cout << "TestLibFunc1" << std::endl;
}
void TestLibFunc2()
{
std::cout << "TestLibFunc2" << std::endl;
}
编译后只生成TestLib.lib
文件。
2 工程TestDll
有两个文件TestDll.h
与TestDll.cpp
。
TestDll.h
内容:
// 注意:动态库导出函数一定要使用__declspec(dllexport)修饰
// 如果不使用__declspec(dllexport)进行修饰,
// 编译时不会生成.lib文件。
#define EXPORT __declspec(dllexport)
EXPORT void TestDllFunc1();
EXPORT void TestDllFunc2();
TestDll.cpp
内容:
#include "TestDll.h"
#include <iostream>
void TestDllFunc1()
{
std::cout << "TestDllFunc1" << std::endl;
}
void TestDllFunc2()
{
std::cout << "TestDllFunc2" << std::endl;
}
编译后生成TestDll.lib
和TestDll.dll
文件。
3 工程UseLib与UseDll
均只有一个文件main.cpp用来调用库中函数,其具体内容根据的载入方式有所不同。
四 静态载入和动态载入:
静态载入:在程序启动时载入。
动态载入:在程序运行过程中通过调用库函数实现载入库。
五 静态库的载入
因为静态库中所有数据在程序的编译链接阶段全部合并到exe文件中,静态库就只有静态载入一种载入方式。
静态载入包括使用IDE配置方式和使用代码方式两种。
1 配置方式
- 将
头文件所在目录
添加到附件包含目录
。(可省略)- 将
.lib文件所在目录
添加到附加库目录
。- 将
.lib文件名
添加到附加依赖项
。- 在代码中
#include 头文件
。(如步骤1省略,此时需要加上头文件的路径,相对路径绝对- 路径都可)
此时main.cpp
内容:
#include "TestLib.h"
// #include "..\\TestLib\\TestLib.h" 未设置附加包含目录时需要给出文件路径
int main()
{
TestLibFunc1();
TestLibFunc2();
return 0;
}
2 代码方式
将头文件所在目录添加到附件包含目录。(可省略)
将.lib文件所在目录添加到附加库目录。(可省略)
在代码中#include 头文件。(如步骤1省略,此时需要加上头文件的路径,相对路径绝对路径都可)
使用#pragma comment(lib, "lib文件名")载入库。(如步骤2省略,此时需要加上lib文件的路劲,相对路径绝对路径都可)
此时main.cpp
内容:
#include "TestLib.h"
// #include "..\\TestLib\\TestLib.h" 未设置附加包含目录时需要给出文件路径
#pragma comment("lib", "TestLib.lib")
// pragma comment("lib", "..\\Output\\TestLib.lib") 未设置附加库目录时需要给出文件路径
int main()
{
TestLibFunc1();
TestLibFunc2();
return 0;
}
六 动态库的载入
又称显示调用与隐士调用
动态库的实现部分在.dll文件中,动态库的载入方式包括静态载入和动态载入两种方式。
动态库的静态载入跟静态库的静态载入一样,只有一点需要注意:需要将.dll文件放到.exe文件所在目录下,exe文件运行时只会在所在目录搜寻dll文件(动态载入也一样)**,如果找不到会弹出系统错误提示框:由于找不到***, 无法继续执行代码,重新安装程序可能会解决此问题。
动态库的动态载入通过在代码中调用相关库函数实现,包括LoadLibrary()
、GetProcAddress()
、FreeLibrary()
等。动态载入方式只需要使用.dll文件,头文件以及.lib文件均不需要(注意这个区别)。
1 动态载入步骤
调用LoadLibrary()载入库, 保存其函数返回值,供之后GetProcessAddress()和FreeLibrary()使用。
调用GetProcessAddress()搜寻所需函数,将其返回值转换成函数指针,使用该函数指针完成函数调用。
调用FreeLibrary()释放库。
此时main.cpp
内容:
#include <windows.h>
typedef int(*FuncAddr)();
int main()
{
HINSTANCE dllDemo = LoadLibraryA("..\\Output\\TestDll.dll"); // 必须给出文件所在路径
if (dllDemo)
{
// 直接通过函数名称获取函数指针,需要注意,动态库的导出函数一定要使用extern "C"进行修饰
// 因为只有通过C编译器编译导出的符号名才直接是函数名。
FuncAddr func1 = (FuncAddr)GetProcAddress(dllDemo, "TestDllFunc1");
if (func1 != nullptr) func1();
FuncAddr func2 = (FuncAddr)GetProcAddress(dllDemo, "TestDllFunc2");
if (func2 != nullptr) func2();
FreeLibrary(dllDemo);
}
return 0;
}
补充: 在生成后事件中复制 DLL
右键单击“解决方案资源管理器”中的“MathClient”节点,然后选择“属性”以打开“属性页”对话框
在“配置” 下拉框中,选择“所有配置” (如果尚未选择)。
在左窗格中,选择“配置属性”>“生成时间”>“后期生成事件” 。
在属性窗格中,在“命令行”字段中选择编辑控件 。 如果已按照指示将客户端项目置于 DLL 项目的单独解决方案中,则输入以下命令:
xcopy /y /d "..\..\MathLibrary\$(IntDir)MathLibrary.dll" "$(OutDir)"