五、Linux多文件与Makefile项目管理

本文详细介绍了在Linux环境下如何管理多个源文件,包括使用gcc进行多文件编译以及通过Makefile实现自动化编译。Makefile的规则、自动化变量、模式规则和伪目标等概念被逐一解析,强调了其在提高软件开发效率中的作用。同时,讲解了如何避免不必要的重复编译,以及如何编写clean目标来清理编译生成的临时文件。
摘要由CSDN通过智能技术生成

五、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,描述哪些文件需要编译、哪些需要重新编译的文件就叫做 makefilemakefile 类似脚本文件,makefile 里面还可以执行系统命令

使用的时候只需要一个 make 命令即可完成整个工程的自动编译,极大的提高了软件开发的效率

注意:执行 make 命令的这个文件最好命名为 makefileMakefile

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 文件名不为 makefileMakefile,则需要用 -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.chello.cgood.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.cgood.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 指定最终目标

当最终要生成的目标文件没有放在首位最终要生成的目标文件有多个时,我们可以使用 ALLall 进行指定

例如:把当前目录下的所有 .c 文件编译成可执行文件,且文件命名为 .c 文件去掉 .c 后缀

src = $(wildcard ./*.c)
obj = $(patsubst %.c, %, $(src))

ALL:$(obj)

%:%.c
	gcc $< -o $@

clean:
	rm -rf $(obj)

.PHONY: clean

此时就需要使用 ALLall 来指定最终要生成的目标文件

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值