Makefile 学习笔记:
为什么要学习makefile :
工作经验让我认识到会不会写makefile从一个侧面说明了一个人是否有完成大型工程的能力,makefile 关系到整个工程的编译规则,一个工程的文件不计其数,其按类型,功能,模块分别放在不同的目录下,makefile定义了一些规则来指定,哪些文件需要先编译,哪些文件需要重新编译,甚至进行更复杂的功能操作,因为makefile就像一个shell脚本一样,其中也可以执行操作系统命令。
而make 只是一个命令工具,是一个解释makefile中的指令的命令工具,一般来说IDE即集成开发环境都有这个命令。
Makefile 的环境:
我是在linux下进行的实验linux 系列下,我用的是ubuntu ,当然你可以用redhat 红旗后其他,我想都没有什么问题的。在做实验的时候我会做一些linux写的c/c++例子来演示,以加深理解。
关于程序的编译和链接:
一般来说,c或者是c++,首先把源文件(*.c 或*.cpp)编译成为中间代码文件,这个中间代码文件在windows下是*.obj文件在linux 或unix 是*.o文件即object file 目标文件这个动作就叫做编译,即把源文件编译成目标文件的过程就叫做编译(compile)。这以后,再把大量的*.obj 或*.o 目标文件合成一个可以执行的文件,这个工程就叫做链接link。编译时,主要是检查程序的语法是否正确,函数,变量是否都有声明。至于链接呢,主要是链接函数,和全局变量。
一.M akefile 的规则
Target:prerequisites
Command
。。。
。。。
。。。
Target就是一个目标文件,可以使obj或是可执行文件还可以是一个标签label,关于标签label会在下面的文目标中讲解。所以我们现在只关注obj 和可执行文件即可,其实大部分还都是obj文件,或许可执行文件就只有一个。
Prerequisites 是先决条件的意识,其实在这里只是依赖的意思,prerequisites在这里是生成target的所需要的文件或目标。Command 就是make 需要执行的命令的,任意的shell 的命令,如果prerequisites中有一个以上的文件比target文件要新的话吗,command所定义的命令的就会被执行。这就是makefile的规则,也是makefile中最核心的东西。
一个例子:
这个makefile有六个源文件和六个头文件分别是:
func1.c func2.c func3.c func4.c func5.c main.c
head1.h head2.h head3.h head4.h head5.h head.h
上面的c源文件分别会用到其下的头文件各个文件的内容分别是:
func1.c 文件
#include "head.h"
#include "head1.h"
void f1()
{
stu.id = 10101;
strcpy(https://www.sodocs.net/doc/f45985747.html,,"ygt1");
stu.sex = 'm';
printf("id = %d\t name = %s\t sex = %c\n",stu.id,https://www.sodocs.net/doc/f45985747.html,,stu.sex); }
func2.c 文件
#include "head.h"
#include "head2.h"
void f2()
{
struct student2 stu;
stu.id = 10102;
strcpy(https://www.sodocs.net/doc/f45985747.html,,"ygt2");
stu.sex = 'm';
printf("id = %d\t name = %s\t sex = %c\n",stu.id,https://www.sodocs.net/doc/f45985747.html,,stu.sex); }
func3.c 文件
#include "head.h"
#include "head3.h"
void f3()
{
struct student3 stu;
stu.id = 10103;
strcpy(https://www.sodocs.net/doc/f45985747.html,,"ygt3");
stu.sex = 'm';
printf("id = %d\t name = %s\t sex = %c\n",stu.id,https://www.sodocs.net/doc/f45985747.html,,stu.sex); }
func4.c 文件
#include "head.h"
#include "head4.h"
void f4()
{
struct student4 stu;
stu.id = 10104;
strcpy(https://www.sodocs.net/doc/f45985747.html,,"ygt4");
stu.sex = 'm';
printf("id = %d\t name = %s\t sex = %c\n",stu.id,https://www.sodocs.net/doc/f45985747.html,,stu.sex); }
func5.c 文件
#include "head.h"
#include "head5.h"
void f5()
{
stu.id = 10105;
strcpy(https://www.sodocs.net/doc/f45985747.html,,"ygt5");
stu.sex = 'm';
printf("id = %d\t name = %s\t sex = %c\n",stu.id,https://www.sodocs.net/doc/f45985747.html,,stu.sex); }
main.c 文件
#include "head.h"
extern void f1();
extern void f2();
extern void f3();
extern void f4();
extern void f5();
int main()
{
f1();
f2();
f3();
f4();
f5();
printf("the end\n");
return 0;
}
以上是这个工程的的所有源文件及其代码
head1.h 头文件
struct student1
{
int id;
char name[20];
char sex;
};
head2.h 头文件
struct student2
{
int id;
char name[20];
char sex;
};
head3.h 头文件
struct student3
{
int id;
char name[20];
char sex;
};
head1.h 头文件
struct student3
{
int id;
char name[20];
char sex;
};
head4.h 头文件
struct student4
{
int id;
char name[20];
char sex;
};
head51.h 头文件
struct student5
{
int id;
char name[20];
char sex;
};
head.h 头文件
#include
#include
#include
以上是头文件的内容
以上文件都准备好后就要开始写makefile 文件了
Makefile 文件的可以这么写:
exefile: main.o func1.o func2.o func3.o func4.o func5.o gcc -o exefile main.o func1.o func2.o func3.o func4.o func5.o
main.o:main.c head.h
gcc -c main.c
func1.o:func1.c head.h head1.h
gcc -c func1.c
func2.o:func2.c head.h head2.h
gcc -c func2.c
func3.o:func3.c head.h head3.h
gcc -c func3.c
func4.o:func4.c head.h head4.h
gcc -c func4.c
func5.o:func5.c head.h head5.h
gcc -c func5.c
clean:
rm -f *.o exefile
在这个makefile中蓝色的就是目标文件或可执行文件目标文件时那些*.o文件,可执行文件就是exefile 文件,它是最终可以执行的文件。依赖文件就是那些*.c 和*.h 文件。每一个*.o 文件都有一组依赖文件,*.o 文件就是靠这些依赖文件生成。而生成的这些*.o 文件又都是exefile 可执行文件的依赖文件,他们生成exefile 可执行文件。依赖关系其实就是说明了目标文件是由哪些文件生成的,换言之,就是目标文件是由哪些文件更新的。定义好依赖关系下一行就是make 执行的命令,这个命令定义了操作系统如何生成这些目标文件的。命令行开始一定要以tab键开头。其执行过程就是make会比较target 文件和prerequisites 文件的修改日期,如果prerequisites 文件的日期比target文件的日期要新,或者target 不存在的话,make就会执行后续定义的命令。
讲解:
exefile: main.o func1.o func2.o func3.o func4.o func5.o
gcc -o exefile main.o func1.o func2.o func3.o func4.o func5.o
exefile 依赖main.o func1.o func2.o func3.o func4.o func5.o这些*.o文件,也即是说exefile 就是开这些文件生成的。一开始这些*.o文件是不存在的,那么make就会往下执行语句,而暂时先不执行gcc -o main main.o func1.o func2.o func3.o func4.o func5.o 这句命令。
main.o:main.c head.h
gcc -c main.c
main.o 依赖main.c head.h这两个文件执行其命令生成main.o目标文件,指着往下执行。。。
func1.o:func1.c head.h head1.h
gcc -c func1.c
同main.o 的执行。。。
func2.o:func2.c head.h head2.h
gcc -c func2.c
同main.o 的执行。。。
func3.o:func3.c head.h head3.h
gcc -c func3.c
同main.o 的执行。。。
func4.o:func4.c head.h head4.h
gcc -c func4.c
同main.o 的执行。。。
func5.o:func5.c head.h head5.h
gcc -c func5.c
同main.o 的执行。。。
当这些*.o文件都别生成了后make 就会执行第一个依赖和第一个依赖之后的命令exefile: main.o func1.o func2.o func3.o func4.o func5.o
gcc -o exefile main.o func1.o func2.o func3.o func4.o func5.o
最终生成exefile 之可行文件。
clean:
rm -f *.o exefile
clean 后面没有依赖文件,make 是不会执行其后的命令的,只能make clean 显视的执行。这句就是伪命令,就是做一些清理,把生成的目标文件*.o文件和exefile 删掉。Make 后执行结果:
[yanggentao@wkp mfile]$ make clean
rm -f *.o exefile
[yanggentao@wkp mfile]$ make
gcc -c main.c
gcc -c func1.c
gcc -c func2.c
gcc -c func3.c
gcc -c func4.c
gcc -c func5.c
gcc -o main main.o func1.o func2.o func3.o func4.o func5.o
[yanggentao@wkp mfile]$
Make clean 后执行结果:
[yanggentao@wkp mfile]$ make clean
rm -f *.o exefile
[yanggentao@wkp mfile]$
根据makefile 的依赖规则我们还可以这样写,至于为什么这样写,我们先且不说。exefile: main.o func1.o func2.o func3.o func4.o func5.o
gcc -o exefile main.o func1.o func2.o func3.o func4.o func5.o
main.o:main.c
gcc -c main.c
func1.o:func1.c
gcc -c func1.c
func2.o:func2.c
gcc -c func2.c
func3.o:func3.c
gcc -c func3.c
func4.o:func4.c
gcc -c func4.c
func5.o:func5.c
gcc -c func5.c
clean:
rm -f *.o exefile
这样写是把头文件都给去掉了,这样也对的,makefile的隐式规则会自动找这些在文件里包含的头文件的。
其实Makefile 中的命令就像是shell 里一样可以使用变量
二.Makefile 中使用变量
Makefile中的变量就像是c 语言的中宏一样
怎样定义变量呢?
我们在makefile最上面定义一个变量
OBJS = main.o func1.o func2.o func3.o func4.o func5.o
引用变量$(OBJS) 这就等价于main.o func1.o func2.o func3.o func4.o func5.o 就像宏一样的会被替换掉。所以我们的makefile 可以这样写了:
OBJS = main.o func1.o func2.o func3.o func4.o func5.o
exefile: $(OBJS)
gcc -o exefile $(OBJS)
main.o:main.c
gcc -c main.c
func1.o:func1.c
gcc -c func1.c
func2.o:func2.c
gcc -c func2.c
func3.o:func3.c
gcc -c func3.c
func4.o:func4.c
gcc -c func4.c
func5.o:func5.c
gcc -c func5.c
clean:
rm -f $(OBJS) exefile
这样写很方便,如果你想在这个工程里面加一个文件的话就不会很麻烦。
三.让make自动推导
Gnu 的make 功能很强大他可以自动推导文件及文件的依赖关系后面的命令所以我们没有必要去为*.O文件都写出其命令。
只要make看到一个*.o文件,它就会自动的吧*.c文件加到依赖关系中,如果make 找到一个func2.o 那么func2.c 就会使func2.o 的依赖文件。并且gcc –c func2.c 也会被推导出来。所以我们的makefile就会简单多了:
我们还可以这样写:
OBJS = main.o func1.o func2.o func3.o func4.o func5.o
exefile: $(OBJS)
gcc -o exefile $(OBJS)
clean:
rm -f $(OBJS) exefile
有时候我们会这样写:
OBJS = main.o func1.o func2.o func3.o func4.o func5.o
exefile: $(OBJS)
gcc -o exefile $(OBJS)
.PHONY:clean
clean:
rm -f $(OBJS) exefile
.PHONY:clean 是声明一下clean是一个伪命令。
到这里makefile的基本东西已经讲完了。还有很多细节,下面来看一下。
四.Makefile 的5个内容
1.显示规则
2.隐晦规则
3.变量的定义
4.文件指示
5.注释
1.显示规则:就是显示的在命令行中写出目标文件的依赖关系
2.隐晦规则:就是利用make 的自动推导的功能
3.变量的定义:就变量的宏替换
4.文件指示:其中包括三部分的内容,一个是在一个makefile中引用另一个makefile,就
像c语言中的include 一样;另一个是根据某些情况指定makefile中的有效部分,就像c语言的预编译#ifdef一样;还有一个就是定义一个多行的命令。
5.注释:只有行注释用#号字符注释如果你的makefile中用到了# 你可以用“\#“转义
Makefile 的文件名默认会找这三个文件GNUmakefile ,makefile 和Makefile
当然你也可以任意起名字比如linuxmakefile mymakefile 等但是如果要用的话,那么你就要指定它这样用make –f linuxmakefile 或make –f mymakefile
引用其他的makefile
Makefile中也有include 命令,这个命令就像c c++里的#include 关键字一样包含
Include 的语法是
Include
伪目标:
先前的一个例子:
Clean:
Rm –f *.o exefile
Clean 就是一个伪目标因为呢,clean并不是一个文件只是一个标签,所以make 无法生成它的依赖关系和决定是否要执行它的命令,所以呢我们只能通过显示的指明这个目标才能让其生效。当然为目标的取名不能喝文件名同名,不然就失去了为目标的意义了。
所以我们可以用.PHONY来显示的指明一个伪目标向make 说明不管是否有这个文件,这个目标就是伪目标。
.PHONY:clean
Clean:
Rm –f *.o
伪目标没有依赖关系但是我们可以为他指定依赖文件。
例子:这个例子可以生成三个可执行文件
先来看:
在linux下创建touch a.c b.c c.c main1.c main2.c main3.c a.h b.h c.h main.h 执行此命令就会创建好所需要的文件,每个文件内容如下:
a.c 文件
#include "main.h"
#include "a.h"
void fa()
{
struct studenta stud;
stud.id = 10101;
strcpy(https://www.sodocs.net/doc/f45985747.html,,"a.c");
stud.sex = 'f';
printf("studenta-->id = %d\tname = %s\t sex = %c\n",stud.id,https://www.sodocs.net/doc/f45985747.html,,stud.sex);
}
b.c 文件
#include "main.h"
#include "b.h"
void fb()
{
struct studentb stud;
stud.id = 10101;
strcpy(https://www.sodocs.net/doc/f45985747.html,,"b.c");
stud.sex = 'f';
printf("studentb-->id = %d\tname = %s\t sex = %c\n",stud.id,https://www.sodocs.net/doc/f45985747.html,,stud.sex); }
c.c 文件
#include "main.h"
#include "c.h"
void fc()
{
struct studentc stud;
stud.id = 10101;
strcpy(https://www.sodocs.net/doc/f45985747.html,,"c.c");
stud.sex = 'f';
printf("studentc-->id = %d\tname = %s\t sex = %c\n",stud.id,https://www.sodocs.net/doc/f45985747.html,,stud.sex); }
main1.c 文件
#include "main.h"
extern void fa();
extern void fb();
extern void fc();
int main()
{
fa();
//fb();
// fc();
printf("in the main1\n");
return 0;
}
main2.c 文件
#include "main.h"
extern void fa();
extern void fb();
extern void fc();
{
fa();
fb();
fc();
printf("in the main2\n");
return 0;
}
main3.c 文件
#include "main.h"
extern void fa();
extern void fb();
extern void fc();
int main()
{
fa();
fb();
fc();
printf("in the main3\n");
return 0;
}
main.h 文件
#include
#include
#include
a.h 文件
struct studenta
{
int id;
char name[20];
char sex;
};
b.h 文件
struct studentb
{
int id;
char name[20];
char sex;
};
c.h 文件
struct studentc
{
char name[20];
char sex;
};
我们的makefile先这样写:
main1:main1.o a.o b.o c.o
gcc -o main1 main1.o a.o b.o c.o
main2:main2.o a.o b.o c.o
gcc -o main2 main2.o a.o b.o c.o
main3:main3.o a.o b.o c.o
gcc -o main3 main3.o a.o b.o c.o
看看执行结果是什么why?
执行结果只执行了第一个生成了main1 why;
正确的写法:
all:main1 main2 main3
.PHONY:all
main1:main1.o a.o b.o c.o
gcc -o main1 main1.o a.o b.o c.o
main2:main2.o a.o b.o c.o
gcc -o main2 main2.o a.o b.o c.o
main3:main3.o a.o b.o c.o
gcc -o main3 main3.o a.o b.o c.o
clean:
rm -f *.o main1 main2 main3
这样就生成了三个可执行文件main1 main2 main3 可见伪目标也可以有依赖关系。
我们再可以运用变量把makefile写简单一些
objs=a.o b.o c.o
all:main1 main2 main3
.PHONY:all
main1:main1.o $(objs)
gcc -o main1 main1.o $(objs)
main2:main2.o $(objs)
gcc -o main2 main2.o $(objs)
main3:main3.o $(objs)
gcc -o main3 main3.o $(objs)
clean:
rm -f $(objs) main1 main2 main3
上面的例子是伪目标做为目标
再看一个清除的例子:这个例子说命了伪目标也可以作为依赖文件objs=a.o b.o c.o
all:main1 main2 main3
.PHONY:all
main1:main1.o $(objs)
gcc -o main1 main1.o $(objs)
main2:main2.o $(objs)
gcc -o main2 main2.o $(objs)
main3:main3.o $(objs)
gcc -o main3 main3.o $(objs)
.PHONY:cleanall cleanobj cleanexe
cleanall:cleanobj cleanexe
rm -f $(objs) main?
cleanobj:
rm -f $(objs)
cleanexe:
rm -f main?
执行make cleanall rm -f $(objs) main?它被执行
执行make cleanexe rm -f main?它被执行
执行make cleanobj rm -f $(objs)它被执行
多目标:
静态模式:
看例子
CC = gcc
FLAGS = -g
LIBS =
OBJS = main.o func1.o func2.o func3.o func4.o func5.o all:$(OBJS)
$(OBJS):%.o:%.c
$(CC) -c $(FLAGS) $< -o $@ $(LIBS)
.PHONY:clean
clean:
rm -f $(OBJS) exefile
这里的源文件是我们在上面第一个例子里的程序。
$(OBJS):%.o:%.c
$(CC) -c $(FLAGS) $< -o $@ $(LIBS)
多目标$(OBJS) 目标集是%.o 就是点o 结尾的文件依赖目标集是:%.c 就是点c文件。$< 表示所有的依赖目标集中的的点c 文件$@ 表示所有的目标集中的点o文件。
展开后相当于:
gcc -c -g func1.c -o func1.o
gcc -c -g func2.c -o func2.o
gcc -c -g func3.c -o func3.o
gcc -c -g func4.c -o func4.o
gcc -c -g func5.c -o func5.o
这也是make后的结果
自动生成依赖关系:
我们可以用这样的命令来查看每一个源文件的都包含了哪几个文件
Gcc –M main.c
执行结结果:
[yanggentao@wkp duomobiao]$ gcc -M main.c
main.o: main.c head.h /usr/include/stdio.h /usr/include/features.h \
/usr/include/sys/cdefs.h /usr/include/bits/wordsize.h \
/usr/include/gnu/stubs.h /usr/include/gnu/stubs-32.h \
/usr/lib/gcc/i586-redhat-linux/4.4.0/include/stddef.h \
/usr/include/bits/types.h /usr/include/bits/typesizes.h \
/usr/include/libio.h /usr/include/_G_config.h /usr/include/wchar.h \
/usr/lib/gcc/i586-redhat-linux/4.4.0/include/stdarg.h \
/usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h \
/usr/include/stdlib.h /usr/include/sys/types.h /usr/include/time.h \
/usr/include/endian.h /usr/include/bits/endian.h \
/usr/include/bits/byteswap.h /usr/include/sys/select.h \
/usr/include/bits/select.h /usr/include/bits/sigset.h \
/usr/include/bits/time.h /usr/include/sys/sysmacros.h \
/usr/include/bits/pthreadtypes.h /usr/include/alloca.h \
/usr/include/string.h /usr/include/xlocale.h
[yanggentao@wkp duomobiao]$
执行完这个gcc –M main.c 后它把所有的main.c 这个文件的包含文件都给打了出来,包括了库文件,我们用-MM两个可以不包含这些库。
Gcc – MM main.c
执行结果:
[yanggentao@wkp duomobiao]$ gcc -MM main.c
main.o: main.c head.h
[yanggentao@wkp duomobiao]$
Main.c 文件只包含head.h 这个头文件
在命令行前加一个@ 字符他就不会打印执行的命令了
@$(CC) -c $(FLAGS) $< -o $@ $(LIBS)
这样子结果什么都没有打印,但命令行还是会执行,只是不打印了而已
而如果是make –n 那么make 只显示执行的命令而不会去执行了
命令执行:
如果你想要上一个命令的执行结果影响下一个命令的执行,那么这两个命令的就要卸载一行上用分号隔开,而不能写在两行上。
例子:
exec:
cd ~;pwd
exec:
cd ~
pwd
结果是不一样的。
命令出错:
如果命令出错了就有可能影响到后面的命令。如果这样
在命令前面加上-,那么命令出了错,他也会执行下面的命令的
Clean:
-rm –f *.o
还有一个全局的办法就是在make 上加上–I 就像了
Make –k 的意思就是如果某个规则出错了,就终止这个规则,但要继续执行下一个规则;嵌套执行make
嵌套执行make 是适用于大工程里的。我们会把不同的模块或不同功能的程序放在不同的目录中,而在每一个目录下都会有一个makefile文件,这个技术对于我们木块编译和分段编译有着非常大的意义;
:= 和= 的区别是= 是可以不用先声明才使用的而:= 必须要用前面的定义过的
?= 的作用是 a ?= b 如果a 定义了已经,那么这条语句什么都不会做,如果没有定义那么a 的值就是b
变量的高级用法:
foo:=a.o b.o c.o
bar:=$(foo:.o=.c)
上面的意思是:bar 等于a.c b.c c.c 就是在foo这个变量中的以.o结尾的字串用.c结尾的代替:
也可以这样写了:
Bar:=$(foo:%.o=%.c)
+= 的意思是追加变量的值:
例如:
Source = a.c b.c c.c d.c
Source += e.c
那么source 就等于 a.c b.c c.c d.c e.c
e.c 就追加上了;