编译生成 C++ 库文件
我们介绍 Windows 和 Ubuntu 下生成和使用库文件的操作。
lib
lib 是 Windows 下的静态库,它具有以下特点:
- 运行不存在
- 静态库源码被链接到调用程序中
- 目标程序的归档
静态库是将代码嵌入到使用程序中,多个程序使用时会有多份代码,所以代码体积会增大。动态库的代码只需要存在一份,其它程序通过函数地址使用,所以代码体积小。静态库发生变化后,新的代码需要重新链接嵌入到执行程序中。
要创建静态库,首先在 VS 中建立库项目,这样会产生一些默认框架,如果不想要的话可以先生成控制台项目,然后设置“项目属性-配置类型”为静态库即可;然后直接添加源代码和头文件,右击项目选择“生成”即可产生 .lib 文件
生成静态库后,将 .lib 文件和头文件复制到指定文件目录下导入
#include "../lib/head.h" // 导入静态库头文件
#pragma comment(lib, "../lib/clib.lib") // 使用 pragma 关键字设置库路径
需要注意,如果使用项目创建静态库文件,之后新建项目测试库文件时,要将新建的项目设为启动项目,否则程序会将库文件错认为可执行文件,导致报错
有时我们需要使用 C 语言的静态库,但由于 C++ 编译器会对函数进行“换名”,而 C 编译器则不会,则当调用 C 语言静态库时会导致函数名无法识别,这时就需要使用 extern 声明
extern "C" int add(int, int); // 以 C 编译器方式编译函数声明
#pragma comment(lib, "../lib/clib.lib") // 使用 pragma 关键字设置库路径
dll
dll 是 Windows 下的动态库,它具有以下特点:
- 运行时独立存在
- 源码不会链接到执行程序
- 使用时加载(使用动态库必须使动态库执行)
动态库变化后,如果库中函数的定义(或地址)未变化,其它使用 DLL 的程序不需要重新链接。
动态库数据存放
- DLL 文件中存放函数名及其相对于文件首地址的相对地址,还有函数源码信息
- LIB 文件中存放函数名及其编号,还有 DLL 文件名
需要注意:使用动态库需要有 .dll 文件和对应的 .lib 文件,其中 .lib 不是静态库。
创建动态库
- 创建动态库项目
- 添加库程序
- 导出库程序:提供给库使用者函数信息
- 声明导出:使用 _declspec(dllexport) 导出函数地址
- 注意:动态库编译链接后,也会生成 LIB 文件作为动态库函数映射使用,通过 LIB 文件可以链接到动态库
- 模块定义文件 .def 导出不改名的函数
- 声明导出:使用 _declspec(dllexport) 导出函数地址
//声明导出动态库中的函数
//库文件
_declspec(dllexport) int add(int a, int b)
{
return a + b;
}
//声明导出会导出改名的函数
//模块定义文件导出函数 .def
LIBRARY dll_name //导出的库名
EXPORTS //库导出表
func_name1 @1 //导出的函数1
func_name2 @2 //导出的函数2
func_name3 @3 //导出的函数3
...
//通过这种方式导出函数,能够得到不改名的函数
链接动态库
隐式链接:操作系统负责使动态库执行
- 头文件和函数原型:在函数原型声明前增加 _declspec(dllimport) ;如果声明在头文件中,需要使用 DLLCLASS_EXPORTS 宏方法
- 导入动态库的 LIB 文件
- 在程序中使用函数
- dll 存放路径:
- 与执行文件同目录(推荐)
- 当前工作目录
- Windows 目录
- Windows/System32 目录
- Windows/System
- 环境变量 PATH 指定目录
// 导入动态库中的函数
_declspec(dllimport) int add(int, int);
// 通知链接器到 .lib 文件中获取函数编号和 .dll 文件名
#pragma comment(lib, "../lib/clib.lib")
显式链接:程序员负责使动态库执行
- 定义函数指针类型 typedef
- 加载动态库 LoadLibrary
- 获取函数地址 GetProcAddress
- 使用函数
- 卸载动态库 FreeLibrary
我们需要通过以下函数实现
// 返回 DLL 实例句柄
HMODULE LoadLibrary(
LPCTSTR lpFileName // 动态库文件名或全路径
);
// 获取函数地址
FARPROC GetProcAddress(
HMODULE hModule, // DLL 句柄
LPCSTR lpProcName // 函数名
);
//卸载动态库
BOOL FreeLibrary(
HMODULE hModule, // DLL 句柄
);
最后给出显式链接动态库的示例:
//定义函数指针
typedef int (*ADD)(int, int);
int main()
{
HINSTANCE hDll = LoadLibrary("CPPdll.dll"); // 加载动态库
ADD myAdd = (ADD)GetProcAddress(hDll, "add"); // 这里使用模块文件导出方法,才能使用不改名的函数名为参数
int sum = myAdd(5, 4);
cout << "sum = " << sum << endl;
FreeLibrary(hDll); // 卸载动态库
return 0;
}
动态库中封装类
在类名前增加 _declspec(dllexport) 定义,导出类成员函数的相对地址,通常使用预编译开关切换类的导入导出定义,例如
#ifdef DLLCLASS_EXPORTS
#define EXT_CLASS _declspec(dllexport)
#else
#define EXT_CLASS _declspec(dllimport)
#endif
如果定义了这一宏,则定义导出,否则定义导入。通过这种方式,程序员在库文件中定义 DLLCLASS_EXPORTS 宏,就可以使用导出类;用户只需要包含头文件,就可以使用导入类
// 导入/导出类
class EXT_CLASS CMath
{
//...
int sum(int a, int b);
int sub(int a, int b);
};
a
a 文件是 Ubuntu 下的静态库,其在主函数编译时就将库导入
# 生成 .o 文件
g++ swap.cpp -Iinclude -c
# 生成静态库 libSwap.a
ar rs libswap.a swap.o
# 链接静态库生成 main
g++ -o main.cpp -Lsrc -lswap main
# 运行main
./main
注意生成的静态库要有 lib 前缀,使用时去掉 lib 前缀
我们也可以生成 Windows 下的静态库文件
ar rs swap.lib swap.o
so
so 文件是 Ubuntu 下的动态库,它不会直接编译进主函数,需要在调用时添加。其中使用了 -fPIC 参数,这在 LAPACK 编译过程中提到,它表示以相对地址来实现代码,从而使其可以加载到任意位置
# 生成动态库 swap.so
g++ swap.cpp -Iinclude -fPIC -shared -o libswap.so
# 以上指令等价于
# gcc swap.cpp -Iinclude -c -fPIC
# gcc -shared -o libswap.so swap.o
# 链接动态库生成main
g++ -o main.cpp -Lsrc -lswap main
# 运行 main,要将动态库所在路径添加到参数
LD_LIBRARY_PATH=src ./main
同样可以生成 Windows 下的动态库文件
gcc -shared -o swap.dll swap.c