超级版主
- 积分
- 4671
- 金钱
- 4671
- 注册时间
- 2019-5-8
- 在线时间
- 1224 小时
|
本帖最后由 正点原子运营 于 2021-6-8 18:20 编辑
1)实验平台:正点原子STM32MP157开发板
2) 章节摘自【正点原子】《STM32MP157嵌入式Linux驱动开发指南》
3)购买链接:https://item.taobao.com/item.htm?&id=629270721801
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/arm-linux/zdyzmp157.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子STM32MP157技术交流群:691905614
第三章 Linux C编程入门
在Windows下我们可是使用各种各样的IDE进行编程,比如强大的Visual Studio。但是在Ubuntu下如何进行编程呢?Ubuntu下也有一些可以进行编程的工具,但是大多都只是编辑器,也就是只能进行代码编辑,如果要编译的话就需要用到GCC编译器,使用GCC编译器肯定就要接触到Makefile。本章就讲解如何在Ubuntu下进行C语言的编辑和编译、GCC和Makefile的使用和编写。通过本章的学习可以掌握Linux进行C编程的基本方法,为以后的Linux驱动学习做准备。
3.1 Hello World!
我们所说的编写代码包括两部分:代码编写和编译,在Windows下可以使用Visual Studio来完成这两部分,可以在Visual Studio下编写代码然后直接点击编译就可以了。但是在Linux下这两部分是分开的,比如我们用VIM进行代码编写,编写完成以后在使用GCC编译器进行编译,其中代码编写工具很多,比如VIM编辑器、Emacs编辑器、VScode编辑器等等,本章使用Ubuntu自带的VIM编辑器。先来编写一个最简单的“Hello World”程序,把Linux下的C编程完整的走一遍。
3.1.1 编写代码
先在用户根目录下创建一个工作文件夹:C_Program,所有的C语言练习都保存到这个工作文件夹下,创建过程如图3.1.1.1所示:
图3.1.1.1 创建工作目录
进入图3.1.1.1创建的C_Program工作文件夹,为了方便管理,我们后面每个例程都创建一个文件夹来保存所有与本例程有关的文件,创建一个名为“3.1”的文件夹来保存我们的“Hello World”程序相关的文件,操作如图3.1.1.2所示:
图3.1.1.2 创建工程文件夹
前面说了我们使用VI编辑器,在使用VI编辑器之前我们先做如下设置:
1、设置TAB键为4字节
VI编辑器默认TAB键为8空格,我们改成4空格,用vi打开文件/etc/vim/vimrc,在此文件最后面输入如下代码:
set ts=4
添加完成如图3.1.1.3所示:
图3.1.1.3 设置TAB为四个空格
修改完成以后保存并关闭文件。
2、VIM编辑器显示行号
VIM编辑器默认是不显示行号的,不显示行号不利于代码查看,我们设置VIM编辑器显示行号,同样是通过在文件/etc/vim/vimrc中添加代码来实现,在文件最后面加入下面一行代码即可:
set nu
添加完成以后的/etc/vim/vimrc文件如图3.1.1.4所示:
图3.1.1.4 设置VIM编辑器显示行号
VIM编辑器可以自行定制,网上有很多的博客讲解如何设置VIM,感兴趣的可以上网看一下。设置好VIM编辑器以后就可以正式开始编写代码了,进入前面创建的“3.1”这个工程文件夹里面,使用vi指令创建一个名为“main.c”的文件,然后在里面输入如下代码:
示例代码 3.1.1.1 main.c文件代码
- 1 #include <stdio.h>
- 2
- 3 int main(int argc, char *argv[])
- 4 {
- 5 printf("Hello World!\n");
- 6 }
复制代码
编写完成以后保存退出vi编辑器,可以使用“cat”命令查看代码是否编写成功,如图3.1.1.5所示:
图3.1.1.5 查阅程序源码
从图3.1.1.5可以看出main.c文件是编辑成功的,代码编辑成功以后我们需要对其进行编译。
3.1.2 编译代码
Ubuntu下的C语言编译器是GCC,Ubuntu18默认没有安装GCC工具,因此需要我们手动安装gcc、g++、make等工具,这里我们直接安装build-essential软件包即可,build-essential提供了编译程序所需的所有软件包,输入如下命令:
- sudo apt-get install build-essential
复制代码
等待安装完成,然后输入如下命令查看GCC编译器的版本号:
gcc -v
在终端中输入上述命令以后终端输出如图3.1.2.1所示:
图3.1.2.1 gcc版本查询
如果输入命令“gcc -v”命令以后,你的终端输出类似图3.1.2.1中的信息,那么说明你的电脑已经有GCC编译器了。最后下面的“gcc version 7.5.0”说明本机的GCC编译器版本为7.5.0的。注意观察在图3.1.2.1中有“Target: x86_64-linux-gnu”一行,这说明这个GCC编译器是针对X86架构的,因此只能编译在X86架构CPU上运行的程序。如果想要编译在ARM上运行的程序就需要针对ARM的GCC编译器,也就是交叉编译器!我们是ARM开发,因此肯定要安装针对ARM架构的GCC交叉编译器,当然了,这是后面的事,现在我们不用管这些,只要知道不同的目标架构,其GCC编译器是不同的。
如何使用GCC编译器来编译main.c文件呢?GCC编译器是命令模式的,因此需要输入命令来使用gcc编译器来编译文件,输入如下命令:
gcc main.c
上述命令的功能就是使用gcc编译器来编译main.c这个c文件,过程如图3.1.2.2所示:
图3.1.2.2 编译main.c文件
在图3.1.2.2中可以看到,当编译完成以后会生成一个a.out文件,这个a.out就是编译生成发的可执行文件,执行此文件看看是否和我们代码的功能一样,执行的方法很简单使用命令:“./+可执行文件”,比如本例程就是命令:./a.out,操作如图3.1.2.3所示:
图3.1.2.3 执行编译得到的文件
在图3.1.2.3中执行a.out文件以后终端输出了“Hello World!”,这正是main.c要实现的功能,说明我们的程序没有错误。a.out这个文件的命名是GCC编译器自动命名的,那我们能不能决定编译完生成的可执行文件名字呢?肯定可以的,在使用gcc命令的时候加上-o来指定生成的可执行文件名字,比如编译main.c以后生成名为“main”的可执行文件,操作如图3.1.2.4所示:
图3.1.2.4 指定可执行文件名字
在图3.1.2.4中,我们使用“gcc main.c –o main”来编译main.c文件,使用参数“-o”来指定编译生成的可执行文件名字,至此我们就完成Linux下C编程和编译的一整套过程。
3.2 GCC编译器
3.2.1 gcc命令
在上一小节我们已经使用过GCC编译器来编译C文件了,我们使用到是gcc命令,gcc命令格式如下:
gcc [选项] [文件名字]
主要选项如下:
-c:只编译不链接为可执行文件,编译器将输入的.c文件编译为.o的目标文件。
-o:<输出文件名> 用来指定编译结束以后的输出文件名,如果使用这个选项的话GCC默认编译出来的可执行文件名字为a.out。
-g:添加调试信息,如果要使用调试工具(如GDB)的话就必须加入此选项,此选项指示编译的时候生成调试所需的符号信息。
-O:对程序进行优化编译,如果使用此选项的话整个源代码在编译、链接的的时候都会进行优化,这样产生的可执行文件执行效率就高。
-O2:比-O更幅度更大的优化,生成的可执行效率更高,但是整个编译过程会很慢。
3.2.2 编译错误警告
在Windows下不管我们用啥编译器,如果程序有语法错误的话编译的时候都会指示出来,比如开发STM32的时候所使用的MDK和IAR,我们可以根据错误信息方便的修改bug。那GCC编译器有没有错误提示呢?肯定是有的,我们可以测试以下,新名为“3.2”的文件夹,使用vi在文件夹“3.2”中创建一个main.c文件,在文件里面输入如下代码:
示例代码3.2.2.1 main.c文件代码
- 1 #include <stdio.h>
- 2
- 3 int main(int argc, char *argv[])
- 4 {
- 5 int a, b;
- 6
- 7 a = 3;
- 8 b = 4
- 9 printf("a+b=\n", a + b);
- 10 }
复制代码
在上述代码中有两处错误:
第8行、第一处是“b=4”少写了个一个“;”号。
第9行、第二处应该是printf(“a+b=%d\n”, a + b);
我们编译以下上述代码,看看GCC编译器是否能够检查出错误,编译结果如图3.2.2.1所示:
图3.2.2.1 错误提示
从图3.2.2.1中可以看出有一个error,提示在main.c文件的第9行有错误,错误类型是在printf之前没有“;”号,这就是第一处错误,我们在“b = 4”后面加上分号,然后接着编译,结果又提示有一个错误,如图3.2.2.2所示:
图3.2.2.2 错误提示
在图3.2.2.2中,提示我们说文件main.c的第9行:printf(“a+b=\n”, a + b)有warning,错误是因为太多参数了,我们将其改为:
- printf(“a+b=%d\n”, a + b);
复制代码
修改完成以后接着重新编译一下,结果如图3.2.2.3所示:
图3.2.2.3 编译成功
在图3.2.2.3中我们编译成功,生成了可执行文件main,执行一下main,看看结果和我们设计的是否一样,如图3.2.2.4所示:
图3.2.2.4 执行结果
可以看出,GCC编译器和其它编译器一样,不仅能够检测出错误类型,而且标记除了错误发生在哪个文件?哪一行?方便我们去修改代码。
3.2.3 编译流程
GCC编译器的编译流程是:预处理、编译、汇编和链接。预处理就是展开所有的头文件、替换程序中的宏、解析条件编译并添加到文件中。编译是将经过预编译处理的代码编译成汇编代码,也就是我们常说的程序编译。汇编就是将汇编语言文件编译成二进制目标文件。链接就是将汇编出来的多个二进制目标文件链接在一起,形成最终的可执行文件,链接的时候还会涉及到静态库和动态库等问题。
上一小节演示的例程都只有一个文件,而且文件非常简单,因此可以直接使用gcc命令生成可执行文件,并没有先将c文件编译成.o文件,然后再链接在一起。
3.3 Makefile基础
3.3.1 何为Makefile
上一小节我们讲了如何使用GCC编译器在Linux进行C语言编译,通过在终端执行gcc命令来完成C文件的编译,如果我们的工程只有一两个C文件还好,需要输入的命令不多,当文件有几十、上百甚至上万个的时候用终端输入GCC命令的方法显然是不现实的。如果我们能够编写一个文件,这个文件描述了编译哪些源码文件、如何编译那就好了,每次需要编译工程的时只需要使用这个文件就行了。这种问题怎么可能难倒聪明的程序员,为此提出了一个解决大工程编译的工具:make,描述哪些文件需要编译、哪些需要重新编译的文件就叫做Makefile,Makefile就跟脚本文件一样,Makefile里面还可以执行系统命令。使用的时候只需要一个make命令即可完成整个工程的自动编译,极大的提高了软件开发的效率。如果大家以前一直使用IDE来编写C语言的话肯定没有听说过Makefile这个东西,其实这些IDE是有的,只不过这些IDE对其进行了封装,提供给大家的是已经经过封装后的图形界面了,我们在IDE中添加要编译的C文件,然后点击按钮就完成了编译。在Linux下用的最多的是GCC编译器,这是个没有UI的编译器,因此Makefile就需要我们自己来编写了。作为一个专业的程序员,是一定要懂得Makefile的,一是因为在Linux下你不得不懂Makefile,再就是通过Makefile你就能了解整个工程的处理过程。
由于Makefile的知识比较多,完全可以单独写本书,因此本章我们只讲解Makefile基础入门,如果想详细的研究Makefile,推荐大家阅读《跟我一起写Makefile》这份文档,文档已经放到了开发板光盘->4、参考资料里面了,本章也有很多地方参考了此文档。
3.3.2 Makefile的引入
我们完成这样一个小工程,通过键盘输入两个整形数字,然后计算他们的和并将结果显示在屏幕上,在这个工程中我们有main.c、input.c和calcu.c这三个C文件和input.h、calcu.h这两个头文件。其中main.c是主体,input.c负责接收从键盘输入的数值,calcu.h进行任意两个数相加,其中main.c文件内容如下:
示例代码3.3.2.1 main.c文件代码
- 1 #include <stdio.h>
- 2 #include "input.h"
- 3 #include "calcu.h"
- 4
- 5 int main(int argc, char *argv[])
- 6 {
- 7 int a, b, num;
- 8
- 9 input_int(&a, &b);
- 10 num = calcu(a, b);
- 11 printf("%d + %d = %d\r\n", a, b, num);
- 12 }
复制代码
input.c文件内容如下:
示例代码3.3.2.2 input.c文件代码
1 #include <stdio.h>
2 #include "input.h"
3
4 void input_int(int *a, int *b)
5 {
6 printf("input two num:");
7 scanf("%d %d", a, b);
8 printf("\r\n");
9 }
calcu.c文件内容如下:
示例代码3.3.2.3 calcu.c文件代码
1 #include "calcu.h"
2
3 int calcu(int a, int b)
4 {
5 return (a + b);
6 }
文件input.h内容如下:
示例代码3.3.2.4 input.h文件代码
1 #ifndef _INPUT_H
2 #define _INPUT_H
3
4 void input_int(int *a, int *b);
5 #endif
文件calcu.h内容如下:
示例代码3.3.2.5 calcu.h文件代码
1 #ifndef _CALCU_H
2 #define _CALCU_H
3
4 int calcu(int a, int b);
5 #endif
以上就是我们这个小工程的所有源文件,我们接下来使用3.1节讲的方法来对其进行编译,在终端输入如下命令:
gcc main.c calcu.c input.c -o main
上面命令的意思就是使用gcc编译器对main.c、calcu.c和input.c这三个文件进行编译,编译生成的可执行文件叫做main。编译完成以后执行main这个程序,测试一下软件是否工作正常,结果如图3.3.2.1所示:
图3.3.2.1 程序测试
可以看出我们的代码按照我们所设想的工作了,使用命令“gcc main.c calcu.c input.c -o main”看起来很简单是吧,只需要一行就可以完成编译,但是我们这个工程只有三个文件啊!如果几千个文件呢?再就是如果有一个文件被修改了以,使用上面的命令编译的时候所有的文件都会重新编译,如果工程有几万个文件(Linux源码就有这么多文件!),想想这几万个文件编译一次所需要的时间就可怕。最好的办法肯定是哪个文件被修改了,只编译这个被修改的文件即可,其它没有修改的文件就不需要再次重新编译了,为此我们改变我们的编译方法,如果第一次编译工程,我们先将工程中的文件都编译一遍,然后后面修改了哪个文件就编译哪个文件,命令如下:
- gcc -c main.c
- gcc -c input.c
- gcc -c calcu.c
- gcc main.o input.o calcu.o -o main
复制代码
上述命令前三行分别是将main.c、input.c和calcu.c编译成对应的.o文件,所以使用了“-c”选项,“-c”选项我们上面说了,是只编译不链接。最后一行命令是将编译出来的所有.o文件链接成可执行文件main。假如我们现在修改了calcu.c这个文件,只需要将caclue.c这一个文件重新编译成.o文件,然后在将所有的.o文件链接成可执行文件即,只需要下面两条命令即可:
- gcc -c calcu.c
- gcc main.o input.o calcu.o -o main
复制代码
但是这样就又有一个问题,如果修改的文件一多,我自己可能都不记得哪个文件修改过了,然后忘记编译,然后……,为此我们需要这样一个工具:
1、如果工程没有编译过,那么工程中的所有.c文件都要被编译并且链接成可执行程序。
2、如果工程中只有个别C文件被修改了,那么只编译这些被修改的C文件即可。
3、如果工程的头文件被修改了,那么我们需要编译所有引用这个头文件的C文件,并且链接成可执行文件。
很明显,能够完成这个功能的就是Makefile了,在工程目录下创建名为“Makefile”的文件,文件名一定要叫做“Makefile”!!!区分大小写的哦!如图3.3.2.2所示:
图3.3.2.2 Makefile文件
在图3.3.2.2中Makefile和C文件是处于同一个目录的,在Makefile文件中输入如下代码:
示例代码3.3.2.6 Makefile文件代码
- 1 main: main.o input.o calcu.o
- 2 gcc -o main main.o input.o calcu.o
- 3 main.o: main.c
- 4 gcc -c main.c
- 5 input.o: input.c
- 6 gcc -c input.c
- 7 calcu.o: calcu.c
- 8 gcc -c calcu.c
- 9
- 10 clean:
- 11 rm *.o
- 12 rm main
复制代码
上述代码中所有行首需要空出来的地方一定要使用“TAB”键!不要使用空格键!这是Makefile的语法要求,编写好得Makefile如图3.3.2.3所示:
图3.3.2.3 Makefile源码
Makefile编写好以后我们就可以使用make命令来编译我们的工程了,直接在命令行中输入“make”即可,make命令会在当前目录下查找是否存在“Makefile”这个文件,如果存在的话就会按照Makefile里面定义的编译方式进行编译,如图3.3.2.4所示:
图3.3.2.4 Make编译工程
在图3.3.2.4中,使用命令“make”编译完成以后就会在当前工程目录下生成各种.o和可执行文件,说明我们编译成功了。使用make命令编译工程的时候可能会提示如图3.3.2.5所示错误:
图3.3.2.5 Make失败
图3.3.2.5中的错误来源一般有两点:
1、Makefile中命令缩进没有使用TAB键!
2、VI/VIM编辑器使用空格代替了TAB键,修改文件/etc/vim/vimrc,在文件最后面加上如下所示代码:
set noexpandtab
我们修改一下input.c文件源码,随便加几行空行就行了,保证input.c被修改过即可,修改完成以后再执行一下“make”命令重新编译一下工程,结果如图3.3.2.6所示:
图3.3.2.6 重新编译工程
从图3.3.2.6中可以看出因为我们修改了input.c这个文件,所以input.c和最后的可执行文件main重新编译了,其它没有修改过的文件就没有编译。而且我们只需要输入“make”这个命令即可,非常方便,但是Makefile里面的代码都是什么意思呢?这就是接下来我们要讲解的。
3.4 Makefile语法
3.4.1 Makefile规则格式
Makefile里面是由一系列的规则组成的,这些规则格式如下:
目标…... : 依赖文件集合……
命令1
命令2
……
比如下面这条规则:
- main : main.o input.o calcu.o
- gcc -o main main.o input.o calcu.o
复制代码
这条规则的目标是main,main.o、input.o和calcu.o是生成main的依赖文件,如果要更新目标main,就必须先更新它的所有依赖文件,如果依赖文件中的任何一个有更新,那么目标也必须更新,“更新”就是执行一遍规则中的命令列表。
命令列表中的每条命令必须以TAB键开始,不能使用空格!
命令列表中的每条命令必须以TAB键开始,不能使用空格
命令列表中的每条命令必须以TAB键开始,不能使用空格!
make命令会为Makefile中的每个以TAB开始的命令创建一个Shell进程去执行。
了解了Makefile的基本运行规则以后我们再来分析一下3.3节中“示例代码3.3.2.6”中的Makefile,代码如下:
- 1 main: main.o input.o calcu.o
- 2 gcc -o main main.o input.o calcu.o
- 3 main.o: main.c
- 4 gcc -c main.c
- 5 input.o: input.c
- 6 gcc -c input.c
- 7 calcu.o: calcu.c
- 8 gcc -c calcu.c
- 9
- 10 clean:
- 11 rm *.o
- 12 rm main
复制代码
上述代码中一共有5条规则,1~2行为第一条规则,3~4行为第二条规则,5~6行为第三条规则,7~8行为第四条规则,10~12为第五条规则,make命令在执行这个Makefile的时候其执行步骤如下:
首先更新第一条规则中的main,第一条规则的目标成为默认目标,只要默认目标更新了那么就认为Makefile的工作,完成了整个Makefile就是为了完成这个工作。在第一次编译的时候由于main还不存在,因此第一条规则会执行,第一条规则依赖于文件main.o、input.o和calcu.o这个三个.o文件,这三个.o文件目前还都没有,因此必须先更新这三个文件。make会查找以这三个.o文件为目标的规则并执行。以main.o为例,发现更新main.o的是第二条规则,因此会执行第二条规则,第二条规则里面的命令为“gcc –c main.c”,这行命令很熟悉了吧,就是不链接编译main.c,生成main.o,其它两个.o文件同理。最后一个规则目标是clean,它没有依赖文件,因此会默认为依赖文件都是最新的,所以其对应的命令不会执行,当我们想要执行clean的话可以直接使用命令“make clean”,执行以后就会删除当前目录下所有的.o文件以及main,因此clean的功能就是完成工程的清理,“make clean”的执行过程如图3.4.1.1所示:
图3.4.1.1 make clean执行过程
从图3.4.1.1可以看出,当执行“make clean”命令以后,前面编译出来的.o和main可执行文件都被删除掉了,也就是完成了工程清理工作。
我们在来总结一下Make的执行过程:
1、make命令会在当前目录下查找以Makefile(makefile其实也可以)命名的文件。
2、当找到Makefile文件以后就会按照Makefile中定义的规则去编译生成最终的目标文件。
3、当发现目标文件不存在,或者目标所依赖的文件比目标文件新(也就是最后修改时间比目标文件晚)的话就会执行后面的命令来更新目标。
这就是make的执行过程,make工具就是在Makefile中一层一层的查找依赖关系,并执行相应的命令。编译出最终的可执行文件。Makefile的好处就是“自动化编译”,一旦写好了Makefile文件,以后只需要一个make命令即可完成整个工程的编译,极大的提高了开发效率。把make和Makefile和做菜类似,目标都是呈现出一场盛宴,它们之间的对比关系如表3.4.1.1所示:
表3.4.1.1 make和做菜对比
总结一下,Makefile中规则用来描述在什么情况下使用什么命令来构建一个特定的文件,这个文件就是规则的“目标”,为了生成这个“目标”而作为材料的其它文件称为“目标”的依赖,规则的命令是用来创建或者更新目标的。
除了Makefile的“终极目标”所在的规则以外,其它规则的顺序在Makefile中是没有意义的,“终极目标”就是指在使用make命令的时候没有指定具体的目标时,make默认的那个目标,它是Makefile文件中第一个规则的目标,如果Makefile中的第一个规则有多个目标,那么这些目标中的第一个目标就是make的“终极目标”。
3.4.2 Makefile变量
跟C语言一样Makefile也支持变量的,先看一下前面的例子:
- main: main.o input.o calcu.o
- gcc -o main main.o input.o calcu.o
复制代码
上述Makefile语句中,main.o input.o和calcue.o这三个依赖文件,我们输入了两遍,我们这个Makefile比较小,如果Makefile复杂的时候这种重复输入的工作就会非常费时间,而且非常容易输错,为了解决这个问题,Makefile加入了变量支持。不像C语言中的变量有int、char等各种类型,Makefile中的变量都是字符串!类似C语言中的宏。使用变量将上面的代码修改,修改以后如下所示:
示例代码3.4.2.1 Makefile变量使用
- 1 #Makefile变量的使用
- 2 objects = main.o input.o calcu.o
- 3 main: $(objects)
- 4 gcc -o main $(objects)
复制代码
我们来分析一下“示例代码3.4.2.1”,第1行是注释,Makefile中可以写注释,注释开头要用符号“#”,不能用C语言中的“//”或者“/**/”!第2行我们定义了一个变量objects,并且给这个变量进行了赋值,其值为字符串“main.o input.o calcu.o”,第3和4行使用到了变量objects,Makefile中变量的引用方法是“$(变量名)”,比如本例中的“$(objects)”就是使用变量objects。
在“示例代码3.4.2.1”中我们在定义变量objects的时候使用“=”对其进行了赋值,Makefile变量的赋值符还有其它两个“:=”和“?=”,我们来看一下这三种赋值符的区别:
1、赋值符“=”
使用“=”在给变量的赋值的时候,不一定要用已经定义好的值,也可以使用后面定义的值,比如如下代码:
示例代码3.4.2.1 赋值符"="使用
- 1 name = zzk
- 2 curname = $(name)
- 3 name = zuozhongkai
- 4
- 5 print:
- 6 [url=home.php?mod=space&uid=18411]@echo[/url] curname: $(curname)
复制代码
我们来分析一下上述代码,第1行定义了一个变量name,变量值为“zzk”,第2行也定义了一个变量curname,curname的变量值引用了变量name,按照我们C写语言的经验此时curname的值就是“zzk”。第3行将变量name的值改为了“zuozhongkai”,第5、6行是输出变量curname的值。在Makefile要输出一串字符的话使用“echo”,就和C语言中的“printf”一样,第6行中的“echo”前面加了个“@”符号,因为Make在执行的过程中会自动输出命令执行过程,在命令前面加上“@”的话就不会输出命令执行过程,大家可以测试一下不加“@”的效果。使用命令“make print”来执行上述代码,结果如图3.4.2.1:
图3.4.2.1 make执行结果
在图3.4.2.1中可以看到curname的值不是“zzk”,竟然是“zuozhongkai”,也就是变量“name”最后一次赋值的结果,这就是赋值符“=”的神奇之处!借助另外一个变量,可以将变量的真实值推到后面去定义。也就是变量的真实值取决于它所引用的变量的最后一次有效值。
2、赋值符“:=”
在“示例代码3.4.2.1”上来测试赋值符“:=”,修改“示例代码3.4.2.1”中的第2行,将其中的“=”改为“:=”,修改完成以后的代码如下:
示例代码3.4.2.2 ":="的使用
- 1 name = zzk
- 2 curname := $(name)
- 3 name = zuozhongkai
- 4
- 5 print:
- 6 @echo curname: $(curname)
复制代码
修改完成以后重新执行一下Makefile,结果如图3.4.2.2所示:
图3.4.2.2 make执行结果
从图3.4.2.2中可以看到此时的curname是zzk,不是zuozhongkai了。这是因为赋值符“:=”不会使用后面定义的变量,只能使用前面已经定义好的,这就是“=”和“:=”两个的区别。
3、赋值符“?=”
“?=”是一个很有用的赋值符,比如下面这行代码:
上述代码的意思就是,如果变量curname前面没有被赋值,那么此变量就是“zuozhongkai”,如果前面已经赋过值了,那么就使用前面赋的值。
4、变量追加“+=”
Makefile中的变量是字符串,有时候我们需要给前面已经定义好的变量添加一些字符串进去,此时就要使用到符号“+=”,比如如下所示代码:
- objects = main.o inpiut.o
- objects += calcu.o
复制代码
一开始变量objects的值为“main.o input.o”,后面我们给他追加了一个“calcu.o”,因此变量objects变成了“main.o input.o calcu.o”,这个就是变量的追加。
3.4.3 Makefile模式规则
在3.3.2小节中我们编写了一个Makefile文件用来编译工程,这个Makefile的内容如下:
示例代码3.4.3.1 Makefile文件代码
- 1 main: main.o input.o calcu.o
- 2 gcc -o main main.o input.o calcu.o
- 3 main.o: main.c
- 4 gcc -c main.c
- 5 input.o: input.c
- 6 gcc -c input.c
- 7 calcu.o: calcu.c
- 8 gcc -c calcu.c
- 9
- 10 clean:
- 11 rm *.o
- 12 rm main
复制代码
上述Makefile中第3~8行是将对应的.c源文件编译为.o文件,每一个C文件都要写一个对应的规则,如果工程中C文件很多的话显然不能这么做。为此,我们可以使用Makefile中的模式规则,通过模式规则我们就可以使用一条规则来将所有的.c文件编译为对应的.o文件。
模式规则中,至少在规则的目标定定义中要包涵“%”,否则就是一般规则,目标中的“%”表示对文件名的匹配,“%”表示长度任意的非空字符串,比如“%.c”就是所有的以.c结尾的文件,类似与通配符,a.%.c就表示以a.开头,以.c结束的所有文件。
当“%”出现在目标中的时候,目标中“%”所代表的值决定了依赖中的“%”值,使用方法如下:
%.o : %.c
命令
因此“示例代码3.4.3.1”中的Makefile可以改为如下形式:
示例代码3.4.3.2 模式规则使用
- 1 objects = main.o input.o calcu.o
- 2 main: $(objects)
- 3 gcc -o main $(objects)
- 4
- 5 %.o : %.c
- 6 #命令
- 7
- 8 clean:
- 9 rm *.o
- 10 rm main
复制代码
“示例代码3.4.3.2”中第5、6这两行代码替代了“示例代码3.4.3.1”中的3~8行代码, 修改以后的Makefile还不能运行,因为第6行的命令我们还没写呢,第6行的命令我们需要借助另外一种强大的变量—自动化变量。
3.4.4 Makefile自动化变量
上面讲的模式规则中,目标和依赖都是一系列的文件,每一次对模式规则进行解析的时候都会是不同的目标和依赖文件,而命令只有一行,如何通过一行命令来从不同的依赖文件中生成对应的目标?自动化变量就是完成这个功能的!所谓自动化变量就是这种变量会把模式中所定义的一系列的文件自动的挨个取出,直至所有的符合模式的文件都取完,自动化变量只应该出现在规则的命令中,常用的自动化变量如表3.4.4.1:
表3.4.4.1 自动化变量
表3.4.4.1中的7个自动化变量中,常用的三种:$@、$<和$^,我们使用自动化变量来完成“示例代码3.4.3.2”中的Makefile,最终的完整代码如下所示:
示例代码3.4.4.1 自动化变量
1 objects = main.o input.o calcu.o
2 main: $(objects)
3 gcc -o main $(objects)
4
5 %.o : %.c
6 gcc -c $<
7
8 clean:
9 rm *.o
10 rm main
上述代码代码就是修改后的完成的Makefile,可以看出相比3.3.2小节中的要精简了很多,核心就在于第5、6这两行,第5行使用了模式规则,第6行使用了自动化变量。
3.4.5 Makefile伪目标
Makefile有一种特殊的目标——伪目标,一般的目标名都是要生成的文件,而伪目标不代表真正的目标名,在执行make命令的时候通过指定这个伪目标来执行其所在规则的定义的命令。
使用伪目标的主要是为了避免Makefile中定义的只执行命令的目标和工作目录下的实际文件出现名字冲突,有时候我们需要编写一个规则用来执行一些命令,但是这个规则不是用来创建文件的,比如在前面的“示例代码3.4.4.1”中有如下代码用来完成清理工程的功能:
上述规则中并没有创建文件clean的命令,因此工作目录下永远都不会存在文件clean,当我们输入“make clean”以后,后面的“rm *.o”和“rm main”总是会执行。可是如果我们“手贱”,在工作目录下创建一个名为“clean”的文件,那就不一样了,当执行“make clean”的时候,规则因为没有依赖文件,所以目标被认为是最新的,因此后面的rm命令也就不会执行,我们预先设想的清理工程的功能也就无法完成。为了避免这个问题,我们可以将clean声明为伪目标,声明方式如下:
我们使用伪目标来更改“示例代码3.4.4.1”,修改完成以后如下:
示例代码3.4.5.1 伪目标
- 1 objects = main.o input.o calcu.o
- 2 main: $(objects)
- 3 gcc -o main $(objects)
- 4
- 5 .PHONY : clean
- 6
- 7 %.o : %.c
- 8 gcc -c [ DISCUZ_CODE_440 ]lt;
- 9
- 10 clean:
- 11 rm *.o
- 12 rm main
复制代码
上述代码第5行声明clean为伪目标,声明clean为伪目标以后不管当前目录下是否存在名为“clean”的文件,输入“make clean”的话规则后面的rm命令都会执行。
3.4.6 Makefile条件判断
在C语言中我们通过条件判断语句来根据不同的情况来执行不同的分支,Makefile也支持条件判断,语法有两种如下:
- <条件关键字>
- <条件为真时执行的语句>
- endif
- 以及:
- <条件关键字>
- <条件为真时执行的语句>
- else
- <条件为假时执行的语句>
- endif
复制代码
其中条件关键字有4个:ifeq、ifneq、ifdef和ifndef,这四个关键字其实分为两对、ifeq与ifneq、ifdef与ifndef,先来看一下ifeq和ifneq,ifeq用来判断是否相等,ifneq就是判断是否不相等,ifeq用法如下:
- ifeq (<参数1>, <参数 2>)
- ifeq ‘<参数1 >’,‘ <参数2>’
- ifeq “<参数1>”, “<参数2>”
- ifeq “<参数1>”, ‘<参数2>’
- ifeq ‘<参数1>’, “<参数2>”
复制代码
上述用法中都是用来比较“参数1”和“参数2”是否相同,如果相同则为真,“参数1”和“参数2”可以为函数返回值。ifneq的用法类似,只不过ifneq是用来了比较“参数1”和“参数2”是否不相等,如果不相等的话就为真。
ifdef和ifndef的用法如下:
ifdef <变量名>
如果“变量名”的值非空,那么表示表达式为真,否则表达式为假。“变量名”同样可以是一个函数的返回值。ifndef用法类似,但是含义用户ifdef相反。
3.4.7 Makefile函数使用
Makefile支持函数,类似C语言一样,Makefile中的函数是已经定义好的,我们直接使用,不支持我们自定义函数。make所支持的函数不多,但是绝对够我们使用了,函数的用法如下:
$(函数名 参数集合)
或者
${函数名 参数集合}
可以看出,调用函数和调用普通变量一样,使用符号“$”来标识。参数集合是函数的多个参数,参数之间以逗号“,”隔开,函数名和参数之间以“空格”分隔开,函数的调用以“$”开头。接下来我们介绍几个常用的函数,其它的函数大家可以参考《跟我一起写Makefile》这份文档。
1、函数subst
函数subst用来完成字符串替换,调用形式如下:
$(subst <from>,<to>,<text>)
此函数的功能是将字符串<text>中的<from>内容替换为<to>,函数返回被替换以后的字符串,比如如下示例:
$(subst zzk,ZZK,my name is zzk)
把字符串“my name is zzk”中的“zzk”替换为“ZZK”,替换完成以后的字符串为“my name is ZZK”。
2、函数patsubst
函数patsubst用来完成模式字符串替换,使用方法如下:
$(patsubst <pattern>,<replacement>,<text>)
此函数查找字符串<text>中的单词是否符合模式<pattern>,如果匹配就用<replacement>来替换掉,<pattern>可以使用包括通配符“%”,表示任意长度的字符串,函数返回值就是替换后的字符串。如果<replacement>中也包涵“%”,那么<replacement>中的“%”将是<pattern>中的那个“%”所代表的字符串,比如:
$(patsubst %.c,%.o,a.c b.c c.c)
将字符串“a.c b.c c.c”中的所有符合“%.c”的字符串,替换为“%.o”,替换完成以后的字符串为“a.o b.o c.o”。
3、函数dir
函数dir用来获取目录,使用方法如下:
此函数用来从文件名序列<names>中提取出目录部分,返回值是文件名序列<names>的目录部分,比如:
提取文件“/src/a.c”的目录部分,也就是“/src”。
4、函数notdir
函数notdir看名字就是知道去除文件中的目录部分,也就是提取文件名,用法如下:
此函数用与从文件名序列<names>中提取出文件名非目录部分,比如:
提取文件“/src/a.c”中的非目录部分,也就是文件名“a.c”。
5、函数foreach
foreach函数用来完成循环,用法如下:
- $(foreach <var>, <list>,<text>)
复制代码
此函数的意思就是把参数<list>中的单词逐一取出来放到参数<var>中,然后再执行<text>所包含的表达式。每次<text>都会返回一个字符串,循环的过程中,<text>中所包含的每个字符串会以空格隔开,最后当整个循环结束时,<text>所返回的每个字符串所组成的整个字符串将会是函数foreach函数的返回值。
6、函数wildcard
通配符“%”只能用在规则中,只有在规则中它才会展开,如果在变量定义和函数使用时,通配符不会自动展开,这个时候就要用到函数wildcard,使用方法如下:
- $(wildcard PATTERN…)
- 比如:
- $(wildcard *.c)
复制代码
上面的代码是用来获取当前目录下所有的.c文件,类似“%”。
关于Makefile的相关内容就讲解到这里,本节只是对Makefile做了最基本的讲解,确保大家能够完成后续的学习,Makefile还有大量的知识没有提到,有兴趣的可以自行参考《跟我一起写Makefile》这份文档来深入学习Makefile。
|
|