库(Library)是一组可重用的函数或代码片段,主要分为动态库(共享库)和静态库两种,它们的主要区别在于链接方式和使用方式。
所有的库(无论动静)的本质,本质都是源文件对应的.o文件。头文件的本质是对源文件的方法说明文档。
1. 静态库
1.1 静态库简介
静态库是指在应用程序中,将一些需要重复使用的公共代码编译成一个“库”文件。在链接阶段,链接器会从该库文件中提取所需的代码,并将其复制到最终生成的可执行文件中。
-
文件格式:
- Linux:
.a
(Archive 文件) - Windows:
.lib
(Library 文件)
- Linux:
-
编译与链接
- 先将多个源文件编译为 目标文件(.o/.obj)
- 再使用
ar
工具(Linux)或lib
工具(Windows)打包成静态库 - 链接时,链接器会从静态库中提取所需代码,并复制到最终的可执行文件中,生成的可执行文件包含了所需的库代码,因此不依赖外部库文件。
-
优点
- 独立性:可执行文件自包含库代码,无需额外的库文件,可以在不同环境下运行
- 加载速度快:因为不需要在运行时加载外部库
-
缺点
- 可执行文件较大:因为库代码被直接复制到可执行文件
- 更新困难:如果库代码有修改,所有使用该库的程序都需要重新编译并链接
静态库的本质就是将.o文件打了个包。.a静态库本质是一种归档文件,不需要解包,直接用gcc/g++链接即可。
1.2 创建并使用静态库
我们先来回忆一下gcc编译链接的过程。
1. 预处理:处理 `#include`、`#define` 等预处理指令,生成预处理后的代码文件。
gcc -E code.c -o code.i
2. 编译:将预处理后的代码转为汇编代码。可以使用 `gcc -S file.i -o file.s` 查看生成的汇编代码。
gcc -S code.i -o code.s
3. 汇编:将汇编代码转为目标文件(机器码),可以使用 `gcc -c file.s -o file.o` 查看目标文件。
gcc -c code.s -o code.o
4. 链接:将目标文件与库文件链接生成可执行文件。最终执行 `gcc file.o -o program`,生成 `program` 可执行文件。
gcc code.o -o code
假设下面的 usercode.c 文件进行编译和运行时,需要依赖前四个文件所提供的相关功能或声明。
我们将这四个文件放入另一个目录my_stdio(usercode.c在目录zhangsan中),然后将.c文件编译为.o文件
然后将.o文件打包并移入zhangsan目录中
ar -rc libmyc.a *.o
//ar 是一个用于创建、修改和提取归档文件(archive file)的工具,在 Linux 系统中,它属于 GNU //Binutils 工具集的一部分。
//rc(replace create)
//库的命名:lib是前缀,.a是后缀
我们直接编译链接usercode.o文件发现找不到usercode.c中使用的方法
因为我们需要指明需要链接哪个库,库的名称需要去掉前缀lib和后缀.a。这里的'l'是链接的作用。下面两种写法都可以。
gcc默认只在默认路径下查找,因此还需使用-L参数使gcc在我们指定的目录下链接库。
至此,可执行程序形成。
如果自定义头文件不在当前路径下,我们需要使用 -I(i的大写) 参数去指定目录下寻找
一步到位
总结:
- 如果我们要链接任何非C/C++标准库(包括其他外部或我们自己写的),都需要指明-L -l
- 查找自定义头文件使用-I (i)
- 如果有大量的.c文件需要拷贝到其他目录使用,我们就可以使用将其.o文件打包成静态库供用户使用。
2. 动态库
2.1 动态库简介
动态库 在linux下后缀为.so、windows下后缀为.dll
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
-
文件格式:
- Linux:
.so
(Shared Object) - Windows:
.dll
(Dynamic Link Library)
- Linux:
-
加载方式:
- 隐式加载(编译时链接):程序启动时自动加载动态库
- 显式加载(运行时加载):程序运行时使用
dlopen()
(Linux)或LoadLibrary()
(Windows)动态加载库
-
优点:
- 节省内存:多个进程可以共享同一个动态库,而不是每个可执行文件都包含一份
- 易于更新:更新库文件时,无需重新编译可执行文件,只需替换
.so/.dll
文件 - 可扩展性:可以在运行时加载不同的动态库,提高灵活性(如插件机制)
-
缺点:
- 运行时依赖:程序必须确保所需的
.so
或.dll
文件存在,否则无法运行 - 加载开销:相比静态库,程序启动时需要额外的时间来加载动态库
- 运行时依赖:程序必须确保所需的
⼀个与动态库链接的可执行件仅仅包含它用到的函数入口地址的⼀个表,而不是外部函数所在目标文件的整个机器码。
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的⼀份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
2.2 创建并使用动态库
首先生成同名.o文件
fPIC:产生位置无关码(position independent code)以支持动态链接
shared:表示生成共享库格式
静态库用ar命令来打包库,而动态库直接用gcc打包
下面使用Makefile一键打包动态库
接下来我们将lib文件放到另一个目录中使用
生成可执行程序
结果报错找不到动态库,通过ldd发现确实找不到动态库
这是因为我们使用的-L和-l命令是告诉gcc去哪寻找库,但是程序执行时系统并不知道。
静态库为什么没有这个问题呢?这是因为静态库在链接时直接把库的实现拷贝到了可执行程序中!
那么如何解决系统找不到动态库的问题,第一我们可以将自定义动态库拷贝到系统默认路径下
其次我们还可以使用软链接
还可以通过环境变量。操作系统除了在默认路径下,也会在$LD_LIBRARY_PATH环境变量下查找动态库。
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/home/wang/linux-learning/lesson23/zhangsan/lib/mylib
最后我们还可以在目录/etc/ld.so.conf.d/下新增一个任何名称的配置文件,将查找路径写入该文件,再重新加载一下配置即可
2.3 动静态库同时存在
由上面的结果可知,当动静态库同时存在时,使用的是动态库。
gcc默认使用的是动态库,如果想使用静态库则需使用参数-static
如果只有动态库,使用-static则会报错。使用-static,必须存在对应的静态库
在linux系统下,默认情况下安装的大部分库,默认优先安装动态库
3. 源码
mystdio.h
1 #pragma once
2
3 #include <stdio.h>
4
5 #define MAX 1024
6 #define NONE_FLUSH (1<<0)
7 #define LINE_FLUSH (1<<1)
8 #define FULL_FLUSH (1<<2)
9
10 typedef struct IO_FILE
11 {
12 int fileno;
13 int flag;
14 char outbuffer[MAX];
15 int bufferlen;
16 int flush_method;
17 }MyFile;
18
19
20 MyFile *MyFopen(const char *path, const char *mode);
21 void MyFclose(MyFile *);
22 int MyFwrite(MyFile *, void *str, int len);
23 void MyFFlush(MyFile *);
~
mystring.h
1 #pragma once
2
3 int my_strlen(const char *s);
mystdio.c
1 #include "mystdio.h"
2 #include <string.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include <stdlib.h>
7 #include <unistd.h>
8
9 static MyFile *BuyFile(int fd, int flag)
10 {
11 MyFile *f = (MyFile*)malloc(sizeof(MyFile));
12 if(f == NULL) return NULL;
13 f->bufferlen = 0;
14 f->fileno = fd;
15 f->flag = flag;
16 f->flush_method = LINE_FLUSH;
17 return f;
18 }
19
20 MyFile* MyFopen(const char* path, const char *mode)
21 {
22 int fd = -1;
23 int flag = 0;
24 if(strcmp(mode, "w") == 0)
25 {
26 flag = O_CREAT | O_WRONLY | O_TRUNC;
27 fd = open(path, flag, 0666);
28 }
29 else if(strcmp(mode, "a") == 0)
30 {
31 flag = O_CREAT | O_WRONLY | O_APPEND;
32 fd = open(path, flag, 0666);
33 }
34 else if(strcmp(mode, "r") == 0)
35 {
36 flag = O_RDWR;
37 fd = open(path, flag);
38 }
39
40 if(fd < 0)return NULL;
41 return BuyFile(fd, flag);
42 }
43
44 void MyFclose(MyFile *file)
45 {
46 if(file->fileno < 0) return;
47 MyFFlush(file);
48 close(file->fileno);
49 free(file);
50 }
51
52 int MyFwrite(MyFile *file, void *str, int len)
53 {
54 memcpy(file->outbuffer+file->bufferlen, str, len);
55 file->bufferlen += len;
56 if((file->flush_method & LINE_FLUSH) && file->outbuffer[file->bufferlen-1] == '\n')
57 {
58 MyFFlush(file);
59 }
60 return 0;
61 }
62 void MyFFlush(MyFile* file)
63 {
64 if(file->bufferlen <= 0) return;
65 ssize_t n = write(file->fileno, file->outbuffer, file->bufferlen);
66 if(n == -1)
67 {
68 perror("write");
69 return;
70 }
71 fsync(file->fileno);
72 file->bufferlen = 0;
73 }
mystring.c
1 #include "mystring.h"
2
3 int my_strlen(const char *s)
4 {
5 const char *start = s;
6 while(*s)
7 {
8 s++;
9 }
10
11 return s - start;
12 }
usercode.c
1 #include "mystring.h"
2 #include "mystdio.h"
3 #include <string.h>
4 #include <unistd.h>
5
6 int main()
7 {
8 MyFile *filep = MyFopen("./log.txt", "a");
9 if(!filep)
10 {
11 perror("open");
12 return 1;
13 }
14
15 int cnt = 10;
16 while(cnt--)
17 {
18 char *msg = (char*)"hello myfile!";
19 MyFwrite(filep, msg, strlen(msg));
20 MyFFlush(filep);
21 printf("buffer: %s\n", filep->outbuffer);
22 sleep(1);
23 }
24 MyFclose(filep);
25
26 const char *str = "hello, henu\n";
27 printf("strlen: %d\n", my_strlen(str));
28 return 0;
29 }
Makefile
libmyc.so:mystdio.o mystring.o
gcc -shared -o $@ $^
mystdio.o:mystdio.c
gcc -fPIC -c $<
mystring.o:mystring.c
gcc -fPIC -c $<
.PHONY:output
output:
mkdir -p lib/include
mkdir -p lib/mylib
cp -f *.h lib/include
cp -f *.so lib/mylib
tar czf lib.tgz lib
.PHONY:clean
clean:
rm -rf *.o libmyc.so lib lib.tgz