动静态库的基本原理
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间
动静态库的本质是可执行程序的"半成品"。
对于文件变成可执行程序的过程:
- 预处理: 完成头文件展开、去注释、宏替换、条件编译等,最终形成xxx.i文件。
- 编译: 完成词法分析、语法分析、语义分析、符号汇总等,检查无误后将代码翻译成汇编指令,最终形成xxx.s文件。
- 汇编: 将汇编指令转换成二进制指令,最终形成xxx.o文件。
- 链接: 将生成的各个xxx.o文件进行链接,最终形成可执行程序。
就像这样,test1.c、test2.c、test3.c、以及main.c形成可执行文件,我们需要先得到各个文件的目标文件test1.o、test2.o、test3.o以及main.o,然后再将这写目标文件链接起来,最终形成一个可执行程序。
而实际上,对于可能频繁用到的源文件,比如这里的test1.c、test2.c、test3.c,我们可以将它们的目标文件test1.o、test2.o、test3.oo进行打包,之后需要用到这四个目标文件时就可以之间链接这个包当中的目标文件了,而这个包实际上就可以称之为一个库。
实际上,所有库本质都是一堆目标文件(xxx.o)的集合,库的文件当中并不包含主函数而只是包含了大量的方法以供调用,所以说动静态库本质是可执行程序的"半成品"。
认识动静态库
在Linux运行:
cpp
#include <stdio.h>
int main()
{
printf("hello world\n"); //库函数
return 0;
}
这份代码通过printf输出hello world,是因为gcc编译器在生成可执行程序,将C标准库也链接进来了。
在Linux下,我们可以通过ldd 文件名 来查看一个可执行程序所依赖的库文件。
这里可执行程序所依赖的libc.so.6实际上就是C动态库,当我们去掉一个动静态库的前缀lib,再去掉后缀.so或者.a及其后面的版本号,剩下的就是这个库的名字。
而gcc/g++编译器默认都是动态链接的,若想进行静态链接,可以携带一个-static选项。
动静态库各自的特征
静态库
静态库是程序在编译链接的时候把库的代码复制到可执行文件当中的,生成的可执行程序在运行的时候将不再需要静态库,因此使用静态库生成的可执行程序的大小一般比较大。
优点:
- 使用静态库生成可执行程序后,该可执行程序就可以独自运行,不再需要库了。
缺点:
- 使用静态库生成可执行程序会占用大量空间,特别是当有多个静态程序同时加载而这些静态程序使用的都是相同的库,这时在内存当中就会存在大量的重复代码。
动态库
动态库是程序在运行的时候才去链接相应的动态库代码的,多个程序共享使用库的代码。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
在可执行文件开始运行前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接。
优点:
- 节省磁盘空间,且多个用到相同动态库的程序同时运行时,库文件会通过进程地址空间进行共享,内存当中不会存在重复代码。
缺点:
- 必须依赖动态库,否则无法运行。
静态库的打包与使用
为了更容易理解,下面演示动静态库的打包与使用时,都以下面的八个文件为例,其中四个源文件Add.c,Sub.c,Mul.c,Div.c四个头文件Add.h , Sub.h,Mul.h,Div.h。
为了便于观看,都放入到一个展示:
cpp
//Add.c
#include "Add.h"
int Add(int x,int y)
{
return x + y;
}
//Add.h
#pragma once
#include <stdio.h>
extern int Add(int,int);
//Div.c
#include "Div.h"
int Div(int x,int y, int *code)
{
*code = 0;
if(y == 0)
{
*code = -1;
return -1;
}
return x / y;
}
//Div.h
#pragma once
#include <stdio.h>
extern int Div(int,int,int*);
//Mul.c
#include "Mul.h"
int Mul(int x,int y)
{
return x * y;
}
//Mul.h
#pragma once
#include <stdio.h>
extern int Mul(int,int);
//Sub.c
#include "Sub.h"
int Sub(int x,int y)
{
return x - y;
}
//Sub.h
#pragma once
#include <stdio.h>
extern int Sub(int,int);
打包
我们使用Makefile来进行打包操作
cpp
static-lib=libmymath.a
$(static-lib):Add.o Div.o Mul.o Sub.o
ar -rc $@ $^
%.o:%.c
gcc -c $<
#Test:Add.o Div.o Mul.o Sub.o TestMain.o
# gcc -o $@ $^
.PHONY:output
output:
mkdir -p mymath_lib/include
mkdir -p mymath_lib/lib
cp -f *.h mymath_lib/include
cp -f *.a mymath_lib/lib
.PHONY:clean
clean:
rm -rf *.o *.a mymath_lib
第一行是替换,$(static-lib)就相当于libmymath.a
output操作是为了封装:
使用
此时使用gcc编译main.c生成可执行程序时需要携带三个选项:
- -I:指定头文件搜索路径。
- -L:指定库文件搜索路径。
- -l:指明需要链接库文件路径下的哪一个库。
说明一下:
- 因为编译器不知道你所包含的头文件add.h在哪里,所以需要指定头文件的搜索路径。
- 因为头文件add.h当中只有my_add函数的声明,并没有该函数的定义,所以还需要指定所要链接库文件的搜索路径。
- 实际中,在库文件的lib目录下可能会有大量的库文件,因此我们需要指明需要链接库文件路径下的哪一个库。库文件名去掉前缀lib,再去掉后缀.so或者.a及其后面的版本号,剩下的就是这个库的名字。
- -I,-L,-l这三个选项后面可以加空格,也可以不加空格。
动态库的打包与使用
打包
我们使用Makefile来进行打包操作
cpp
dy-lib=libmymath.so
$(dy-lib):Add.o Div.o Mul.o Sub.o
gcc -shared -o $@ $^
%.o:%.c
gcc -fPIC -c $<
.PHONY:output
output:
mkdir -p mymath_lib/include
mkdir -p mymath_lib/lib
cp -f *.h mymath_lib/include
cp -f *.so mymath_lib/lib
.PHONY:clean
clean:
rm -rf *.o *.so mymath_lib
output操作同样是为了封装