本文档用于记录 Visual Studio 2019 平台下库文件的生成与使用方法,主要包括静态库和动态库两部分内容。
完成本文档学习后,应能够完成以下操作:
.lib;.lib;.dll;.def 生成动态库的方法;.lib 调用动态库;LoadLibrary、GetProcAddress 加载 .dll 并调用函数;Library 学习文档按照基础学习任务要求进行存放,推荐目录结构如下:
Library
|---pictures
| |---Library_001_学习目标.png
| |---Library_002_目录结构.png
| |---Library_003_库的作用.png
| |---Library_004_静态库和动态库关系.png
| |---Library_005_创建静态库项目.png
| |---Library_006_静态库项目属性.png
| |---Library_007_生成静态库.png
| |---Library_008_调用静态库配置.png
| |---Library_009_创建动态库项目.png
| |---Library_010_导出语句生成动态库.png
| |---Library_011_DEF文件生成动态库.png
| |---Library_012_动态库调用配置.png
| |---Library_013_DLL运行目录.png
| |---Library_014_常见错误.png
| +---Library_015_验收总结.png
+---Library.md
图片统一放在 pictures 文件夹下,Markdown 文档中通过相对路径进行引用。
库可以理解为已经写好并封装好的功能模块。库中通常包含一组函数、类或接口,其他工程可以通过包含头文件并链接库文件的方式来使用这些功能。
在 C/C++ 工程中,常见库文件包括:
| 类型 | 常见后缀 | 说明 |
|---|---|---|
| 静态库 | .lib |
在链接阶段被合并到最终程序中 |
| 动态库 | .dll |
程序运行时加载和使用 |
| 动态库导入库 | .lib |
配合 .dll 使用,保存 DLL 中函数的导入信息 |
| 头文件 | .h |
提供函数声明、类型定义、宏定义等接口信息 |
需要注意:VS2019 中 .lib 可能有两种含义。第一种是静态库文件,里面包含真正的函数实现;第二种是动态库的导入库,里面不保存完整实现,只保存链接 DLL 所需的导入信息。
自建库或调用库的意义主要体现在代码复用、工程结构清晰、隐藏源码和团队协作等方面。
| 意义 | 说明 |
|---|---|
| 代码复用 | 一份稳定代码可以被多个工程重复使用 |
| 工程清晰 | 将算法、通信、驱动等模块拆分出来,主工程更清楚 |
| 隐藏源码 | 调用方只看到 .h 和 .lib/.dll,看不到内部实现源码 |
| 方便协作 | 不同人员负责不同库,通过接口进行协作 |
| 降低耦合 | 主程序只依赖接口,不直接关心库内部实现 |
| 提高编译效率 | 稳定模块可提前编译成库,减少主工程重复编译 |
例如在嵌入式软件项目中,常见的库包括:
MotorControl.lib
Modbus.lib
CanProtocol.lib
FilterAlgo.lib
DriverLib.lib
主程序只需要调用库提供的接口,例如:
Motor_Start();
Motor_SetSpeed(1000);
Modbus_Poll();
而不需要关心库内部如何实现。
静态库和动态库都可以实现代码复用,但它们参与程序的时机不同。
| 对比项 | 静态库 | 动态库 |
|---|---|---|
| 常见文件 | .lib |
.dll + .lib |
| 使用时机 | 链接阶段合并到程序中 | 程序运行时加载 DLL |
| 生成结果 | 最终 .exe 中包含库代码 |
.exe 运行时依赖 .dll |
| 发布文件 | 通常只需要 .exe |
需要 .exe 和 .dll 一起发布 |
| 更新方式 | 修改库后需要重新链接程序 | 替换 DLL 后可能不需要重新编译主程序 |
| 源码隐藏 | 可以隐藏源码 | 可以隐藏源码 |
| 运行依赖 | 不依赖单独 DLL | 运行时必须能找到 DLL |
简单理解:
静态库:编译链接时放进 exe 里面。
动态库:运行时由 exe 去加载 dll。
调用库时通常需要理解三个文件的作用:
| 文件 | 作用 |
|---|---|
.h |
告诉编译器有哪些函数可以调用 |
.lib |
告诉链接器函数实现在哪里,或者如何导入 DLL 中的函数 |
.dll |
动态库真正运行时被加载的文件 |
.exe |
最终运行的主程序 |
以调用一个数学库为例:
MathLib.h 提供函数声明
MathLib.lib 提供静态库实现,或者提供 DLL 导入信息
MathDll.dll 动态库真正运行文件
MainApp.exe 调用库的主程序
编译、链接、运行的关系如下:
编译阶段:检查头文件和函数声明
链接阶段:检查 .lib 和函数实现
运行阶段:如果使用 DLL,需要能找到 .dll 文件
静态库是在链接阶段被合并进最终程序的库文件。在 VS2019 中,静态库文件通常以 .lib 为后缀。
静态库本身不能直接运行,因为它不是完整程序,没有 main 函数。它的作用是把一部分功能代码编译成库,供其他应用程序工程调用。
例如:
MathLib.lib
可以提供如下函数:
int Add(int a, int b);
int Sub(int a, int b);
主程序工程通过包含头文件并链接 MathLib.lib 后,就可以调用 Add 和 Sub 函数。
操作步骤:
打开 Visual Studio 2019;
点击 创建新项目;
搜索并选择 空项目;
输入项目名称,例如:
MathLib
创建项目后,右键项目,选择 属性;
进入:
配置属性 -> 常规 -> 配置类型
将配置类型设置为:
静态库(.lib)
点击 应用,然后点击 确定。
在静态库项目中添加头文件 MathLib.h:
#ifndef MATH_LIB_H
#define MATH_LIB_H
int Add(int a, int b);
int Sub(int a, int b);
#endif
头文件的作用是向调用方提供函数声明。调用方包含该头文件后,编译器才能知道 Add 和 Sub 函数的参数和返回值。
在静态库项目中添加源文件 MathLib.cpp:
#include "MathLib.h"
int Add(int a, int b)
{
return a + b;
}
int Sub(int a, int b)
{
return a - b;
}
源文件中保存函数的真正实现。生成静态库时,这些函数实现会被编译进 .lib 文件中。
生成步骤:
在顶部选择正确配置,例如 Debug 或 Release;
选择正确平台,例如 Win32 或 x64;
点击菜单:
生成 -> 生成解决方案
或使用快捷键:
Ctrl + Shift + B
生成成功后,可以在输出目录中找到:
MathLib.lib
默认情况下,常见输出目录可能类似:
项目目录\Debug\MathLib.lib
项目目录\x64\Debug\MathLib.lib
具体路径以项目属性中的输出目录为准。
静态库调用方通常是一个应用程序项目,例如 MainApp.exe。调用静态库需要完成三件事:
包含头文件
配置库文件路径
配置库文件名称
假设静态库文件结构如下:
LibraryDemo
|---MathLib
| |---MathLib.h
| +---MathLib.lib
|
+---MainApp
+---main.cpp
调用方工程需要完成以下配置。
第一步,配置头文件路径:
项目属性 -> 配置属性 -> C/C++ -> 常规 -> 附加包含目录
添加 MathLib.h 所在目录,例如:
$(SolutionDir)MathLib
第二步,配置库文件路径:
项目属性 -> 配置属性 -> 链接器 -> 常规 -> 附加库目录
添加 MathLib.lib 所在目录,例如:
$(SolutionDir)MathLib\$(Platform)\$(Configuration)
第三步,配置库文件名称:
项目属性 -> 配置属性 -> 链接器 -> 输入 -> 附加依赖项
添加:
MathLib.lib
主程序 main.cpp 示例:
#include <stdio.h>
#include "MathLib.h"
int main(void)
{
int ret1 = Add(1, 2);
int ret2 = Sub(5, 3);
printf("ret1 = %d\n", ret1);
printf("ret2 = %d\n", ret2);
return 0;
}
运行结果应为:
ret1 = 3
ret2 = 2
除了在工程属性中填写 附加依赖项,也可以在代码中使用 #pragma comment(lib, "xxx.lib") 指定需要链接的库文件。
示例:
#include <stdio.h>
#include "MathLib.h"
#pragma comment(lib, "MathLib.lib")
int main(void)
{
int ret = Add(1, 2);
printf("ret = %d\n", ret);
return 0;
}
需要注意:#pragma comment(lib, "MathLib.lib") 只指定库文件名,库文件所在目录仍然需要通过 附加库目录 配置,或者将 .lib 放到链接器可以搜索到的位置。
也就是说,即使使用代码语句加载 .lib,通常仍然需要配置:
项目属性 -> 配置属性 -> 链接器 -> 常规 -> 附加库目录
静态库调用时需要重点注意以下几点:
Win32 或都为 x64;/MDd 或都使用 /MD;.h 文件;.lib 所在文件夹,不是 .lib 文件本身;.lib 文件名;.cpp 没有加入静态库项目,也可能导致函数无法解析。常见错误对应关系如下:
| 问题 | 优先检查内容 |
|---|---|
| 找不到头文件 | 附加包含目录 |
找不到 .lib |
附加库目录、附加依赖项 |
| 函数无法解析 | 函数实现、源文件是否加入项目、库是否链接 |
| x64 正常但 Win32 异常 | 平台配置是否一致 |
| Debug 正常但 Release 异常 | Debug/Release 属性是否都配置 |
动态库是在程序运行时被加载的库文件。在 Windows 平台下,动态库文件通常以 .dll 为后缀。
在 VS2019 中生成动态库时,通常会生成两个重要文件:
MathDll.dll
MathDll.lib
其中:
| 文件 | 作用 |
|---|---|
.dll |
动态库运行时真正被加载的文件,保存函数实现 |
.lib |
动态库导入库,提供链接阶段所需的导入信息 |
需要注意:动态库项目生成的 .lib 通常不是静态库,它只是导入库。真正运行时仍然需要 .dll 文件存在。
简单理解:
静态库的 .lib:保存真正实现,链接进 exe。
动态库的 .lib:保存导入信息,运行时还要找 dll。
动态库生成方式主要有两种:
.def 生成。操作步骤:
打开 Visual Studio 2019;
创建一个空项目,例如:
MathDll
右键项目,选择 属性;
进入:
配置属性 -> 常规 -> 配置类型
将配置类型设置为:
动态库(.dll)
点击 应用,然后点击 确定。
添加头文件 MathDll.h:
#ifndef MATH_DLL_H
#define MATH_DLL_H
#ifdef MATHDLL_EXPORTS
#define MATHDLL_API __declspec(dllexport)
#else
#define MATHDLL_API __declspec(dllimport)
#endif
extern "C" MATHDLL_API int Add(int a, int b);
extern "C" MATHDLL_API int Sub(int a, int b);
#endif
说明:
| 内容 | 说明 |
|---|---|
__declspec(dllexport) |
生成 DLL 时导出函数 |
__declspec(dllimport) |
调用 DLL 时导入函数 |
extern "C" |
防止 C++ 函数名修饰,方便按函数名查找 |
MATHDLL_EXPORTS |
用于区分当前是生成 DLL 还是调用 DLL |
在 DLL 项目中需要定义宏:
MATHDLL_EXPORTS
通常配置位置为:
项目属性 -> 配置属性 -> C/C++ -> 预处理器 -> 预处理器定义
添加源文件 MathDll.cpp:
#include "MathDll.h"
int Add(int a, int b)
{
return a + b;
}
int Sub(int a, int b)
{
return a - b;
}
生成动态库后,Add 和 Sub 函数会被导出,其他程序可以调用它们。
点击:
生成 -> 生成解决方案
或使用快捷键:
Ctrl + Shift + B
生成成功后,通常可以得到:
MathDll.dll
MathDll.lib
其中 .dll 是运行时文件,.lib 是导入库。
模块定义文件通常以 .def 为后缀,用于明确指定动态库导出的函数名称。
MathDllDef.h 示例:
#ifndef MATH_DLL_DEF_H
#define MATH_DLL_DEF_H
extern "C" int Add(int a, int b);
extern "C" int Sub(int a, int b);
#endif
这里不使用 __declspec(dllexport),而是通过 .def 文件指定导出函数。
MathDllDef.cpp 示例:
#include "MathDllDef.h"
int Add(int a, int b)
{
return a + b;
}
int Sub(int a, int b)
{
return a - b;
}
添加文件 MathDllDef.def:
LIBRARY "MathDllDef"
EXPORTS
Add
Sub
.def 文件中:
| 关键字 | 说明 |
|---|---|
LIBRARY |
指定生成的 DLL 名称 |
EXPORTS |
指定需要导出的函数列表 |
然后在项目属性中配置模块定义文件:
项目属性 -> 配置属性 -> 链接器 -> 输入 -> 模块定义文件
填写:
MathDllDef.def
使用 .def 文件导出函数时需要注意:
.def 文件中的函数名必须和实际函数名对应;extern "C" 使用;.def 文件适合集中管理导出函数;.def 文件比在每个函数前写导出语句更清晰。动态库的调用方式主要有三种:
.lib 调用;.dll 调用。这种方式也叫隐式链接。调用方在编译链接阶段链接动态库生成的 .lib 导入库,程序运行时再加载 .dll。
调用方需要完成三件事:
包含头文件
配置导入库 .lib 路径
配置导入库 .lib 名称
第一步,配置头文件路径:
项目属性 -> 配置属性 -> C/C++ -> 常规 -> 附加包含目录
第二步,配置 .lib 所在目录:
项目属性 -> 配置属性 -> 链接器 -> 常规 -> 附加库目录
第三步,配置 .lib 文件名:
项目属性 -> 配置属性 -> 链接器 -> 输入 -> 附加依赖项
例如:
MathDll.lib
第四步,保证运行时能找到 .dll 文件。常见做法是将 DLL 放到:
主程序 exe 同级目录
主程序示例:
#include <stdio.h>
#include "MathDll.h"
int main(void)
{
int ret = Add(1, 2);
printf("ret = %d\n", ret);
return 0;
}
调用动态库时,也可以在代码中使用 #pragma comment(lib, "xxx.lib") 指定导入库。
示例:
#include <stdio.h>
#include "MathDll.h"
#pragma comment(lib, "MathDll.lib")
int main(void)
{
int ret = Add(1, 2);
printf("ret = %d\n", ret);
return 0;
}
需要注意:
.lib;MathDll.dll;.dll 一般需要放到 .exe 同级目录;这种方式也叫显式加载,程序运行过程中主动调用 Windows API 加载 DLL。
常用函数包括:
| 函数 | 说明 |
|---|---|
LoadLibrary |
加载 DLL |
GetProcAddress |
获取 DLL 中导出函数地址 |
FreeLibrary |
释放 DLL |
示例代码:
#include <stdio.h>
#include <windows.h>
typedef int (*PFN_ADD)(int, int);
int main(void)
{
HMODULE hDll = LoadLibraryA("MathDll.dll");
if (hDll == NULL)
{
printf("LoadLibrary failed\n");
return -1;
}
PFN_ADD pAdd = (PFN_ADD)GetProcAddress(hDll, "Add");
if (pAdd == NULL)
{
printf("GetProcAddress failed\n");
FreeLibrary(hDll);
return -1;
}
int ret = pAdd(1, 2);
printf("ret = %d\n", ret);
FreeLibrary(hDll);
return 0;
}
这种方式不需要在链接器中配置 MathDll.lib,但必须保证运行时能找到 MathDll.dll,并且函数名和导出名一致。
动态库调用时需要注意以下几点:
.h、.lib 和 .dll;.lib 用于链接阶段,.dll 用于运行阶段;extern "C" 避免函数名被修饰;LoadLibrary 和 GetProcAddress 的返回值。静态库和动态库都是代码复用的重要方式,但使用场景不同。
| 对比项 | 静态库 | 动态库 |
|---|---|---|
| 主要文件 | .lib |
.dll 和 .lib |
| 是否可直接运行 | 不可以 | 不可以 |
| 是否需要主程序 | 需要 | 需要 |
| 链接方式 | 链接阶段合入程序 | 链接导入库,运行时加载 DLL |
| 运行依赖 | 通常不依赖额外 DLL | 必须能找到 DLL |
| 更新方式 | 更新库后通常要重新链接主程序 | 可通过替换 DLL 更新实现 |
| 适合场景 | 稳定模块、简单发布 | 插件、模块更新、运行时加载 |
简单选择原则:
想让程序发布简单,可以优先考虑静态库。
想让模块可以单独更新,可以考虑动态库。
想运行时决定是否加载某个模块,可以使用 LoadLibrary 显式加载 DLL。
常见报错:
无法打开包括文件: “xxx.h”: No such file or directory
原因通常是 附加包含目录 没有配置,或者 #include 路径写错。
优先检查:
项目属性 -> 配置属性 -> C/C++ -> 常规 -> 附加包含目录
常见报错:
LINK : fatal error LNK1104: 无法打开文件 “xxx.lib”
原因通常是 .lib 文件路径错误,或者附加依赖项中库文件名写错。
优先检查:
项目属性 -> 配置属性 -> 链接器 -> 常规 -> 附加库目录
项目属性 -> 配置属性 -> 链接器 -> 输入 -> 附加依赖项
常见报错:
error LNK2019: 无法解析的外部符号
可能原因:
常见现象:程序编译和链接成功,但运行时提示找不到 DLL。
可能原因:
常见解决方法:
将 DLL 放到 exe 所在目录。
如果主程序是 x64,而库文件是 Win32,通常会出现链接错误或运行异常。
排查时需要确认:
主程序平台 = 库文件平台 = DLL 平台
例如都使用:
x64 Debug
或者都使用:
Win32 Debug
练习目标:掌握静态库的生成和工程配置调用方法。
要求:
MathLib;MathLib.h 和 MathLib.cpp;Add 和 Sub 函数;MathLib.lib;MainApp;Add 函数并输出结果。练习目标:掌握通过代码语句指定 .lib 的方法。
要求:
在主程序中包含静态库头文件;
使用:
#pragma comment(lib, "MathLib.lib")
配置附加库目录;
调用库函数并运行程序。
练习目标:掌握 __declspec(dllexport) 和 __declspec(dllimport) 的使用。
要求:
MathDll;Add 函数;MathDll.dll 和 MathDll.lib;Add 函数并运行。练习目标:掌握 .def 文件导出函数的方法。
要求:
.def 文件;.def 文件中写入 EXPORTS;练习目标:掌握运行时显式加载 DLL 的方法。
要求:
LoadLibraryA;GetProcAddress 获取函数地址;FreeLibrary 释放 DLL。Library 部分验收时,应重点掌握以下内容:
| 分类 | 必须掌握内容 |
|---|---|
| 静态库概念 | 理解静态库 .lib 的作用 |
| 静态库生成 | 能够创建静态库项目并生成 .lib |
| 静态库调用 | 能够通过工程配置调用静态库 |
| 代码加载 lib | 能够使用 #pragma comment(lib, "xxx.lib") |
| 动态库概念 | 理解 .dll 和导入库 .lib 的关系 |
| 导出语句 | 能够使用 __declspec(dllexport) 导出函数 |
| 模块定义文件 | 能够使用 .def 文件导出函数 |
| 动态库工程配置调用 | 能够配置头文件路径、导入库路径和导入库名称 |
| 代码加载 DLL | 能够使用 LoadLibrary 和 GetProcAddress 调用 DLL |
| 错误排查 | 能够排查找不到头文件、找不到 lib、找不到 DLL、平台不一致等问题 |
通过本次学习,掌握了 VS2019 平台下静态库和动态库的基本概念、生成方法和调用方法。
静态库适合将稳定代码直接链接进主程序,发布简单,运行时不需要额外 DLL。动态库适合需要运行时加载、模块独立更新或隐藏实现的场景,但发布时需要保证 DLL 能被程序找到。
在实际使用中,需要重点理解以下关系:
.h 文件:提供接口声明
.lib 文件:静态库实现,或动态库导入信息
.dll 文件:动态库运行时真正加载的实现
.exe 文件:调用库的主程序
库调用问题排查时,应按照以下顺序分析:
编译阶段找不到 .h -> 检查附加包含目录
链接阶段找不到 .lib -> 检查附加库目录和附加依赖项
链接阶段函数无法解析 -> 检查函数实现和库是否正确链接
运行阶段找不到 .dll -> 检查 DLL 是否在 exe 同级目录或 PATH 中
平台相关错误 -> 检查 Win32/x64 是否一致
掌握库的生成与使用后,可以更好地理解公司项目中模块封装、源码隐藏、库文件发布和多人协作的基本方式。