Bootstrap

什么是库?库的开发与使用(C/C++)

什么是库(library)?

库这个概念,阿宋觉得对于初学者来说会比较陌生。所以我们先范范的讲一下这个东西是干什么的,接下来再将一下,其中有什么区别。

所谓程序库,一般来说就是软件作者为了实现发布方便或替换方便又或者是二次发布方便这个目的,所制作的一组可以单独与应用程序进行静态链接或动态链接的二进制可重定位目标码文件。
说到底,一个库就是一个文件而已。这个库(文件)可以是在编译时由编译器直接链接到可执行程序之中,也可以在运行时根据操作系统的运行环境按需要动态加载到内存之中。
再简单点,我们可以认为库(文件)就是一个代码仓库,里面有一些我们可以直接拿来用的变量,函数或者类。

库文件的类型只有两种,分别是静态库与动态库(共享库)。它们之间的区别是:静态库的代码在程序的链接阶段就已经复制到程序之中了。而动态库的代码没有在链接阶段复制到程序当中,而是程序在运行时由系统动态加载到内存中,供程序调用。

从上文中,我们就可以得到使用库的第一个好处,方便程序的部署与开发。

在Linux的平台下,静态库文件的后缀是 .a ,动态库的后缀是 .so
在window的平台下,静态库文件的后缀是 .lib ,动态库的后缀是.dll

静态库的制作与使用

首先我们先讲一下命名规则
在Linux平台下,前缀lib是固定的,后面紧跟库的名字(这里用xxx代替),最后以.a后缀为结尾。总体一个静态库的文件名长这样:libxxx.a

在Linux平台制作方法如下:
首先通过gcc/g++获得c或c++的 .o文件(还未被链接的机器代码)。紧接着使用ar命令(archive)将这些 .o文件进行打包。
具体命令的格式如下:

ar rcs libxxx.a xxx.o xxx.o //命令ar 参数rcs 库的名字 需要进行打包的文件名
参数r:将文件插入备存文件当中
参数c:建立备存文件
参数s:索引

编程演示:静态库的制作

阿宋这里的目的是生成一个简单进行加减乘除运算的静态库。并且使用这个静态库中自己定义的函数,然后去完成加减乘除的运算。

制作静态库前的准备:

为了更好的去展示将函数打包成一个静态库。阿宋将主函数单独放在一个文件下,所有加减乘除的函数声明放在一个文件下,4个函数分别放在4个文件夹下。具体的代码如下所示,每一个代码块单独是一个文件。

add.c

#include <stdio.h>
#include "head.h"
int add(int a, int b)
{
    return a+b;
}

sub.c

#include <stdio.h>
#include "head.h"
int sub(int a, int b)
{
    return a-b;
}

mult.c

#include <stdio.h>
#include "head.h"
int mult(int a, int b)
{
    return a*b;
}

div.c

#include <stdio.h>
#include "head.h"
int div(int a, int b)
{
    return a/b;
}

head.h

#ifndef _HEAD_H
#define _HEAD_H
// 加法
int add(int a, int b);
// 减法
int subtract(int a, int b);
// 乘法
int multiply(int a, int b);
// 除法
double divide(int a, int b);
#endif

main.c

#include <stdio.h>
#include "head.h"
int main()
{
    int a = 20;
    int b = 12;
    printf("a = %d, b = %d\n", a, b);
    printf("a + b = %d\n", add(a, b));
    printf("a - b = %d\n", subtract(a, b));
    printf("a * b = %d\n", multiply(a, b));
    printf("a / b = %f\n", divide(a, b));
    return 0;
}

add.c sub.c mult.c div.c 这4个文件中存放的是这4个函数的源代码,在链接过程中,机器并不会将源代码链接到程序之中。所以我们需要将这4个函数翻译成汇编代码之后再打包成静态库,才能链接到可执行程序当中去。
使用命令:

gcc -c add.c sub.c mult.c div.c

这个命令会生成对应的4个 .o文件。里面存放着源函数生成的汇编代码。这个时候我们就可以将这4个存放着汇编代码的文件打包成一个静态库了。
使用命令:

ar rcs libCalc.a add.o sub.o mult.o div.c

这个时候我们就会得到一个名为libCalc.a的文件。这个就是我们的静态库文件。
⚠️注意:这里我们的静态库名为Calc,而不是libCalc.a,其中lib是前缀,.a是后缀。

代码演示:静态库的使用

