五、Linux多文件与Makefile项目管理
目录:
一、gcc多文件编译生成一个可执行文件
gcc main.c hello.c good.c -o a.out
上述命令就是多个 .c 文件编译生成一个可执行文件 a.out
它要求多个 .c 文件中只能包含一个 main() 主函数;其他文件不能包含 main() 函数,但可以有结构体或其他函数定义…在 main.c 文件调用其他文件中的函数时,需要声明
例如:执行 gcc main.c hello.c good.c -o a.out
的文件联编
main.c 如下:
#include <stdio.h>
//必须声明函数
int any_name_function1(int);
int any_name_function2(int);
int main(int argc, char *argv[])
{
//调用函数
int a = any_name_function1(10);
int b = any_name_function2(10);
printf("%d,%d\n",a,b);
return 0;
}
hello.c 如下:
int any_name_function1(int temp)
{
......
return temp;
}
good.c 如下:
int any_name_function2(int temp)
{
......
return temp;
}
满足只有一个 main() 函数的条件,而且 main.c 在调用其他文件的函数时,需要声明
二、Makefile项目管理
当需要编译的文件数量十分巨大时,在终端一个个输入 gcc 命令的方法显然不现实,为此提出了一个解决大工程编译的工具:make,描述哪些文件需要编译、哪些需要重新编译的文件就叫做 makefile,makefile 类似脚本文件,makefile 里面还可以执行系统命令
使用的时候只需要一个 make 命令即可完成整个工程的自动编译,极大的提高了软件开发的效率
注意:执行 make 命令的这个文件最好命名为 makefile 或 Makefile
1.一个规则
写 makefile 文件时遵循一个书写规则:
目标:依赖文件集合
< Tab缩进 >命令目标 目标产物/文件,即要生成的文件名
依赖文件集合 生成目标文件所依赖的源文件
命令 即需要执行的命令
命令前需要有一个 Tab(四个字符) 缩进
注意:一定要使用 Tab 键,不能使用空格键!
例如:使用 makefile 执行gcc hello.c -o hello
首先新建一个 makefile 文件,打开文件
touch makefile
sudo vi makefile
在 makefile 文件中添加以下内容,保存并退出
hello:hello.c
gcc hello.c -o hello
在 makefile 文件所在终端中执行 make 即可执行
make
如果 makefile 文件名不为 makefile 或 Makefile,则需要用 -f
指定文件名:这类文件可能为 .mk 文件
make -f my_makefile
基本原则:若想要生成目标,检查规则中的依赖文件集合是否存在,如不存在,则寻找是否有规则用来生成该依赖文件集合;
例如:使用 makefile 执行
gcc -c hello.c -o hello.o
gcc hello.o -o hello
则 makefile 文件中的内容为:
hello:hello.o
gcc hello.o -o hello
hello.o:hello.c
gcc -c hello.c -o hello.o
这里需要注意的是,我们最终要生成的目标文件要放在首位,其他文件命令的顺序我们不关心
2.产生新的疑问
如果我们想要执行 gcc main.c hello.c good.c -o a.out
的文件联编,makefile 文件应该怎么写?
根据上述知识,我们可能会这样写 makefile 文件:
a.out:main.c hello.c good.c
gcc main.c hello.c good.c -o a.out
这样写是没有错误的,但会产生一个问题:如果我现在把 hello.c 中的函数增加一些功能,返回值和参数值都不变,其他文件不变,然后重新执行 make,这个时候 make 会把 main.c、hello.c、good.c再重新编译一次。显然,我实际上只需要让 hello.c 重新编译就行了,但这样写的 makefile 会把所有文件重新编译一次,既浪费内存又浪费时间,那我们该怎么写呢?
我们把gcc链接阶段与gcc编译的前三个阶段分开写:
a.out:main.o hello.o good.o
gcc main.o hello.o good.o -o a.out
main.o:main.c
gcc -c main.c -o main.o
hello.o:hello.c
gcc -c hello.c -o hello.o
good.o:good.c
gcc -c good.c -o good.o
这样,当我们第一次 make 的时候,我们正常获得 a.out 文件
现在,即使我修改一下 hello.c,第二次 make 的时候,系统也只会对 hello.c 进行编译
其原理是:makefile 要求:目标文件的产生时间或最后修改时间必须晚于依赖文件集合的产生时间或最后修改时间,若满足该条件,则指令不会执行,当我们对 hello.c 文件进行了修改后,则 hello.o 的产生时间或最后修改时间便早于 hello.c 的产生时间或最后修改时间,于是便执行了 gcc -c hello.c -o hello.o
;同理,由于 hello.o 的重新生成,导致 a.out 的产生时间或最后修改时间早于 hello.o 的产生时间或最后修改时间,便执行了gcc main.o hello.o good.o -o a.out
,其他命令遵循了时间的先后顺序,则不执行
因此,makefile 遵循一个规则:检查规则中的目标是否需要更新,必须先检查它的所有依赖,依赖中有任一个被更新,则目标必须更新
总结:一个规则
1. 遵循 目标:依赖文件集合 < Tab >命令 的书写规范,最终目标放在首位
2. 目标的时间必须晚于依赖文件及和的时间,否则,更新目标
3. 如果依赖文件集合不存在,找寻新的规则去产生依赖文件集合
3.两个函数
src = $(wildcard ./*.c)
函数,匹配当前工作目录下的所有.c 文件组成列表,幅值给 src
*
是通配符,表示任意长度任意字符obj = $(patsubst %.c, %.o, $(src))
参数1:
%.c
, 参数2:%.o
, 参数3:$(src)
函数,将参数3中包含参数1的部分替换为参数2,幅值给 obj
$(src) 就是取 src 里的内容
其中 % 类似于通配符,它可以表示任意长度任意字符
例如:假设我当前目录下只有 main.c、hello.c、good.c 三个函数,我想要让这三个文件联编得到 a.out,利用上述两个函数,据此写出 makefile 文件
前面我们是这些写 makefile 的:
a.out:main.o hello.o good.o
gcc main.o hello.o good.o -o a.out
main.o:main.c
gcc -c main.c -o main.o
hello.o:hello.c
gcc -c hello.c -o hello.o
good.o:good.c
gcc -c good.c -o good.o
利用两个函数,我们可以这样写:
src = $(wildcard ./*.c) # main.c hello.c good.c
obj = $(patsubst %.c, %.o, $(src)) # main.o hello.o good.o
a.out:$(obj)
gcc $(obj) -o a.out
main.o:main.c
gcc -c main.c -o main.o
hello.o:hello.c
gcc -c hello.c -o hello.o
good.o:good.c
gcc -c good.c -o good.o
注意:如果 wildcard 中不是 ./*.c
,那么 patsubst 中也需要修改
例如:若现在 .c 文件放在 ./src 下,我想把生成的 .o 文件放在 ./obj 下,makefile 就要修改为:
src = $(wildcard ./src/*.c) # ./src/main.c ./src/hello.c ./src/good.c
obj = $(patsubst ./src/%.c, ./obj/%.o, $(src)) # ./obj/main.o ./obj/hello.o ./obj/good.o
a.out:$(obj)
gcc $(obj) -o a.out
./obj/main.o:./src/main.c
gcc -c ./src/main.c -o ./obj/main.o
./obj/hello.o:./src/hello.c
gcc -c ./src/hello.c -o ./obj/hello.o
./obj/good.o:./src/good.c
gcc -c ./src/good.c -o ./obj/good.o
4.make clean执行删除指令
在 makefile 中添加如下内容:
clean:
rm -rf $(obj) a.out
如下图所示:
意思是,当我们执行以下指令时:就会执行 rm -rf $(obj) a.out
,删除 main.o、hello.o、good.o、a.out 这些文件
make clean
值得注意的是,由于执行的是删除操作,为了保险起见,我们可以在执行删除前打印一下删除信息,增加 -n
后,-n
的意思是模拟执行
加上 -n
后,模拟执行删除,打印信息,不会真正执行删除,这样就可以在删除之前,查看会删除哪些文件
make clean -n
如图所示,make clean -n
并不会执行删除,但是会打印删除信息
当然,如果被删除的对象不存在,假设 a.out 不存在,执行 make clean
就会报错,无法执行;不过在新版本的 Ubuntu 中不会报错,会正常执行
在旧版本的 Ubuntu 中,我们可以将 rm
改成 -rm
,它表示报错依然执行
clean:
-rm -rf $(obj) a.out
5.三个自动化变量
自动化变量只能表示规则中命令
$@
在规则的命令中,它表示规则中的目标
$<
在规则的命令中,它表示第一个依赖文件
$^
在规则的命令中,它表示所有依赖文件集合
例如:使用自动化变量改写下述文件:
src = $(wildcard ./*.c) # main.c hello.c good.c
obj = $(patsubst %.c, %.o, $(src)) # main.o hello.o good.o
a.out:$(obj)
gcc $(obj) -o a.out
main.o:main.c
gcc -c main.c -o main.o
hello.o:hello.c
gcc -c hello.c -o hello.o
good.o:good.c
gcc -c good.c -o good.o
clean:
rm -rf $(obj) a.out
如下列代码所示:
src = $(wildcard ./*.c) # main.c hello.c good.c
obj = $(patsubst %.c, %.o, $(src)) # main.o hello.o good.o
a.out:$(obj)
gcc $^ -o $@
main.o:main.c
gcc -c $< -o $@
hello.o:hello.c
gcc -c $< -o $@
good.o:good.c
gcc -c $< -o $@
clean:
rm -rf $(obj) a.out
其中,这里的 $<
可以替换为 $^
,因为各规则各自的依赖文件只有一个
此外,这里的 $@
分别表示各规则各自的目标
6.模式规则
我们发现,上一个例子的上述三个规则都遵循下面这一个模式:即模式规则
%.o:%.c
gcc -c $< -o $@
$<
特性:如果将 $<
应用在模式规则中,它可将依赖文件集合中的依赖文件依次取出,套用模式规则,相当于将 $(obj)
里的内容依次取出
因此,我们可以将上述代码最终改写为:
src = $(wildcard ./*.c) # main.c hello.c good.c
obj = $(patsubst %.c, %.o, $(src)) # main.o hello.o good.o
a.out:$(obj)
gcc $^ -o $@
%.o:%.c
gcc -c $< -o $@
clean:
rm -rf $(obj) a.out
这样做的目的是:
1.
我们实现了即使我的项目需要更改,需要增加 .c 文件或者需要修改 .c 文件,我都不需要对 makefile 进行修改,可以直接使用 make 对项目进行编译;
2.
它仍然保留了目标时间晚于依赖文件时间不编译的特性;
3.
最后,即使 .c 文件再多,我也只需通过 make 一条指令进行编译
如果 .h 头文件在当前目录下,则不需要写进 makefile 中,因为在编译时会自动展开;但如果有非系统库 .h 头文件且不在当前目录,则需要用 -I
指定头文件,并写在 gcc 最后
7.静态模式规则
假如工作变得复杂时,如下面所示,a.out:$(obj)
需要寻找依赖文件的生成方式,发现下面有两种方式可以生成 .o 依赖文件,但系统不知道应该按哪个规则去生成依赖文件,此时就需要我们去指定
src = $(wildcard ./*.c) # main.c hello.c good.c
obj = $(patsubst %.c, %.o, $(src)) # main.o hello.o good.o
a.out:$(obj)
gcc $^ -o $@
%.o:%.c
gcc -c $< -o $@
%.o:%.s
gcc -S $< -o $@
指定规则:
src = $(wildcard ./*.c) # main.c hello.c good.c
obj = $(patsubst %.c, %.o, $(src)) # main.o hello.o good.o
a.out:$(obj)
gcc $^ -o $@
$(obj):%.o:%.c
gcc -c $< -o $@
%.o:%.s
gcc -S $< -o $@
这样我们就指定了 $(obj)
依赖文件的生成就由 %.o:%.c
来执行,就是所谓的静态模式规则
8.伪目标
在前面,我们通过 make clean
来执行删除指令,但如果我们当前目录下有 clean 这个文件的话,系统就会以为 clean:
中的 clean
是要生成的目标文件,就无法正常执行删除指令,伪目标的作用就是避免 clean 被当成目标文件:
.PHONY: <目标>
将目标视为伪目标,不管是否有依赖文件,都要执行指令
因此,最后我们可以得到 makefile 的完整格式:
src = $(wildcard ./*.c) # main.c hello.c good.c
obj = $(patsubst %.c, %.o, $(src)) # main.o hello.o good.o
a.out:$(obj)
gcc $^ -o $@
%.o:%.c
gcc -c $< -o $@
clean:
rm -rf $(obj) a.out
.PHONY: clean
假如:若现在 .c 文件放在 ./src 下,我想把生成的 .o 文件放在 ./obj 下,makefile 就要修改为:
src = $(wildcard ./src/*.c) # ./src/main.c ./src/hello.c ./src/good.c
obj = $(patsubst ./src/%.c, ./obj/%.o, $(src)) # ./obj/main.o ./obj/hello.o ./obj/good.o
a.out:$(obj)
gcc $^ -o $@
./obj/%.o:./src/%.c
gcc -c $< -o $@
clean:
rm -rf $(obj) a.out
.PHONY: clean
此外,我们还可以扩展一些其他内容,例如:假设 .h 头文件在 ./inc 下
src = $(wildcard ./*.c) # main.c hello.c good.c
obj = $(patsubst %.c, %.o, $(src)) # main.o hello.o good.o
inc_path = ./inc
myArgs = -Wall
a.out:$(obj)
gcc $^ -o $@ $(myArgs)
%.o:%.c
gcc -c $< -o $@ $(myArgs) -I $(inc_path)
clean:
rm -rf $(obj) a.out
.PHONY: clean
9.ALL 或 all 指定最终目标
当最终要生成的目标文件没有放在首位或最终要生成的目标文件有多个时,我们可以使用 ALL 或 all 进行指定
例如:把当前目录下的所有 .c 文件编译成可执行文件,且文件命名为 .c 文件去掉 .c 后缀
src = $(wildcard ./*.c)
obj = $(patsubst %.c, %, $(src))
ALL:$(obj)
%:%.c
gcc $< -o $@
clean:
rm -rf $(obj)
.PHONY: clean
此时就需要使用 ALL 或 all 来指定最终要生成的目标文件