在之前的Linux系统编程中,学习了文件的打开;关闭;读写;进程;线程等概念....
本节补充“Linux库概念 & 相关编程”,这是一个面试的重点!
分文件编程
在之前的学习中,面对较大的项目比如 STM32的小车 或 香橙派实现的智能垃圾桶 ,都使用了分文件编程的思路。
其 实现的核心思想就是:将功能性函数的实现单独写在其他的地方,在main函数中调用那些封装好的功能性函数。
这样做的好处是:
- 分模块的编程思想:在实际工作中面对大型项目,可以让A完成串口开发;B完成网络开发,最后只需要他们提供h文件中的函数接口就可以在主函数中直接调用了,测试时发现哪部分有问题可以直接找负责的人,方便调试
- 代码可移植性更强:因为分文件编程了,串口,网络,语音可能都被封装好了,那么后续如果其他项目需要这些功能就可以直接调用封装好的接口了,最多只需要微调
- main函数更加精简:由于把功能性函数的实现步骤都封装到其他文件了,main函数就可以专注于项目的整体调用逻辑,使得整个main看起来更加清晰,逻辑通畅
具体步骤
- 将功能性函数名和具体实现步骤写在一个.c文件中
- 创建一个同名的.h文件,包含所有可能会被调用的函数原型,去除函数体
- 在main中包含刚刚创建的.h文件
- 在main中调用被封装好的函数接口
- 使用gcc 编译所有相关的.c文件(封装函数的.c文件;main函数所在的.c文件)
- h文件的大概格式:
int add(int x, int y); int min(int x, int y); float div(int x, int y);
- main函数调用h文件的格式:
#include <stdio.h> #include "XXX.h"
Q:为什么同样是调用头文件,有时候使用' <> ',而有时候使用' "" '呢?
A:使用' <> '时,gcc在编译时会去“/usr/include/”或“/usr/local/include/”下找这个头文件;而使用' "" '时,gcc则会优先从代码运行的当前路径去找这个头文件!如果找不到,才会再去“/usr/include/”或“/usr/local/include/”下找这个头文件。
Linux的库
分文件编程的好处已经在刚刚说到,但是实际工作中会出现这种情况:程序员允许别人可以调用他封装好的功能性函数,但是他不希望别人可以看到他具体实现的函数体。在这种情况下,就要引入Linux的库的概念了!
库(程序函数库)是一种可执行的二进制形式、就是将源代码转化为二进制格式,相当于进行了加密,别人可以使用库,但是看不到库中的内容。
库是别人写好的现有的,可以复用的代码,现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。
程序函数库可分为3种类型:静态函数库(static libraries)、共享函数库(shared libraries)、动态加载函数库(dynamically loaded libraries):
- 静态函数库:在程序执行前就加入到目标程序中的库, 文件后缀是.a
- 共享函数库:在程序执行时动态(临时)由目标程序去调用,共享函数库=动态函数库=共享对象库(Linux), 文件后缀是.so
- 动态加载函数库:本质上和共享函数库是一个东西,“动态加载数据库”是windows中的叫法,文件后缀是.dll
因此,对于Linux系统来说可以简单的将库分为 动态库 和 静态库
静态库和动态库的比较
静态数据库(libXXX.a)
优点
- 运行快
- 发布程序无需提供静态库,因为已经在app中,移植方便
缺点
- 程序大
- 更新部署发布麻烦
动态数据库(libXXX.so)
优点
- 程序小
- 升级简单
- 不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享(动态)库的实例
缺点
运行相对慢
需要提供依赖的动态库
静态库的制作(不太常用了)
制作步骤
- 使用以下指令将.c文件生成.o文件
gcc a.c b.c -c
- 使用以下指令将.o文件打包成.a库文件
ar rcs 静态库的名字 原材料
例:ar rcs libXXX.a a.o b.o
这两步完成后,就生成了.a库文件,此时实现功能函数的.c文件和.o文件对于程序运行就不必要了,使得main函数可以调用这个库的条件就是有.h和.a文件,此时代码执行者可以调用库但却无法得知库中函数具体的实现步骤了。
库的使用
gcc XXXXX.c -L 库文件所在目录 -lXXXX -o XXX
//-L:将-L之后跟着的目录作为第一个寻找库文件的目录,寻找的顺序是:-L之后跟着的目录 -->/lib-->/usr/lib-->/usr/local/lib
//-l(小写L):指定库的名字(去掉lib和.a)
//-o:指定生成的最终应用程序的名字
小插曲:gcc编译时“-I(大写i)” 和“-L"的区别:
- -I(大写i):将-I之后跟着的目录作为第一个寻找头文件的目录,寻找的顺序是:-I之后跟着的目录-->/usr/include-->/usr/local/include
- -L:将-L之后跟着的目录作为第一个寻找库文件的目录,寻找的顺序是:-L之后跟着的目录 -->/lib-->/usr/lib-->/usr/local/lib
- 头文件和库文件的关系:库文件可以包含头文件,头文件不可以包含库文件,头文件可视,库文件不可视
由于之前提到过,静态库的优点之一是“发布程序无需提供静态库” ,所以编译完成后,就可以直接运行程序了,不需要任何后缀!
动态库的制作(更常用)
制作步骤
- 使用以下指令生成动态库:
gcc -shared -fpic xxx.c -o libxxx.so
//-shared用来生成动态库
//-fpic选项作用于编译阶段,在生成目标文件时就得使用该选项,以生成位置无关的代码
库的使用
编译的语句其实和静态库相同:
gcc XXXXX.c -L 库文件所在目录 -lXXXX -o XXX
//-L:将-L之后跟着的目录作为第一个寻找库文件的目录,寻找的顺序是:-L之后跟着的目录 -->/lib-->/usr/lib-->/usr/local/lib
//-l(小写L):指定库的名字(去掉lib和.a)
//-o:指定生成的最终应用程序的名字
注意!虽然编译的语句相同,但是回顾动态库的缺点“需要提供依赖的动态库” ,所以编译完成后不能像使用静态库那样直接运行,这是因为动态库是程序运行中临时调用的,解决办法是将动态库拷贝到/usr/lib/下:
sudo cp libXXXX.so /usr/lib/
然后,再直接运行程序就可以了!
将动态库复制到/usr/lib/或/lib/下是因为程序执行时动态库的默认搜索路径就是/lib和/usr/lib;那么如果可以指定动态库的搜索路径,就可以不需要将库复制了,这就是另一种方法:使用环境变量LD_LIBRARY_PATH指定动态库搜索路径
export LD_LIBRARY_PATH="动态库所在的绝对路径"
通过添加这个环境变量,也可以成功运行程序了,但是这样做有一个问题:这个环境变量是临时的,也就是说只有在当前窗口生效,如果此时通过SSH再连接一个窗口,又会找不到动态库了,解决办法是:写一个脚本start.sh:
export LD_LIBRARY_PATH="动态库所在的绝对路径"./可执行文件
然后给脚本一个可执行的权限:
chmod +x start.sh
其实这个脚本的作用就是在每次执行程序前设置一个临时的环境变量。
动态库的实操演示
首先在树莓派家目录下创建一个mjm_code文件夹,学习用的代码全放在这里面:
然后分别创建一个“test_main.c”和一个“test_func.c”来模拟main函数所在的C文件和封装功能函数的C文件:
test_main.c:
#include <stdio.h>
#include "test_func.h"int main()
{int a;int b;int ret;float ret1;printf("请输入第一个数\n");scanf("%d",&a);printf("请输入第二个数\n");scanf("%d",&b);printf("开始计算\n");ret = add(a,b);printf("相加为%d\n",ret);ret = min(a,b);printf("相减为%d\n",ret);ret = mul(a,b);printf("相乘为%d\n",ret);ret1 = div(a,b);printf("相除为%f\n",ret1);return 0;
}
test_func.c:
int add(int a,int b)
{return a+b;
}
int min(int a,int b)
{return a-b;
}
int mul(int a,int b)
{return a*b;
}
float div(int a,int b)
{return (float)a/b;
}
test_func.h:
int add(int a,int b);
int min(int a,int b);
int mul(int a,int b);
float div(int a,int b);
然后尝试编译运行:
没有报错,程序正常运行,到此为止就是一个分文件编程的典型例子。
接下来,尝试将test_func.c做成一个动态库:(库名我这里叫“scalc”)
gcc -shared -fpic test_func.c -o libscalc.so
然后进行编译:(我生成了一个叫“calc”的可执行程序)
gcc test_main.c -L . -lscalc -o calc
//-L后的“.”代表当前路径
然后将生成的.so文件复制到usr/lib/下,并删除当前路径下的.so文件:
1. sudo cp libscalc.so /usr/lib/
2. rm libscalc.so //可以不删,删了是为了证明复制到/usr/lib下之后当前路径的.so就没啥用了
最后尝试运行:
成功运行!此时程序的执行只依赖.h文件和.so文件了,执行者可以调用接口但并不能查看test_func.c中功能函数的具体实现,因为这个.c文件已经被制作成.so的库了。