自学Linux 6—Makefile 文件简介

  使用 GCC 的命令进行程序编译在单个文件下是比较方便的,当工程中的文件逐渐增多,甚至变得十分庞大的时候,使用 GCC 命令编译就会变得力不从心。Linux 中的 make 工具提供了一种管理工程的功能,可以方便地进行重新编译。

一、一个多文件的工程例子

  有一个工程中的文件列表如下图(文件目录结构)所示。工程中共有 5 个文件,在 add 目录中有 add_int.cadd_float.c,两个文件分别计算整型和浮点型的相加:在 sub 目录下有文件 sub_int.csub_float.c ,分别计算整型和浮点型的相减;顶层目录有 main.c 文件负责整个程序。
文件目录结构
  工程中的代码分别存放在 add/add_int.cadd/add_float.cadd/add.hsub/sub_int.csub/sub_float.csub/sub.hmain.c 中。

1. 文件 main.c

  文件 main.c 的代码如下。在 main() 函数中调用整数、浮点的加减运算函数进行数值计算。

/* main.c */
#include <stdio.h>
/* 需要包含的头文件 */
#include "add.h"
#include "sub.h" 
int main(void)
{
	/* 声明计算所用的变量 a、b为整型,x、y为浮点型 */
	int a = 10, b = 12;
	float x = 1.23456,y = 9.87654321;
	/* 调用函数并将计算结果打印出来 */
	printf("int a+b IS:%d\n",add_int(a,b)); 		/* 计算整型加 */ 
	printf("int a-b IS:%d\n",sub_int(a,b)); 		/* 计算整型减 */
	printf("float x+y IS:%f\n",add_float(x,y)); 	/* 计算浮点型加 */ 
	printf("float x-y IS:%f\n",sub_float(x,y)); 	/* 计算浮点型减 */
	return 0;
}
2. 加操作

  文件 add.h 的代码如下,包含整数和浮点数的求和函数声明。

/* add.h */ 
#ifndef _ADD_H_
#define _ADD_H_
/* 整型加和浮点型加的声明 */
extern int add_int(int a, int b);
extern float add_float(float a, float b);
#endif /* _ADD_H_*/

  文件 add_float.c 的代码如下,函数 add_float() 进行浮点型数值的相加计算。

/* add_float.c */
/* 浮点数求和函数 */
float add_float(float a, float b)
{
	return a+b;
}

  文件 add_int.c 的代码如下,函数 add_int() 进行整数型数值的相加计算。

/* add_int.c */
/* 整数求和函数 */
int add_int(int a, int b)
{
	return a+b;
}
3. 减操作

  文件 sub.h 的代码如下,包含整数和浮点数的相减函数声明:

/* sub.h */
#ifndef _SUB_H_ 
#define _SUB_H_
/* 整型减和浮点型减的声明 */
extern float sub_float(float a, float b); 
extern int sub_int(int a, int b);
#endif	/*_SUB_H_ */

  文件 sub_int.c 的代码如下,函数 sub_int() 进行整型的相减计算。

/* sub_int.c */
/* 整数相减函数 */
int sub_int(int a, int b)
{
	return a-b;
}

  文件 sub_float.c 的代码如下,函数 sub_float() 进行浮点型的相减计算。

/* sub_float.c */
/* 浮点数相减函数 */
float sub_float(float a, float b)
{
	return a-b;
}
二、多文件工程的编译

  前面提到的多文件工程编译成可执行文件有两种方法。
  一种是命令行操作,手动输入将源文件编译为可执行文件。
  另一种是编写 Makefile 文件,通过 make 命令将多个文件编译为可执行文件。

1.命令行编译程序

  要将此文件编译为可执行文件 cacu,如果用 gcc 进行手动编译,是比较麻烦的。例如, 下面的编译方式每行编译一个 C 文件,生成目标文件,最后将 5 个目标文件编译成可执行文件。

$gcc -c add/add_int.c -o add/add_int.o		(生成 add_int.o 目标函数)
$gcc -c add/add_float.c -o add/add_float.o 	(生成 add_float.o 目标函数) 
$gcc -c sub/sub_int.c -o sub/sub_int.o 		(生成 sub_int.o 目标函数)
$gcc -c sub/sub_float.c -o sub/sub_float.o 	(生成 sub_float.o 目标函数)
$gcc -c main.c -o main.o					(生成 main.o 目标函数)
$gcc -o cacu add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o (链接生成 cacu )

  或者使用 gcc 的默认规则,使用一条命令直接生成可执行文件 cacu

