C++中的静态库和动态库|GCC|Makefile|CMake学习! |
文章目录
- 一. 什么是库
- 二. 静态库
- 1.1. 静态库生成
- 1.2. 静态库制作举例
- 1.2.1 准备测试程序
- 1.2.2 生成静态库
- 1.3. 静态库的使用
- 三. 动态库
- 3.1. 为什么需要动态库
- 3.2. 生成动态链接库
- 3.3. 动态库制作举例
- 3.4. 动态库的使用
- 3.4. 解决动态库无法加载问题
- 3.4.1. 库的工作原理
- 3.4.2. 动态链接器
- 3.4.3. 解决方案
- 3.4.4. 验证
- 补充:GCC学习
- 1.1 gcc 工作流程|常用命令|多文件编译|gcc和g++
- 补充:Makefile学习
- 1.1 规则|工作原理|变量|模式匹配|函数
- 补充:CMake学习
- 1.1 CMake 概述|CMake的使用|预定义宏|嵌套的CMake|流程控制
- 参考文献
一. 什么是库
- 库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll)。所谓静态、动态是指链接。回顾一下,将一个程序编译成可执行程序的步骤:
二. 静态库
- 之所以成为【静态库】,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。
- 试想一下,静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结:
- 静态库对函数库的链接是放在编译时期完成的。
- 程序在运行时与函数库再无瓜葛,移植方便。
- 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
- 在 Linux 中静态库由程序
ar(gcc工具集里的)
生成,现在静态库已经不像之前那么普遍了,这主要是由于程序都在使用动态库。关于静态库的命名规则如下:- 在 Linux 中静态库以 lib 作为前缀,以.a 作为后缀,中间是库的名字自己指定即可,即: libxxx.a
- 在 Windows 中静态库一般以 lib 作为前缀,以 lib 作为后缀,中间是库的名字需要自己指定,即: libxxx.lib
1.1. 静态库生成
- 生成静态库,需要先对源文件进行汇编操作 (使用参数
-c
) 得到二进制格式的目标文件 (.o 格式
), 然后在通过ar
工具将目标文件打包就可以得到静态库文件了 (libxxx.a
)。- 使用 ar 工具创建静态库的时候需要三个参数:
- 生成静态链接库的具体步骤如下:
- 1. 需要将源文件进行汇编,得到 .o 文件,需要使用参数 -c
# 执行如下操作, 默认生成二进制的 .o 文件
# -c 参数位置没有要求
gcc 源文件(*.c) -c
- 2. 将得到的 .o 进行打包,得到静态库
ar rcs 静态库的名字(libxxx.a) 原材料(*.o)
- 3. 发布静态库
# 发布静态库
1. 提供头文件 **.h
2. 提供制作出来的静态库 libxxx.a
1.2. 静态库制作举例
1.2.1 准备测试程序
- 在某个目录中有如下的源文件,用来实现一个简单的计算器:
# 目录结构 add.c div.c mult.c sub.c -> 算法的源文件, 函数声明在头文件 head.h
# main.c中是对接口的测试程序, 制作库的时候不需要将 main.c 算进去
.
├── add.c
├── div.c
├── include
│ └── head.h
├── main.c
├── mult.c
└── sub.c
- 加法计算源文件 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 subtract(int a, int b)
{
return a-b;
}
- 乘法计算源文件 mult.c:
#include <stdio.h>
#include "head.h"
int multiply(int a, int b)
{
return a*b;
}
- 除法计算源文件 div.c:
#include <stdio.h>
#include "head.h"
double divide(int a, int b)
{
return (double)a/b;
}
- 头文件 head.h:
- 在C头文件中写上#ifndef(或者简写为ifdef)的作用是为了防止头文件的重复包含。当一个C源文件(或者另一个头文件)包含了该头文件时,预处理器会先检查头文件中是否已经定义了指定的标识符。如果已经定义了,则表示该头文件已经被包含过了,预处理器会跳过对该头文件的再次包含。
#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;
}
1.2.2 生成静态库
- 第一步:将源文件 add.c, div.c, mult.c, sub.c 进行汇编,得到二进制目标文件 add.o, div.o, mult.o, sub.o
# 1. 生成.o
gcc add.c div.c mult.c sub.c -c
sub.c:2:18: fatal error: head.h: No such file or directory
compilation terminated.
# 提示头文件找不到, 添加参数 -I 重新头文件路径即可
gcc add.c div.c mult.c sub.c -c -I ./include/
# 查看目标文件是否已经生成
tree
.
├── add.c
├── add.o # 目标文件
├── div.c
├── div.o # 目标文件
├── include
│ └── head.h
├── main.c
├── mult.c
├── mult.o # 目标文件
├── sub.c
└── sub.o # 目标文件
- 第二步:将生成的目标文件通过 ar 工具打包生成静态库
# 2. 将生成的目标文件 .o 打包成静态库
$ ar rcs libcalc.a a.o b.o c.o # a.o b.o c.o在同一个目录中可以写成 *.o
# 查看目录中的文件
$ tree
.
├── add.c
├── add.o
├── div.c
├── div.o
├── include
│ └── `head.h ===> 和静态库一并发布
├── `libcalc.a ===> 生成的静态库
├── main.c
├── mult.c
├── mult.o
├── sub.c
└── sub.o
- 第三步:将生成的的静态库 libcalc.a 和库对应的头文件 head.h 一并发布给使用者就可以了。
# 3. 发布静态库
1. head.h => 函数声明
2. libcalc.a => 函数定义(二进制格式)
1.3. 静态库的使用
- 当我们得到了一个可用的静态库之后,需要将其放到一个目录中,然后根据得到的头文件编写测试代码,对静态库中的函数进行调用。
# 1. 首先拿到了发布的静态库
`head.h` 和 `libcalc.a`
# 2. 将静态库, 头文件, 测试程序放到一个目录中准备进行测试
.
├── head.h # 函数声明
├── libcalc.a # 函数定义(二进制格式)
└── main.c # 函数测试
- 编译测试程序,得到可执行文件。
# 3. 编译测试程序 main.c
gcc main.c -o app
/tmp/ccR7Fk49.o: In function `main':
main.c:(.text+0x38): undefined reference to `add'
main.c:(.text+0x58): undefined reference to `subtract'
main.c:(.text+0x78): undefined reference to `multiply'
main.c:(.text+0x98): undefined reference to `divide'
collect2: error: ld returned 1 exit status
# 4. 编译的时候指定库信息
-L: 指定库所在的目录(相对或者绝对路径)
-l: 指定库的名字, 掐头(lib)去尾(.a) ==> calc
# -L -l, 参数和参数值之间可以有空格, 也可以没有 -L./ -lcalc
gcc main.c -o app -L ./ -l calc
# 查看目录信息, 发现可执行程序已经生成了
tree
.
├── app # 生成的可执行程序
├── head.h
├── libcalc.a
└── main.c
三. 动态库
- 动态链接库是程序运行时加载的库,当动态链接库正确部署之后,运行的多个程序可以使用同一个加载到内存中的动态库,因此在 Linux 中 动态链接库也可称之为共享库。
3.1. 为什么需要动态库
为什么需要动态库,其实也是静态库的特点导致。
- ①. 空间浪费是静态库的一个问题。
- ②. 另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.a更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。
- 动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
3.2. 生成动态链接库
生成动态链接库是直接使用 gcc 命令并且需要添加
-fPIC(-fpic)
以及-shared
参数。-fpic
是一个选项,用于生成位置独立的代码(Position Independent Code,PIC)
- ①
-fPIC或-fpic
参数的作用是使得 gcc 生成的代码是与位置无关的,也就是使用相对位置。- ②
-shared
参数的作用是告诉编译器生成一个动态链接库
生成动态链接库的具体步骤如下:
- ①将源文件进行汇编操作,需要使用参数
-c
, 还需要添加额外参数-fpic /-fPIC
# 得到若干个 .o文件
gcc 源文件(*.c) -c -fpic
- ②将得到的 .o 文件打包成动态库,还是使用 gcc, 使用参数
-shared
指定生成动态库 (位置没有要求)
gcc -shared 与位置无关的目标文件(*.o) -o 动态库(libxxx.so)
- ③发布动态库和头文件
# 发布
1. 提供头文件: xxx.h
2. 提供动态库: libxxx.so
3.3. 动态库制作举例
- 在此还是以上面制作静态库使用的实例代码为例来制作动态库,代码目录如下:
# 举例, 示例目录如下:
# 目录结构 add.c div.c mult.c sub.c -> 算法的源文件, 函数声明在头文件 head.h
# main.c中是对接口的测试程序, 制作库的时候不需要将 main.c 算进去
.
├── add.c
├── div.c
├── include
│ └── head.h
├── main.c
├── mult.c
└── sub.c
- 第一步:使用 gcc 将源文件进行汇编
(参数-c)
, 生成与位置无关的目标文件,需要使用参数-fpic或者-fPIC
# 1. 将.c汇编得到.o, 需要额外的参数 -fpic/-fPIC
$ gcc add.c div.c mult.c sub.c -c -fpic -I ./include/
# 查看目录文件信息, 检查是否生成了目标文件
$ tree
.
├── add.c
├── add.o # 生成的目标文件
├── div.c
├── div.o # 生成的目标文件
├── include
│ └── head.h
├── main.c
├── mult.c
├── mult.o # 生成的目标文件
├── sub.c
└── sub.o # 生成的目标文件
- 第二步:使用 gcc 将得到的目标文件打包生成动态库,需要使用参数
-shared
# 2. 将得到 .o 打包成动态库, 使用gcc , 参数 -shared
$ gcc -shared add.o div.o mult.o sub.o -o libcalc.so
# 检查目录中是否生成了动态库
$ tree
.
├── add.c
├── add.o
├── div.c
├── div.o
├── include
│ └── `head.h ===> 和动态库一起发布
├── `libcalc.so ===> 生成的动态库
├── main.c
├── mult.c
├── mult.o
├── sub.c
└── sub.o
- 其实上面两个步骤可以合并为一个命令:
gcc -fPIC -shared -o libcalc.so -c add.c div.c mult.c sub.c -I ./include/
- 第三步:发布生成的动态库和相关的头文件
# 3. 发布库文件和头文件
1. head.h
2. libcalc.so
3.4. 动态库的使用
当我们得到了一个可用的动态库之后,需要将其放到一个目录中,然后根据得到的头文件编写测试代码,对动态库中的函数进行调用。
# 1. 拿到发布的动态库
`head.h libcalc.so
# 2. 基于头文件编写测试程序, 测试动态库中提供的接口是否可用
`main.c`
# 示例目录:
.
├── head.h ==> 函数声明
├── libcalc.so ==> 函数定义
└── main.c ==> 函数测试
编译测试程序
# 3. 编译测试程序
gcc main.c -o app
/tmp/ccwlUpVy.o: In function `main':
main.c:(.text+0x38): undefined reference to `add'
main.c:(.text+0x58): undefined reference to `subtract'
main.c:(.text+0x78): undefined reference to `multiply'
main.c:(.text+0x98): undefined reference to `divide'
collect2: error: ld returned 1 exit status
添加库信息相关参数,重新编译测试代码:
# 在编译的时候指定动态库相关的信息: 库的路径 -L, 库的名字 -l
gcc main.c -o app -L./ -lcalc
# 查看是否生成了可执行程序
tree
.
├── app # 生成的可执行程序
├── head.h
├── libcalc.so
└── main.c
# 执行生成的可执行程序, 错误提示 ==> 可执行程序执行的时候找不到动态库
$ ./app
./app: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory
3.4. 解决动态库无法加载问题
3.4.1. 库的工作原理
3.4.2. 动态链接器
3.4.3. 解决方案
- 可执行程序生成之后,根据动态链接器的搜索路径,我们可以提供三种解决方案,我们只需要将动态库的路径放到对应的环境变量或者系统配置文件中,同样也可以将动态库拷贝到系统库目录(或者是将动态库的软链接文件放到这些系统库目录中)。
3.4.4. 验证
补充:GCC学习
- GCC:https://subingwen.cn/linux/gcc/?highlight=gcc
- GNU 是一个递归缩写,表示 “GNU’s Not Unix!”(GNU 不是 Unix),它是一个广泛的自由软件项目,由 Richard Stallman 在 1983 年发起,目的是创建一个类似 Unix 的完全自由的操作系统。自由软件是指用户有运行,复制,分发,研究,修改和改进软件的自由的软件。
- GNU 项目包含了大量的软件,包括软件许可证(如 GNU 通用公共许可证,简称 GPL),操作系统工具和组件(如 shell 工具,核心工具等),编程库,编译器(如 GCC),以及其他各种工具和应用程序。
- GCC 是 Linux 下的编译工具集,是 GNU Compiler Collection 的缩写,包含 gcc、g++ 等编译器。这个工具集不仅包含编译器,还包含其他工具集,例如 ar、nm 等。
1.1 gcc 工作流程|常用命令|多文件编译|gcc和g++
- gcc常用命令
-fPIC
:表示编译为位置独立的代码,用于编译共享库。目标文件需要创建成位置无关码, 念上就是在可执行程序装载它们的时候,它们可以放在可执行程序的内存里的任何地方。-shared
:指定生成动态链接库。-static
:指定生成静态链接库。-L
:表示要连接的静态或冬天库所在的目录-l
:指定链接时需要的动态库。编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.a/.so来确定库的名称。-ggdb
:此选项将尽可能的生成gdb 的可以使用的调试信息。-Wl,options
:把参数(options)传递给链接器ld 。如果options 中间有逗号,就将options分成多个选项,然后传递给链接程序。
- 多文件编译
- gcc和g++
补充:Makefile学习
- https://subingwen.cn/linux/makefile/
1.1 规则|工作原理|变量|模式匹配|函数
补充:CMake学习
- CMake 保姆级教程(上)https://subingwen.cn/cmake/CMake-primer/
- CMake 保姆级教程(下):https://subingwen.cn/cmake/CMake-advanced/
1.1 CMake 概述|CMake的使用|预定义宏|嵌套的CMake|流程控制
参考文献
以上内容主要来自大神“爱编程的大丙”
- C++静态库与动态库:https://www.cnblogs.com/skynet/p/3372855.html
- Linux 静态库和动态库:https://subingwen.cn/linux/library/