C++简明教程(13)创建和配置dll与lib库_怎样生成lib库和dll库-CSDN博客
C++ 动态库与静态库详解
一、为什么要引入库的概念
在 C++ 编程中,随着项目规模的不断扩大,代码量也会急剧增加。如果将所有代码都写在一个源文件中,会导致代码难以维护、可读性差且编译时间过长。库的出现解决了这些问题,它具有以下重要意义:
- 代码复用:可以将一些常用的功能封装成库,例如数学运算函数、文件操作函数等,在多个不同的项目中重复使用这些库代码,避免了重复开发,大大提高了开发效率。
- 模块化:将代码分成不同的模块(库),每个模块专注于特定的功能,使得代码结构更加清晰。不同的开发人员可以同时开发不同的库模块,加快项目的整体进度,并且便于后期的维护和升级。
- 保密:对于一些商业软件或者不想公开源代码的项目,库可以将代码进行封装,只提供对外的接口(头文件),隐藏了内部的实现细节,保护了知识产权。
二、库的分类
C++ 中的库主要分为静态库和动态库:
- 静态库(Static Library):英文名称为“Static Library”,后缀在 Windows 下通常是 .lib,在 Linux 下是 .a。静态库在编译时会被链接到可执行文件中,成为可执行文件的一部分,所以最终生成的可执行文件体积相对较大,但好处是可执行文件在运行时不依赖于外部的库文件,具有更好的独立性和移植性,因为它自身已经包含了所需的所有代码。
- 动态库(Dynamic Library):英文名称为“Dynamic Library”,在 Windows 下后缀是 .dll(Dynamic Link Library),在 Linux 下是 .so(Shared Object)。动态库在程序运行时才被加载和链接,可执行文件在运行时需要找到相应的动态库才能正常工作。这使得可执行文件体积较小,但依赖于动态库的存在。动态库的优点是多个程序可以共享同一个动态库的代码,节省内存空间,而且更新动态库时,不需要重新编译所有依赖它的可执行文件,只要接口不变,只需要替换动态库文件即可。
三、库项目的代码写法
无论是静态库还是动态库项目,通常不需要 main
函数,因为它们不是独立运行的程序,而是被其他程序调用的代码集合。主要工作是将函数和类导出供外部使用。
- 在 Windows 下,对于要导出的函数和类,需要使用
__declspec(dllexport)
关键字(注意前面是两个下划线)。例如:
__declspec(dllexport) int add(int a, int b) {
return a + b;
}
这里将 add
函数导出,以便其他程序可以调用。同时,为了保证函数名在 C 和 C++ 中的兼容性(因为 C++ 会对函数名进行修饰,而 C 不会),通常会结合 extern "C"
使用,如下:
extern "C" __declspec(dllexport) int add(int a, int b) {
return a + b;
}
extern "C"
的作用是告诉编译器按照 C 语言的函数命名规则来处理函数名,避免 C++ 的函数名修饰,这样在其他语言(如 C)调用这个动态库中的函数时也能正确找到函数入口。
- 在头文件中,需要声明这些导出的函数和类,并且对于使用动态库的项目,头文件的声明也需要一些特殊处理。例如:
#ifndef MATH_FUNCTIONS_H
#define MATH_FUNCTIONS_H
#ifdef BUILDING_DLL
# define DLL_EXPORT __declspec(dllexport)
#else
# define DLL_EXPORT __declspec(dllimport)
#endif
extern "C" DLL_EXPORT int add(int a, int b);
#endif
这里通过 BUILDING_DLL 这个预定义宏来区分是在构建动态库还是使用动态库。当构建动态库时,使用 __declspec(dllexport) 导出函数;当使用动态库时,使用 __declspec(dllimport) 导入函数,这样可以提高程序的运行效率(虽然在很多情况下不写 __declspec(dllimport) 也能正常工作,但明确写出是一种更好的编程习惯)。
四、动态库相关问题
- 为什么使用动态库依然会生成.lib 文件(在 Windows 下):在 Windows 环境中,生成的 .lib 文件是动态库的导入库(Import Library)。当我们链接一个动态库时,链接器需要知道动态库中函数和变量的符号信息,以便在程序运行时能够正确地加载和链接动态库。这个导入库就包含了这些符号信息,它在链接阶段被使用,而真正的代码实现在 .dll 文件中,在程序运行时才被加载。
五、库的使用配置
- 静态库的配置:
- 首先需要获取静态库的头文件(.h)和库文件(.lib)。
- 在项目中,将头文件复制到项目目录下,并在源文件中使用 #include 指令包含头文件。
- 在编译器的链接选项中,指定要链接的静态库文件。例如在 Visual Studio 中,在项目属性的“链接器 - 输入 - 附加依赖项”中添加静态库文件名(如 mylib.lib);在 Linux 下使用 g++ 编译时,使用 -L 选项指定库文件所在目录,-l 选项指定库文件名(如 g++ main.cpp -L. -lmylib -o main,这里 -L. 表示在当前目录下查找库文件,-lmylib 表示链接 libmylib.a 库,main.cpp 是使用库的源文件,main 是生成的可执行文件名)。
- 动态库的导入:
- 同样需要头文件来获取函数和类的声明信息。
- 在 Windows 下,除了头文件,还需要导入库(.lib)文件和动态库(.dll)文件。导入库用于链接阶段,而 .dll 文件需要在程序运行时能够被找到,可以将 .dll 文件复制到可执行文件所在目录或者系统的动态库搜索路径中。
- 在 Linux 下,通常只需要头文件和动态库(.so)文件,在链接时使用 -L 和 -l 选项指定库文件,并且在运行时,需要确保动态库文件在系统的库搜索路径中(可以通过设置 LD_LIBRARY_PATH 环境变量来临时添加搜索路径,例如 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/dll,但这种方式在终端关闭后失效,更好的方法是将动态库安装到系统的标准库目录或者在 /etc/ld.so.conf 文件中添加库文件所在目录,并运行 ldconfig 命令更新库缓存)。
- 如果没有 .lib 库(在 Windows 下),也可以使用 Windows 的 API 函数 LoadLibrary 和 GetProcAddress 来在运行时动态加载动态库并获取函数指针,从而调用动态库中的函数,但这种方式相对复杂,不建议初学者使用,除非有特殊需求。例如:
#include <windows.h>
#include <iostream>
typedef int (*ADD_FUNC)(int, int);
int main() {
// 加载动态库
HINSTANCE hDll = LoadLibrary("mydll.dll");
if (hDll!= NULL) {
// 获取函数地址
ADD_FUNC addFunction = (ADD_FUNC)GetProcAddress(hDll, "add");
if (addFunction!= NULL) {
int result = addFunction(3, 5);
std::cout << "加法结果:" << result << std::endl;
} else {
std::cout << "无法获取函数地址" << std::endl;
}
// 释放动态库
FreeLibrary(hDll);
} else {
std::cout << "无法加载动态库" << std::endl;
}
return 0;
}
六、什么是 pdb 文件及如何使用
- PDB 文件(Program Database File):它是在 Windows 平台上用于调试的文件,包含了程序的调试信息,例如变量名、函数名、行号、类型信息等。当在 Visual Studio 等开发环境中进行调试时,如果有对应的 .pdb 文件,调试器可以显示更详细准确的调试信息,如变量的当前值、函数的调用栈等,帮助开发者更容易地找出程序中的错误。
- 如何使用:在 Visual Studio 中,默认情况下,当编译项目时,如果是调试模式(Debug 配置),会自动生成 .pdb 文件。在调试时,只要可执行文件和 .pdb 文件在相同的目录或者在调试器能够找到的路径下,调试器就会自动使用它来提供调试信息。不需要开发者手动进行额外的操作来“使用” .pdb 文件,它是无缝集成在调试过程中的。
总之,理解和掌握 C++ 中的静态库和动态库对于编写高效、可维护的代码至关重要。通过合理地使用库,可以提高代码的复用性、模块化程度,并且在不同的项目场景中选择合适的库类型能够优化程序的性能和部署方式。希望这篇教程能够帮助 C++ 编程的初学者清晰地理解这些概念和技术。
实践
以下是在 Windows 下创建和使用静态库与动态库的示例代码:
一、静态库
1. 创建静态库项目
- 创建静态库项目。
- 修改下配置:
- 移除所有VS自动创建的文件,在项目中添加以下源文件
math_functions.cpp
:
// math_functions.cpp
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
- 以及头文件
math_functions.h
:
// math_functions.h
#ifndef MATH_FUNCTIONS_H
#define MATH_FUNCTIONS_H
int add(int a, int b);
int multiply(int a, int b);
#endif
- 编译该项目,将会生成静态库文件。
- 我们可以生成多个平台下的库,根据自己的需要来生成,这里我就生成全部配置了。
- 将库打包。给其他项目项目使用,创建一个文件夹,把对应目录下把lib文件(PDB可选)、头文件放进去。此时我们就可以让其他同学或同事用我们 的库了。
2. 使用静态库
-
创建一个新的控制台应用程序项目。
-
配置第三方库
右键属性
或者在属性管理器找到对项目的属性。
这里只演示配置x64 Release 模式下配置库,其他模式类似。注意,如果在x64 Release 模式下,那么就要配置对应的在x64 Release模式下生成的库,不然就有问题。
- 首先把包含路径配置下,就是我们库的头文件路径,
注意我把 \
替换为了/
- 然后配置lib文件
- 然后写代码
// main.cpp
#include <iostream>
#include "math_functions.h"
int main() {
int num1 = 5, num2 = 3;
std::cout << "加法结果:" << add(num1, num2) << std::endl;
std::cout << "乘法结果:" << multiply(num1, num2) << std::endl;
return 0;
}
- 编译运行即可
二、动态库
1. 创建动态库项目
- 创建动态库项目
- 与静态库类似,删除默认的代码文件,取消预编译头。
- 在项目中添加
math_functions_of_dynamic_lib.cpp
文件,并修改如下:
// math_functions_of_dynamic_lib.h.cpp
#include"math_functions_of_dynamic_lib.h"
int add_of_dynamic_lib(int a, int b) {
return a + b;
}
int multiply_of_dynamic_lib(int a, int b) {
return a * b;
}
- 以及头文件
math_functions_of_dynamic_lib.h
:
// math_functions_of_dynamic_lib.h
#ifndef MATH_FUNCTIONS_H
#define MATH_FUNCTIONS_H
#ifdef BUILDING_DLL
# define DLL_EXPORT __declspec(dllexport)
#else
# define DLL_EXPORT __declspec(dllimport)
#endif
DLL_EXPORT int add_of_dynamic_lib(int a, int b);
DLL_EXPORT int multiply_of_dynamic_lib(int a, int b);
#endif
- 因为导出库(编译库),和导入库(其他项目调用此库)是不同的关键字,导出是__declspec(dllexport),导入是__declspec(dllimport)。所以我们可以在库项目中添加宏定义BUILDING_DLL,这样在编译库项目是有宏定义就会使用__declspec(dllexport),在导入的时候因为没有宏定义那么就会使用__declspec(dllimport)。
- 编译该项目,我们可以像前面的静态库那样来批生成
- 将库打包。给其他项目项目使用,创建一个文件夹,把生成的文件和头文件放进去。此时我们就可以让其他同学或同事用我们 的库了。
2. 使用动态库
-
使用动态库的方法和使用静态库的方法类似,都需要配置lib文件,头文件路径。这里不赘述。
- 我们在应用程序写:
// main.cpp
// main.cpp
#include <iostream>
#include "math_functions_of_dynamic_lib.h"
int main() {
int num1 = 5, num2 = 3;
std::cout << "加法结果:" << add_of_dynamic_lib(num1, num2) << std::endl;
std::cout << "乘法结果:" << multiply_of_dynamic_lib(num1, num2) << std::endl;
return 0;
}
- 然后编译并运行,发现报错。
- 我们把对应的dll复制到exe的同级目录下即可运行成功注意我们这个应用程序是x64 release 下生成的,那么我们要复制的也是在x64 release 下生成的dll 库。