Linux程序设计 学习笔记 第九章 开发工具(部分)

编写小程序时,人们会在编辑完源文件后重新编译所有文件来重建应用程序。但对大型程序来说,改动一个源文件会重新编译所有其他的源文件太费时。

如有三个头文件和三个源文件:
在这里插入图片描述
如果我们只修改了头文件c.h,则源文件main.c和2.c无需重新编译,因为它们并不依赖于这个头文件。而如果被修改的是b.h,而程序员又忘记重新编译源文件2.c,那么最终的程序就可能不能正常工作了。

使用make工具可以只重新编译所有受改动影响的源文件。

make命令不仅仅用于编译程序,当需要通过多个输入文件来生成输出文件时,都可以使用它。

make不了解如何建立应用程序,我们必须为其提供一个被称为makefile的文件,告诉make应用程序应如何构造。

makefile文件一般会和项目的其他源文件放在同一目录下,机器上可以同时存在多个不同的makefile文件。我们还可以通过多个makefile文件来管理大型项目的不同部分。

makefile文件由一组依赖关系和规则构成,每个依赖关系由一个目标(即要创建的文件)和目标所依赖的源文件组成,而规则描述了如何通过这些依赖文件创建目标。目标一般是一个单独的可执行文件。

make命令会读取makefile文件的内容,它先确定目标文件(或要创建的文件),然后比较该目标所依赖的源文件的日期和时间以决定该采用哪条规则来构造目标。通常在创建最终的目标之前,它需要先创建一些中间目标。make命令会根据makefile文件确定目标文件的创建顺序以及正确的规则调用顺序。

make命令常用的三个选项:
1.-k,作用是让make在发现错误时继续执行。可使用该选项在一次操作中发现所有未编译成功的源文件。
2.-n,作用是让make命令输出将要执行的操作步骤,而不真正执行。
3.-f <filename>,作用是告诉make命令将哪个文件作为makefile文件。如未使用该选项,首先在当前目录下查找名为makefile的文件,如不存在,会查找名为Makefile的文件。但如果是在Linux系统中,你可能使用的是GNU Make,它在搜索makefile和Makefile文件之前,先搜索GNUmakefile文件。很多Linux程序员使用Makefile作为文件名,因为如果一个目录下都是小写文件名,那么Makefile会出现在第一个位置。建议不要使用GNUmakefile,因为它是特定于GNU Make的命令实现。

为了告诉make要创建的目标(通常是一个可执行文件),可以将目标名作为make的参数。如果不这么做,make会尝试创建makefile中列出目标中的第一个目标。很多程序员将第一个目标定义为all,然后再列出其他从属目标。

上例的依赖关系:
在这里插入图片描述
这组依赖关系形成一个层次结构,显示了源文件之间的关系,若文件b.h发生改变,就需要重新编译2.o和3.o,而由于2.o和3.o发生了改变,就需要重新编译myapp。

如果想一次性创建多个文件,可利用伪目标all,假设应用由二进制文件myapp和使用手册myapp.1组成,可使用下列语句定义:

all: myapp myapp.1

makefile第二部分是规则,它们定义了目标的创建方式。在上例中,当make需要重建2.o时,具体只需使用命令gcc -c 2.c即可(make内置了很多默认规则),但如果需要指定头文件所在目录,或为了今后的调试需要设置编译选项时,需要明确定义一些规则。

makefile中空格和tab是有区别的,规则所在的行必须以制表符tab开头。如果makefile某行以空格结尾,也可能导致make命令执行失败。

一个简单的makefile文件,将其命名为Makefile1:

myapp: main.o 2.o 3.o
	gcc -o myapp main.o 2.o 3.o

main.o: main.c a.h
	gcc -c main.c

2.o: 2.c a.h b.h
	gcc -c 2.c

3.o: 3.c b.h c.h
	gcc -c 3.c

调用它,需要使用-f选项,因为makefile文件未使用默认文件名makefile或Makefile,如果在没有任何源文件的目录下执行这个命令,将出现以下结果:
在这里插入图片描述
这是由于make命令假设在makefile文件中的第一个目标myapp是想创建的目标文件,然后它会检查其依赖关系,并确定需要一个main.c文件,由于找不到main.c文件,且makefile中也没有说明如何创建该文件,因此make报告一个错误。

创建空的头文件:
在这里插入图片描述
.c文件的内容:

// main.c
#include <stdlib.h>
#include "a.h"

extern void function_two();
extern void function_three();

int main() {
    function_two();
    function_three();
    exit(EXIT_SUCCESS);
}

// 2.c
#include "a.h"
#include "b.h"

void function_two() { }

// 3.c
#include "b.h"
#include "c.h"

void function_three() { }

之后再执行make命令:
在这里插入图片描述
虽然把如何创建目标myapp列在最前面,但make命令能自行判断出创建文件的正确顺序。它调用在规则部分给出的命令来创建相应的文件。