在实际项目中,会有一个工作空间。我们将所有的头文件,库文件,源代码文件分别存放单独在工作空间的一个独立文件夹下,然后存放着main函数的文件和生成的可执行程序独立存放在工作空间之中。工作空间的目录树长这样:

nowcoder@nowcoder:~/Linux/project$ tree
.
├── include
│   └── head.h
├── lib
│   └── libCalc.a
├── main.c
└── src
    ├── add.c
    ├── div.c
    ├── mult.c
    └── sub.c

这个时候我们还没有去生成我们的可执行文件。接下来我们就要使用自己制作的静态库,去生成可执行程序啦。

使用命令:

gcc main.c -o calcApp -I include/ -l Calc -L lib/
参数 -o 生成一个可执行文件
参数-l 到指定文件夹下去引入头文件
参数-l 指定引入到静态库名称
参数-L 到指定的文件夹下去找静态库

这个时候我们就会生成一个calcApp的可执行文件。运行一下试试:

nowcoder@nowcoder:~/Linux/project$ ./calcApp 
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667

到此,静态库的制作与使用就全部讲完啦。

动态库的制作与使用

首先阿宋来讲解一下命名规范,lib是前缀,xxx代指库的名字,.so是后缀。最后库的文件名就长这个样子:libxxx.so

在Linux平台下,制作方式大致如下:
先是通过使用命令:gcc -c -fpic/fPIC a.c b.c 生成.o文件。也就是将.c文件中的源代码生成可以被动态库可以使用的地址无关代码1
我们来解释一下参数:

参数-c:生成未被链接的目标代码(机器代码)
参数-fpic/fPIC:生成地址无关代码(机器代码)

与生成静态库所需要的ar命令不同,我们使用gcc编译器去生成动态库。
具体的命令如下

gcc -shared a.o b.o -o libCalc.so
参数-shared:生成动态库
参数-o:生成可执行的二进制文件。

编程演示:制作一个动态库

与静态库一样,阿宋这里的目的是生成一个简单进行加减乘除运算的静态库。并且使用这个静态库中自己定义的函数,然后去完成加减乘除的运算。
省略与静态库相同的操作,我们这里直接展示如何使用命令去生成动态库。

首先第一步,将我们的源代码编译成地址无关代码,使用命令:

gcc -c -fpic add.c sub.c mult.c div.c

经过这一步,我们就得到了add.o sub.o mult.o div.o 这4个含义地址无关代码的文件。下一步我们要将这4个文件打包成一个动态库。使用命令:

gcc -shared *.o -o libCalc.so

通过这一步,我们就得到了一个文件名叫libCalc.o的动态库文件。将它放到我们工作空间的lib文件夹下,现在我们工作空间的目标树如下:

nowcoder@nowcoder:~/Linux/project$ tree
.
├── include
│   └── head.h
├── lib
│   └── libCalc.so //这就是我们的动态库文件的位置。
├── main.c
└── src
    ├── add.c
    ├── div.c
    ├── mult.c
    └── sub.c

之后我们边可以尝试着去将动态库链接到程序中,然后运行一次程序试试看。
使用命令:

nowcoder@nowcoder:~/Linux/lession06/library$ gcc main.c -o calcApp -I include/ -l Calc -L lib/
nowcoder@nowcoder:~/Linux/lession06/library$ ./calcApp 
./calcApp: error while loading shared libraries: 
libCalc.so: cannot open shared object file: No such file or directory

从上面这个代码片段我们可以知道,运行失败了,错误指出我们程序在加载这个共享库时打不开这个共享库文件,然后说没有这样的文件或目录。

出现错误如何解决?

我们明明编译成功了,说明编译器是查询到有这个动态库的名字的。那为什么会报出找不到这个库呢?
这是因为,动态库与静态库的原理不同,静态库在链接时会直接被链接到程序之中,程序可以随时使用静态库中的代码。而动态库不会被链接到程序之中,而是在程序需要用时,系统再将其加载到内存之中供程序调取。所以这个时候,虽说程序是知道这个库存在,但是系统不知道这个动态库的地址到底在哪里,所以系统找不到,就无法将程序需要的函数返回给程序,也就会报出找不到这个共享库文件了。
⚠️注意:这里找不到,不是程序不知道这个库在哪,而是系统不知道这个库在哪。

那应该怎么办呢?很简单,想办法让系统可以定位到这个库在哪就行。
阿宋在这里给大家提供一种解决方式,我们可以先使用ldd命令去查看一下这个程序和动态库的依赖关系。

nowcoder@nowcoder:~/Linux/lession06/library$ ldd calcApp 
        linux-vdso.so.1 (0x00007ffef2945000)
        libCalc.so => not found
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f95d4277000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f95d486a000)

