Makefile使用记录
Makefile 文件描述了 Linux 系统下 C/C++ 工程的编译规则,它用来自动化编译 C/C++ 项目。一旦写编写好 Makefile 文件,只需要一个 make 命令,整个工程就开始自动编译,不再需要手动执行 GCC 命令。===>此外拓展功能不至于C++,其相当于组合一系列Linux命令,因此也可以用来编译其他工程。
什么是Makefile?
Windows 下的集成开发环境(IDE)已经内置了 Makefile,或者说会自动生成 Makefile,因此windows下不用去手动编写makefile。但是在 Linux 下的C语言开发会碰到
- S:编译的时候需要链接库的的问题——编译的时候 gcc 只会默认链接一些基本的C语言标准库,很多源文件依赖的标准库都需要我们手动链接,因为有很多的文件,还要去链接很多的第三方库。所以在编译的时候命令会很长,并且在编译的时候我们可能会涉及到文件链接的顺序问题,所以手动编译会很麻烦。
- A:把要链接的库文件放在 Makefile 中,制定相应的规则和对应的链接顺序。这样只需要执行 make 命令,工程就会自动编译。每次想要编译工程的时候就执行 make ,省略掉手动编译中的参数选项和命令,非常的方便。
- S:编译大的工程会花费很长的时间
- Makefile 支持多线程并发操作,会极大的缩短我们的编译时间,并且当我们修改了源文件之后,编译整个工程的时候,make 命令只会编译我们修改过的文件,没有修改的文件不用重新编译,也极大的解决了我们耗费时间的问题。
使用 Makefile 的方式:首先需要编写好 Makefile 文件,然后在 shell 中执行 make 命令,程序就会自动执行,得到最终的目标文件。
makefile规则是什么样的?
规则主要是两个部分组成,分别是依赖的关系和执行的命令,其结构如下所示:
1 | targets: prerequisites |
- targets:规则的目标,可以是 Object File(一般称它为中间文件),也可以是可执行文件,还可以是一个标签;
- prerequisites:是我们的依赖文件,要生成 targets 需要的文件或者是目标。可以是多个,也可以是没有;
- command:make 需要执行的命令(任意的 shell 命令)。可以有多条命令,每一条命令占一行。
makfile执行流程
在我们编译项目文件的时候,默认情况下,make 执行的是 Makefile 中的第一规则(Makefile 中出现的第一个依赖关系),此规则的第一目标称之为“最终目标”或者是“终极目标”。
案例分析:
1 | main:main.o test1.o test2.o |
它的具体工作顺序是:当在 shell 提示符下输入 make 命令以后。 make 读取当前目录下的 Makefile 文件,并将 Makefile 文件中的第一个目标作为其执行的“终极目标”,开始处理第一个规则(终极目标所在的规则)。在我们的例子中,第一个规则就是目标 “main” 所在的规则。规则描述了 “main” 的依赖关系,并定义了链接 “.o” 文件生成目标 “main” 的命令;make 在执行这个规则所定义的命令之前,首先处理目标 “main” 的所有的依赖文件(例子中的那些 “.o” 文件)的更新规则(以这些 “.o” 文件为目标的规则)。
对这些 “.o” 文件为目标的规则处理有下列三种情况:
- 目标 “.o” 文件不存在,使用其描述规则创建它;
- 目标 “.o” 文件存在,
目标 “.o” 文件所依赖的 “.c” 源文件和 “.h” 文件中的任何一个比目标 “.o” 文件**“更新”**(依赖的.h、.c在上一次 make 之后被修改)。则根据规则重新编译生成它; - 目标 “.o” 文件存在,
目标 “.o” 文件比它的任何一个依赖文件(".c" 源文件、".h" 文件)“更新”(它的依赖文件.h、.c在上一次 make 之后没有被修改),则什么也不做。
通过上面的更新规则我们可以了解到中间文件的作用,也就是编译时生成的 “.o” 文件。作用是检查某个源文件是不是进行过修改,最终目标文件是不是需要重建。我们执行 make 命令时,只有修改过的源文件或者是不存在的目标文件会进行重建,而那些没有改变的文件不用重新编译,这样在很大程度上节省时间,提高编程效率。小的工程项目可能体会不到,项目工程文件越大,效果才越明显。
变量
- 定义变量的基本语法如下:
变量的名称=值列表
- 调用变量的时候可以用
"$(VALUE_LIST)"
或者是"${VALUE_LIST}"
来替换,这就是变量的引用。
知道了如何定义,下面我们来说一下 Makefile 的变量的四种基本赋值方式:
- 简单赋值 ( := ) 编程语言中常规理解的赋值方式,只对当前语句的变量有效。
- 递归赋值 ( = ) 赋值语句可能影响多个变量,所有目标变量相关的其他变量都受影响。
- 条件赋值 ( ?= ) 如果变量未定义,则使用符号中的值定义变量。如果该变量已经赋值,则该赋值语句无效。
- 追加赋值 ( += ) 原变量用空格隔开的方式追加一个新值。
条件判断
ifeq 和 ifneq
1 | ifeq (ARG1, ARG2) |
ifdef 和 ifndef
1 | ifdef VARIABLE-NAME |
伪目标
并不会创建目标文件,只是想去执行这个目标下面的命令。
伪目标的存在可以帮助我们找到命令并执行。
使用伪目标有两点原因:
- 避免我们的 Makefile 中定义的只执行的命令的目标和工作目录下的实际文件出现名字冲突。
- 提高执行 make 时的效率,特别是对于一个大型的工程来说,提高编译的效率也是我们所必需的。
1 | clean: |
规则中 rm 命令不是创建文件 clean 的命令,而是执行删除任务,删除当前目录下的所有的 .o 结尾和文件名为 test 的文件。
-
当工作目录下不存在以 clean 命令的文件时,在 shell 中输入 make clean 命令,命令 rm -rf *.o test 总会被执行 ,这也是我们期望的结果。
-
如果当前目录下存在文件名为 clean 的文件时情况就会不一样了,当我们在 shell 中执行命令 make clean,由于这个规则没有依赖文件,所以目标被认为是最新的而不去执行规则所定义的命令。因此命令 rm 将不会被执行。为了解决这个问题,删除 clean 文件或者是在 Makefile 中将目标 clean 声明为伪目标。将一个目标声明称伪目标的方法是将它作为特殊的目标
.PHONY
的依赖,如下:.PHONY:clean
这样 clean 就被声明成一个伪目标,无论当前目录下是否存在 clean 这个文件,当我们执行 make clean 后 rm 都会被执行。而且当一个目标被声明为伪目标之后,make 在执行此规则时不会去试图去查找隐含的关系去创建它。这样同样提高了 make 的执行效率,同时也不用担心目标和文件名重名而使我们的编译失败。
伪目标实现多文件编辑
如果在一个文件里想要同时生成多个可执行文件,我们可以借助伪目标来实现。使用方式如下:
1 | .PHONY:allall:test1 test2 test3test1:test1.o gcc -o $@ $^test2:test2.o gcc -o $@ $^test3:test3.o gcc -o $@ $^ |
我们在当前目录下创建了三个源文件,目的是把这三个源文件编译成为三个可执行文件。将重建的规则放到 Makefile 中,约定使用 “all” 的伪目标来作为最终目标,它的依赖文件就是要生成的可执行文件。这样的话只需要一个 make 命令,就会同时生成三个可执行文件。
之所以这样写,是因为伪目标的特性,它总会被执行,所以它依赖的三个文件的目标就不如 “all” 这个目标新,所以,其他的三个目标的规则总是被执行,这也就达到了我们一口气生成多个目标的目的。我们也可以实现单独的编译这三个中的任意一个源文件(我们想去重建 test1,我们可以执行命令make test1
来实现 )。
-l参数和-L参数
**-l
**参数就是用来指定程序要链接的库,-l参数紧接着就是库名(-l<packageName>
),那么库名跟真正的库文 件名有什么关系呢?A: 就拿数学库来说,他的库名是m,他的库文件名是libm.so,很容易看出,把库文件名的 头lib和尾.so去掉就是库名了。
好了现在我们知道怎么得到库名了,比如我们自已要用到一个第三方提供的库名字叫libtest.so,那么我们只要①把 libtest.so拷贝到/usr/lib 里,②编译时加上-ltest参数,我们就能用上libtest.so库了(当然要用libtest.so库里的函数,我们还需要与libtest.so配套的头文件)。
注:放在 /lib 和 /usr/lib 和 /usr/local/lib 里的库直接用-l参数就能链接了,但如果库文件如果没有放在这三个目录里,而是放在其他目录里,这时我们只用-l参数的话,链接还是会出错,出错信息大概是:“/usr/bin/ld: cannot find -lxxx”,也就是提醒开发人员:链接程序ld在那3个目录里找不到libxxx.so。
这时另外一个参数**-L
**就派上用场了,比如常用的X11的库,它放在/usr/X11R 6/lib目录下,我们编译时就要用-L/usr/X11R6/lib - lX11
参数,-L参数跟着的是库文件所在的目录名(-L <packagePath>
)。比如我们把libtest.so在/aaa/bb b/ccc目录下,那链接参数就是-L /aaa/bbb/ccc -ltest
, 注意需要加上-ltest
(RFID工程代码中的makefile就是因为没有加上这个导致的)
附录
通用范式
清除工作目录中的过程文件
我们在使用的时候会产生中间文件会让整个文件看起来很乱,所以在编写 Makefile 文件的时候会在末尾加上这样的规则语句来清除工作目录中的过程文件:
1 |
|
完整范式
1 | OBJS = base64.o faceSearch.o camera.o CRC.o cQueue.o myQueue.o myUart.o info.o mcuio.o global.o print.o myMQTT.o rfid.o serialscreen.o sscreenupdate.o hmiFSM.o main.o |
Gcc CFLAGs
- -I: (include)包含.h头文件
- -o:(output) 指定输出文件名
gcc -o app test.c
将生成可执行程序exe
- -c: (compile) 只编译不链接:产生.o文件,就是obj文件,不产生执行文件
gcc -c test.c
将生成test.o的目标文件
▲.gcc -c a.c -o a.o
表示把源文件a.c编译成指定文件名a.o的中间目标文件(其实在这里,把-o a.o省掉,效果是一样的,因为中间文件默认与源文件同名,只是后缀变化)。
- 如果GCC不带-C参数,编译一个源代码文件(test.c)。那么会自动将编译和链接一步完成,并生成可执行文件。可执行文件可以有-o参数指定(test.o)
- 如果是多个文件,则需要先编译成中间目标文件(一般是.o文件),在链接成可执行文件,一般习惯目标文件都是以.o后缀,也没有硬性规定可执行文件不能用.o文件。
嵌入式编程:
全局变量
-
如果是main中声明的全局变量,工程中的其他文件都不需要引入"main.c" or "main.h"就可以直接使用
-
如果不是,则最好的方式还是用一个源文件里(如global.c)声明全局变量,其他cpp若使用某个全局变量,在相应的头文件中包含该头文件(global.h)即可。
- Q: 为什么不直接在global.h头文件里定义全局变量?
- A: 由于全局变量的定义有且只能有一次,如果是在头文件中定义了,那么就会导致重复定义。===>因此头文件只能用来声明,不能用来定义。
-
声明与定义
-
函数或变量在声明时,并没有给它实际的物理内存空间,它有时候可保证你的程序编译通过;
-
函数或变量在定义时,它就在内存中有了实际的物理空间。
如果你在编译单元中引用的外部变量没有在整个工程中任何一个地方定义的话,那么即使它在编译时可以通过,在连接时也会报错,因为程序在内存中找不到这个变量。
函数或变量可以声明多次,但定义有且只能有一次。
-
sprintf
C函数:将格式化的数据写入字符串, 原型为:`int sprintf(char *dest_str, char * format [, argument, …]);
1 | // sprintf()最常见的应用之一莫过于把整数打印到字符串中,如: |
sscanf
C 库函数 : 从字符串读取格式化输入。原型为:int sscanf(const char *str, const char *format, ...)
1 | // example: 1 |
memcpy
C 库函数: 从存储区 str2 复制 n 个字节到存储区 str1。原型为:
void memcpy(void str1, const void str2, size_t n)
1 | int getUser(char *cmd,char *user){ |
memset()
C 库函数:复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。原型为:
void *memset(void *str, int c, size_t n)
strcpy()
C 库函数:把 src 所指向的字符串复制到 dest。原型:
char *strcpy(char *dest, const char *src)
Author: Mrli
Link: https://nymrli.top/2021/09/30/makefile使用记录/
Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.