改变b.h,再调用make:
在这里插入图片描述
可见只执行了重建myapp所需的最少命令,并以正确的顺序执行了它们。

如果我们删除一个目标文件:
在这里插入图片描述
make会重建被删除的文件和以被删除文件为依赖关系的其他文件。

makefile中的注释以#开头,是行注释。

在编写包含非常多的源文件的大型项目时,可以使用宏代替某些常出现的内容。可通过语句MACRONAME=value在makefile文件中定义宏。引用宏的方法是$(MACRONAME)或${MACRONAME},make的有些版本还接受$MACRONAME的写法。如果想把一个宏的值设为空,可令等号后面为空。

makefile中的宏常被用来设置编译器选项,在软件开发过程中,开发人员通常不会对编译结果进行优化,而是将调试信息包含进去,但对于软件的发行版,编译结果需要是一个不包含调试信息的容量较小的二进制可执行文件,使其执行速度尽可能快。

上例的Makefile1文件假设编译器名为gcc,而在其他UNIX系统中,编译器的名字可能是cc或c89,此时,可用宏定义来方便地修改这些内容。

宏通常在makefile文件中定义,但也可以在调用make命令时在命令行上给出宏的定义:

make CC=c89

命令行上的宏定义将覆盖makefile中的宏定义。命令行中的宏定义必须以单个参数传递,因此不能使用空格,如使用空格,需要这样:

make "CC = c89"

带宏定义的makefile文件,命名为Makefile2:

all: myapp

# which compiler
CC = gcc

# where are include files kept
INCLUDE = .

# options for development
CFLAGS = -g -Wall -ansi

# options for release
# CFLAGS = -O -Wall -ansi

myapp: main.o 2.o 3.o
	$(CC) -o myapp main.o 2.o 3.o

main.o: main.c a.h
	$(CC) -I$(INCLUDE) $(CFLAGS) -c main.c

2.o: 2.c a.h b.h
	$(CC) -I$(INCLUDE) $(CFLAGS) -c 2.c

3.o: 3.c b.h c.h
	$(CC) -I$(INCLUDE) $(CFLAGS) -c 3.c

执行它:
在这里插入图片描述
make内置了一些宏定义,常用的如下:
在这里插入图片描述
这些宏在使用前才展开,它们的含义随着makefile文件的处理进展发生变化。

在命令之前,可以出现以下两个特殊字符:
1.-告诉make忽略该命令的所有错误。
2.@告诉make在执行某条命令前不要将该命令显示在标准输出上,如用echo命令给出一些说明信息。

Makefile3中clean选项删除不需要的目标文件,install选项将编译成功的应用安装在另一目录:

all: myapp

# which compiler
CC = gcc

# where to install
INSTDIR = /usr/local/bin

# where are include files kept
INCLUDE = .

# options for development
CFLAGS = -g -Wall -ansi

# options for release
# CFLAGS = -O -Wall -ansi

myapp: main.o 2.o 3.o
	$(CC) -o myapp main.o 2.o 3.o

main.o: main.c a.h
	$(CC) -I$(INCLUDE) $(CFLAGS) -c main.c

2.o: 2.c a.h b.h
	$(CC) -I$(INCLUDE) $(CFLAGS) -c 2.c

3.o: 3.c b.h c.h
	$(CC) -I$(INCLUDE) $(CFLAGS) -c 3.c

clean: 
	-rm main.o 2.o 3.o

install: myapp
	@if [ -d $(INSTDIR) ]; \
	    then \
	    cp myapp $(INSTDIR); \
	    chmod a+x $(INSTDIR)/myapp; \
	    chmod og-w $(INSTDIR)/myapp; \
	    echo "Installed in $(INSTDIR)"; \
	else \
	    echo "Sorry, $(INSTDIR) does not exist"; \
	fi

特殊目标中只有一个myapp,因此它只生成myapp。

新增加的目标clean删除目标文件,rm命令以-开头,这样即使由于目标文件不存在而导致rm返回错误,命令make clean也是成功的。目标clean没有依赖关系,clean后面是空的,因此执行make时,如指定目标clean,则该目标对应的规则总是执行。

目标install依赖于myapp,因此make会先创建myapp,然后才能执行制作目标install所需的其他命令。用于制作install目标的规则由几个shell脚本命令组成,make在执行规则时会调用一个shell,并且针对每个规则使用一个新shell,上面每行代码的结尾处都加上了一个反斜杠,让所有shell脚本命令在逻辑上处于同一行,并作为一个整体传递给一个shell执行。这个命令由@开头,表示make执行它们时不会在标准输出上显示命令。

目标install并没有在执行下一个命令前检查前一个命令是否执行成功,可以将两个命令间的分号改为&&连接,这样后面的命令只在前面命令都执行成功的前提下才会被执行。

