六、C语言宏定义与预处理、函数和函数库

6.1、编译工具链

源码.c->(预处理)->预处理过的.i文件->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序。

预处理用预处理器,编译用编译器,汇编用汇编器,链接用链接器,这几个工具再加上其他一些额外的会用到的可用工具,合起来叫编译工具链。gcc就是一个编译工具链。

<1>预处理的意义:
(1)编译器本身的主要目的是编译源代码,将C的源代码转化成.S的汇编代码。编译器聚焦核心功能后,就剥离出了一些非核心的功能到预处理器去了。

(1)预处理器帮编译器做一些编译前的杂事。如:

(1)#include(#include<>和#include""的区别)
(2)注释
(3)#if #elif #endif #ifdef
(4)宏定义

备注:
gcc中各选项的使用方法:
-o生成可执行文件名
-c只编译不链接
-E只预处理不编译
-I(是大i,不是L)编译时从某个路径下寻找头文件

(1)gcc编译时可以给一些参数来做一些设置,譬如gcc xx.c -o xx可以指定可执行程序的名称;譬如gcc xx.c -c -o xx.o可以指定只编译不连接,也可以生成.o的目标文件。

(2)gcc -E xx.c -o xx.i可以实现只预处理不编译。一般情况下没必要只预处理不编译,但有时候这种技巧可以用来帮助我们研究预处理过程,帮助debug程序。

(3)链接器:链接的时候是把目标文件(二进制)通过有序的排列组合起来,如star.smain.cled.c这三个源文件,分别被编译成三个目标文件,每个目标文件有很多函数集合。链接的时候会根据运行思路把这些杂乱的函数给排列组合起来,不是把目标文件简单的排列组合。

(4)当生成可执行程序之后,这个可执行程序里面有很多符号信息,有个符号表,里面的符号与一个地址相对应,如函数名max对应一个地址,虽然这个程序有符号信息,但是为什么还是可以执行呢?因为如windows的exe程序,有专门的一套程序来执行这个.exe文件,就好比压缩文件,就有一套“好压”的软件,然后去压缩(执行).rar.zip的文件,而这套程序就把这些符号信息给过滤掉,然后得到纯净的二进制代码,最后把他们加载到内存中去。

(5)debug版本就是有符号信息,而Release版本就是纯净版本的。可用strip工具:strip是把可执行程序中的符号信息给拿掉,以节省空间。(Debug版本和Release版本)objcopy:由可执行程序生成可烧录的镜像bin文件。

6.2、预处理:

<1>头文件有” “是本目录去找,找不到就去库头文件找,和<>只到库头文件去找,库头文件可以自己制作,用-I(是大i,不是L)参数去寻找路径。
头文件在预处理时,会把文件的内容原封不动的赋值到c文件里面。

<2>注释:在预处理时,把注释全部拿掉。注意:#define zf1再判断#undef zf2时,也是通过的。其意思是有没有定义过zf.

<3>条件编译:当作一个配置开关#define NUM表示定义了NUM,则执行下一条语句,且NUM用空格替代,而且预处理会删掉条件编译,留下正确的执行语句。

<4>宏定义:#define cdw1在预处理阶段,会替代那些宏,可以多重替代宏;也可以表示多个语句,如

#define cdw printf("cdw\n");
printf("zf\n");
cdw;

这条语句会直接展开
还有带参宏,

#define max(a,b) ((a)+(b))

注意的是带参宏一定要()不然有时候会引起错误,每一个”形参“都应该要();

#define year(365*24*60*60*60*60)

安理说是可以的,但是year是int型的已经超过了范围,所以要把它搞成无符号长整形。

#define year(365*24*60*60*60*60ul)

这样才是正确的
宏定义的变量是不占内存空间的,直接替换减少开销,但是变量替换是不进行类型检查;
函数的变量要占用空间、要压栈等操作,就需要很大的开销,但是调用函数时,编译器会检查函数变量的类型是否相同。
内联函数集合普通函数、宏定义的两个优势,它直接就地展开,直接替换,减少开销,同时编译器也会检查变量的类型。但是函数体积要小,不然效率反而很低,至于原因暂时不详。