$gcc -o cacu add/add_int.c add/add_float.c sub/sub_int.c sub/sub_float.c main.c
2. 多文件的 Makefile

  利用上面一条命令直接产生可执行文件的方法,生成执行文件是比较容易的。但是当频繁修改源文件或者当项目中的文件比较多、关系比较复杂时,用 gcc 直接进行编译就会变得十分困难。

  使用 make 进行项目管理,需要一个 Makefile 文件,make 在进行编译的时候,从 Makefile 文件中读取设置情况,进行解析后运行相关的规则。make 程序査找当前目录下的文件 Makefile 或者 makefile,按照其规则运行。例如,建立一个如下规则的 Makefile 文件。

#生成 cacu ,":" 右边为目标
cacu:add_int.o add_float.o sub_int.o sub_float.o main.o 
	gcc -o cacu add/add_int.o add/add—float.o \ 		(连接符)
		sub/sub_int.o sub/sub一float.o main.o 
#生成 add_int.o 的规则,将 add_int.o 编译成目标文件 add_int.o 
add_int.o:add/add_int.c add/add.h
	gcc -c -o add/add一int.o add/add_int.c 
#生成 add_float.o 的规则
add_float.o:add/add_float.c add/add.h
	gcc -c -o add/add_float.o add/add_float.c 
#生成 sub_int.o 的规则 
sub_int.o:sub/sub_int.c sub/sub.h
	gcc -c -o sub/sub_int.o sub/sub_int.c 
#生成 sub_float.o 的规则 
sub_float.o:sub/sub_float.c sub/sub.h
	gcc -c -o sub/sub_float.o sub/sub_float.c 
#生成 main.o 的规则
main.o:main.c add/add.h sub/sub.h
	gcc -c -o main.o main.c -Iadd -Isub
#清理的规则 
clean:
	rm -f cacu add/add_int.o add/add_float.o sub/sub int.o sub/sub float.o main.o
3. 多文件的编译

  编译多文件的项目,在上面 Makefile 文件编写完毕后,运行 make 命令:

$make
gcc -c -o add/add_int.o add/add_int.c 
gcc -c -o add/add_float.o add/add_float.c 
gcc -c -o sub/sub_int.o sub/sub_int.c 
gcc -c -o sub/sub_float.o sub/sub_float.c 
gcc -c -o main.o main.c -Iadd -Isub 
gcc -o cacu add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o

  在 add 目录下生成了 add_int.oadd_float.o 两个目标文件,在 sub 目录下生成了 sub_int.osub_float.o 两个文件,在主目录下生成了 main.o 目标文件,并生成了 cacu 最终文件。
  默认情况下会执行 Makefile 中的第一个规则,即 cacu 相关的规则,而 cacu 规则依赖于多个目标文件 add_int.oadd_float.osub_int.osub_float.omain.o。编译器会先生成上述目标文件后执行下述命令:

$(CC) -o $(TARGET)  $(OBJS)  $(CFLAGS)

  上述命令将多个目标文件编译成可执行文件 cacu,即:

gcc -o cacu add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o -Iadd -Isub -O2

  命令 make clean 会调用 clean 相关的规则,清除编译出来的目标文件,以及 cacu。 例如:

$make clean
rm -f cacu add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o

  clean 规则会执行 “ -$(RM)$(TARGET)$(OBJS) ” 命令,将定义的变量扩展后为:

rm -f cacu add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o
三、Makefile 的规则

  Makefile 的框架是由规则构成的。make 命令执行时先在 Makefile 文件中查找各种规则,对各种规则进行解析后运行规则。规则的基本格式为:

TARGET… :DEPENDEDS... 
COMMAND
...
...

  █ TARGET:规则所定义的目标。通常规则是最后生成的可执行文件的文件名或者为了生成可执行文件而依赖的目标文件的文件名,也可以是一个动作,称之为 “伪目标”。
  █ DEPENDEDS:执行此规则所必需的依赖条件,例如生成可执行文件的目标文件。DEPENDEDS 也可以是某个 TARGET,这样就形成了 TARGET 之间的嵌套。
  █ COMMAND:规则所执行的命令,即规则的动作,例如编译文件、生成库文件、进入目录等。动作可以是多个,每个命令占一行。