执行Makefile3:
在这里插入图片描述
单独执行make时,它会使用默认目标all,并创建可执行程序myapp。

有以下程序,命名为foo.c:

#include <stdlib.h>
#include <stdio.h>

int main() {
    printf("Hello World\n");
    exit(EXIT_SUCCESS);
}

不创建makefile时,直接执行:
在这里插入图片描述
可见,make知道如何调用编译器,此例中,它选择的是cc而非gcc(在Linux中,这没有问题,cc通常是gcc的一个链接,相当于快捷方式)。

内置规则又被称为推导规则,内置规则会使用宏定义,我们可以通过给宏赋予新值来改变其默认行为:
在这里插入图片描述
可通过-p选项打印出make的所有内置规则。使用内置规则,可将makefile中用于制作目标的规则去掉,只需指定依赖关系,从而简化makefile:
在这里插入图片描述
内置规则在使用时用到了文件的后缀名,make会根据特定的后缀名选择规则创建带有另一个不同后缀名的文件,最常见的规则是从一个.c文件创建出一个.o文件,该规则使用编译器编译,但不对源文件链接。

如果我们想用一个c++编译器编译后缀名为.cpp的文件,如果内置规则中没有这样的规则,要么在每个makefile中的规则部分指定,要么添加一条新的规则。

要想添加一条新的后缀规则,首先要在makefile文件中增加以下片段:
在这里插入图片描述
.cpp.o告诉make紧随其后的规则用于将.cpp文件转换为.o文件。规则部分特殊宏名称$<含义为依赖文件的名字。

我们只需告诉make如何用.cpp文件得到.o文件,从.o文件到二进制可执行文件的规则make已经知道了。

现在make已经知道如何处理.cpp文件了,当需要将一种类型文件转换为另一种类型文件时这个技术很有用。

新版make支持%通配符来匹配文件名,而非仅依赖于后缀名,以下写法与上例效果相同:
在这里插入图片描述
大型项目可以用函数库方便地管理多个编译产品,函数库就是文件,它们通常以.a结尾,该文件中包含一组目标文件,make可处理函数库,使其管理变得简单。

make用一个内置规则管理函数库:
在这里插入图片描述
宏$(AR)和$(ARFLAGS)的默认值通常分别为ar和rv。第一条规则告诉make怎样编译源文件以生成目标文件;第二条规则告诉make用ar命令将新的目标文件添加到函数库中。$@表示当前目标文件的名字,即.a文件名字,而$*表示不带后缀名的依赖的.c文件名。

要想将2.o和3.o放到函数库mylib.a中,只需添加以下内容,并修改myapp的制作规则:

# local libraries
MYLIB = mylib.a

myapp: main.o $(MYLIB)
	$(CC) -o myapp main.o $(MYLIB)

$(MYLIB): $(MYLIB)(2.o) $(MYLIB)(3.o)

clean:
	-rm main.o 2.o 3.o $(MYLIB)

之后执行做出了上述修改的文件:
在这里插入图片描述
可见,make首先编译并创建函数库,然后把main.o和函数库链接起来以创建myapp。接下来重新touch一下c.h,这样源文件3.c必须重新被编译,make正确完成了这一工作。

对于大型项目,我们有时希望把构成函数库的几个文件从主文件中分离出来,并将它们保存到一个子目录,用make有两个方法完成这一工作:
1.在子目录下编写第二个makefile文件,它的作用是编译该子目录下的源文件,并将它们保存到一个函数库中,然后将该库复制到上一级的主目录中,在主目录中的makefile文件包含一条用于制作函数库的规则,该规则会调用第二个makefile文件:
在这里插入图片描述
当make命令调用这条规则创建函数库时,它将切换到子目录mylibdirectory中,然后调用一个新的make命令管理函数库。由于make会针对每个命令调用一个新shell,因此必须将这两个命令括起来,确保它被一个单独的shell处理。

2.在原来的makefile中添加一些宏,可用以下宏代替.c.o规则:
在这里插入图片描述
以上新添加的宏通过在我们已见过的宏的尾部追加一个字母得到,D代表目录,F代表文件名。这条规则将编译子目录中的源文件并将目标文件放在该子目录中。

然后用以下依赖关系和规则更新当前目录下的函数库:
在这里插入图片描述
以上$?对依赖列表中比当前目标mylib.a还要新的文件执行规则。

GNU的make命令和GNU的gcc编译器有两个有趣的选项:
1.make命令的-jN选项,它允许make命令同时执行N条命令,如果项目的不同部分可以彼此独立地编译,make可同时调用几条规则。
2.gcc的-MM选项,它产生一个适用于make的依赖关系清单:
在这里插入图片描述
gcc编译器扫描源文件以查找include语句,然后以一种适合于直接插入到makefile中的格式输出依赖关系清单。

makedepend工具与-MM选项类似,但它直接将依赖关系附加到指定的makefile文件末尾。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值