廖雪峰
资深软件开发工程师,业余马拉松选手。
C程序的编译通常分两步:
将每个.c文件编译为.o文件;
将所有.o文件链接为最终的可执行文件。
我们假设如下的一个C项目,包含hello.c、hello.h和main.c。
hello.c内容如下:
#include
int hello()
{
printf("hello, world!\n");
return 0;
}
hello.h内容如下:
int hello();
main.c内容如下:
#include
#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.o
cc -o world.out hello.o main.o
# 编译 hello.c:
hello.o: hello.c
cc -c hello.c
# 编译 main.c:
main.o: main.c hello.h
cc -c main.c
clean:
rm -f *.o world.out
执行make,输出如下:
$ make
cc -c hello.c
cc -c main.c
cc -o world.out hello.o main.o
在当前目录下可以看到hello.o、main.o以及最终的可执行程序world.out。执行world.out:
$ ./world.out
start...
hello, world!
exit.
与我们预期相符。
修改hello.c,把输出改为"hello, bob!\n",再执行make,观察输出:
$ make
cc -c hello.c
cc -o world.out hello.o main.o
仅重新编译了hello.c,并未编译main.c。由于hello.o已更新,所以,仍然要重新生成world.out。执行world.out:
$ ./world.out
start...
hello, bob!
exit.
与我们预期相符。
修改hello.h:
// int 变为 void:
void hello();
以及hello.c,再次执行make:
$ make
cc -c hello.c
cc -c main.c
cc -o world.out hello.o main.o
会触发main.c的编译,因为main.c依赖hello.h。
执行make clean会删除所有的.o文件,以及可执行文件world.out,再次执行make就会强制全量编译:
$ make clean && make
rm -f *.o world.out
cc -c hello.c
cc -c main.c
cc -o world.out hello.o main.o
这个简单的Makefile使我们能自动化编译C程序,十分方便。
不过,随着越来越多的.c文件被添加进来,如何高效维护Makefile的规则?我们后面继续讲解。
参考源码
可以从GitHub下载源码。
GitHub
小结
在Makefile正确定义规则后,我们就能用make自动化编译C程序。