6.3、内联函数:对函数就地展开,像宏定义一样,这样减少开销,同时也检查变量的类型。但是必须函数的内部体积小才用这种方式,以达到更好的效率。体积大的函数就作为普通函数。内联函数通过在函数定义前加inline关键字实现。

6.4、条件编译的应用:
做一个调试开关。

#define DEBUG 是定义DEBUG宏
#undef DEBUG  是注销DEBUG宏


#ifdef DEBUG
#define debug(x) printf(x)
#else
#define debug(x)
#endif

6.5、函数:

(1)、整个程序分成多个源文件,一个文件分成多个函数,一个函数分成多个语句,这就是整个程序的组织形式。这样组织的好处在于:分化问题、便于编写程序、便于分工。

(2)、函数的出现是人(程序员和架构师)的需要,而不是机器(编译器、CPU)的需要。

(3)、函数的目的就是实现模块化编程。说白了就是为了提供程序的可移植性。

<1>函数书写的一般原则:

第一:遵循一定格式。函数的返回类型、函数名、参数列表等。

第二:一个函数只做一件事:函数不能太长也不宜太短(一个屏幕的大小),原则是一个函数只做一件事情。

第三:传参不宜过多:在ARM体系下,传参不宜超过4个。如果确实需要多个参数,则应考虑采用结构体。

第四:尽量少碰全局变量:函数最好用传参返回值来和外部交换数据,不要用全局变量。

<2>之所以函数能被调用,根本实质是在编译时,检查到了该函数的声明,不是因为函数定义了(当然也要定义才行,只是不是本质)。

6.6、递归函数:自己调用自己的函数,常用例如:阶乘或斐波那契数例:

栈溢出:递归函数会不停的耗费栈空间,所以要注意递归不要太多。
收敛性:必须要有一个终止递归的条件。

6.7、函数库
<1>静态链接库:其实就是商业公司将自己的函数库源代码经过只编译不连接形成.o的目标文件,然后用ar工具将.o文件归档成.a的归档文件(.a的归档文件又叫静态链接库文件)。

商业公司通过发布.a库文件和.h头文件来提供静态库给客户使用;客户拿到.a和.h文件后,通过.h头文件得知库中的库函数的原型,然后在自己的.c文件中直接调用这些库文件,在连接的时候链接器会去.a文件中拿出被调用的那个函数的编译后的.o二进制代码段链接进去形成最终的可执行程序。

<2>动态链接库:本身不将库函数的代码段链接入可执行程序,只是做个标记。然后当应用程序在内存中执行时,运行时环境发现它调用了一个动态库中的库函数时,会去加载这个动态库到内存中,然后以后不管有多少个应用程序去调用这个库中的函数都会跳转到第一次加载的地方去执行(不会重复加载)。也就是在运行时,会把库函数代码放入内存中,然后多个程序要用到库函数时,就从这段内存去找,而静态链接对于多程序就是重复使用库函数,比较占内存。

(1)gcc中编译链接程序默认是使用动态库的,要想静态链接需要显式用-static来强制静态链接。

(2)库函数的使用需要注意4点:第一,包含相应的头文件;第二,调用库函数时注意函数原型;第三,有些库函数链接时需要额外用-lxxx来指定链接;第四,如果是动态库,要注意-L指定动态库的地址。

6.8、常见的两个库函数
<1>字符串处理函数:包含在string.h中,这个文件在ubuntu系统中在/usr/include中字符串函数如:memcpy(内存字符串复制,直接复制到目标空间)确定src和dst不会overlap重复,则使用memcpy效率高memmove(内存字符串复制,先复制到一个内存空间,然后再复制到目标空间)确定会overlap或者不确定但是有可能overlap,则使用memove比较保险。

memset strncmp
memcmp strdup
memchr strndup
strcpy strchr
strncpy strstr
strcat strtok
strncat strcmp

<2>数学函数:math.h需要加-lm就是告诉链接器到libm中去查找用到的函数。

