C++(GCC)生成和使用动态库 原创
C++(GCC)生成和使用动态库
文章目录
更多精彩内容 |
---|
👉个人内容分类汇总 👈 |
👉C++软件开发👈 |
1、前言
1.1 什么是动态库
动态库(也称为共享库)是一种可重定位的二进制文件,包含了程序可以在运行时动态加载的一组函数、变量、对象等代码和数据。
它们可以被多个进程共享,因此可以减少内存空间的占用,并提高程序的运行效率和可维护性。
与静态库不同,动态库在编译时不会被链接到可执行文件中,而是在程序运行时才被加载到内存中,并且可以被多个程序共享使用。
动态库可以使用dlopen函数进行动态加载,使用dlsym函数获取动态库中的函数地址,使用dlclose函数进行动态卸载。
动态库可以分为两种类型:系统动态库和自定义动态库。
系统动态库是操作系统提供的,包含了一些通用的函数和系统调用,例如libc、libm等。
自定义动态库是程序开发人员自己编写的,包含了程序中需要的一些函数和数据结构等。
使用动态库的好处是可以减少代码冗余,提高代码复用率,方便程序的开发和维护,同时也可以减少内存空间的占用,提高程序的运行效率。
1.2 为什么要用动态库
使用动态库的好处有以下几点:
- 节省内存空间:动态库在程序运行时才会被加载,不会像静态库那样在编译时就被全部链接进可执行文件中,因此可以节省内存空间。
- 便于更新:当动态库更新时,只需要替换动态库文件即可,不需要重新编译整个程序。
- 提高程序运行效率:由于动态库可以被多个程序共享,因此可以减少内存中的重复代码,提高程序运行效率。
- 方便程序的开发和维护:动态库可以被多个程序共享,因此可以减少代码冗余,提高代码复用率,方便程序的开发和维护。
- 支持动态加载:动态库可以使用dlopen函数进行动态加载,因此可以实现动态加载和卸载,提高程序的灵活性和可扩展性。
1.3 C++使用动态库的方法
linux下,在C++中,使用动态库的方法主要有以下两种:
- 显式链接:显式链接是通过代码中直接调用动态库中的函数来使用动态库的功能。这种方式需要在程序中显式地链接动态库,并在代码中使用动态库中的函数。使用显式链接的步骤如下:
- 编写动态库的代码,并将其编译为动态库文件(.so文件)。
- 在程序中使用dlopen函数打开动态库,并使用dlsym函数获取动态库中的函数地址。
- 通过获取到的函数地址调用动态库中的函数。
- 在程序结束时使用dlclose函数关闭动态库。
- 隐式链接:隐式链接是在编译时将动态库链接到可执行文件中,程序运行时自动加载动态库的方式。这种方式需要在编译时指定动态库的名称,并在程序中使用动态库中的函数,编译器会自动将动态库链接到可执行文件中。使用隐式链接的步骤如下:
- 编写动态库的代码,并将其编译为动态库文件(.so文件)。
- 在程序中包含动态库的头文件,并在编译时指定动态库的名称。
- 在代码中使用动态库中的函数。
- 需要注意的是,使用隐式链接时需要将动态库文件放置在系统的动态库搜索路径中,或者通过设置环境变量LD_LIBRARY_PATH来指定动态库的搜索路径。
总的来说,显式链接需要手动打开和关闭动态库,并且需要在代码中显式地调用动态库中的函数;
而隐式链接则需要在编译时指定动态库的名称,并将动态库文件放置在系统动态库搜索路径中,但是在代码中使用动态库中的函数时与静态库没有区别。
2、linux下C++生成动态库so
-
创建用于生成动态库的test.h文件
#ifndef __TEST__ #define __TEST__ int testFun(int a, int b); #endif
-
创建用于生成动态库的test.cpp文件
#include "test.h" int testFun(int a, int b) { return a + b; }
-
使用下列命令将test.cpp文件编译成动态库
g++ test.cpp -fPIC -shared -o libtest.so
- -fPIC:生成位置无关代码,方便动态链接,常用于编译动态链接库(shared library)。
- -shared:用于生成动态链接库(shared library)。使用-shared选项将多个目标文件链接在一起生成动态链接库。
- 动态库命名:必须是以lib开头,以so为后缀。
3、隐式链接使用动态库
-
创建一个使用动态库的main.cpp文件;
#include <iostream> #include "test.h" using namespace std; int main() { cout << testFun(10, 20) << endl; return 0; }
-
使用下列命令编译main.cpp,并链接到动态库libtest.so
g++ main.cpp -L. -ltest -o main
- -L:使用"-L"参数编译源文件时,G++编译器会在指定的目录中寻找库文件。如果库文件不在默认的系统路径下,我们就需要使用"-L"参数来告诉编译器库文件的位置。
- -l:用于指定链接的库文件名,即告诉编译器链接哪个库文件。去掉libtest.so开头的lib和后缀。
-
可用使用
ldd
命令查看是否成功链接到动态库,如下图所示,没有链接成功,需要使用LD_LIBRARY_PATH指定动态库环境变量; -
LD_LIBRARY_PATH
是一个Linux环境变量,用于指定动态链接库的搜索路径。当一个程序运行时,如果需要使用动态链接库,系统会在LD_LIBRARY_PATH
中指定的路径中搜索相应的库文件,以便程序能够正确运行。 -
LD_LIBRARY_PATH使用方法一:直接在终端命令行中执行
export LD_LIBRARY_PATH=/path/to/library:$LD_LIBRARY_PATH
,但是这时临时的,退出当前终端后就失效了; -
方法二:打开下列三个文件中任意一个
- /etc/profile (适用于所有用户) - ~/.bashrc (适用于当前用户) - /etc/bashrc (适用于所有用户)
-
在文件中填入
export LD_LIBRARY_PATH=/path/to/library:$LD_LIBRARY_PATH
,执行source .bashrc
立即生效。
4、显示链接使用动态库
1.1 C++名称修饰
- 如果使用显示连接动态库的方式去调用前面编译的动态库,会发现打开动态库成功,但是使用
dlsym()
函数查找动态库函数地址时找不到,这是因为C++中默认开启了名称修饰。
在C++中,函数的名称修饰(Name Mangling)是指编译器根据函数的参数类型和返回值类型等信息生成的一个唯一的符号名称,用于在链接时标识不同的函数。
名称修饰的目的在于支持函数重载,在函数名相同但参数类型不同的情况下,生成不同的符号名称,以避免函数名冲突。
然而,名称修饰也导致了动态库中函数的名称与实际使用时的名称不匹配的问题,从而无法正确链接和调用动态库中的函数。
为了解决这个问题,我们可以采用以下两种方法:
使用extern "C"来关闭名称修饰:在C++中,extern "C"是一种链接指示符号,用于告诉编译器不要采用C++名称修饰,并按照C语言的方式生成符号名称(关闭后函数重载就失效了)。
例如,我们可以使用以下方式定义函数:
extern "C" void my_function(int arg1, int arg2) { // function body }
在使用这种方式定义函数时,编译器将不会对函数名进行名称修饰,因此可以避免函数名称不匹配的问题。
2. 使用编译器选项关闭名称修饰:一些编译器提供了选项来关闭名称修饰,例如在GCC编译器中,可以使用-fno-rtti
选项关闭名称修饰。在使用这种方式时,需要注意一些问题,例如关闭名称修饰可能会影响C++的RTTI(Run-Time Type Information)功能,因此需要根据具体情况进行选择。
总的来说,为了避免动态库中函数名称不匹配的问题,我们应该尽可能使用extern "C"来关闭名称修饰。如果使用其他方式关闭名称修饰,在做出选择之前需要仔细考虑影响。
-
使用
extern "C"
和不使用extern "C"
修饰分别定义两个函数,然后编译成动态库;extern "C" int add1(int a, int b) { return a + b; } int add2(int a, int b) { return a + b; }
-
再使用
objdump -t libtest.so > test.txt
命令 输出文件libtest.so的符号列表(变量名与函数名就是符号)内容,如下图所示,经过extern "C"
修饰的函数名称不变,没有修饰的就变成了_Z4add2ii
;当然,如果在使用dlsym()函数查找函数地址时使用_Z4add2ii
也是可以的。
1.2 显示连接动态库
-
创建一个test.cpp文件
extern "C" int add1(int a, int b) // 关闭名称修饰 { return a + b; } int add2(int a, int b) // 未关闭名称修饰 { return a + b; }
-
创建一个main.cpp文件
#include <iostream> #include <dlfcn.h> using namespace std; typedef int (*Add)(int, int); int main() { void* handle = dlopen("./libtest.so", RTLD_LAZY); if (!handle) { cout << "未能加载动态库: " << dlerror() << endl; return 1; } Add testFun1 = (Add)dlsym(handle, "add1"); // 关闭名称修饰 if (!testFun1) { cout << "找不到函数: " << dlerror() << endl; dlclose(handle); return 1; } cout << testFun1(10, 20) << endl; Add testFun2 = (Add)dlsym(handle, "_Z4add2ii"); // 未关闭名称修饰 if (!testFun2) { cout << "找不到函数: " << dlerror() << endl; dlclose(handle); return 1; } cout << testFun2(30, 20) << endl; dlclose(handle); return 0; }
-
使用
g++ -shared test.cpp -o libtest.so -fPIC
命令编译动态库; -
使用
g++ main.cpp -o main
命令编译main.cpp文件,修饰链接动态库不需要使用-l
指定动态库; -
执行结果如下所示;
1.3 显示链接动态库函数说明
- 显示连接动态库需要用到头文件
dlfcn.h
,主要用于在Unix/Linux系统上动态加载共享库(动态链接库)。
dlfcn.h是一个C语言的头文件,定义了一些函数和结构体,用于动态链接共享库的操作。
以下是dlfcn.h文件中常用的函数的详细说明:
- dlopen函数
函数原型:void *dlopen(const char *filename, int flag);
函数作用:打开指定的动态共享对象,并返回一个句柄,该句柄可以用于后续的操作,比如获取动态库中的符号地址。
参数说明:
- filename:要打开的动态共享对象的文件名,可以是完整路径或相对路径。
- flag:打开标志,可以是下列值的按位或组合:
- RTLD_LAZY:指定动态库的延迟绑定,只有在使用到某个符号时才解析并加载该符号,常用于优化启动时间和内存占用。
- RTLD_NOW:指定动态库的立即绑定,加载动态库时会立即解析并加载所有符号,常用于减少动态库初始的启动延迟。
- RTLD_GLOBAL:指定动态库的符号可以被其他动态库和程序共享。
- RTLD_LOCAL:指定动态库的符号只能被当前动态库内部使用,不能被其他动态库和程序使用。
返回值说明:返回打开的动态共享对象的句柄,如果打开失败,则返回NULL。
dlerror函数
函数原型:char *dlerror(void);
函数作用:返回最近一次发生的动态链接错误信息。
返回值说明:返回一个指向动态链接错误信息的字符串指针,如果没有错误发生,则返回NULL。dlsym函数
函数原型:void *dlsym(void *handle, const char *symbol);
函数作用:在打开的动态共享对象中查找指定名称的符号,并返回符号地址。
参数说明:
- handle:打开的动态共享对象的句柄。
- symbol:要查找的符号名称。
返回值说明:返回指定符号的地址,如果查找失败,则返回NULL。
4. dlclose函数
函数原型:int dlclose(void *handle);
函数作用:关闭指定的动态共享对象。
参数说明:
- handle:要关闭的动态共享对象的句柄。
返回值说明:如果关闭成功,则返回0,否则返回非零值。