规则的形式比较简单,要写好一个 Makefile 需要注意一些地方,并对执行的过程有所了解。

1. 规则的书写

  在书写规则的时候,为了使 Makefile 更加清晰,要用反斜杠( ** )将较长的行分解为多行,例如将 “rm -f cacu add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o” 分解为了两行。

  命令行必须以 Tab 键开始,make 程序把出现在一条规则之后的所有连续的以 Tab 键开始的行都作为命令行处理。
  ㊨ 注意:规则书写时要注意 COMMAND 的位置,COMMAND 前面的空白是一个 Tab 键, 不是空格。Tab 告诉 make 这是一个命令行,make 执行相应的动作.

2. 目标

  Makefile 的目标可以是具体的文件,也可以是某个动作。例如目标 cacu 就是生成 cacu 的规则,有很多的依赖项及相关的命令动作。而 clean 是清除当前生成文件的一个动作,不会生成任何目标项。

3. 依赖项

  依赖项是目标生成所必须满足的条件,例如生成 cacu 需要依赖 main.omain.o 必须存在才能执行生成 cacu 的命令,即依赖项的动作在 TARGET 的命令之前执行。依赖项之间的顺序按照自左向右的顺序检查或者执行。例如,下面的规则:

main.o:main.c add/add.h sub/sub.h
	gcc -c -o main.o main.c -Iadd -Isub

  main.cadd/add.hsub/sub.h 必须都存在才能执行动作 “ gcc-c-o main.o main.c-Iadd-Isub ”。当 add/add.h 不存在时,是不会执行规则的命令动作的,而且也不会检査 sub/sub.h 文件的存在,当然 main.c 由于在 add/add.h 依赖项之前,会先确认此项没有问题。

4. 规则的嵌套

  规则之间是可以嵌套的,这通常通过依赖项实现。例如生成 cacu 的规则依赖于很多的 .o 文件,而每个 .o 文件又分别是一个规则。要执行规则 cacu 必须先执行它的依赖项,即 add_int.oadd_float.osub_int.osub_float.omain.o,这 5 个依赖项生成或者存在之后才进行 cacu 的命令动作。

5. 文件的时间戳

  make 命令执行的时候会根据文件的时间戳判定是否执行相关的命令,并且执行依赖于此项的规则。例如对 main.c 文件进行修改后保存,文件的生成日期就发生了改变,再次调用 make 命令编译的时候,就会只编译 main.c,并且执行规则 cacu,重新链接程序。

6. 执行的规则

  在调用 make 命令编译的时候,make 程序会査找 Makefile 文件中的第 1 个规则,分析并执行相关的动作。例子中的第 1 个规则为 cacu,所以 make 程序执行 cacu 规则。由于其依赖项包含 5 个,第 1 个为 add_int.o,分析其依赖项,当 add/add_int.c add.h 存在的时候, 执行如下命令动作:

gcc -c -o add/add_int.o add/add_int.c

  当命令执行完毕的时候,会按照顺序执行第 2 个依赖项,生成 add/add_flaot.o。当第 5 个依赖项满足时,即 main.o 生成的时候,会执行 cacu 的命令,链接生成执行文件 cacu

  当把规则 clean 放到第一个的时候,再执行 make 命令不是生成 cacu 文件,而是清理文件。要生成 cacu 文件需要使用如下的 make 命令。

$make cacu
7. 模式匹配

  在上面的 Makefile 中,main.o 规则的书写方式如下:

main.o:main.c add/add.h sub/sub.h
	gcc -c -o main.o main.c -Iadd -Isub

  有一种简便的方法可以实现与上面相同的功能:

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

  这种方法的规则 main.o 中依赖项中的 “%o:%c” 的作用是将 TARGET 域的 .o 的扩展名替换为 .c,即将 main.o 替换为 main.c 。而命令行的 $< 表示依赖项的结果,即 main.c$@ 表示 TARGET 域的名称,即 main.o

