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 构建静态库的步骤如下:

  1. 生成可重定位目标文件:gcc -c tools.c -o tools.o
  2. 将可重定位目标文件做成静态库: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

  1. -shared 表示生成共享库(即动态库);
  2. -fpic 表示生成位置无关代码(Position Independent Code)
  3. 基本格式: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 下却是去默认的动态库查找路径下查找,我们构建出的动态库还没放到里面,自然是找不到的。

image-20230308145125613

image-20230308145315184

(ldd 用于检测一个可执行程序所需的动态库)

解决方案1

在环境变量 LD_LIBRARY_PATH 中添加动态库所在的路径,该环境变量初始为空,因此可以直接赋值:export LD_LIBRARY_PATH=$(pwd)

  • 若要拼接,使用冒号分隔export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(pwd)
  • 使用export仅本次登录有效。

image-20230308150008503

解决方案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=.

image-20230308154231699

粗暴使用 . 代表当前路径的缺点是只能在当前目录下执行程序,否则也找不到动态库。更好的方法是指定完整的动态库路径。

image-20230308154541799

解决方案4

直接编辑 ld.so.conf 文件:sudo vim /etc/ld.so.conf,写入动态库所在的路径。

然后要执行 sudo ldconfig 更新 /etc/ld.so.cache 文件。

image-20230308155217384

关于 lddldconfigld.so.confld.so.cache 这些东西可以参考Linux的so文件到底是干嘛的?浅析Linux的动态链接库

简单地说就是:

  1. ld.so.conf 指定操作系统查找动态库的默认路径;
  2. 因为动态库路经太多,暴力搜索要太久,所以搞了个 ld.so.cache
  3. ldconfig 就是用来更新 ld.so.cache 的,每次安装了第三方库都必须更新一下。

解决方案对比

方案 1 和方案 4 里额外添加的搜索路径中若包含了旧路径中已有的、不同版本的库,可能导致其他可执行程序出错。

方案 2 和方案 4 互为表里,造成的全局性的影响,所有可执行程序在执行时都会被这些规则影响。

方案 3 可以将影响影响缩小到单个可执行程序,为了达到这个目的,需要将程序所需的动态库都拷贝一份到指定路径(当然你也可以让系统库默认搜索,只指定自己安装的第三方库)。

posted @ 2023-03-08 16:22  zwjason  阅读(200)  评论(0编辑  收藏  举报