C程序的编译通常分两步:

  • 将每个.c文件编译为.o文件;
  • 将所有.o文件链接为最终的可执行文件。

我们假设如下的一个C项目,包含hello.chello.hmain.c

hello.c内容如下:

  1. #include <stdio.h>
  2. int hello()
  3. {
  4. printf("hello, world!\n");
  5. return 0;
  6. }

hello.h内容如下:

main.c内容如下:

  1. #include <stdio.h>
  2. #include "hello.h"
  3. int main()
  4. {
  5. printf("start...\n");
  6. hello();
  7. printf("exit.\n");
  8. return 0;
  9. }

注意到main.c引用了头文件hello.h。我们很容易梳理出需要生成的文件,逻辑如下:

  1. ┌───────┐ ┌───────┐ ┌───────┐
  2. hello.c main.c hello.h
  3. └───────┘ └───────┘ └───────┘
  4. └────┬────┘
  5. ┌───────┐ ┌───────┐
  6. hello.o main.o
  7. └───────┘ └───────┘
  8. └───────┬──────┘
  9. ┌─────────┐
  10. world.out
  11. └─────────┘

假定最终生成的可执行文件是world.out,中间步骤还需要生成hello.omain.o两个文件。根据上述依赖关系,我们可以很容易地写出Makefile如下:

  1. # 生成可执行文件:
  2. world.out: hello.o main.o
  3. cc -o world.out hello.o main.o
  4. # 编译 hello.c:
  5. hello.o: hello.c
  6. cc -c hello.c
  7. # 编译 main.c:
  8. main.o: main.c hello.h
  9. cc -c main.c
  10. clean:
  11. rm -f *.o world.out

执行make,输出如下:

  1. $ make
  2. cc -c hello.c
  3. cc -c main.c
  4. cc -o world.out hello.o main.o

在当前目录下可以看到hello.omain.o以及最终的可执行程序world.out。执行world.out

  1. $ ./world.out
  2. start...
  3. hello, world!
  4. exit.

与我们预期相符。

修改hello.c,把输出改为"hello, bob!\n",再执行make,观察输出:

  1. $ make
  2. cc -c hello.c
  3. cc -o world.out hello.o main.o

仅重新编译了hello.c,并未编译main.c。由于hello.o已更新,所以,仍然要重新生成world.out。执行world.out

  1. $ ./world.out
  2. start...
  3. hello, bob!
  4. exit.

与我们预期相符。

修改hello.h

  1. // int 变为 void:
  2. void hello();

以及hello.c,再次执行make

  1. $ make
  2. cc -c hello.c
  3. cc -c main.c
  4. cc -o world.out hello.o main.o

会触发main.c的编译,因为main.c依赖hello.h

执行make clean会删除所有的.o文件,以及可执行文件world.out,再次执行make就会强制全量编译:

  1. $ make clean && make
  2. rm -f *.o world.out
  3. cc -c hello.c
  4. cc -c main.c
  5. cc -o world.out hello.o main.o

这个简单的Makefile使我们能自动化编译C程序,十分方便。

不过,随着越来越多的.c文件被添加进来,如何高效维护Makefile的规则?我们后面继续讲解。

参考源码

可以从GitHub下载源码。

GitHub

小结

Makefile正确定义规则后,我们就能用make自动化编译C程序。