在这里我们主要是去看libCalc.so这一行就行了,这里显示的是not found 也就是系统找不到这个动态库。我们可以将这个动态库所在文件夹的绝对路径,添加到系统的环境变量2中,这样系统就知道去哪里找这个动态库了。使用命令:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库所在的文件夹的绝对路径

我们这个时候再来使用ldd命令来看看程序与动态库的依赖关系

nowcoder@nowcoder:~/Linux/lession06/library$ ldd calcApp 
        linux-vdso.so.1 (0x00007fff1f1f4000)
        libCalc.so => /home/nowcoder/Linux/lession06/
        			  library/lib/libCalc.so (0x00007f5719b59000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5719768000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f5719f5d000)

这个时候,我们的系统就成功定位到了这个动态库的地址了。我们来运行一下程序试试看:

nowcoder@nowcoder:~/Linux/lession06/library$ ./calcApp 
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667

运行成功,没有问题。
但是还有一个问题就是,我们此时配置的这个环境变量,是一个临时性的环境变量,如果关闭了此时的终端,再重启这个终端,我们还是会出现一样的错误。
这个时候我们有两种方法可以解决这个问题。一种是用户级别3的,一种是系统级别的。

我们先来讲解一下用户级别的解决方法。
首先进入到这个用户的家目录的空间下,然后再用vim打开.bashrc这个文件,在这个文件的最后把我们刚刚到那串命令添加上去就好。演示过程:

nowcoder@nowcoder:~$ cd
nowcoder@nowcoder:~$ vim .bashrc

打开vim后,移动到最后一行,按i进入插入模式,和刚才一样,将自己配置的环境变量的语句输入到这里面就行了。再按:wq退出。
这样,这个Linux用户就拥有了访问这个动态库地址的权利。我们也不用担心退出Linux终端后,再去使用这个程序,还会出现一样的错误了。

最后我们来讲一下系统级别的解决方法,和用户级别的解决方法类似,将环境变量的那串命令,写到 /etc/profile这个文件的最后一行,然后保存退出就好了。

总结:静态库与动态库的区别,以及各自的优缺点

静态库与动态库之间的区别

静态库与动态库的区别在于,静态库是在程序链接时就已经将整个静态库打包到整个程序之中。程序需要使用静态库中的内容时,可以直接使用。而动态库在程序链接时不会将整个动态库的代码全部链接到程序之中,而只是将动态库一些简单的信息链接到程序里,比如库的名字。而是在程序运行的时根据需要让系统加载到内存里,供其调用。

从上面的区别,我们就可以看出各种的优缺点了。

静态库的优缺点

静态库的优点是:由于静态库整体被复制到应用程序当中,应用程序的加载速度快。当发布程序的时候无需提供静态库,方便程序的移植。
同理,我们也可以看出静态库的缺点:因为静态库整体都被复制到了应用程序当中,所以如果系统同时运行多个应用程序,会消耗更多的系统资源,造成内存空间的浪费。还有,如果我们更新了静态库,就需要重新将静态库链接进每一个程序之中,从而造成了更新,部署,发布的麻烦。

动态库的优缺点

动态库的优点是:动态库的代码不会被加载到程序之中,而是被需要时才被系统加载到内存之中。所以动态库的第一个优点就是当同时运行多个相同的程序时,也不会占有更多的内存资源,还可以实现进程之间的资源共享(动态库又称之为共享库)。动态库的第二个优点便是更新部署发布简单。程序可以控制何时再去加载动态库。
动态库的缺点是:相比于静态库,程序加载的时间会慢一些,因为需要调取动态库之中的资源。还有就是发布程序时需要提供所需要的动态库。

至此,有关于库的开发与使用方面的知识点就全部讲完啦!


  1. 地址无关代码:在计算机领域中,地址无关代码 (英文: position-independent code缩写为PIC),是指可在主存储器中任意位置正确地运行,而不受其绝对地址影响的一种机器码。PIC广泛使用于共享库,使得同一个库中的代码能够被加载到不同进程的地址空间中。 ↩︎

  2. 环境变量:环境变量就是操作系统中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。在这里我们将这个动态库的绝对路径添加到了环境变量中,系统就会知道这个动态库的地址,通过这个地址就可以定位到这个动态库,从而就可以调取库中的代码信息了。 ↩︎

  3. 这里的用户级别就是指Linux里这一个用户所拥有的权限,所对应的系统级别就是指所有用户都拥有的权限。 ↩︎

;