C/C++构建静态库和动态库
环境说明
ubuntu 18.04
gcc 7.5.0
为什么需要库
为什么需要库?为了代码的方便复用,一是方便,二是复用。
无论静态库还是动态库,都有它们产生的历史背景,都是从旧方法中改进而来的,想了解的朋友可以查阅《深入理解计算机系统》第 7.6.2 部分。
库的一般使用方式
通常我们需要使用库(无论是 C/C++ 内置库还是第三方库),通常都是 #include
相关头文件,然后调用里面的函数。
#include <iostream> // 内置库
#include <jsoncpp/json/json.h> // 第三方库, 需要先自行安装
int main()
{
// ......
Json::Reader reader;
Json::Value root;
if (!reader.parse(ifs, root, false)) {
cout << "error: failed to parse config file, syntax error.\n" << endl;
exit(-1);
}
// ......
return 0;
}
在一些集成 IDE 里,我们可以跳转进入parse
函数查看其接口详细信息,但具体实现一定不在里面。.h 头文件声明接口规范,.cpp 文件才是具体实现。
以这种库调用当时,我们自己制作简单的静态库和动态库。
构建静态库
问题说明
假设我们构建一个库(tools.h 和 tools.c),库中提供两个接口:两个数相加和两个数相乘,那么头文件内容可能如下所示。
#ifndef TOOLS_H_
#define TOOLS_H_
/**
* @brief
*
* @param x 加数
* @param y 加数
* @return 返回两数之和
*/
int add(int x, int y);
/**
* @brief
*
* @param x 乘数
* @param y 乘数
* @return 返回两数之积
*/
int multiply(int x, int y);
#endif // TOOLS_H_
头文件给出了接口声明,接下来要到源文件中实现接口。
#include "tools.h"
int add(int x, int y)
{
return x + y;
}
int multiply(int x, int y)
{
return x * y;
}
没构建静态库时
若不把它们构建成静态库就直接在 main.cpp 中调用,手动编译 gcc tools.c main.c -o main
,因为 main.c 依赖 tools.c,所以 main.c 在 tools.c 后面。
#include <stdio.h>
#include "tools.h" // 引用自己写的库使用双引号
int main(int argc, char* argv[])
{
int x = 6, y = 7;
printf("x + y = %d\n", add(x, y)); // 求和
printf("x * y = %d\n", multiply(x, y)); // 乘积
return 0;
}
构建静态库后
使用 gcc + ar 构建静态库的步骤如下:
- 生成可重定位目标文件:
gcc -c tools.c -o tools.o
- 将可重定位目标文件做成静态库:
ar rcs libtools.a tools.o
- 格式大致为
ar rcs libxxx.a yyy.o zzz.o
- 注意静态库的命名格式为
libxxx.a
- 格式大致为
使用方法如下:
gcc -static main.c -o main ./libtools.a
直接指定所需库的路径,其等价用法如下(更普遍):
gcc -static main.c -o main -L. -ltools
-static
表示链接时使用静态库,若无该选项则默认使用动态库。-L
表示增加头文件的搜索路径,.
表示当前路径。-l
指定库的名称,不需要加前缀lib
,也不需要加后缀.a
构建动态库
可以直接使用 gcc 工具构建动态库,方法如下:
gcc -shared -fpic -o libtools.so tools.c
- -shared 表示生成共享库(即动态库);
- -fpic 表示生成位置无关代码(Position Independent Code)
- 基本格式:
gcc -shared -fpic -o libxxx.so yyy.c zzz.c
使用方法如下:
gcc main.c -o main ./libtools.so
等价用法如下:
gcc main.c -o main -L. -ltools
没有 -static
选项,默认使用动态库。
执行找不到动态库
在 Windows 下链接动态库得到可执行文件后,可以直接执行,而在 Linux 下却会发生找不到动态库的问题。
这里面涉及一个“编译时库查找”和“运行时库查找”问题。
“编译时库查找”如前所述,在命令行里使用 -L
和 -l
选项即可。
但是“运行时库查找”在 Linux 下却是去默认的动态库查找路径下查找,我们构建出的动态库还没放到里面,自然是找不到的。
(ldd 用于检测一个可执行程序所需的动态库)
解决方案1
在环境变量 LD_LIBRARY_PATH
中添加动态库所在的路径,该环境变量初始为空,因此可以直接赋值:export LD_LIBRARY_PATH=$(pwd)
。
- 若要拼接,使用冒号分隔
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(pwd)
。 - 使用
export
仅本次登录有效。
解决方案2
直接将我们构建的动态库放到系统默认的查找路径(关于运行时动态库的查找顺序可以参考:C/C++库和头文件的查找顺序):sudo scp libtools.so /usr/lib
- 需要 root 权限;
- scp 是基于 ssh 的安全 cp 版本;
- 不仅只有
usr/lib
这个路径,还有很多其他路径。
解决方案3
使用 -Wl,-rpath
指定程序在运行时所需动态库的目录,-Wl
的意思是将逗号后面的内容传递给链接器:gcc main.c -o main -L. -ltools -Wl,-rpath=.
粗暴使用 .
代表当前路径的缺点是只能在当前目录下执行程序,否则也找不到动态库。更好的方法是指定完整的动态库路径。
解决方案4
直接编辑 ld.so.conf
文件:sudo vim /etc/ld.so.conf
,写入动态库所在的路径。
然后要执行 sudo ldconfig
更新 /etc/ld.so.cache
文件。
关于 ldd
,ldconfig
,ld.so.conf
和 ld.so.cache
这些东西可以参考Linux的so文件到底是干嘛的?浅析Linux的动态链接库。
简单地说就是:
ld.so.conf
指定操作系统查找动态库的默认路径;- 因为动态库路经太多,暴力搜索要太久,所以搞了个
ld.so.cache
; ldconfig
就是用来更新ld.so.cache
的,每次安装了第三方库都必须更新一下。
解决方案对比
方案 1 和方案 4 里额外添加的搜索路径中若包含了旧路径中已有的、不同版本的库,可能导致其他可执行程序出错。
方案 2 和方案 4 互为表里,造成的全局性的影响,所有可执行程序在执行时都会被这些规则影响。
方案 3 可以将影响影响缩小到单个可执行程序,为了达到这个目的,需要将程序所需的动态库都拷贝一份到指定路径(当然你也可以让系统库默认搜索,只指定自己安装的第三方库)。