C链接器的工作特点:因为库函数有很多,链接器去库函数目录搜索的时间比较久。为了提升速度想了一个折中的方案:链接器只是默认的寻找几个最常用的库,如果是一些不常用的库中的函数被调用,需要程序员在链接时明确给出要扩展查找的库的名字。

链接时可以用-lxxx来指示链接器去到libxxx.so中去查找这个函数。

6.9、自制静态链接库:

(1)第一步,自己制作静态链接库
首先,使用gcc -c只编译不连接,生成.o文件;
然后,使用ar工具进行打包成.a归档文件。库名不能随便乱起,一般是lib+库名称,后缀名是.a表示是一个归档文件
注意:制作出来了静态库之后,发布时需要发布.a文件和.h文件。

(2)第二步:使用静态链接库
首先,把.a和.h都放在自己引用的文件夹下;
其次,在.c文件中包含库的.h;
最后,使用库函数。

备注:

<1>命名.a文件时,前缀一定要加lib,如libzf.a;
链接属性-l(小L),表示库名;
属性-L表示库的路径。
所以:gcc cdw.c -o cdw -lzf -L./include -I(大i)./include

<2>头文件用“ ”表示外部自定义,如果没加路径属性,默认当前路径找,如果在其他文件夹下,必须用-I(大i)路径。
用<>表示的头文件一种是在编译器下的库文件找,第二种是自己定义的库文件找,但是要定义其路径。

<3>在makefile文件中用到gcc/arm-gcc那么在shell中就用相应的编译器gcc/arm-gcc.

<4>nm ./include/libmax.a查看max库的信息,有哪些.o文件,各自含有哪些函数。

举例:

makefile:arm-gcc aston.c -o aston.o -c
arm-ar -rc libaston.a aston.o

6.9.1、自制动态链接库:

<1>动态链接库的后缀名是.so(对应windows系统中的dll),静态库的扩展名是.a.

<2>
第一步:创建一个动态链接库。

gcc aston.c -o aston.o -c -fPIC(-fPIC表示设置位置无关码)

gcc -o libaston.so aston.o -shared(-shared表示使用共享库)

注意:做库的人给用库的人发布库时,发布libxxx.so和xxx.h即可。

第二步:使用自己创建的共享库。

gcc cdw.c -o cdw -lmax.so -L./动态链接库 -I./动态链接库

第三步:上述编译成功了,但是在./cdw执行时会报错,原因是采用动态链接,在可执行文件只是做了一个标记,标记是使用了哪个函数库的哪个函数。但并没有将库函数加载到源文件中,所以可执行文件很小,在执行时,需要立即从系统里面找到使用到的函数库,然后加载到内存中,在linux系统中默认是从/usr/bin中寻找,(不确定:如果使用shell中运行)会先执行环境变量的路径然后再查找/usr/bin;所以我们可以用两种办法解决运行的问题

第四步:将动态库libmax.so复制到/usr/lib下面,但是如果以后所有的库都这样放的话,会越来越臃肿,导致运行速度变慢(一个一个查找);或者是新添加一个环境变量

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/mnt/hgfs/share/include

将库libmax.so复制到这个路径下面,这样就可以运行了。

<3>使用ldd命令判断一个可执行文件是否能运行成功;

ldd cdw
linux-gate.so.1=>(0xb77a8000)
libmax.so=>notfound//发现notfound意思就是没有找到对应的函数库
libc.so.6=>/lib/i386-linux-gnu/libc.so.6(0xb75e2000)
/lib/ld-linux.so.2(0xb77a9000)

往期文章列表:****往期热文:
基础C语言知识串串香(1)

基础C语言知识串串香(2)

基础C语言知识串串香(3)

基础C语言知识串串香(4)

基础C语言知识串串香(5)

基础C语言知识串串香(6)

基础C语言知识串串香(7)

基础C语言知识串串香(8)

基础C语言知识串串香(9)

基础C语言知识串串香(10)


===========我是华丽的分割线===========


更多知识:
点击关注专题:嵌入式Linux&ARM

或浏览器打开:https://www.jianshu.com/c/42d33cadb1c1

或扫描二维码: