C程序的编译通常分两步:
- 将每个
.c文件编译为.o文件; - 将所有
.o文件链接为最终的可执行文件。
我们假设如下的一个C项目,包含hello.c、hello.h和main.c。
hello.c内容如下:
#include <stdio.h>int hello(){printf("hello, world!\n");return 0;}
hello.h内容如下:
main.c内容如下:
#include <stdio.h>#include "hello.h"int main(){printf("start...\n");hello();printf("exit.\n");return 0;}
注意到main.c引用了头文件hello.h。我们很容易梳理出需要生成的文件,逻辑如下:
┌───────┐ ┌───────┐ ┌───────┐│hello.c│ │main.c │ │hello.h│└───────┘ └───────┘ └───────┘│ │ ││ └────┬────┘│ │▼ ▼┌───────┐ ┌───────┐│hello.o│ │main.o │└───────┘ └───────┘│ │└───────┬──────┘│▼┌─────────┐│world.out│└─────────┘
假定最终生成的可执行文件是world.out,中间步骤还需要生成hello.o和main.o两个文件。根据上述依赖关系,我们可以很容易地写出Makefile如下:
# 生成可执行文件:world.out: hello.o main.occ -o world.out hello.o main.o# 编译 hello.c:hello.o: hello.ccc -c hello.c# 编译 main.c:main.o: main.c hello.hcc -c main.cclean:rm -f *.o world.out
执行make,输出如下:
$ makecc -c hello.ccc -c main.ccc -o world.out hello.o main.o
在当前目录下可以看到hello.o、main.o以及最终的可执行程序world.out。执行world.out:
$ ./world.outstart...hello, world!exit.
与我们预期相符。
修改hello.c,把输出改为"hello, bob!\n",再执行make,观察输出:
$ makecc -c hello.ccc -o world.out hello.o main.o
仅重新编译了hello.c,并未编译main.c。由于hello.o已更新,所以,仍然要重新生成world.out。执行world.out:
$ ./world.outstart...hello, bob!exit.
与我们预期相符。
修改hello.h:
// int 变为 void:void hello();
以及hello.c,再次执行make:
$ makecc -c hello.ccc -c main.ccc -o world.out hello.o main.o
会触发main.c的编译,因为main.c依赖hello.h。
执行make clean会删除所有的.o文件,以及可执行文件world.out,再次执行make就会强制全量编译:
$ make clean && makerm -f *.o world.outcc -c hello.ccc -c main.ccc -o world.out hello.o main.o
这个简单的Makefile使我们能自动化编译C程序,十分方便。
不过,随着越来越多的.c文件被添加进来,如何高效维护Makefile的规则?我们后面继续讲解。
参考源码
可以从GitHub下载源码。
小结
在Makefile正确定义规则后,我们就能用make自动化编译C程序。
