目录
[1 库的简介](#1 库的简介)
[1.1 库的基本概念](#1.1 库的基本概念)
[1.2 操作系统间的差异](#1.2 操作系统间的差异)
[1.3 库的编译](#1.3 库的编译)
[1.4 库的存放路径](#1.4 库的存放路径)
[1.5 库的类型](#1.5 库的类型)
[2 静态库](#2 静态库)
[2.1 静态库的特点](#2.1 静态库的特点)
[2.1.1 编译(链接)时把静态库中相关代码复制到可执行文件中](#2.1.1 编译(链接)时把静态库中相关代码复制到可执行文件中)
[2.1.2 程序中已包含代码,运行时不再需要静态库](#2.1.2 程序中已包含代码,运行时不再需要静态库)
[2.1.3 程序运行时无需加载库,运行速度更快](#2.1.3 程序运行时无需加载库,运行速度更快)
[2.1.4 占用更多磁盘和内存空间](#2.1.4 占用更多磁盘和内存空间)
[2.1.5 静态库升级后,程序需要重新编译链接](#2.1.5 静态库升级后,程序需要重新编译链接)
[2.2 静态库的创建并调用过程](#2.2 静态库的创建并调用过程)
[2.2.1 编写库源码](#2.2.1 编写库源码)
[2.2.2 编译生成目标文件](#2.2.2 编译生成目标文件)
[2.2.3 创建静态库](#2.2.3 创建静态库)
[2.2.4 编写应用程序](#2.2.4 编写应用程序)
[2.2.5 编译应用程序并链接静态库](#2.2.5 编译应用程序并链接静态库)
[2.2.6 执行可执行文件](#2.2.6 执行可执行文件)
[2.3 静态库的优缺点](#2.3 静态库的优缺点)
[3 动态库](#3 动态库)
[3.1 动态库的特点](#3.1 动态库的特点)
[3.1.1 编译(链接)时仅记录用到哪个共享库中的哪个符号,不复制共享库中的代码](#3.1.1 编译(链接)时仅记录用到哪个共享库中的哪个符号,不复制共享库中的代码)
[3.1.2 程序不包含库中代码,尺寸小](#3.1.2 程序不包含库中代码,尺寸小)
[3.1.3 多个程序可共享同一个库](#3.1.3 多个程序可共享同一个库)
[3.1.4 程序运行时需要加载库](#3.1.4 程序运行时需要加载库)
[3.1.5 库升级方便,无需重新编译程序](#3.1.5 库升级方便,无需重新编译程序)
[3.2 动态库创建并调用过程](#3.2 动态库创建并调用过程)
[3.2.1 编写库源码](#3.2.1 编写库源码)
[3.2.2 编译生成目标文件](#3.2.2 编译生成目标文件)
[3.2.3 创建共享库](#3.2.3 创建共享库)
[3.2.4 编写应用程序](#3.2.4 编写应用程序)
[3.2.5 编译并链接共享库](#3.2.5 编译并链接共享库)
[3.3 设置共享库的加载路径](#3.3 设置共享库的加载路径)
[3.3.1 将共享库拷贝到系统库目录(不推荐)](#3.3.1 将共享库拷贝到系统库目录(不推荐))
[3.3.2 设置 LD_LIBRARY_PATH 环境变量](#3.3.2 设置 LD_LIBRARY_PATH 环境变量)
[3.3.3 配置 ld.so.conf 文件(推荐)](#3.3.3 配置 ld.so.conf 文件(推荐))
[3.4 动态库的优缺点](#3.4 动态库的优缺点)
[4 静态库 和 动态库 的对比](#4 静态库 和 动态库 的对比)
前言:本篇讲的库默认基于C语言
1 库的简介
1.1 库的基本概念
库(Library)是包含一组可供程序调用的预编译代码的文件。这些代码通常是实现某些功能模块或通用操作的,比如数学计算、文件操作、字符串处理等。
-
源码库(Source Library):包含源代码,开发者可以下载并编译成二进制库。
-
二进制库(Binary Library):已经编译好的库文件,开发者可以直接使用这些库。
通过使用库,程序员可以复用已经编写好的功能,避免重复造轮子,提高开发效率。
Linux中的库有很多,通常按功能划分为不同的类型,比如:
-
标准 C 库(libc) :提供了一些基本的功能,比如输入输出、字符串操作、内存管理等。C 标准库的实现通常是
glibc(GNU C Library),它是 Linux 上最常用的标准 C 库。它包含了大部分 C 程序需要的函数,如printf、malloc等。 -
数学库(libm) :提供数学运算函数,如三角函数、指数函数等。在 Linux 中,
libm是一个常见的数学库,它提供了诸如sin()、cos()、sqrt()等函数。 -
线程库(pthread) :线程库提供多线程编程所需的功能,例如线程的创建、同步和管理。在 Linux 中,
libpthread是一个标准的 POSIX 线程库,支持多线程编程。 -
.......
1.2 操作系统间的差异
不同系统的库通常不可以直接互相调用,例如Windows 和 Linux 的库文件格式并不兼容:
-
Linux 使用的库格式是
.so(共享库)和.a(静态库),它们基于 ELF(可执行和可链接格式)文件格式。 -
Windows 使用的是
.dll(动态链接库)和.lib(静态库)格式,这些文件基于 PE(可执行文件格式)。
因此,Windows 和 Linux 下的库文件格式和链接方式不同,两者之间的库不能直接互用。例如,你不能在 Linux 上直接加载一个 Windows 的 .dll 文件,也不能在 Windows 上使用 .so 文件。要跨平台使用库,通常需要分别为不同平台编译相应的库。、
当然这是说的C语言的库,由于 C 语言直接与操作系统交互,因此其库文件(如 .so、.dll)是与操作系统紧密相关的,Windows 和 Linux 使用不同的格式(PE、ELF),无法直接互操作。不同平台上的 C 库需要分别编译。
对于 高级语言 ,如 Python 、Java 、C# 等,它们的库通常不依赖于操作系统的底层二进制文件格式。因此,对于高级语言的库,其 跨平台兼容性 比 C 语言的库要强得多。
跨平台支持的关键因素
-
抽象化层(Virtual Machine 或 Runtime):高级语言(如 Java、Python、C#)通常依赖一个抽象化层(例如 JVM 或 Python 解释器)来实现跨平台的运行。这些语言的库和应用不需要关心底层操作系统如何实现,而只需要依赖虚拟机/运行时环境提供的接口。
-
标准化接口:高级语言的库通常遵循跨平台的标准,提供相同的 API,确保无论在哪个平台上运行,功能都是一致的。
-
跨平台框架/库:许多高级语言有专门的跨平台框架或库,使得开发者可以编写一次代码,在不同平台上运行。例如:
-
Qt(C++):提供跨平台的 GUI 和应用程序开发框架。
-
Electron(JavaScript):可以用 Web 技术(HTML、CSS、JavaScript)构建跨平台桌面应用。
-
Xamarin(C#):用于开发跨平台的移动应用。
-
1.3 库的编译
源码库的使用
源代码库可以下载并编译。通常,Linux 中的源代码库会提供 Makefile 文件或类似的构建脚本,使用 make 工具进行编译。比如编译一个库可能的步骤如下:
-
下载源代码。
-
解压源代码并进入目录。
-
执行
./configure(配置构建参数)。 -
执行
make(编译)。
1.4 库的存放路径
在 Linux 系统中,常见的库文件存放路径有:
-
/lib:存放系统级别的库文件,通常是操作系统核心部分所需的库。 -
/usr/lib:存放常规程序需要的库文件,这些库通常与用户安装的软件和应用程序相关。 -
/usr/local/lib:存放用户自行安装的库文件,这些库通常是手动编译并安装的库。
1.5 库的类型
在 Linux 下,库可以分为 静态库 (Static Library)和 共享库(Shared Library)。
2 静态库
2.1 静态库的特点
2.1.1 编译(链接)时把静态库中相关代码复制到可执行文件中
在使用静态库时,编译器 在程序链接阶段会把静态库中用到的代码直接复制到最终的可执行文件中。换句话说,当你链接静态库时,静态库的内容(如函数、变量等)会被嵌入到最终生成的可执行文件中。
例子 :如果你在程序中使用了静态库中的 hello() 函数,编译器会将 hello() 函数的代码复制到最终的可执行文件中,而不再依赖外部的 .a 静态库文件。
好处:程序变成了一个独立的可执行文件,不再依赖外部库,因此部署时更加简便,不需要关心库文件是否存在。
2.1.2 程序中已包含代码,运行时不再需要静态库
因为在链接时静态库的代码已经被复制到程序中,所以程序运行时不需要加载静态库。程序启动时,操作系统会直接加载可执行文件,不需要再去查找和加载外部的静态库文件。
例子 :如果你使用了 libhello.a,在程序编译完成后,libhello.a 中的代码已经包含在可执行文件 test 中,程序运行时不会再依赖 libhello.a。
好处:程序不需要额外的依赖,能够快速启动,因为不需要动态链接器加载外部库。
2.1.3 程序运行时无需加载库,运行速度更快
由于静态库的代码已经在程序编译时被嵌入,程序启动时不需要动态加载任何外部库,这意味着静态库程序的启动速度通常会更快。动态库需要在程序运行时被加载,这会花费一些额外的时间和资源。
例子 :假设你有一个静态库 libmath.a,在链接时,所有数学计算函数的代码都被嵌入到最终的可执行文件中。因此,当程序启动时,操作系统仅需要加载可执行文件,而不需要去查找并加载 libmath.a。
好处:较快的启动时间,因为没有动态库的加载过程。
2.1.4 占用更多磁盘和内存空间
由于静态库的代码被复制到每个使用它的可执行文件中,因此 每个程序都会有一份库的副本 。这不仅会导致 磁盘空间 的浪费,也可能导致 内存占用增加,尤其是在多个程序使用相同的库时。
磁盘空间:如果多个程序都使用同一个静态库,那么每个程序的可执行文件都需要包含这份静态库的代码,导致磁盘空间的浪费。
内存空间:在运行时,每个程序都需要加载自己的静态库副本,即使多个程序使用相同的库,它们各自都会加载一份相同的代码到内存中,这就会造成内存的浪费。
例子 :假设你有三个程序 prog1、prog2 和 prog3 都使用了同一个静态库 libmath.a,那么在内存中,三个程序每个都会加载 libmath.a 中的代码,这样内存的使用就显得不够高效。
2.1.5 静态库升级后,程序需要重新编译链接
静态库的 一个主要缺点 是如果库发生了更新(例如修复了 bug 或新增了功能),程序必须重新编译和重新链接 才能使用库的最新版本。这是因为静态库的代码已经被复制到程序中,程序与静态库的连接是静态的,不像动态库那样可以在运行时动态加载。
例子 :如果你有一个程序 test 使用了静态库 libmath.a,如果 libmath.a 进行了升级(比如新增了一个函数 sqrt),你必须重新编译 test 并链接最新版本的 libmath.a,才能使 test 使用库中的新功能。
缺点:静态库的更新不如动态库方便。每次更新静态库时,程序都需要重新编译和链接,增加了开发和部署的复杂度。
2.2 静态库的创建并调用过程
2.2.1 编写库源码
新建一个hello.c文件,在里面写入:
cpp
#include <stdio.h>
void hello(void) {
printf("hello world\n");
return;
}
这是一个只会打印 hello world 的函数,我们需要将它放入静态库。
2.2.2 编译生成目标文件
在编译 hello.c 文件时,使用 -c 标志来生成目标文件 .o,而不是生成可执行文件。这样,hello.o 就是一个中间文件,里面包含了编译后的代码,但是还没有被链接到程序中。
cpp
gcc -c hello.c
编译后,会得到一个目标文件 hello.o。

2.2.3 创建静态库
静态库是由一个或多个目标文件组成的,使用 ar 工具将目标文件打包成一个静态库文件。静态库的文件名通常以 lib 开头,并以 .a 作为扩展名。
cpp
ar crs libhello.a hello.o
-
ar:是一个创建和管理归档文件的工具,常用于创建静态库。 -
c:创建静态库。 -
r:将目标文件插入归档(静态库)中。 -
s:为静态库创建符号表索引(加速链接过程)。
这条命令会将 hello.o 文件打包成 libhello.a 静态库文件。

2.2.4 编写应用程序
接下来,需要编写一个应用程序test.c,在程序中调用静态库中的 hello() 函数。
cpp
#include <stdio.h>
void hello(void);
int main() {
hello();
return 0;
}

2.2.5 编译应用程序并链接静态库
在编译应用程序时,使用 -L 选项指定静态库的位置,并使用 -l 选项指定要链接的库。
cpp
gcc -o test test.c -L. -lhello
-
L.:指定静态库所在目录,这里使用当前目录(.)作为库的路径。 -
-lhello:链接名为libhello.a的静态库。注意,-lhello会自动去除前缀lib和后缀.a,所以它链接的是libhello.a文件。
这条命令会将 test.c 编译成可执行文件 test,并且将静态库 libhello.a 中的 hello 函数链接到最终的可执行文件中。
2.2.6 执行可执行文件
cpp
./test
执行可执行文件,可以看到程序调用了hello()函数,在终端上输出了 hello world。

2.3 静态库的优缺点
优点:
-
无需依赖外部库文件:程序不依赖外部的动态库文件,库的代码已经嵌入到可执行文件中。
-
启动速度快:由于不需要在程序启动时加载库,启动速度相对较快。
-
易于部署:程序不需要外部的共享库,因此部署起来更加简单。
缺点:
-
可执行文件大:每个使用静态库的程序都包含了库的代码,导致可执行文件的体积较大。
-
内存浪费:如果多个程序使用相同的静态库,它们各自会加载一份相同的代码,导致内存的浪费。
-
更新不便:如果库的代码有更新,程序需要重新编译和重新链接,才能使用更新后的版本。
总结:
-
静态库是一种将库的代码在编译时嵌入到程序中的方式,程序运行时不需要额外加载库文件。
-
静态库通过
ar工具创建,程序在编译时通过链接静态库将其代码嵌入到可执行文件中。 -
静态库的优点是简单、快速,但也带来了程序文件体积大、内存浪费和更新麻烦的问题。
3 动态库
3.1 动态库的特点
3.1.1 编译(链接)时仅记录用到哪个共享库中的哪个符号,不复制共享库中的代码
在 静态库 中,编译器会将库中所需的代码(函数、变量等)复制到最终的可执行文件中。而在 共享库 中,编译器仅仅将程序需要使用的符号(例如函数名)记录下来,并在程序运行时动态加载这些符号对应的代码。换句话说,程序在链接时只会知道它需要哪个符号(如 hello() 函数),而不包含该函数的代码本身。
这种做法让共享库具有更大的灵活性和复用性,因为代码并不直接嵌入到每个应用中,而是可以在运行时动态加载。
3.1.2 程序不包含库中代码,尺寸小
由于共享库的代码是在运行时加载的,而不是编译时嵌入到可执行文件中,所以最终生成的可执行文件的尺寸通常比静态库小。这是因为共享库本身包含了实际的代码,而程序只是记录了如何调用这些代码。
举个例子,假设有 10 个程序使用了同一个共享库,在静态库的情况下,每个程序的可执行文件都包含了库的副本;而在共享库的情况下,只有一个共享库文件存在,所有程序在运行时都通过该共享库来访问函数。
3.1.3 多个程序可共享同一个库
共享库的一个重要优势是共享性 。多个程序可以使用同一个共享库,节省了磁盘空间和内存资源。程序不需要在每次运行时复制库中的代码,而是通过操作系统的动态链接器(ld.so 或 ld-linux.so)共享这个库。
例如,多个程序如果都使用 libcommon.so,操作系统会将该库加载到内存中,并将内存中的相同代码区域映射到每个程序的地址空间中。这样,多个程序会共享同一份库代码,而不需要重复加载,显著减少了内存消耗。
3.1.4 程序运行时需要加载库
不同于静态库的编译时链接,共享库 在程序运行时需要通过动态链接加载。操作系统通过动态链接器将共享库加载到内存中,并将符号(函数、变量等)与程序的代码进行链接。
这种动态加载的方式意味着共享库的代码只有在程序运行时才会被加载到内存中,而不是在编译时就固定下来。因此,程序的启动时间可能稍慢,因为操作系统需要查找并加载共享库。
3.1.5 库升级方便,无需重新编译程序
共享库的一个关键优势是 库升级无需重新编译程序 。如果共享库的功能有更新,只需替换掉共享库文件(如 libcommon.so),程序会自动使用更新后的库。程序不需要重新编译,只要库的接口(符号)保持一致。
例如,假设你有一个程序依赖 libcommon.so,如果库中有 bug 被修复,或者你添加了新的功能,你只需要更新库文件,程序就会自动使用新版本的库,而不需要重新编译和链接。
这使得共享库在维护和升级方面具有很大的优势,尤其是在大型系统中,减少了因库的更新而导致的编译和部署工作。
3.2 动态库创建并调用过程
3.2.1 编写库源码
和静态库一样,动态库的创建也需要库源码,我直接将静态库的hello.c复制过来。
3.2.2 编译生成目标文件
为了生成共享库,我们需要先将 hello.c 编译成目标文件(.o 文件)。使用 -fPIC(Position Independent Code)选项是很重要的,因为共享库需要支持位置无关代码(PIC),即库中的代码能在内存中的任何位置加载。
cpp
gcc -c -fPIC hello.c

-fPIC:指示编译器生成位置无关代码(PIC),这对于共享库是必需的。
这条命令会生成目标文件 hello.o 。
3.2.3 创建共享库
接下来,使用 gcc 创建共享库文件。使用 -shared 选项,告诉编译器将目标文件链接成共享库。
cpp
gcc -shared -o libcommon.so hello.o
-
-shared:告诉编译器创建共享库,而不是生成可执行文件。 -
-o libcommon.so:指定输出的共享库文件名为libcommon.so。
这条命令将 hello.o 目标文件链接成共享库 libcommon.so。
3.2.4 编写应用程序
由于库函数写的和静态库的一样,应用程序也跟静态库一样的就行,我直接将静态库的应用函数复制过来。

3.2.5 编译并链接共享库
使用 gcc 编译应用程序时,链接共享库的方式与静态库有所不同。这里,你需要使用 -L 选项指定共享库的路径,并使用 -l 选项指定库的名称。
cpp
$ gcc -o test test.c -L. -lcommon
-
-L.:指定共享库所在的目录(这里是当前目录.)。 -
-lcommon:告诉编译器链接libcommon.so(忽略扩展名.so)。编译器会先查找动态库(共享库),如果找不到,再尝试查找静态库(例如libcommon.a)。
如果一切顺利,test 程序就会链接上 libcommon.so,并包含其中的 hello 函数。
3.2.6 执行可执行文件
当你尝试运行程序时,系统可能会提示找不到共享库。
cpp
./test
错误消息:

这个错误表明程序在运行时没有找到 libcommon.so 文件。系统需要知道在哪里查找这个共享库。
所以我们需要先设置共享库的加载路径。
3.3 设置共享库的加载路径
有几种方法可以让系统找到共享库:
3.3.1 将共享库拷贝到系统库目录(不推荐)
可以将共享库拷贝到系统的共享库路径,如 /usr/lib 或 /lib 目录下:
cpp
$ sudo cp libcommon.so /usr/lib
$ sudo ldconfig
ldconfig用来刷新系统的共享库缓存。它会更新库的路径信息,让系统能够正确找到新安装的共享库。
不推荐修改系统的共享库。
3.3.2 设置 LD_LIBRARY_PATH 环境变量
可以通过设置 LD_LIBRARY_PATH 环境变量来告诉系统在哪些目录查找共享库。比如,当前目录(.):
cpp
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
-
D_LIBRARY_PATH是 Linux 用来查找共享库的环境变量。 -
.表示当前目录,$LD_LIBRARY_PATH保持之前的设置。
这样做可以在程序运行时找到当前目录下的共享库 libcommon.so。
但这种方法只在当前目录和窗口有效,我重新打开一个终端执行 ./test 依旧会报错。
将test移到上层目录,也无法正常运行。
要解决这个问题,看第三种方法。
3.3.3 配置 ld.so.conf 文件(推荐)
可以在 /etc/ld.so.conf.d/ 目录下创建一个配置文件,指定额外的共享库路径。然后执行 ldconfig 更新缓存:
①在 /etc/ld.so.conf.d/ 目录下创建一个新的配置文件,例如 libcommon.conf,并写入共享库路径:


②执行 ldconfig 刷新共享库缓存:
cpp
sudo ldconfig
③可以看到无论在哪个目录都能链接到libcommon.so 文件,正常运行程序。
3.4 动态库的优缺点
优点:
-
节省磁盘和内存空间:多个程序可以共享同一个动态库,不需要为每个程序复制库的代码,减少了磁盘和内存占用。
-
方便更新:更新共享库后,程序无需重新编译,只需要替换库文件即可,简化了程序的维护和升级。
-
灵活性高:程序与动态库松耦合,更新或替换库时不需要重新编译程序,提升了程序的灵活性。
缺点:
-
程序启动慢:程序需要在启动时加载动态库,可能会增加启动时间。
-
依赖管理复杂:程序需要确保在运行时能够找到正确版本的库文件,配置不当会导致加载失败。
-
版本兼容性问题:库的接口更新可能会导致与旧版程序的不兼容,需要谨慎管理版本。
总结:
-
动态库是通过程序运行时加载的方式来共享功能代码,它可以节省资源,方便更新,但也带来了启动时间和依赖管理上的挑战。
-
动态库通过
gcc -shared命令创建,程序运行时通过动态链接加载库代码。 -
动态库的优点是节省资源和方便更新,但也存在启动慢和依赖管理复杂的问题。
4 静态库 和 动态库 的对比
| 特性 | 静态库 (Static Library) | 动态库 (Shared Library) |
|---|---|---|
| 代码链接时机 | 在编译时将库的代码嵌入到程序中 | 程序运行时动态加载库 |
| 程序依赖 | 程序无需依赖外部库文件,库的代码已嵌入到可执行文件中 | 程序依赖外部库文件,运行时加载库 |
| 可执行文件大小 | 可执行文件包含所有库的代码,文件较大 | 可执行文件较小,只有引用库的符号信息 |
| 内存占用 | 每个程序都有自己的库副本,内存占用较大 | 多个程序共享同一份库,内存占用较小 |
| 启动速度 | 程序启动时不需要加载库,启动速度较快 | 程序启动时需要加载动态库,启动速度较慢 |
| 更新方式 | 更新库时需要重新编译程序并链接新的库 | 更新库时只需替换共享库文件,程序不需要重新编译 |
| 部署复杂度 | 不需要额外的依赖,部署简单 | 需要确保库文件存在,可能需要配置 LD_LIBRARY_PATH 等路径 |
| 版本兼容性 | 程序和库的版本固定,不会发生兼容性问题 | 可能会有版本兼容问题,库的接口变化可能导致程序崩溃 |
| 代码共享 | 每个程序都拥有自己的库副本,无法共享 | 多个程序可以共享同一个库,节省资源 |
| 调试难度 | 相对简单,因为所有代码都在程序中 | 可能需要解决符号解析、库加载等问题,调试较复杂 |
总结:
-
静态库:适合需要独立执行的程序,部署简单,但会导致程序体积较大、内存占用较高。更新时需要重新编译程序。
-
动态库:适合多个程序共享相同功能代码,节省磁盘和内存空间。库更新更方便,但需要依赖外部库文件,可能会导致版本兼容性问题和启动慢的问题。