编辑推荐: |
本文来自于CSDN,介绍了makefile,编译器的编译规则,makefile
有什么用,文件的编写规则等。 |
|
一、makefile简介
makefile是一个工具,是帮助我们编译和链接程序的。我们都知道,C程序从源文件到一个可执行文件需要预处理、编译、链接几个步骤。在Windows下,我们有各种各样的IDE(比如VS、Eclipse、codeblocks等等)来帮我们完成这些工作,我们要做的仅仅是点击一个按钮。但在Linux系统下,我们必须用命令行来完成这些操作,相信你对此也有所了解,比如要编译一个名为main.c的称为,我们要输入命令gcc
-o main main.c。当文件比较少时,还可以勉强对付,一旦我们的工程变大一些,比如有七八个源文件,五六个头文件,它们之间相互包含引用,这时候再用这样的方法就显得非常笨拙了。而makefile就是为了帮助我们实现类似IDE中点一下按钮那个功能的,它定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译。
二、编译器的编译规则
为了弄清写makefile的原理,有必要先稍微了解一下C程序的编译规则。编译的最终目的是生成几个(一般是一个)可执行程序,如上面的命令gcc
-o main main.c就是要生成一个名为main的可执行程序。这个可执行程序是由几个目标文件(Windows下为.obj,Linux下为.o)链接而成的,而这些目标文件是由源文件编译来的,所以就有了下面的这个生成关系图。
从这个图我们可以看出两点:一是要生成一个可执行文件,就必须找到所有目标文件,这就形成了一种依赖关系,要想生成可执行文件,必须先要有目标文件,目标文件和源文件之间、源文件和头文件之间也是这种依赖关系。二是因为有这种依赖关系,当我们一旦修改了源代码后,比如b.c被修改了,那么就必须更新b.o,进而修改可执行文件。而与此无关的其他文件,如c.o,则不需要做修改,也就是不需要更新。而我们的makefile就是要表示这种依赖关系的,并且可以判断出一个文件的依赖文件是否被更新了,进而判断出这个文件要不要更新。
三、makefile有什么用
在学习具体书写规则前,我们先来说一下makefile要达到或者说可以达到的效果。当我们写好一个项目所需的所有源文件和头文件后,我们可以把它们放到Linux下的一个文件夹下(也可以是不同的文件夹,后面我们会说到这种情况),然后在这个文件夹下建一个文件,将其命名为makefile或者Makefile,然后根据我们项目中的头文件和源文件写这个文件(这是我们的重点),等编写完后,保存退出,然后就可以在当前目录下输入make命令并执行,我们的所有文件就会自动编译链接,生成一个可执行文件。我们还可以用make
clean命令来清除刚才产生的那些文件,最重要的,当我们修改某些代码后,只需执行以下make命令就可以了,系统就会产生新的可执行文件。怎么样,是不是非常方便?让我们来总结一下makefile的工作任务:
1.如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。
2.如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。
3.如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。
4.可以自动清除自己产的的各种文件,即相当于清除清理解决方案(这一条不是必须的,但一般会实现)
四、makefile文件的编写规则
终于到正题了,首先要明确一下我们写makefile是要表达什么,其实前面已经多次提到了,就是要把各种文件间的依赖关系表示出来,为了避免过多理论解释,我们从一个例子开始。
假如我们有一个项目,里面有8个源文件(.c)三个头文件(.h),它们之间的包含关系如下
<pre name
= "code" class= "cpp">
<span style = "font-size :18px; ">
main.c 包含了defs.h
kbd.c 包含了defs.h command.h
command.c 包含了defs.h command.h
display.c 包含了defs.h buffer.h
insert.c 包含了defs.h buffer.h
search.c 包含了defs.h buffer.h
files.c 包含了defs.h buffer.h command.h
utils.c 包含了defs.h </span> |
里面的具体内容我们不去理会,这样一个项目对应的makefile文件可以这么写
<pre name
= "code" class = "cpp">
<span style="font-size :18px;">
edit : main.o kbd.o command .o display.o insert
.o search.o files.o utils.o
cc - o edit main.o kbd.o command.o display.o insert.o
search.o files.o utils.o
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o insert.o
search.o files.o utils.o </span> |
先不要被这些“乱七八糟”的东西吓到,其实里面的规则非常简单明了。结合图1和上面各个文件的包含关系,我们可以理出这样的脉络:
首先是edit是一个标识符,或者说的在直白点儿,就是个名字,这个名字出现在makefile最前面,好了,那这个名字就是我们图1中的那个可执行文件的名字,也就是说我们整个makefile的目的就是产生这个文件。
看后面,冒号就是一个分隔符,再后面是一串目标文件名,这些东西都是怎么出来的呢?其实看一下图1就明白了,他们就是那些目标文件,是源文件编译的结果。
总之,
<pre name="code"
class = "cpp"> <span style="font-size
:18px;"> edit : main.o kbd.o command.o
display.o insert .o search.o files.o utils.o </span> |
这一句就是表达了一层依赖关系,即要想得到edit这个可执行文件(Linux下可执行文件没有后缀),就必须要有后面这一串目标文件。再看下一行
<span style
= "font-size:18px;"> cc -o edit main.o
kbd .o command.o display .o insert.o search.o
files.o utils .o </span> </span> |
这句话是一句程序(注意书写时一定要以Tab键开头),是用来生成edit文件的,其中用到了那些目标文件。
再看后面几乎是一样的结构,都是一种依赖关系加一句代码。这其实就是makefile文件的书写规则,一个文件名,后面一个冒号,再加几个其他文件名,表示冒号前面的文件的生成依赖后面的文件,而紧跟着下面就是生成这个文件的语句。
最后那句clean是清除语句,我们一会儿再说。
五、程序执行原理
弄清makefile的工作原理没有立竿见影的作用,但这是一个程序员的内功,正所谓磨刀不费砍柴工。我们其实我们前面也已经说了,makefile要表示的就是一连串的依赖关系,实际上,文件之间形成了一种依赖树。如下图所示
将图和上面的程序对应一下就会发现,文件就是把每一层的父节点和子节点之间的关系表示出来了。程序的执行过程就是遍历这棵树的过程,遍历的次序就是edit->main.o->main.c->defs.h->kbd.o->...在遍历过程中,会判断每个节点中的文件是否是最新的:
①如果是叶子节点,也就是.c、.h这些文件,那它肯定是最新的
②如果不是叶子节点,则访问其所有子节点,只有当其所有节点为最新且当前节点的生成时间晚于所有子节点,当前节点为最新的
③访问判断子节点是否最新的条件和当前节点一样,即这是一个递归过程
如果当前节点是最新的,那什么也不需要做,即不用执行节点下面对应的程序语句。而一旦发现某个结点对应的文件不是最新的,就必须执行文件标号下面的语句,而且从这个结点到根节点所有通过的结点都要更新,因为他们肯定都不是最新的了。
总之,程序的执行过程就是遍历这棵树的过程,在这个过程中不断判断文件是否是最新的,再根据判断结果决定是否执行对应语句。而这套规则达到的效果就是我们上面说的makefile文件的那几条工作任务。
下面说一下clean标号的作用,最后一句clean标号并没有出现在依赖树中,这说明它是不会被判断的,下面的语句也不会被自动执行,要执行它只有一个方法,就是执行make
clean命令,这个命令告诉系统从clean标号开始执行程序。而后面那句话就是清除make指令产生的各种文件。
还有两点要注意的是:
①关于紧跟某个标号(一般对应一个文件)后面的那句程序,makefile并没有规定那句程序的作用,也就是说你可以在那儿执行任何语句,makefile都会去执行。但为了编译工作正常进行,这个语句一般是用来更新前面这个文件的,这里假定你已经对gcc的编译指令比较熟悉了,就不讲这些语句的语法了。
②文件产生的时间是makefile判断文件是否最新的一个关键条件,其实makefile并没去去判断文件内容有没有更改,而只是简单的判断了一下最后的保存时间,如果保存时间比较靠后,那就认为这个文件被修改了。
六、其他技术问题
这里主要讲一下在具体使用过程中我们会遇到的一些问题以及如何解决这些问题。
6.1 makefile文件的简化
前面的代码只是最基本的版本,书写和维护起来都比较麻烦。简化makefile文件的方法有很多,其基本思路就是压缩信息,将重复的信息去除,用最少的字符表达最多的信息,但这种压缩也是有限度的,毕竟压缩的越多,理解起来就会越困难,就像中国的古文,一个字表达的含义太多了,理解起来就会有障碍。鉴于此,我们只介绍两种简化方法,一是使用变量,而是使用makefile的自动推导机制。
6.1.1 使用变量
makefile中的变量和C语言中的宏类型,就是把大量重复的字符用一个标号表示。从最初版本的程序中我们可以看出,重复最多的就是那些目标文件了,所以我们可以定义一个变量,代替这些目标文件,重写后的makefile文件如下
<span style
= "font-size:18px;"> objects = main.o
kbd .o command.o display.o insert.o search.o files.o
utils .o
edit : $(objects)
cc -o edit $(objects)
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit $(objects) </span> |
程序中,我们定义了一个变量objects来代替所有的目标文件,定义的规则非常简单就是直接用a=b的形式就可以了。然后在使用时用$(a)的格式就可以了。当然makefile文件中的变量作用还远远不止于此,这里就不多说了。
6.1.2 自动推导机制
makefile是非常强大的,为了最大限度的省去程序员的工作,makefile支持从依赖关系自动推导出后面要执行的语句。前面也提到了,标号后面的那句程序一般是用来生成这个文件的,而在编译过程中,那些类型的文件需要那些指令都是固定的,所以makefile就帮我们生成了这些代码,而不用我们自己去写了。这样就得到了下面这个版本的makefile
<span style
= "font-size:18px;">objects = main.o
kbd.o command .o display.o insert.o search.o files.o
utils.o
edit : $(objects)
cc -o edit $(objects)
main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h
clean :
rm edit $(objects)</span> |
注意,只有当标号行下面没有任何语句时,makefile才会自动推导,否则就不会自动推导而出现错误,这一点类似C++的构造函数,要么啥也别写,要写就写好。
6.2 如何设置搜索路径
如果我们的文件不是放在同一个文件夹下,那就需要设置makefile的文件搜索路径,设置方式如下
<span style
= "font-size:18px;">VPATH = path1
:path2 :path3 <span style="white-space:pre">
</span>//中间是冒号 </span> |
只要把这句话放到文件最最前面,那makefile就会按照当前路径、path1、path2、path3的顺序去寻找要添加的文件了。
6.3 最后版本
<span style="font-size:18px;">#VPATH
= path1:path2 :path3
CC = gcc #set Compiler
objects = main.o kbd.o command.o display.o insert.o
search .o files.o utils.o
edit : $(objects)
$(CC) -o edit $ (objects)
@echo "^_^ ^_^ ^_^ ^_^ ^_^ ^_^ Compile completed
^_^ ^_^ ^_^ ^_^ ^_^ ^_^" #display Compile
result
main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h
.PHONY : clean
clean :
rm edit $(objects)</span> |
在最后的版本里,我们添加了这样几个东西
①注释,井号(#)后面的为注释内容,类似于C语言中的//
②加了一个变量CC,作为不同编译器的选择,如果是C++程序,把CC改为g++就可以了(前面我们用的cc命令来自Unix)
③增加了交互信息,当整个编译过程完成后,我们向控制台输出一句话,即
<span style="font-size:18px
;">^_^ ^_^ ^_^ ^_^ ^_^ ^_^ Compile completed
^_^ ^_^ ^_^ ^_^ ^_^ ^_^</span> |
④.PHONY是说明虚拟标号用的,前面说clean标号,不会出现在依赖树中,这样的标号是虚的,为了更明确这一点,也为了其他一些功能,makefile用这种方式说明一个标号是虚的。
七、彩蛋
makefile的介绍就此结束了,下面我们说点儿题外话。makefile是Linux做C/C++开发的必备工具,那现实中程序员是如何做Linux开发的呢,我们简单介绍一下,当然,这些都是本人听到或见到的,不一定就对。
首先,Linux下做开发是用vim敲代码吗?答案是否定的,就算是专门做Linux开发的程序员,也很少有人直接在vim中编辑代码的,原因很简单,就是太不方便了。所以人们都是在VS或Eclipse中把代码写好了,然后传到Linux系统中,甚至我们讲的makefile文件都是在Windows下写好的。而且为了方便Windows和Linux直接的文件传递,有专门的软件是干这个的,我个人用的就是WinScp这款软件,所以如果你要做Linux下的开发,下载这样一个软件是非常有必要的,而不是去vim下敲代码。
其次,很多刚开始学Linux系统的同学都是在自己的Windows下装的虚拟机然后装的Linux系统,而在真实的开发中,Linux是装在服务器上的,我们的电脑只要安装一“壳”软件就可以了,这个软件可以连到服务器上,让我们直接操作服务器系统。这些软件也有很多,比如Xchell就是一个。
最后,作为一个专业的程序员,一款专业的文本查看器是非常有必要的。如果你还用txt查看文件,就太low了。UltraEdit是一款专业的文本查看编译软件,具体怎么好用就不说了。
之所以说这些,是想告诉大家,道虽然重要,术也是不可缺少的!很多东西不是你不能理解,而是你根本就不知道有这么个东西!所以作为一个程序员最痛苦的其实不是算法设计不出来,而是软件装不好,因为道理虽然重要,但我们是生活在具体的生活中的。再引申一点儿,我们最好不要陷入对理性世界的盲目追随中,只有过好具体的每一天才是真正的成功!
|