树莓派学习笔记(二):Linux库

Linux库概念引入

这一篇开始,进入树莓派开发阶段,即编程部分。之前Linux系统编程部分学习了文件编程、进程与进程通信、线程、网络编程等内容,但主要以单文件编程为主。在Linux中,库是一个比较重要的概念,常在面试中考察到,因此在开始之前,先来引入一下库的基本概念与使用

分文件编程实例

在之前阶段的上官一号单片机项目:智能小车开发案例中,我们将主函数放在main.c中,负责整体应用的初始化、维护和信号处理,而外围的超声波测距、wifi建立tcp server、舵机等模块分别放到单独的C文件中,这些文件只有具体的封装好的函数实现,没有main函数。

这就是分文件编程的基本思想。功能性函数分文件实现,符合模块化编程思想,利于责任划分、方便调试,且主程序简洁。

下面就是一个简单的四则运算C程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<stdio.h>

float sum1(float num1,float num2){
return (num1+num2);
}
float sub1(float num1,float num2){
return (num1-num2);
}
float mul1(float num1,float num2){
return (num1*num2);
}
float div1(float num1,float num2){
return (num2==0?0:num1/num2);
}
int main(){
float num1,num2;
printf("请输入数1:");
scanf("%f",&num1);
printf("\n请输入数2:");
scanf("%f",&num2);
printf("%f+%f=%f\n",num1,num2,sum1(num1,num2));
printf("%f-%f=%f\n",num1,num2,sub1(num1,num2));
printf("%f*%f=%f\n",num1,num2,mul1(num1,num2));
printf("%f/%f=%f\n",num1,num2,div1(num1,num2));
}

我们按照功能与逻辑分离的原则将其拆分为三个文件:

控制程序主体运行逻辑的main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
#include"calculator.h"

int main(){
float num1,num2;
printf("请输入数1:");
scanf("%f",&num1);
printf("\n请输入数2:");
scanf("%f",&num2);
printf("%f+%f=%f\n",num1,num2,sum1(num1,num2));
printf("%f-%f=%f\n",num1,num2,sub1(num1,num2));
printf("%f*%f=%f\n",num1,num2,mul1(num1,num2));
printf("%f/%f=%f\n",num1,num2,div1(num1,num2));
}

实现具体功能的calculator.c

1
2
3
4
5
6
7
8
9
10
11
12
float sum1(float num1,float num2){
return (num1+num2);
}
float sub1(float num1,float num2){
return (num1-num2);
}
float mul1(float num1,float num2){
return (num1*num2);
}
float div1(float num1,float num2){
return (num2==0?0:num1/num2);
}

以及用于函数声明的calculator.h

1
2
3
4
float sum1(float num1,float num2);
float sub1(float num1,float num2);
float mul1(float num1,float num2);
float div1(float num1,float num2);

三个文件编写好后使用命令gcc calculator.h main.c 可编译成功。

动态库与静态库

前面一节了解了分文件编程的基本概念,但这种方式仅仅是将功能与逻辑分开,不同功能的代码写在不同的文件中,并没有实现“封装”。对于功能完善、经过测试可以使用的代码,一般而言我们既希望别人能够直接使用,但又不想使用者对其进行修改(即不开放源代码,比较鸡贼),以保护文件的完整性。这种情况下,就引入了“库”的概念。

一个“程序函数库”简单的说就是一个文件包含了一些编译好的代码和数据,这些编译好的代码和数据可以在事后供其他的程序使用。程序函数库可以使整个程序更加模块化,更容易重新编译,而且更方便升级。(参考自Linux共享库、静态库、动态库详解

程序函数库又可分为3种类型:

  • 静态库(static libraries),在程序执行前,编译阶段就加入到目标程序中去了 ;
  • 共享库(shared libraries)与动态(加载)库(dynamically loaded libraries)是同一个概念,都是在执行过程中由目标程序调用,动态加载库。只是在Linux上叫共享对象库,文件名后缀.so;在Windows上叫动态加载函数库,文件名后缀dll。

相应地,它们具有的特点是:引用静态库的加载速度快,移植性强(发布时无需另外再提供静态库)但目标程序文件较大,更新、部署麻烦;动态库加载速度较慢,但目标程序文件比较轻量,更新升级的操作也比较简单,系统加载一次后多个程序之间可以共享。

静态库的制作与使用

这里依然以上面简单的四则运算为例学习静态库的基本用法。

静态库的制作,第一步是将所有的功能实现的源代码(.c/.cpp)转化为对象文件(.o),这一步一般一个源文件对应一个对象文件,这里只有一个功能模块就只写了一个文件:

1
2
gcc calculator.c -c
# -c选项代表编译、组装,但不链接

第二步,将所有的对象文件打包,合成一个静态程序函数库:

1
2
3
ar rcs libcalcf.a calculator.o
# ar命令的作用是创建/修改/提取压缩文件,命令的格式是:
# ar rcs 静态库名 原材料对象文件

注意静态库名最好以lib开头,既便于标识也易于使用

**静态库的使用**,这里还是以gcc编译C文件产生程序为例:

1
2
gcc main.c calculator.h -lcalcf -o calculator
#-l选项代表编译的时候链接至xx库,库名掐头(libxx)去尾(后缀)

不过,此时由于gcc默认从/usr/include/usr/lib/usr/local/lib等目录查找标准库,而我们的静态库此时并未放进去,因此会报错找不到。只需要在上面的命令加一选项-L./,告诉gcc优先查找当前目录即可。

如果后面不想麻烦指定位置,可以在静态库制作的时候添加选项-I /usr/include,指定生成的静态库存放目录

最后的效果:

动态库的制作与使用

第一步依然是编译,不过此时需要加一个参数fpc,生成专用于动态库的对象文件,加了该选项后库与具体位置无关:

1
gcc calculator.c -c -fpic

第二步,使用gcc将对象文件打包为.so文件,shared选项用于胜场动态链接:

1
gcc -shared calculator.o -o libcalcf.so

(**编译与打包可以合为一个步骤**)

使用共享库编译目标程序:与静态库一致,gcc main.c calculator.h -lcalcf -L./ -o calculator2

但在执行生成的目标程序时,由于使用的是动态库,所以没有编译到目标程序中,因此会自动到/usr/lib中寻找动态库,找不到就会报错。

此时的解决方法还是两种:

  • 第一种将动态库拷贝一份到/usr/lib中;

  • 第二种是使用一个环境变量LD_LIBRARY_PATH,指定到生成的动态库所在目录,如下图中就该是在/etc/profile中写入:export LD_LIBRARY_PATH ="/home/pi/libtest",然后source /etc/profile。这种方式和第一种其实原理是一样的

  • 第三种,在目标程序的编译阶段使用-WI, -rpath选项:

    1
    gcc main.c calculator.h -lcalcf -WI,-rpath=./ -L./ -o calculator2

补充,关于共享库命名规则(参考自Linux共享库、静态库、动态库详解):


树莓派学习笔记(二):Linux库
https://dockingyuan.top/2023/01/03/raspberry/2-Linux库引入/
作者
Yuan Yuan
发布于
2023年1月3日
许可协议