四、Makefile 中使用变量

  在前面 多文件编译 中的 Makefile 中,生成 cacu 的规则如下:

cacu:add_int.o add_float.o sub_int.o sub_float.o main.o 
	gcc -o cacu add/add_int.o add/add_float.o \ 
		sub/sub_int.o sub/sub_float.o main.o

  生成 cacu 的时候,多次使用同一组 .o 目标文件:在 cacu 规则的依赖项中出现一次,在生成 cacu 执行文件的时候又出现一次。直接使用文件名进行书写的方法不仅书写起来麻烦,而且进行增加或者删除文件时容易遗忘。例如增加一个 mul.c 文件,需要修改依赖项和命令行两个部分。

1. Makefile中的用户自定义变量

  使用 Makefile 进行规则定义的时候,用户可以定义自己的变量,称为用户自定义变量。例如,可以用变量来表示上述的文件名,定义 OBJS 变量表示目标文件:

OBJS = add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o

  调用 OBJS 的时候前面加上 $,并且将变景的名称用括号括起来。例如,使用 gcc 的默认规则进行编译,cacu 规则可以采用如下的形式:

cacu:
gcc -o cacu $(OBJS)

  用 CC 变量表示 gcc,用 CFLAGS 表示编译的选项,RM 表示 rm -fTARGET 表示最终的生成目标 cacu

CC = gcc				( CC 定义成 gcc )
CFLAGS = -Isub -Iadd	(加入头文件搜索路径 sub 和 add )
TARGET = cacu			(最终生成目标)
RM = rm -f				(删除的命令)

  之前冗长的 Makefile 可以简化成如下方式:

CC = gcc
CFLAGS = -Iadd -Isub -O2	( O2 为优化)
OBJS = add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o 
TARGET = cacu 
RM = rm -f 
$(TARGET):$(OBJS)
	$(CC) -o $(TARGET) $(OBJS) $(CFLAGS)
$(OBJS) :%.o:%.o	(将 OBJS 中所有扩展名为 .o 的文件替换成扩展名为 .c 的文件)
	$(CC) -c $(CFLAGS) $< -o $@	(编译生成目标文件}
clean:
	-$(RM) $(TARGET) $(OBJS)

  执行命令的情况如下:

$ make
gcc -Iadd -Isub -O2 -c -o add/add_int.o add/add int.c
#编译 add_int.c 为目标文件
gcc -Iadd -Isub -O2 -c -o add/add_float.o add/add_float.c
#编译 add_float.c 为目标文件
gcc -Iadd -Isub -O2 -c -o sub/sub_int.o sub/sub_int.c
#编译 sub_int.c 为目标文件
gcc -Iadd -Isub -O2 -c -o sub/sub_float.o sub/sub_float.c
#编译 sub_float.c 为目标文件
gcc -Iadd -Isub -O2 -c -o main.o main.c	
#编译 main.c 为目标文件
#将文件 add_int.o、add_float.o、sub_int.o、sub_float.o、main.o 链接成 cacu 可执行文件
#并指定默认的头文件搜索目录 add 和 sub ,编译的优化选项为 O2
gcc -o cacu add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o -Iadd -Isub -O2

  make 查找到第一个执行的规则为生成 cacu,但是 main.o5 个文件不存在,make 按照默认的规则生成 main.o5 个目标文件。

2. Makefile 中的预定义变量

  在 Makefile 中有一些已经定义的变量,用户可以直接使用这些变量,不用进行定义。在进行编译的时候,某些条件下 Makefile 会使用这些预定义变量的值进行编译。经常采用的预定义变量如下表(Makefile 中经常使用的变量及含义)所示。

