💢 由于CentOS 8系统2021年12月31日已停止维护服务,CentOS 7系统将于2024年06月30日停止维护服务。CentOS官方不再提供CentOS 9及后续版本,不再支持新的软件和补丁更新。CentOS用户现有业务随时面临宕机和安全风险,并无法确保及时恢复。
因此我把系统换成了 ubuntu ,现在及后面的 Linux 相关博客都会用 ubuntu 来做演示
🔥 在实践中,我们一定会使用别人的库(不限于C、C++的库),在实践中,我们会使用成熟、被广泛使用的第三方库,而不会花费很多时间自己造轮子,为了能更好地使用库,就要在学习阶段了解其本质。那么对于库而言,可以从两方面认识它:
一个 c 程序生成一个可执行程序(.exe)步骤如下
补充:各类文件后缀含义(了解)
a.out
💧 当有多个不同的源文件中的main函数调用这些功能函数时,每次都要重新对这几个函数重复预处理、编译、汇编操作,各自生成.o文件,然后再和调用功能函数的源文件(一般是main函数)生成的.o,最后才生成可执行程序
这样会有很多重复的操作,所以一般将这些常用的函数所在的.cpp文件预处理、编译、汇编生成的多个.o文件打包在一起,称之为库。而事实上我们经常使用的<stdio.h>、<iostream> 以及使用各种STL容器包含的头文件都是这么做的
由此可以见库的本质:
严格地说,库并不是可执行程序的半成品
先来看一段代码
#include <stdio.h>
#include <string.h>
int main()
{
char buffer[1024];
strcpy(buffer, "Hello Island1314");
printf("%s\n", buffer);
return 0;
}
上图中,我们只是调用了接口,并没有去实现该函数。
但是可执行程序执行必须要有对应的实现,所以编译时,由gcc默认帮我们链接了对应的库。
其中,libc.so.6就是这个可执行程序依赖的库文件,通过ll
指令查看这个该路径下这个库文件的属性:
表明它其实是软链接到同目录下的 libc.so.6 文件,通过file
指令,查看该文件的文件类型:
🎐如果一个库文件是symbolic link,那么它是一种特殊的目标文件,可以在程序运行时被加载(链接)进来。
去掉前缀和后缀,剩下的就是库的名字,即 libc.so.6实际上是C语言的动态库,库名是 c
如果我们要生成静态链接如下:
gcc code.c -o code -static
注意:系统本身为了支持编程,除了提供标准库.h,还提供了标准库的实现.so / .a
静态库
1. 定义
2. 特点
动态库
1. 定义 动态库是编译后生成的共享对象文件,通常以 .so为后缀(如 libmylib.so)。这些库在程序运行时动态加载,多个程序可以共享同一个动态库。 2. 特点
静态库
优点
缺点
动态库
优点
缺点
静态库
动态库
在学习制作库之前,我们先来看看 库的链接
下面是一个简单的示例,展示了如何编写一个 add 函数,并且如何将它们编译成目标文件(.o 文件),最后将这些目标文件链接起来形成一个可执行文件。 首先,我们需要创建 2 个源文件:
// Add.h
#pragma once // 防止头文件重复包含
#include <stdio.h>
int add(int x, int y);
// Add.c
#include "Add.h"
int add(int x, int y)
{
return x + y;
}
// Main.c
#include <stdio.h>
extern int add(int a, int b);
int main()
{
int a = 10, b = 20;
printf("Add: %d + %d = %d\n",a, b, add(a, b));
return 0;
}
extern 告诉编译器 add 函数存在,但它的定义(函数体)在其他地方
现在,我们将这些源文件编译成目标文件,并链接它们来创建一个可执行文件。
编译和链接步骤
在 Linux 命令行中执行以下命令:
这样,我们就测试了 Linux 下的 GCC 编译器如何将 .o
文件链接起来形成可执行文件。
🦋 我们在给被人静态库的时候,需要把 .a/.so 库文件和 .h 头文件给到对方。.a/.so 让别人可以调用库中的函数,.h 告诉别人库中有哪些函数可以被调用,相当于函数使用的说明书
下面我们做库需要用到的文件如下:
Main.c 文件
#include <my_stdio.h> // <> 系统默认路径
#include <my_string.h>
// #include "my_stdio.h" // " " 指 头文件在当前路径下
// #include "my_string.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
mFILE *fp = mfopen("./log.txt", "a");
if(fp == NULL)
{
return 1;
}
int cnt = 3;
while(cnt)
{
printf("write: %d\n", cnt);
char buffer[64];
snprintf(buffer, sizeof(buffer), "hello message, number is : %d", cnt);
cnt--;
mfwrite(buffer, strlen(buffer), fp);
mfflush(fp);
sleep(1);
}
mfclose(fp);
}
my_stdio 系列文件
// my_stdio.h
#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE
{
int flag; // 刷新方式
int fileno; // 文件描述符
char outbuffer[SIZE];
// 缓冲区
int cap; // 容量
int size; // 大小
// TODO
};
typedef struct IO_FILE mFILE;
mFILE *mfopen(const char *filename, const char*mode); // 打开文件
int mfwrite(const void *ptr, int num, mFILE *stream); // 写入数据
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);
// my_stdio.c
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
mFILE *mfopen(const char *filename, const char*mode)
{
int fd = -1;
if(strcmp(mode, "r") == 0)
{
fd = open(filename, O_RDONLY);
}
else if(strcmp(mode, "w") == 0)
{
fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
}
else if(strcmp(mode, "a") == 0)
{
fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
}
if(fd < 0) return NULL;
mFILE *mf = (mFILE*)malloc (sizeof(mFILE));
if(!mf)
{
close(fd);
return NULL;
}
mf->fileno = fd;
mf->flag = FLUSH_LINE;
mf->size = 0;
mf->cap = SIZE;
return mf;
}
void mfflush(mFILE *stream)
{
if(stream->size > 0) // 缓冲区里面有内容
{
// 写到内核文件的文件缓冲区中!!
write(stream->fileno, stream->outbuffer, stream->size);
// 刷新到外设
fsync(stream->fileno);
stream->size = 0;
}
}
int mfwrite(const void *ptr, int num, mFILE *stream)
{
// 1. 拷贝
memcpy(stream->outbuffer + stream->size, ptr, num);
stream->size += num;
// 2. 检测缓冲区是否要刷新
if(stream->flag == FLUSH_LINE && stream->size > 0 && stream->outbuffer[stream->size - 1] == '\n')
{
mfflush(stream);
}
return num;
}
void mfclose(mFILE *stream)
{
// 刷新之前,先判断缓冲区是否有内容
// 将内容刷新到缓冲区
if(stream->size > 0)
{
mfflush(stream);
}
close(stream->fileno); // 进行文件刷新
}
my_string 系列文件
// my_string.h
#pragma once
int my_strlen(const char *s);
// my_string.c
#include "my_string.h"
int my_strlen(const char *s)
{
const char *end = s;
while(*end != '\0') end++;
return end - s;
}
Makefile 文件
# 生成静态库
libmystdio.a: my_stdio.o my_string.o
@ar -rc $@ $^
@echo "build $^ to $@ ... done"
%.o:%.c
@gcc -c $<
@echo "compling $< to $@ ... done"
.PHONY: clean # 清除
clean:
@rm -rf *.a *.o stdc*
@echo "clean ... done"
.PHONY: output # 发布静态库
output:
@mkdir -p stdc/include
@mkdir -p stdc/lib
@cp -f *.h stdc/include
@cp -f *.a stdc/lib
@tar -czf stdc.tgz stdc # 打包
@echo "output stdc ... done"
# 生成动态库
libmystdio.so: my_stdio.o my_string.o
gcc -o $@ $^ -shared
%.o:%.c
gcc -fPIC -c $<
.PHONY: clean
clean:
@rm -rf *.so *.o stdc*
@echo "clean ... done"
.PHONY: output # 发布静态库
output:
@mkdir -p stdc/include
@mkdir -p stdc/lib
@cp -f *.h stdc/include
@cp -f *.so stdc/lib
@tar -czf stdc.tgz stdc # 打包
@echo "output stdc ... done"
编译同名的目标文件
gcc -c my_stdio.c
gcc -c my_string.c
生成静态库
ar
,使用 -c
(创建)选项创建库文件,使用 -r
(添加替换)选项将文件添加到库文件,以及-s
(索引)选项,用于在库文件中创建文件索引
ar -rc libmystdio.a my_stdio.o my_string.o
注意:
sudo cp *.h /usr/include/
sudo cp libmystdio.a /lib64/
查询指定目录结果如下:
注意:即使我们将头文件和库文件拷贝到系统目录下,但是 gcc 在编译main.c时,还需要显式地说明要链接的库在哪一路径下。
通过-l 命令选项让 gcc 知道这是一个第三方库,而库名就是llibmysdtio.a去掉 前缀lib 和 后缀.a 剩下的部分,即lmysdtio
gcc -main.c -lmystdio
原因:这是因为gcc在默认路径下是链接C/C++的库,它只知道哪些是内置的库,而不知道第三方库的存在
删去我们之前安装到系统的路径
sudo rm /usr/include/my_*
sudo rm /lib64/libmystdio.a
现在我们再去指定目录下查找:(发现其就就不在了)
注意:但是将头文件和库文件添加到系统目录下是非常不推荐的,因为这样会污染系统库目录,而这就是安装库的过程
调用的函数的库文件在哪
调用的库的名称
gcc main.c -o main -L. -lmystdio # 当前路径
注意:此时使用的时候,头文件以及库其实都处于当前路径下了
调用上面的 Makefile 文件(静态库)
.PHONY:output # 发布静态库
output:
@mkdir -p stdc/include
@mkdir -p stdc/lib
@cp -f *.h stdc/include
@cp -f *.a stdc/lib
@tar -czf stdc.tgz stdc
@echo "output stdc ... done"
gcc 编译 main.c 链接库时,需要由以下三个选项定位文件:
-I
:指定头文件搜索路径。-L
:指定库文件搜索路径。-l
:指明需要链接库文件路径下的哪一个库。先把之前做好的stdc 目录打包复制到当前 main.c 的目录下,然后解包运行程序
gcc main.c -Istdc/include -Lstdc/lib -lmystdio
依然使用之前的四个文件和一个main.c文件示例。
gcc 需要增加-fPIC选项(position independent code):位置无关码
gcc -fPIC -c my_stdio.c
gcc -fPIC -c my_string.c
gcc -o libmystdio.so my_stdio.o my_string.o -shared
位置无关码(了解)
🎈 位置无关代码(Position Independent Code,PIC)是一种特殊的机器代码,它可以在内存中的任何位置运行,而不需要重新定位。这意味着,当程序被加载到内存中时,它的代码段可以被放置在任何可用的内存地址,无需修改代码中的任何地址引用
而使用位置无关代码可以避免这些问题
和静态库采用的绝对编址相比,动态库采用的就是相对编址,各个模块在库中的地址可能不相同,但是它们之间的相对位置是固定的。就好像房车旅行一样,房车的位置虽然一直在变,但是房车内家具的相对位置一直不变。
位置无关代码对于 gcc 来说:
sudo cp *.h /usr/include/
sudo cp libmystdio.so /lib/
和静态库那里类似,使用 -l 命令告诉 gcc 我们使用的库是什么即可
gcc main.c -lmystdio
注意:Ubuntu 自身系统设定的相应的设置的原因,即其只在 /lib和 /usr/lib 下搜索对应 的.so 文件,故需将对应 .so 文件拷贝到对应路径。 Centos则是 /lib/64
当我们有了头文件和库文件的时候不安装到系统里,和静态库类似
.PHONY:output #动态库
output:
@mkdir -p stdc/include
@mkdir -p stdc/lib
@cp -f *.h stdc/include
@cp -f *.so stdc/lib
@tar -czf stdc.tgz stdc
@echo "output stdc ... done"
gcc 编译 main.c 链接库时,需要由以下三个选项定位文件:
-I
:指定头文件搜索路径。-L
:指定库文件搜索路径。-l
:指明需要链接库文件路径下的哪一个库。先把之前做好的 stdc 目录打包复制到当前 main.c 的目录下,然后解包运行程序
gcc main.c -Istdc/include -Lstdc/lib -lmystdio
当我们对文件进行运行的时候,却发现:
在Linux下,有以下几种使用第三方动态库的方法(解决以上问题):
(1)拷贝到系统默认路径
sudo cp libmystdio.so /lib/
在编译链接时,只需要记录需要链接文件的编号,运行程序时才会进行真正的“链接”,所以称为“动态链接”。因此,只要将动态库的.so文件拷贝到系统目录下,这个可执行程序就可以被链接到动态库,而不需要重新编译链接。
也就是说,编译器只负责生成一个main.c对应的二进制编码文件,而链接的工作要等到运行程序时才会进行链接,所以生成可执行程序以后就没有编译器的事了。
(2)在 /lib/ 下建立软链接
sudo ln -s ~/112/lesson22/stdio/libmystdio.so /lib/libmystdio.so
(3)设置LD_LIBRARY_PATH环境变量
LD_LIBRARY_PATH是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH环境变量中,告诉系统程序依赖的动态库所在的路径:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:动态库的绝对路径
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/lighthouse/112/lesson22/other/stdc/lib
注意:要用 :
隔开,否则会覆盖原来的环境变量。但是这个方法是临时的,因为这个环境变量是内存级别的环境变量,机器会在下次登录时清理。
(4)使用ldconfig指令
🎈 /etc/ld.so.conf.d/ 目录下的文件用来指定动态库搜索路径。这些文件被包含在/etc/ld.so.conf. 文件中,ldconfig 命令会在默认搜寻目录 (/lib 和 /usr/lib)以及动态库配置文件/etc/ld.so.conf.内所列的目录下,搜索可共享的动态链接库,并创建出动态装入程序(ld.so)所需的连接和缓存文件
这些.conf 文件中存储的都是各种文件的路径,只要将我们写的第三方库的路径保存在.conf 文件中,程序运行时在就会通过它链接到它依赖的动态库
(1)将路径存放在.conf 文件中
echo /home/xy/Linux/libtest/libtest/mylib/lib > libtest.conf
(2)将.conf 文件拷贝到/etc/ld.so.conf.d/ 下
sudo cp libmystdio.conf /etc/ld.so.conf.d/
ldd一下:
系统还是没有找到a.out依赖的动态库,原因是此时的系统的数据库还未更新,使用命令ldconfig更新配置文件:
sudo ldconfig
🎈 如果同时存在动态库和静态库文件,gcc会优先链接动态库。如果你想强制gcc链接静态库,可以直接指定静态库的全称,或者使用 -static 选项。你也可以使用 -Bdynamic 和-Bstatic 选项在命令行中切换首选项。
🎈 动态库在程序运行时才被加载,它和可执行文件是分开的,只是可执行文件在运行的某个时期调用了它。动态库可以实现进程之间资源共享,有一份就行
💦可执行程序先加载,代码区的代码运行到动态库的方法时,可以先跳转到共享区,而共享区是保存了页表和动态库的映射关系的,所以使用完动态库的方法以后,再返回到代码区继续执行即可。由此可见,静态库是在可执行程序自己的进程地址空间中跳转的。
👁 动态库把对一些库函数的链接载入推迟到程序运行的时期,可以实现进程之间的资源共享,将一些程序升级变得简单。
但这不意味着静态库是一无是处的
🔥 当你更希望简单易用,尽量免除后顾之忧,那就首选静态(隐式)连接。静态库在链接时就和可执行文件在一块了,因此,对于同样的程序,静态链接的要比动态链接加载更快。所以选择静态库还是动态库是空间和时间的考量。但是通常来说,牺牲这点性能来换取程序在空间上的节省和部署的灵活性时值得的。
【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果我的这篇博客可以给你提供有益的参考和启示,可以三连支持一下 !!