文章目录
1. Makefile
项目复杂性带来的问题
在大型项目中,代码通常被组织成多个文件和模块,每个模块可能包含许多源文件、头文件、库文件等。直接在终端输入所有的 GCC 命令行来编译整个项目显然是不现实的。手动管理这些命令既费时又容易出错,特别是在以下情况下:
- 文件数量庞大:需要编译的文件数目非常多,手动输入命令变得繁琐。
- 模块化设计:项目被拆分成多个模块,每个模块可能由多个文件组成。
- 频繁变动:代码在开发过程中频繁修改和更新,需要重新编译部分或全部代码。
Makefile 的必要性
为了解决上述问题,我们需要一个工具来自动化和管理这些编译过程,这就是 make
。Makefile 是 make
的配置文件,定义了如何编译和链接程序的规则和依赖关系。通过 Makefile,我们可以实现以下目的:
- 自动化编译:定义好规则后,只需运行
make
命令,make
工具会自动完成编译过程。 - 依赖管理:Makefile 可以管理文件之间的依赖关系,只在必要时重新编译受影响的文件,避免重复工作。
- 提高效率:减少手动输入命令的工作量,提高开发效率和准确性。
Makefile 的规则和功能
Makefile 是由一系列规则组成的,每条规则描述了如何生成一个或多个目标文件。基本的 Makefile 规则包括:
- 目标文件:需要生成的文件,如可执行文件或对象文件。
- 依赖文件:目标文件所依赖的源文件或头文件。
- 命令:生成目标文件所需执行的命令。
2. Makefile和GCC对比
示例代码
下面用一个例子来说明Makefile如何简化编译流程。
创建一个工程内容分别main.c,sub.c,sub.h,add.c,add.h五个文件。sub.c负责计算两个数减法运算,add.c负责计算两个数加法运算,然后编译出可执行文件。
main.c
这是主程序文件,包含了add.h
和sub.h
头文件,定义了main
函数,调用了add
和sub
函数并输出结果。
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main(int argc, char *argv[])
{
printf("abc, add:%d\n", add(10, 10));
printf("abc, sub:%d\n", sub(20, 10));
return 0;
}
add.c
这个文件定义了add
函数,函数体简单地返回两个整数的和。
#include "add.h"
int add(int a, int b)
{
return a + b;
}
add.h
这是add
函数的头文件,使用了预处理指令来避免重复包含,声明了add
函数。
#ifndef __ADD_H
#define __ADD_H
int add(int a, int b);
#endif
sub.c
这个文件定义了sub
函数,函数体简单地返回两个整数的差。
#include "sub.h"
int sub(int a, int b)
{
return a - b;
}
sub.h
这是sub
函数的头文件,使用了预处理指令来避免重复包含,声明了sub
函数。
#ifndef __SUB_H
#define __SUB_H
int sub(int a, int b);
#endif
GCC
首先使用 gcc
命令对多个源文件进行编译和链接,生成一个可执行程序。
1. 编译所有源文件并链接生成可执行程序
gcc main.c sub.c add.c -o output
- 该命令将
main.c
、sub.c
和add.c
文件进行编译,并将生成的目标文件链接成名为output
的可执行文件。 - 使用
ls
命令查看当前目录,可以看到源文件和生成的可执行文件。 - 运行生成的可执行文件
./output
,可以看到程序输出的结果。
输出:
abc, add:20
abc, sub:10
该命令将所有源文件一起编译并链接,生成了最终的可执行文件。
2. 拆分编译和链接步骤
对于一个大型项目,仅仅修改了一个源文件时,如果重新编译所有文件将非常耗时。因此,可以使用拆分编译和链接步骤来提高效率。
-
分别编译每个源文件,生成目标文件:
gcc -c main.c
gcc -c sub.c
gcc -c add.c
-
将生成的目标文件链接成一个可执行文件:
gcc main.o sub.o add.o -o output
这样做的好处是,如果只修改了某个源文件,只需重新编译该文件,而无需重新编译所有文件。例如,如果我们修改了 add.c
文件,只需要重新编译 add.c
:
gcc -c add.c
gcc main.o sub.o add.o -o output
这种方法显然可以节省时间,但仍然存在几个问题:
- 如果源文件很多,手动管理编译和链接命令会非常繁琐。
- 每次添加新源文件或修改文件时,必须手动更新编译和链接命令。
- 如果头文件内容变化,手动管理依赖关系会非常复杂。
Makefile
为了自动化管理编译和链接过程,我们可以使用 Makefile
。Makefile
是一个定义了一系列规则和依赖关系的文件,用于自动化构建过程。通过 Makefile
,可以简化复杂项目的编译过程,提高开发效率。
Makefile
可以自动检测文件的变化,并根据定义的规则只重新编译需要更新的文件,而不必每次都重新编译所有文件。这解决了手动管理编译命令和依赖关系的问题,使得项目构建更加高效和可靠。
编写 Makefile
output: main.o add.o sub.o
gcc -o output main.o add.o sub.o
main.o: main.c
gcc -c main.c
add.o: add.c
gcc -c add.c
sub.o: sub.c
gcc -c sub.c
clean:
rm *.o output
这个 Makefile
定义了以下几项规则:
目标文件 output
:
- 依赖于
main.o
、add.o
和sub.o
。 - 命令行
gcc -o output main.o add.o sub.o
表示当main.o
、add.o
和sub.o
这些目标文件发生变化时,重新链接生成output
可执行文件。
目标文件 main.o
:
- 依赖于源文件
main.c
。 - 命令行
gcc -c main.c
表示当main.c
文件发生变化时,重新编译生成main.o
目标文件。
目标文件 add.o
:
- 依赖于源文件
add.c
。 - 命令行
gcc -c add.c
表示当add.c
文件发生变化时,重新编译生成add.o
目标文件。
目标文件 sub.o
:
- 依赖于源文件
sub.c
。 - 命令行
gcc -c sub.c
表示当sub.c
文件发生变化时,重新编译生成sub.o
目标文件。
清理命令 clean
:
- 删除所有的目标文件和可执行文件。
使用 Makefile
编写好 Makefile
之后,只需要在终端中执行 make
命令,make
工具会根据 Makefile
中定义的规则自动化地管理编译过程。
-
查看当前目录内容:
$ ls
add.c add.h main.c Makefile sub.c sub.h
-
执行
make
命令:
$ make
gcc -c main.c
gcc -c add.c
gcc -c sub.c
gcc -o output main.o add.o sub.o
-
查看生成的文件:
$ ls
add.c add.h main.c Makefile output sub.c sub.h main.o add.o sub.o
-
再次执行
make
命令:如果没有文件发生变化,
make
会显示up to date
,表示所有文件已经是最新的,不需要重新编译:
$ make
make: 'output' is up to date.
-
修改某个源文件并重新编译:
假设修改了
add.c
文件,只需要重新编译该文件,并将所有的目标文件链接成新的可执行文件:
$ make
gcc -c add.c
gcc -o output main.o add.o sub.o
通过这种方式,Makefile
使得编译过程自动化,避免了手动管理多个文件的复杂性,提高了开发效率。
3. Makefile的规则
3.1 命名规则
Makefile
文件的命名可以是 Makefile
或 makefile
,都可以被 make
工具识别。尽量使用这两个标准命名,以避免配置 make
命令时的额外麻烦。不过,你也可以使用其他名字,比如 makefile.linux
,此时需要使用 -f
参数指定文件名:
make -f makefile.linux
Makefile
的基本语法规则包括:
目标(target):
- 目标是
Makefile
中需要生成的文件或执行的任务。例如,可执行文件或目标文件。
依赖(prerequisites):
- 依赖是生成目标所需要的一些文件。如果依赖文件发生变化,
make
将重新生成目标。
命令(command):
- 命令是为了生成目标所需要执行的具体命令。这些命令通常是
shell
命令,必须以Tab
键开头。
一个目标和依赖的基本格式如下:
target: prerequisites
[Tab]command
示例:
假设在命令行中输入以下编译命令:
gcc main.c -o main
对应的 Makefile
可以写成:
main: main.c
gcc main.c -o main
- 第一行定义了目标
main
依赖于main.c
文件。 - 第二行是生成
main
可执行文件所需要的命令gcc main.c -o main
。注意命令前必须有一个Tab
键。
注意事项
Makefile
中的命令行必须以Tab
键开头,不能使用空格。Makefile
中的每条命令都在一个新的shell
环境中执行,这意味着在同一规则的多行命令中,前一行的环境变化不会影响到下一行。- 使用
-f
参数可以指定不同的Makefile
文件名,方便在不同环境或项目中使用。
3.2 目标生成规则
目标生成是指 Makefile
根据规则生成目标文件的过程。
检查依赖文件:
Makefile
首先会检查目标文件所依赖的文件是否存在。
生成依赖文件:
- 如果依赖文件不存在,
Makefile
将根据规则生成依赖文件。
- 检查依赖1:如果存在,继续检查下一个依赖。
- 检查依赖2:如果存在,继续检查下一个依赖。
- 检查依赖3:如果不存在,寻找规则生成依赖3。
- 编译生成目标:所有依赖都存在后,最终生成目标文件。
3.3 目标更新
目标更新是指当依赖文件发生变化时,Makefile
重新生成目标文件的过程。
检查依赖更新:
Makefile
会检查目标文件的所有依赖文件,任何一个依赖文件有更新,都会触发目标文件的重新生成。
时间比较:
Makefile
通过比较目标文件和依赖文件的时间戳来决定是否需要更新。如果依赖文件的时间戳比目标文件更新,则需要更新目标文件。
- 检查依赖更新1:如果未更新,继续检查下一个依赖。
- 检查依赖更新2:如果未更新,继续检查下一个依赖。
- 检查依赖更新3:如果更新,重新生成依赖并更新目标。
4. 代码示例
Makefile 示例
output: main.o add.o sub.o
gcc -o output main.o add.o sub.o
main.o: main.c
gcc -c main.c
add.o: add.c
gcc -c add.c
sub.o: sub.c
gcc -c sub.c
clean:
rm *.o output
编译执行过程
当前目录下有 main.c
、add.c
、sub.c
以及上述 Makefile 文件。现在在终端中执行 make
命令来编译这些源文件:
$ make
这个命令的输出如下:
gcc -c main.c
gcc -c add.c
gcc -c sub.c
gcc -o output main.o add.o sub.o
初次执行 make
命令:
-
make
命令会检测output
目标的依赖main.o
、add.o
和sub.o
是否存在。如果不存在,则会寻找生成这些依赖的命令。 -
output
依赖于main.o
、add.o
和sub.o
,这三个文件都不存在,所以make
会首先生成它们。依赖关系如下:main.o
依赖main.c
,于是使用命令gcc -c main.c
来生成main.o
。add.o
依赖add.c
,于是使用命令gcc -c add.c
来生成add.o
。sub.o
依赖sub.c
,于是使用命令gcc -c sub.c
来生成sub.o
。
-
现在
main.o
、add.o
和sub.o
都已经生成,但是output
文件还没有生成,所以make
会继续执行gcc -o output main.o add.o sub.o
命令来生成output
。
当修改某个文件时:
比如,我们修改了 add.c
文件,然后再次执行 make
命令:
$ make
这个命令的输出如下:
gcc -c add.c
gcc -o output main.o add.o sub.o
-
make
命令会再次检查依赖关系,发现add.c
文件比add.o
文件更新,所以需要重新编译add.c
文件生成add.o
。 -
其他文件
main.o
和sub.o
没有变化,所以不需要重新编译。 -
最后,
make
会执行gcc -o output main.o add.o sub.o
命令来生成新的可执行文件output
。
通过 Makefile
,我们可以轻松管理源文件的编译过程,避免手动输入复杂的编译命令。make
命令会自动检测依赖关系,只编译需要更新的文件,提高了编译效率。