Makefile初学指南
前言
C语言程序从代码到可执行文件(*.exe)需要经过预处理、编译、汇编和链接这几个步骤。每当修改源文件(*.c)或源文件所包含的头文件(*.h)后,我们都需要重新执行上述几个步骤,以得到修改后的程序。
通常将预处理、编译和汇编这三个步骤统称为编译。
一个项目通常有多个源文件,如果只修改其中一个,就对所有源文件重新执行编译、链接步骤,就太浪费时间了。因此十分有必要引入 Makefile 工具:Makefile 工具可以根据文件依赖,自动找出那些需要重新编译和链接的源文件,并对它们执行相应的动作。
开始前的准备
本文章目的在于帮助你理解和掌握Makefile的编写方法和编写技巧,在开始阅读和动手编写Makefile前,你可能需要准备一下环境。
本篇文章的示例运行在wsl2上(Windows Subsystem for Linux 2),我的系统信息如下:
1 | gee@JiPing_Desktop:~/workspace/test$ lsb_release -a |
可以看到,我使用Ubuntu系统,且系统发行版本是22.04。如果你是Windows系统,则可以在启用或关闭 Windows 功能
中点击开启适用于 Linux 的 Windows 子系统
,并在微软商店中下载和安装Ubuntu系统,以获得与我一致的代码编写环境。具体步骤可以参考:安装 WSL。
相比 vim
如果你更熟悉 VSCode
的操作,则可以参考:开始通过 WSL 使用 VS Code 来搭建自己熟悉的代码编写环境。
如果你在阅读或实践过程中遇到任何问题,欢迎在评论区中留下你的疑问,我们会尽力尝试解答。
从代码编译开始
在开始编写 Makefile 前,我们先写一段简单的代码,并尝试使用编译工具链将代码变为可执行文件。
编写简单的代码
1 | /* main.c */ |
编译得到可执行文件
编辑完文件后,回到终端,使用编译工具链将代码变为可执行文件:
如果你在执行
gcc main.c -o main
时遇到问题,很有可能是没有安装gcc
导致的,在终端中输入sudo apt-get install build-essential
以安装所需的编译工具。
1 | gee@JiPing_Desktop:~/workspace/example$ vim main.c |
可以看到,我们顺利得到了可执行文件,并且执行结果也符合预期。
上面所执行的几条命令中,gcc main.c -o main
这条命令负责调用编译工具链,将源文件 main.c
编译、链接为可执行文件 main
。这里的GCC(GNU Compiler Collection)就是上文中提及的编译工具链,它是预处理、编译、汇编、链接所使用到的各种工具的集合,它们彼此搭配协作,才最终得到我们所需的可执行文件。
你可能会好奇gcc
命令中的 -o
选项的作用,它是用来指定输出文件的命名的,随后紧跟的参数就是所要指定的命名,在上面的示例中,我们将输出文件的命名指定为了 main
。
动手写简单的Makefile
现在我们已经掌握了将代码编译、链接为可执行文件的方法,是时候开始写最简单的Makefile文件了:
编写Makefile并执行make
1 | # Makefile |
编写好后回到终端,使用 make
来执行Makefile:
1 | gee@JiPing_Desktop:~/workspace/example$ vim Makefile |
可以看到 Makefile 给出了它的处理结果 make: 'main' is up to date.
,意思是 main
已经是最新的了,无需执行任何操作。此时我们的 main.c
没有做任何修改,也就是说即使重新编译、链接得到一个新的 main
,它与旧的 main
也不会存在任何的不同,所以Makefile没有执行任何的步骤。
尝试修改 main.c
再执行 make
,看看这次的结果会怎样:
1 | /* main.c */ |
回到终端执行 make
:
1 | gee@JiPing_Desktop:~/workspace/example$ vim main.c |
可以看到,在修改了 main.c
后重新执行 make
,Makefile会自动地执行 gcc main.c -o main
,以得到新的可执行文件 main
。从结果来看,代码中的修改确实反应到了可执行文件上。
Makefile三要素
那么问题就来了,Makefile中的两行语句分别是什么意思呢?拆解来看,两行语句可以分为三部分,分别是目标(target)、依赖(prerequisite)和执行语句(recipe):
延伸思考:目标、依赖和执行语句,三者在Makefile中是否缺一不可?在不修改源文件的前提下尝试修改目标,再执行make时会得到怎样的结果?
上面的例子中,可执行文件 main
就是我们想要得到的目标,而 main
的生成依赖于 main.c
,所以将 main.c
填写在依赖的位置。在发现目标文件不存在,或依赖的文件有所修改后,Makefile 就会执行下方的执行语句,其任务通常是生成目标文件。
延伸阅读
http://gnu.org上关于三要素的描述如下:
A target is usually the name of a file that is generated by a program; examples of targets are executable or object files. A target can also be the name of an action to carry out, such as ‘clean’ (see Phony Targets).
A prerequisite is a file that is used as input to create the target. A target often depends on several files.
A recipe is an action thatmake
carries out. A recipe may have more than one command, either on the same line or each on its own line. Please note: you need to put a tab character at the beginning of every recipe line!
当增加源文件和修改源文件名称
回看已经写好的Makefile,会发现其中的内容都是有具体指向的:main
、main.c
。试想这样一个场景:我们在文件夹中添加新的源文件 bar.c
,并将 main.c
重命名为 entry.c
,这时再执行 make
会得到怎样的结果呢?
思考题:在函数 Print_Progress_Bar 中,数组
bar
的定义和赋值能否由char bar[] = PROGRESS_BAR
改为char *bar = PROGRESS_BAR
。为什么?两者有什么不同?
1 | /* bar.c */ |
修改完成后在终端执行 make
,结果如下:
1 | gee@JiPing_Desktop:~/workspace/example$ vim bar.c |
可以看到,make
提示“No rule to make target ‘main.c’, needed by ‘main’.”,并停止了执行。从提示中我们大致可以猜到,由于找不到依赖文件 main.c
, make
停止了执行。解决问题的方法有两种,简单粗暴的做法是:直接根据新的文件命名修改 Makefile 文件:
1 | # Makefile |
由于主函数调用了 bar.c
中定义的函数,所以在编译时我们需要将 bar.c
一起编译、链接到可执行文件里,同时别忘了把它加进依赖中。修改好后回到终端重新执行 make
:
1 | gee@JiPing_Desktop:~/workspace/example$ vim Makefile |
这一次 make
命令没有再报错。
想象一下,如果我们保持当前的 Makefile 写法,那么之后每次添加源文件,或者修改源文件名称时,都需要我们重新修改 Makefile 文件。当文件数量爆炸多的时候,这样的手动调整显然是十分麻烦的。所以我们迫切需要一种更为通用的写法,来免除这些“痛苦”。
变量和通配符和wildcard函数
仔细观察源文件的命名 main.c
、 bar.c
,我们会发现它们有着共同的模式(或称为规律):都以 .c
结尾,这意味着可以用这种模式匹配所有源文件。在 Makefile 中我们可以使用 wildcard 函数(wildcard function)来达到这一目的。
使用wildcard函数
在 Makefile 中,$(function arguments)
的写法用于函数调用, wildcard 函数的使用方法如下:
1 | $(wildcard pattern…) |
如果我们想匹配当前目录下的所有源文件,就可以这样写:$(wildcard *.c)
,其中通配符 *
用于匹配任意长度的任何字符,可以是 main
、bar
,也可以是其他任何你能想得到的字符组合,后面加上 .c
则是要求匹配的字符组合必须以 .c
结尾。
当前示例下,$(wildcard *.c)
展开后得到的结果就是: bar.c entry.c
,所以我们的 Makefile
文件可以修改为:
1 | # Makefile |
修改后保存,再重新执行 make
,得到的结果与之前一致:(这里我将进度条从进度33%改为了52%,以确保 make
执行编译命令)
1 | gee@JiPing_Desktop:~/workspace/example$ vim Makefile |
利用变量
上面的 Makefile
还可以再优化一下可读性和效率,我们可以利用变量保存 wildcard 函数展开后的结果。Makefile 中变量定义的形式与C语言类似:var := value
,调用则和函数调用类似:$(var)
,所以 Makefile
可以进一步修改为:
1 | # Makefile |
相比上面的 Makefile
,进一步修改后的 Makefile
减少了一次函数调用,并且增加了可读性。
变量的赋值和修改
我们在刚才的示例中使用到了赋值符号 :=
,该符号与C语言中的赋值符号 =
作用效果相同。以下是几个常用符号的简介:
=
:递归赋值(Recursively Expanded Variable Assignment),使用变量进行赋值时,会优先展开引用的变量,示例如下:
1 | foo = $(bar) |
:=
:简单赋值(Simply Expanded Variable Assignment),最常用的赋值符号:
1 | x := foo |
+=
:文本增添(Appending),用于向已经定义的变量添加文本:
1 | objects = main.o foo.o bar.o utils.o |
?=
:条件赋值(Conditional Variable Assignment),仅在变量没有定义时创建变量:
1 | FOO ?= bar |
动手写进阶的Makefile
到目前为止,我们已经写出一个简单能用的Makefile了,它能应对不太复杂的场景,在没有多级目录的情况下已经足够使用。但我们实际面对的场景往往要复杂得多:源文件和头文件按照功能或层级区分,散落在一个个子文件夹下,这样做更容易管理工程文件,但也带来了两点小麻烦。
先让我们先改造一下当前的目录结构,使其更贴合实际应用场景:
tree
命令的作用是以树的形式展现目录结构,你可能无法直接使用该命令,尝试sudo apt install tree
以安装和使用tree
命令。
1 | gee@JiPing_Desktop:~/workspace/example$ tree |
这里我新建了目录 func
,并将 bar.c
转移到了 func
目录下,同时在 func
目录下创建了头文件 bar.h
。然后在 entry.c
中将手动声明函数改为了头文件包含:
1 | // bar.h |
现在再让我们尝试执行 make
,看看会发生什么:
1 | gee@JiPing_Desktop:~/workspace/example$ make |
首先出现的问题是编译 entry.c
时提示找不到 bar.h
的头文件,这是编译时没有指定到哪些路径下寻找头文件导致的,解决办法是执行 gcc
命令时通过 -I
选项指定头文件所在路径:
1 | # Makefile |
再来执行make:
1 | gee@JiPing_Desktop:~/workspace/example$ vim Makefile |
我们察觉到执行 make
时又发生了错误,提示主函数中调用了未定义的函数 Print_Progress_Bar
,这个函数定义在 bar.c
中。仔细观察可以发现 gcc
的调用中缺少 bar.c
,这就引发了我们遇到的问题。显然在 bar.c
装进 ./func
目录后,Makefile
就找不到 bar.c
文件了,这就是我们在刚才提到的小麻烦。
应对复杂的目录结构
首先还是让我们来看一下 make
的报错问题如何解决。思路和方法很简单,使用 wildcard 函数在 ./func
目录下也匹配一遍源文件,再把这些源文件一同添加到 SRCS
变量中就可以了:
1 | # Makefile |
尝试执行下:
1 | gee@JiPing_Desktop:~/workspace/example$ vim Makefile |
可以看到问题得到了解决。但这样的方案还是存在缺点的,它不够通用和直观,从中我们很难看出哪些路径得到了使用。或许还有什么办法能将 Makefile
写得更清晰一些。
如果你曾使用过一些 IDE
,那你可能会对配置路径感到熟悉,这要求你将一些文件目录添加到工程文件配置中去。我们也可以效仿这样的做法,手动将目录添加到 Makefile
中去。
1 | # Makefile |
这里定义了变量 SUBDIR
,我们将使用它来指定那些存放着源文件和头文件的目录。接下来我们将请出另一个功能强大的函数 foreach 来帮助我们完成一项复杂的功能。
1 | $(foreach var,list,text) |
foreach(for each)函数的功能与 Python 和C语言中的 for 循环类似,但会更接近 Python 的 for 循环。它的功能描述起来就是:从 list
中逐个取出元素,赋值给 var
,然后再展开 text
。下面是一个使用示例。
1 | SUBDIR := . |
有了 foreach 函数,我们就能配合 wildcard 函数,通过指定路径来获取源文件,并指定头文件所在路径:
1 | # Makefile |
在终端里试试效果(可以使用 rm ./main
移除可执行文件,来确保 make
会执行编译命令):
1 | gee@JiPing_Desktop:~/workspace/example$ vim ./Makefile |
它可以正常工作,且效果与之前是一致的。现在来看,指定路径的做法较之前并没有太大的优势,我们要做的仍是手动指定目录,只是将获取源文件的任务交给了 foreach 函数来完成。在后面,我们会继续深入了解 Makefile,到时指定路径的优势会逐渐显现。
分析编译过程
到目前为止,我们的示例程序还保持着较短的编译、链接时间。但当源文件逐渐增多后,只改动其中一个源文件,我们还能在短时间内获得可执行文件吗?为了解答这个问题,我们先来回顾一下编译、链接的过程。
源文件和头文件需要经过四个步骤才能得到可执行文件,分别是预处理、编译、汇编和链接。
- 预处理:预处理器将以字符
#
开头的命令展开、插入到原始的C程序中。比如我们在源文件中能经常看到的、用于头文件包含的#include
命令,它的功能就是告诉预编译器,将指定头文件的内容插入的程序文本中。
- 编译阶段:编译器将文本文件
*.i
翻译成文本文件*.s
,它包含一个汇编语言程序。 - 汇编阶段:汇编器将
*.s
翻译成机器语言指令,把这些指令打包成可重定位目标程序(relocatable object program)的格式,并保存在*.o
文件中。 - 链接阶段:在
bar.c
中我们定义了Print_Progress_Bar
函数,该函数会保存在目标文件bar.o
中。直到链接阶段,链接器才以某种方式将Print_Progress_Bar
函数合并到main
函数中去。在链接时如果没有指定bar.o
,链接器就无法找到Print_Progress_Bar
函数,也就会提示找不到相关函数的定义。
保存 *.o 文件
从编译过程的分析中,我们能找到当前 Makefile
存在的两点问题:
- 没有保存
.o
文件,这导致我们每次文件变动都要重新执行预处理、编译和汇编来得到目标文件,即使新得到的文件与旧文件完全没有差别(即编译用到的源文件没有任何变化,就跟bar.c
一样)。 - 有保存
.o
文件,则会遇到第二个问题,即依赖中没有指定头文件,这意味着只修改头文件的情况下,源文件不会重新编译得到新的可执行文件!
为了证明以上两个问题,我们对 Makefile
做一些改动:
1 | INCS := -I. -I./func |
gcc
命令指定 -c
选项后,会只执行编译步骤,而不执行链接步骤,最后得到 *.o
文件。这里我们添加新的目标和依赖,目的是编译得到 main.o bar.o
,最后再手动将它们链接为可执行文件 main
。值得一提的是 Makefile 文件会自动匹配依赖和目标,如果依赖的依赖有更新,则目标文件也会得到更新。
现在让我们看看 make
执行的效果:
1 | gee@JiPing_Desktop:~/workspace/example$ vim Makefile |
make
执行了我们指定的每一个步骤。现在让我们修改 entry.c
,手动删除 bar.o
后再执行 make
。(模拟不保存 *.o
文件的情况)
1 | // main.c |
试验下执行 make
的效果:
1 | gee@JiPing_Desktop:~/workspace/example$ vim entry.c |
我们不仅重新编译了 entry.o
,还重新编译了 bar.o
,现在再试试保存 bar.o
的情况下执行 make
。
1 | gee@JiPing_Desktop:~/workspace/example$ vim entry.c |
可以发现,相较于不保存 bar.o
的情况,我们少执行了 bar.o
的编译步骤,这对于工程文件编译速度的提升,可能是巨大的!
现在再让我们尝试修改 bar.h
。
1 | // bar.h |
执行 make
:
1 | gee@JiPing_Desktop:~/workspace/example$ vim ./func/bar.h |
不出所料,源文件果然没有重新编译。
模式规则和自动变量
我们还是先来解决问题,首先是 *.o
文件的保存问题,这个问题其实在上面已经解决了,我们再来看一遍:
1 | SUBDIR := . |
通过手动添加目标和依赖,我们实现了 *.o
文件的保存,同时还确保了源文件在更新后,只会在最小限度内重新编译 *.o
文件。现在我们可以利用符号 %
和自动变量,来让 Makefile
变得更加通用。首先聚焦于编译过程:
1 | ./entry.o : ./entry.c |
上下比较 ./entry.o
和 ./func/bar.o
的目标依赖及执行,可以发现新添加的、用于生成 *.o
文件的目标和依赖,有着相同的书写模式,这意味着存在通用的写法:
1 | %.o : %.c |
这里我们用上了 %
,它的作用有些难以用语言概括,上述例子中, %.o
的作用是匹配所有以 .o
结尾的目标;而后面的 %.c
中 %
的作用,则是将 %.o
中 %
的内容原封不动的挪过来用。
更具体地例子是,%.o
可能匹配到目标 ./entry.o
或 ./func/bar.o
,这样 %
的内容就会是 ./entry
或 ./func/bar
,最后交给 %.c
时就变成了 ./entry.c
或 ./func/bar.c
。
另外我们还使用到了自动变量 $< $@
,其中 $<
指代依赖列表中的第一个依赖;而 $@
指代目标。注意自动变量与普通变量不同,它不使用小括号。
结合起来使用,我们就得到了通用的生成 *.o
文件的写法:
1 | # Makefile |
patsubst 函数
接下来再让我们关注链接步骤,它需要指定 *.o
文件:
1 | main : ./entry.o ./func/bar.o |
这看起来十分眼熟,我们最初解决多文件编译问题时也是采用类似的写法,只有文件后缀不一样:
1 | main : ./entry.c ./func/bar.c |
这给了我们一点提示,是不是能够通过 wildcard 函数来实现通用的写法?可惜的是,在最开始我们是无法匹配到 *.o
文件的,因为起初我们只有 *.c
文件, *.o
文件是后来生成的。但转换一下思路,我们在获取所有源文件后,直接将 .c
后缀替换为 .o
,不就能得到所有的 .o
文件了吗?正巧 patsubst 函数可以用于模式文本替换。
1 | $(patsubst pattern,replacement,text) |
patsubst 函数的作用是匹配 text
文本中与 pattern
模式相同的部分,并将匹配内容替换为 replacement
。于是链接步骤可以改写为:
1 | SRCS := $(foreach dir,$(SUBDIR),$(wildcard $(dir)/*.c)) |
这里我们先用 wildcard 函数获取所有的 .c
文件,并将结果保存在 SRCS
中,接着利用 patsubst 函数替换 SRCS
的内容,最后将所有的 .c
替换为 .o
以获得执行编译所得到的目标文件。
于是我们的 Makefile
就可以改写为:
1 | SUBDIR := . |
试试效果:
1 | gee@JiPing_Desktop:~/workspace/example$ rm main |
看起来没有太大问题(但仔细看还是会发现,编译时 ./
被吃了)!至此我们解决了第一个问题,而第二个问题,我们留到后面再解决。现在先让我们看看 Makefile 还有哪些可以丰富和完善的功能。
丰富完善Makefile的功能
到目前为止,我们已经写出足够使用的 Makefile 文件了,接下来我们可以继续完善它的功能。
指定*.o文件的输出路径
细心的你可能会发现,目前编译得到的 .o
文件,都是放在与 .c
文件同一级目录下的,从代码编辑习惯考虑,这可能会导致我们无法方便地寻找源文件或头文件。
1 | gee@JiPing_Desktop:~/workspace/example$ tree |
理想的做法是将 *.o
文件保存至指定目录,与源文件和头文件区分开:
1 | gee@JiPing_Desktop:~/workspace/example$ tree |
但应该如何实现呢?先来看一下单个 .o
文件怎么输出到指定文件夹:
1 | ./output/func/bar.o : ./func/bar.c |
我们解决问题的思路是:把输出目录下的 .o
文件作为编译目标,原始目录下的 .c
文件作为依赖,来编译得到目标文件。这里我们需要解决两个问题:1. 如何得到 ./output/func/bar.o
这个路径;2. 如何保证 ./output/func
目录存在。
问题1我们可以在执行 patsubst 函数时解决,就像这样:
1 | OUTPUT := ./output |
在替换 .c
的同时,在内容头部添加输出路径 ./output/
,这样 ./func/bar.c
就会替换成 ./output/func/bar.o
。
接着问题2我们可以使用 mkdir 命令配合 dir 函数解决,dir 函数可以从文本中获得路径,配合 mkdir -p 命令创建目录,可以确保输出路径存在:
1 | mkdir -p $(dir ./output/func/bar.o) |
dir 函数会把 ./output/func/bar.o
修改成 ./output/func
,于是上面的命令就变为了:
1 | mkdir -p ./func/func |
通过修改 Makefile 来一起解决两个问题:
1 | SUBDIR := ./ |
这里我们还在 %.o
前面加了 $(OUTPUT)/
,确保匹配到的目标是要生成在输出目录的目标。
测试一下,是可以使用的:
1 | gee@JiPing_Desktop:~/workspace/example$ rm entry.o |
伪目标
在 Makefile 中我们可以利用目标执行某些动作。比如定义一个 clean
目标,用于清理编译生成的过程文件:
1 | OUTPUT := ./output |
修改后执行 make
命令时传入参数 clean
就会执行 clean
目标下的语句:
1 | gee@JiPing_Desktop:~/workspace/example$ vim ./Makefile |
在没有解决头文件依赖问题时,clean
后重新编译,也是一种临时解决方案。
但这样做存在隐患:当前目录下有与目标同名的文件时,在没有依赖的情况下,Makefile 会认为目标文件已经是最新的状态了,目标下的语句也就不再执行。
1 | gee@JiPing_Desktop:~/workspace/example$ tree |
为解决这一问题,我们可以将 clean
声明为伪目标,表明其并非是文件的命名。向特殊内置目标 .PHONY
添加 clean
依赖以达成这一目的:
1 | .PHONY : clean |
添加上 .PHONY : clean
后再执行 make clean
:
1 | gee@JiPing_Desktop:~/workspace/example$ make clean |
可以看到 clean
目标下的 rm -r $(OUTPUT)
得到了执行。
简化终端输出
现在我们的 Makefile 所输出的内容会有些杂乱无章,我们很难直观看出哪条命令在编译哪个文件。所以我们常通过 @
符号,来禁止 Makefile 将执行的命令输出至终端上:
1 | $(OUTPUT)/%.o : %.c |
添加 @
符号后,编译命令 gcc -c $(INCS) $< -o $@
就不会输出在终端上了:
1 | gee@JiPing_Desktop:~/workspace/example$ make |
同时你可能注意到 Makefile 中是可以使用终端命令的,所以我们可以用 echo 命令来拟定自己的输出信息:
1 | SUBDIR := ./ |
这是修改 Makefile
后再次执行 make
时,终端的输出:
1 | gee@JiPing_Desktop:~/workspace/example$ make clean |
相比之前简洁了许多!
自动生成依赖
还记得修改头文件后,包含该头文件的源文件不会重新编译的问题吗?现在让我们试试看解决这个问题。
问题的解决思路也很简单,就是将头文件一同加入到 *.o 文件的依赖中:
1 | ./entry.o : ./entry.c ./func/bar.h |
但这实现起来并不容易,我们需要在 Makefile 中为每个源文件单独添加头文件依赖,手动维护这些依赖关系会是一件极其痛苦的事情。幸运的是,gcc 提供了强大的自动生成依赖功能,仅需在编译时指定 -MMD
选项,就能得到记录有依赖关系的 *.d 文件。
-MMD
选项包含两个动作,一是生成依赖关系,二是保存依赖关系到 *.d 文件。与其类似的选项还有-MD
,其作用与-MMD
相同,差别在于-MD
选项会将系统头文件一同添加到依赖关系中。
另外我们还可以指定 -MP
选项,这会为每个依赖添加一个没有任何依赖的伪目标。-MP
选项生成的伪目标,可以有效避免删除头文件时,Makefile 因找不到目标来更新依赖所报的错误:``make: *** No rule to make target ‘dummy.h’, needed by ‘dummy.o’. Stop.`。
1 | gee@JiPing_Desktop:~/workspace/example_makefile$ gcc -MMD -MP -c -I. -I./func entry.c -o entry.o |
接着我们还需要将 *.d 文件记录的依赖关系,手动包含到 Makefile 中,这样才能使其真正发挥作用。所以 Makefile 又可以修改为:
-MMD
选项生成的 .d 文件保存在与 .o 文件相同的路径下。
1 | SUBDIR := ./ |
最后一行的 include
用于将指定文件的内容插入到当前文本中。初次编译,或者 make clean 后再次编译时,*.d 文件是不存在的,这通常会导致 include 操作报错。所以我们在 include
前加了 -
符号,其作用是指示 make 在 include 操作出错时忽略这个错误,不输出任何错误信息并继续执行接下来的操作。
通用模板
文章的末尾,放一个通用的 Makefile 模板吧!
1 | ROOT := $(shell pwd) |
参考链接
- 如何在 Ubuntu 20.04 上安装 GCC(build-essential)
- linux - What’s the meaning of gcc ‘-c’ and gcc ‘-o’?
- GNU make
- make - What does % symbol in Makefile mean
- What do the makefile symbols $@ and $< mean?
- What does the ‘-c’ option do in GCC?
- Automatic Prerequisites (GNU make)
- c++ - makefile dependency generation - Code Review Stack Exchange
- Include (GNU make)
文章名: 《写给初学者的Makefile入门指南》
作者: 吉平.集
写作日期: 2023.3.6 ~ 2023.3.30
发布日期: 2023.3.30