变量名含义默认值
AR生成静态库库文件的程序名称ar
AS汇编编译器的名称as
CCC 语言编译器的名称cc
CPPC 语言预编译器的名称$(CC) -E
CXXC++ 语言编译器的名称g++
FCFORTRAN语言编译器的名称f77
RM删除文件程序的名称rm -f
ARFLAGS生成静态库库文件程序的选项无默认值
ASFLAGS汇编语言编译器的编译选项无默认值
CFLAGSC 语言编译器的编译选项无默认值
CPPFLAGSC 语言预编泽的编译选项无默认值
CXXFLAGSC++ 语言编译器的编译选项无默认值
FFLAGSFORTRAN 语言编译器的编译选项无默认值

  在 Makefile 中经常用变量 CC 来表示编译器,其默认值为 cc,即使用 cc 命令来进行 C 语言程序的编译;在进行程序删除的时候经常使用命令 RM,它的默认值为 rm -f

  另外还有 CFLAGS 等默认值是调用编译器时的选项默认配置,例如修改后的 Makefile 生成 main.o 时,没有指定编译选项,make 程序自动调用了文件中定义的 CFLAGS 选项 -Iadd-Isub-O2 来增加头文件的搜索路径,在所有的目标文件中都采用了此设置。经过简化后,之前的 Makefile 可以采用如下形式:

CFLAGS = -Iadd -Isub -O2				#编译选项 
OBJS = add/add一int.o add/add_float.o \	#目标文件 main.o
	sub/sub_int.o sub/sub_float.o main.o	
TARGET = cacu 							#生成的可执行文件
$(TARGET):$(OBJS)						#TARGET 目标,需要先生成 OBJS 目标
	$(CC) -0  $(TARGET)  $(OBJS)  $(CFLAGS) #生成可执行文件
clean:									#清理
	-$(RM)  $(TARGET)  $(OBJS)			#删除所有的目标文件和可执行文件

  上面的 Makefileclean 目标中的 $(RM)$(TARGET)$(OBJS) 之前的符号 “ - ” 表示当操作失败时不报错,命令继续执行。如果当前目录不存在 cacu 时,它会继续删除其他的目标文件。例如下面的 clean 规则在没有 cacu 文件时会报错。

clean:
	rm $(TARGET) 
	rm $(0BJS)

  执行 clean 命令:

$make clean 
rm cacu
rm: cannot remove 'cacu': No such file or directory	 (删除 cacu 失败)
make: *** [clean] Error 1
3. Makefile 中的自动变量

  Makefile 中的变量除了用户自定义变量和预定义变量外,还有一类自动变量。
  Makefile 中的编译语句中经常会出现目标文件和依赖文件,自动变量代表这些目标文件和依赖文件。下表(Makefile 中常见的自动变量和含义)中是一些常见的自动变量。

变量含义
$*表示目标文件的名称,不包含目标文件的扩展名
$+表示所有的依赖文件,这些依赖文件之间以空格分开,按照出现的先后为顺序,其中可能包含重复的依赖文件
$<表示依赖项中第一个依赖文件的名称
$?依赖项中,所有目标文件时间戳晚的依赖文件,依赖文件之间以空格分开
$@目标项中目标文件的名称
$^依赖项中,所有不重复的依赖文件,这些文件之间以空格分开

  按照上表中的说明对 Makefile 进行重新编写,Makefile 的代码如下:

CFLAGS = -Iadd -Isub -O2 				#编译选项 
OBJS = add/add_int.o add/add_float.o \ 	#目标文件 
			sub/sub_int.o sub/sub_float.o main.o	
TARGET = cacu 							#生成的可执行文件
$(TARGET):$(OBJS)						# TARGET 目标,需要先生成 OBJS 目标
	$(CC) $@ -o $^ $(CFLAGS)			#生成可执行文件
$(OBJS):%.o:%.c						#目标文件的选项
	$(CC) $< -c $(CFLAGS) -o $@ 		#采用 CFLAGS 指定的选项生成目标文件 
clean:									#清理
	-$(RM)  $(TARGET)  $(OBJS)			#删除所有的目标文件和可执行文件

  重新编写后的 Makefile 中,生成 TARGET 规则的编译选项使用 $@ 表示依赖项中的文件名称,使用 $< 表示目标文件的名称。下面的命令:

$(TARGET)$(OBJS)	#TARGET 目标,需要先生成 OBJS 目标
$(CC) -〇 $A $(CFLAGS)	#生成可执行文件

  与

$(TARGET)$(OBJS)	#TARGET 目标,需要先生成 OBJS 目标
	$(CC) -o $(TARGET) $(OBJS) $(CFLAGS)	#生成可执行文件

  是一致的。

