简单实用Makefile教程
#Motivation
在我们初学编程时,通常只写一个 .c 文件就能完成相应的计算(力扣 or 牛客算法题)。此时要运行写好的程序,有两种方式,
- 借助 IDE ,比如 Visual Studio ,点击运行按钮,自动帮我们编译,之后顺利运行
- 文本编辑器 + GCC ,通常是在 Linux 平台下,这时候就需要我们自己手动编译。但也就一条 gcc 命令就能搞定,
gcc nowcoder01.c -o newcoder01
上述是初学的状态,工程量很小,涉及到的文件也不多,编译也不耗时。所以,这样做完全没有问题
但是,如果工程量变得很大,工程有很多 .c 和 .h 文件。此时,再采用手动输入 gcc 编译命令的方式,效率就未免太低下了。理由我认为主要有两点,
- 每次重新编译都要在终端中输入冗长的编译命令且易出错。设想工程中有7个 .c 和 2个 .h ,那么每次的编译命令长这样,
gcc main.c create.c read.c update.c delete.c data.c
- 每次的命令输入都会将所有文件都重新编译一次,哪怕只修改了一个文件(比如,仅仅修改了 create.c ,但却把其他6个 .c 一并也重新编译了)。这样做,很耗时。最正常的想法,是没改动的文件就不需要,也不应该被重新编译
基于以上两点,提出了 make 的想法。在 Windows 平台下,Visual C++ 帮我们做了此类自动化编译的工作。但在 Linux 下,就没有这种好事了,需要我们自己手写一个叫做 Makefile 的文件,来间接地完成自动化编译工作
#Solution
我们都知道,程序从 .c 到可执行,这期间有两大步要走。这第一步,就是要将 .c 转为 .obj ,这个过程叫做编译( complie );这第二步,需将生成的 .obj 中一些函数和全局变量重定位,这叫链接( link )。完事之后,才能翻译成可执行的二进制代码
在清楚基本编译链接常识之后,就可以着手开始编写 Makefile 啦
假设,现在工程里有7个 .c 和 2个 .h ,分别为程序的入口 main.c ,业务模块 create.c 、read.c 、update.c 和 delete.c ,数据模块 data.c 及 data.h ,还有掌管绝大多数变量和函数声明的 defs.h 和工具包 utils.c
各文件之间的关系,大概就是,main.c 会调用业务模块的各个功能,而业务模块又依赖于 data.h 中定义的数据结构。当然啦,要想程序正常编译运行,自然离不开 defs.h 。有些功能还会需要 utils.c
这里姑且贴下各模块文件的大致代码,待会写 Makefile 时也好心中有数。代码在文末,可查阅
好,文件依赖关系分析清楚之后,就可以动手开始写 Makefile 了。但是,在此之前,还要注意两点,
- Makefile 的书写基本规则
target ... : prerequisites ...
command
...
其中 target 是目标文件,可以是 Object File ,也可以是可执行文件。prerequisites 是生成 target 所需的文件。command 就是 make 要执行的命令,请注意 command 在第二行,它不能顶格写,要 Tab 回退才行
- GCC 的编译选项
gcc -o testmk main.o ...
-o 可以理解成链接命令,把后面跟着的所有 .o 重定向,生成 testmk 可执行文件
gcc -c main.c
-c 可以理解成编译命令,将 main.c 翻译成 Object File
#Example 1
注意点就如以上这么多,下面开始书写第一版本(入门级),
# 第一版本(入门级)
testmk : main.o utils.o data.o \
create.o read.o update.o delete.o
gcc -o testmk main.o utils.o data.o \
create.o read.o update.o delete.o
main.o : main.c defs.h
gcc -c main.c
utils.o : utils.c defs.h
gcc -c utils.c
data.o : data.c data.h
gcc -c data.c
create.o : create.c defs.h
gcc -c create.c
read.o : read.c defs.h
gcc -c read.c
update.o : update.c defs.h
gcc -c update.c
delete.o : delete.c defs.h
gcc -c delete.c
clean :
rm testmk main.o utils.o data.o \
create.o read.o update.o delete.o
Makefile 一开始最好就要表明最终需要的可执行文件 testmk 的依赖关系,从上面的代码可以看出 testmk 需要 main.o 等全部 Object Files 。其中 \
表示换行符,紧接着就是输入命令,我们是想通过链接所有的 Object Files 生成可执行文件,所以用,
gcc -o testmk main.o utils.o data.o \
create.o read.o update.o delete.o
往下的各个 target 都是 Object Files ,生成 Object File 需要 .c or .h 等文件依赖,这个很容易理解(自己可以细细体会)
main.o : main.c defs.h
gcc -c main.c
最后的 clean 是 Makefile 一般都会有的一个环节,通常也是放在文件的最后。主要是用来清除之前生成的 Object Files 和可执行文件
如此写完,其实已经可以正常工作了。但是,这种写法还是存在很多问题的。比如,没有将众多 Object Files 合并管理,当 Object File 越来越多时,Makefile 也更难修改
#Example 2
针对 Example 1 的缺点,想出了第二版本(中级版),
# 第二版本(中级版)
OBJ = main.o utils.o data.o \
create.o read.o update.o delete.o
testmk : $(OBJ)
gcc -o testmk $(OBJ)
main.o : main.c defs.h
gcc -c main.c
utils.o : utils.c defs.h
gcc -c utils.c
data.o : data.c data.h
gcc -c data.c
create.o : create.c defs.h
gcc -c create.c
read.o : read.c defs.h
gcc -c read.c
update.o : update.c defs.h
gcc -c update.c
delete.o : delete.c defs.h
gcc -c delete.c
clean :
rm testmk $(OBJ)
这就很好的管理了众多的 Object Files ,用 OBJ
标签来替代。但是,我们还是觉得有提升的空间,从何处突破呢?
在编译各个 .c 和 .h 时,之前的两种写法都是把依赖关系写得很清晰。其实大可不必,因为 Makefile 会自己帮我们找与 Object File 同名的 .c 和 .h 作为依赖关系。所以,写法还可以简化
#Example 3
第三版本(精简版),
# 第三版本(精简版)
OBJ = main.o utils.o data.o \
create.o read.o update.o delete.o
testmk : $(OBJ)
gcc -o testmk $(OBJ)
main.o : defs.h
utils.o : defs.h
create.o : defs.h
read.o : defs.h
update.o : defs.h
delete.o : defs.h
clean :
rm testmk $(OBJ)
这就省去了各个同名的 .c 文件,没有出现 data.o 的原因,是 data.o 仅仅依赖于 data.c 和 data.h ,这都是同名
以上就完成了 Makefile 大致的基本写法
#Source
如需深入学习 Makefile ,了解其更多的细节,可以查阅资料,
- 百度网盘-陈皓《跟我一起写makefile》
提取码: ekdq
#Code
/** data.h */
typedef int pkey_t;
typedef struct {
int pkey;
} data_t;
#define DATASIZE 1024
/** data.c */
#include "data.h"
data_t datas[DATASIZE];
/** defs.h */
#include "data.h"
#define IN
#define OUT
// utils.c
void func();
// create.c
int create_table(IN data_t*, OUT data_t**);
// read.c
int read_data(IN pkey_t, IN data_t*, OUT data_t*);
// update.c
int update_data(IN pkey_t, IN data_t*, IN data_t*);
// delete.c
int delete_data(IN pkey_t, IN data_t*);
/** utils.c */
#include <stdio.h>
#include "defs.h"
void func()
{
printf("in func\n");
printf("out func\n");
}
/** create.c */
#include "defs.h"
int
create_table(IN data_t* data, OUT data_t** tbl)
{
// 新建tbl
// 并把data塞到tbl中
}
/** read.c */
#include "defs.h"
int
read_data(IN pkey_t pkey, IN data_t* tbl, OUT data_t* result)
{
// 在tbl中寻找主键为pkey的data
// 并将其写回至result中
}
/** update.c */
#include "defs.h"
int
update_data(IN pkey_t pkey, IN data_t* new_data, IN data_t* tbl)
{
// 先在tbl中定位到主键为pkey的data
// 然后将其替换为new_data
}
/** delete.c */
#include "defs.h"
int
delete_data(IN pkey_t pkey, IN data_t* tbl)
{
// 在tbl中定位到主键为pkey的data
// 并删除
}
/** main.c */
#include <stdio.h>
#include "defs.h"
extern data_t datas[];
int main()
{
// 抽象逻辑,别当真
// 悟出其中的道,就可以啦
printf("in main\n");
// func();
// create_table(...);
// read_data(...);
// update_data(...);
// delete_data(...);
printf("out main\n");
}