五、搜索路径

  在大的系统中,通常存在很多目录,手动添加目录的方法不仅十分笨拙而且容易造成错误。Make 的目录搜索功能提供了一个解决此问题的方法,指定需要搜索的目录,make 会自动找到指定文件的目录并添加到文件上,VPATH 变量可以实现此目的。VPATH 变量的使用方法如下:

VPATH=path1 : path2…

  VPATH 右边是冒号**(:)**分隔的路径名称,例如下面的指令:

VAPTH=add:sub (加入 add 和 sub 搜索路径)
add_int.o:%o:%c
$(CC) -c -o $@ $<

  Make 的搜索路径包含 addsub 目录。add_int.o 规则自动扩展成如下代码。

add_int.o:add/add_int.c
	cc -c -o add_int.o add/add_int.c

  将路径名去掉以后可以重新编译程序,但是会发现目标文件都放到了当前的目录下,这样会对文件的规范化造成危害。可以将输出的目标文件放到同一个目录下来解决此问题, 重新书写上面的 Makefile 为如下的代码。

CFLAGS = -Iadd -Isub -O2 
OBJSDIR = objs 
VPATH=add:sub: .
OBJS = add_int.o add_float.o sub_int.o sub_float.o main.o 
TARGET = cacu
$(TARGET) : $(OBJSDIR) $(OBJS)	(要执行 TARGET 的命令,先査看 OBJSDIR 和 OBJS 依赖项是否存在)
	$(CC) -o $(TARGET) $(OBJSDIR)/*.o $(CFLAGS) (将 OBJSDIR 目录中所有的 .o 文件链接成 cacu )
$(OBJS):%.o:%.c	(将扩展名为 .o 的文件替换成扩展名为 .c 的文件)
	$(CC) -c $(CFLAGS) $< -o $(OBJSDIR) /$@ (生成目标文件,存放在 OBJSDIR 目录中)
$(OBJSDIR):
	mkdir -p ./$@ (建立目录,-p 选项可以忽略父目录不存在的错误)
clean:
	-$(RM) $(TARGET)  (删除 cacu)
	-$(RM) $(OBJSDIR)/*.o (删除 OBJSDIR 下的所有 .o 文件)

  这样,目标文件都放到了 objs 目录下,只有最终的执行文件 cacu 放在当前目录,执行 make 的结果如下:

$make cacu 
mkdir -p ./objs
cc -c	-Iadd	-Isub	-O2	add/add	int.c -o objs/add int.o
cc -c	-Iadd	-Isub	-O2	add/add	float.c -o objs/add float.o
cc -c	-Iadd	-Isub	-O2	sub/sub	int.c -o objs/sub int.o
cc -c	-Iadd	-Isub	-O2	sub/sub	float.c -o objs/sub float.o
cc -c -Iadd -Isub -O2 main.c -o objs/main.o 
cc -o cacu objs/*.o -Iadd -Isub -O2

  编译目标文件时会自动地加上路径名,例如 add_int.o 所依赖的文件 add_int.c 自动变成 了 add/add_int.c;并且当 objs 不存在时会创建此目录。

六、自动推导规则

  使用命令 make 编译扩展名为 .cC 语言文件的时候,源文件的编译规则不用明确给出。这是因为 make 进行编译的时候会使用一个默认的编译规则,按照默认规则完成对 .c 文件的编译,生成对应的 .o 文件。它执行命令 cc -c 来编译 .c 源文件。在 Makefile 中只要给出需要重建的目标文件名(一个 .o 文件),make 会自动为这个 .o 文件寻找合适的依赖文件(对应的 .c 文件),并且使用默认的命令来构建这个目标文件。

  对于上边的例子,默认规则是使用命令 cc -c main.c -o main.o 来创建文件 main.o。对一个目标文件是 “文件名.o”,依赖文件是 “文件名.c” 的规则,可以省略其编译规则的命令行,由 make 命令决定如何使用编译命令和选项。此默认规则称为 make 的隐含规则。

  这样,在书写 Makefile 时,就可以省略描述 .c 文件和 .o 依赖关系的规则,而只需要给出那些特定的规则描述(.o 目标所需要的 .h 文件)。因此上面的例子可以使用更加简单的方式书写,Makefile 文件的内容如下:

CFLAGS = -Iadd -Isub -O2 
VPATH=add:sub
OBJS = add_int.o add__float.o sub_int.o sub_float.o main.o 
TARGET = cacu
$(TARGET) : $(OBJS) ( OBJS 依赖项的规则自动生成)
$(CC) -o $(TARGET) $(OBJS) $(CFLAGS)	(链接文件)
clean:
-$(RM) $(TARGET)
-$(RM)  $(OBJS)

  在此 Makefile 中,不用指定 OBJS 的规则,make 自动会按照隐含规则形成一个规则来生成目标文件。

七、递归 make

  当有多人在多个目录下进行程序开发,并且每个人负责一个模块,而文件在相对独立的目录中,这时由同一个 Makefile 维护代码的编译就会十分蹩脚,因为他们对自己目录下的文件增减都要修改此 Makefile,这通常会造成项目的维护问题。

1. 递归调用的方式

  Make 命令有递归调用的作用,它可以递归调用每个子目录中的 Makefile。例如,在当前目录下有一个 Makefile,而目录 addsub 及主控文件 main.c 由不同的人进行维护,可 以用如下方式编译 add 中的文件:

add:
cd add && $(MAKE)

  它等价于:

add:
$(MAKE) -C add

  上面两个例子的意思都是先进入子目录下 add 中,然后执行 make 命令。

2. 总控 Makefile

  调用 “$(MAKE) -C” 的 Makefile 叫做总控 Makefile。如果总控 Makefile 中的一些变量需要传递给下层的 Makefile,可以使用 export 命令,例如需要向下层的 Makefile 传递目标文件的导出路径:

export OBJSDIR=./objs	

  例如上面的文件布局,需要在 addsub 目录下分别编译,总控 Makefile 代码如下:

CC = gcc 
CFLAGS = -O2 
TARGET = cacu 
export OBJSDIR = ${shell pwd} /objs	(生成当前目录的路径字符串,并赋值给 OBJSDIR,外部可调用)
$(TARGET):$(OBJSDIR)  main.o	
	$(MAKE) -C add	(在目录 add 中递归调用 make)
	$(MAKE) -C sub	(在目录 sub 中递归调用 make)
	$(CC) -o $(TARGET) $(OBJSDIR)/*.o	(生成 main.o 放到 OBJSDIR 中)
main.o:%.o:%.o	(main.o 规则)
	$(CC) -C $< -o $(OBJSDIR)/$@ $(CFLAGS)	-Iadd -Isub
$(OBJSDIR):	
	mkdir -p $(OBJSDIR)	
clean:	
	-$(RM) $(TARGET) 
	-$(RM) $(OBJSDIR)/*.o

  CC 编译器变量由总控 Makefile 统一指定,下层的 Makefile 直接调用即可。生成的目标文件都放到 ./objs 目录中,export 了一个变量 OBJSDIR。其中的 ${shell pwd} 是执行一个 shell 命令 pwd 获得总控 Makefile 的当前目录。
  生成 cacu 的规则是先建立目标文件的存放目录,再编译当前目录下的 main.cmain.o 目标文件。在命令中,递归调用 addsub 目录下的 Makefile 生成目标文件放到目标文件存放路径中,最后的命令将目标文件全部编译生成执行文件 cacu

3. 子目录 Makefile 的编写

  add 目录下的 Makefile 如下:

OBJS = add_int.o add_float.o
all:$(OBJS)
$(OBJS):%.o:%.c
	$ (CC) -c $< -o $(OBJSDIR) /$@ $(CFLAGS)	(CC 和 OBJSDIR 在总控 Makefile 声明)
clean:
	$(RM) $(OBJS)

  这个 Makefile 很简单,编译 add 目录中的两个 C 文件,并将生成的目标文件按照总控 Makefile 传入的目标文件存放路径放置。
  sub 目录下的 Makefileadd 目录下的一致,也是将生成的目标文件放到总控 Makefile 指定的路径中。

OBJS = sub_int.o sub_float.o 
all:$(OBJS)
$(OBJS):%.o:%.c
	$ (CC) -c $< -o $(OBJSDIR) /$@  $(CFLAGS)  (CC 和 OBJSDIR 在总控 Makefile 声明〉
clean:
	$(RM)  $(OBJS)
八、Makefile 中的函数

  在比较大的工程中,经常需要些匹配操作或者自动生成规则的功能,接下来对最常用的使用方式进行介绍。

1. 获取匹配模式的文件名 wildcard

  这个函数的功能是査找当前目录下所有符合模式 PATTERN 的文件名,其返回值是以空格分割的、当前目录下的所有符合模式 PATTERN 的文件名列表。其原型如下:

$(wildcard PATTERN)

  例如,如下模式返回当前目录下所有扩展名为 .c 的文件列表:

$(wildcard *.c)
2. 模式替换函数 patsubst

  这个函数的功能是査找字符串 text 中按照空格分开的单词,将符合模式 pattern 的字符串替换成 replacement
  pattern 中的模式可以使用通配符, 代表 0 个到 n 个字符,当 patternreplacement 中都有 时,符合条件的字符将被 replacement 中的替换。函数的返回值是替换后的新字符串。其原型如下:

$(patsubst pattern,replacement,text)

  返回值:例如需要将 C 文件替换为 .o 的目标文件可以使用如下模式。

$(patsubst %.c,%.o, add.c)

  上面的模式将 add.c 字符串作为输入,当扩展名为 .c 时符合模式 %.c,其中 在这里代表 add,替换为 add.o,并作为输出字符串。

$(patsubst %.c,%.o, $(wildcard *.c))

  输出的字符串将当前扩展名为 .c 的文件替换成扩展名为 .o 的文件列表。

3. 循环函数 foreach

  这个函数的函数原型为:

$(foreach VAR,LIST,TEXT)

  函数的功能为 foreach 将 LIST 字符串中一个空格分隔的单词,先传给临时变量 VAR,然后执行 TEXT 表达式,TEXT 表达式处理结束后输出。其返回值是空格分割表达式 TEXT 的计算结果
  例如,对于存在 addsub 的两个目录,设置 DIRS 为 “addsub ./” 包含目录 addsub 和当前目录。表达式 $(wildcard$(dir)/*.c),可以取出目录 addsub 及当前目录中的所有扩展名为 .cC 语言源文件。

DTRS = sub add ./  (DIRS 字符串的值为目录 add、sub 和当前目录)
FILES = $(foreach dir, $(DIRS),$(wildcard $(dir)/*.c))
	(查找所用目录下的扩展名为 .c 的文件,赋值给变量 FILES )

  利用上面几个函数对原有的 Makefile 文件进行重新编写,使新的 Makefile 可以自动更新各个目录下的 C 语言源文件:

CC = gcc
CFLAGS = -O2 -Iadd -Isub 
TARGET = cacu
DIRS = sub add .	( DIRS 字符串的值为目录 add、sub 和当前目录)
FILES = $(foreach dir, $ (DIRS), $(wildcard $(dir)/*.c))  (查找所用目录下的扩展名为 **.c** 的文件,赋值给变量 FILES )
0BJS = $(patsubst %.c,%.o,$(FILES)) (替换字符串,将扩展名为 .c 的替换成扩展名为 .o ) 
$(TARGET) :$(OBJS)	(OBJS 依赖项规则默认生成)
	$(CC) -o $(TARGET) $(OBJS)	(生成 cacu)
clean:
	-$(RM) $(TARGET)
	-$(RM) $(OBJS)

  编译程序,输出结果如下:

debian # make 
gcc -O2 -Iadd -Isub -c -o sub/sub_float.o sub/sub_float.c 
gcc -O2 -Iadd -Isub -c -o sub/sub_int.o sub/sub_int.c 
gcc -O2 -Iadd -Isub -c -o add/add_float.o add/add_float.c 
gcc -O2 -Iadd -Isub -c -o add/add_int.o add/add_int.c
gcc -O2 -Iadd -Isub -c -o main.o main.c
gcc -o cacu sub/sub_float.o sub/sub_int.o add/add_float.o add/add_int.o ./main.o

  ㊨ 注意:Windows 下的 nmake 是和 make 类似的工程管理工具,只是由于 Visual Studio 系列的强大 IDE 开发环境而通常没有注意。所有平台的 Makefile 都是相似的,大概有 80% 的相似度,不同之处主要是函数部分和外部命令,而最核心的规则部分则是一致的,都是基于目标、依赖项和命令的方式进行解析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值