《C程序设计新思维》一1.4 使用Makefile

简介:

本节书摘来自异步社区《C程序设计新思维》一书中的第1章,第1.4节,作者 【美】Ben Klemens,更多章节内容可以访问云栖社区“异步社区”公众号查看

1.4 使用Makefile

makefile提供一个解决所有以上这些麻烦的方案。它基本上可以看作一组有组织的变量和shell脚本。POSIX标准的make程序输入makefile作为指令和变量,然后自动化处理那些冗长繁琐的命令行。在这部分讲解之后,就没有什么必要去直接调用编译器了。

在“3.2 makefile还是shell脚本”中,会讲述关于makefile的更多细节;这里,先给出一个最小的、实用的,并且能够编译一个依赖于一个库的基本程序的makefile:


f65836d68f702bc28380c49fe676df6a206fd168

用法:

曾经的用法:将这段存为一个名为makefile的文件,与.c文件放在同一个目录中。如果你在试用GNU Make,如果你感觉需要将该文件与其他文件区别开,可以将首字母大写,即命名为Makefile。把你的程序的名字放在第一行(用progname,而不是progname.c)。
每次你需要重新编译的时候,输入make命令。


b19331687732a697bdb0807a0d4394fe04b1e92a

自己动手:下面是编程世界著名的hello.c程序,就两行:


2b6e8f2623a49ed760558b8fff33240ecb141e40

把这个源文件和前面的makefile存在同一个目录中,然后试着按前面的步骤编译和运行程序。成功后,修改makefile使其能编译erf.c。

1.4.1 设定变量

很快我们会介绍makefile的实际应用,但你可能注意到前述makefile里6行中有5行是关于变量设定的(目前多数被设定为空),意味着我们还需要多花一点时间在环境变量的细节上。


e6eb2206f56ac943c6d41ba14a020d48152d6230

历史上,曾经出现过两种主流的shell语法:一种基本上是基于Bourne shell的,另一种主要基于C shell。C shell的变量语法稍有不同,例如,采用set CFLAGS=”-g –Wall –O3”来设定CFLAGS的值。但是POSIX标准是写成Bourne类型的语法,也就是我这本书余下部分所采用的。
shell和make用$来指代变量的值,但是shell用$var,而make假设任何变量的名字多于一个字母:$(var)。所以,前面的makefile中,$(P):$(OBJECTS)将相当于


738dc7a319ce31b16b2835ec1639d8a5d84f5d46

有几种办法来让make识别变量:

首先,在调用make之前从shell中设定变量,并且使用export命令导出这些变量,也就是说当shell产生一些子进程的时候,它有自己的环境变量列表。从一个POSIX标准的命令行中设定CFLAGS,可以这样:


fda7d00ee8cdb3b1891770744e3a6175da1ead07

我自己经常忽略这个makefile中的第一行,P=program_name,取而代之的是在每个会话中通过export P=program_name来设定,这意味着我免不了有时还得编辑一下makefile。

你也可以将那些export命令放在你的shell启动脚本中,比如.bashrc或.zshrc。这可以确保每次你登录或开始一个新的shell,这个变量会被设定并导出。如果你很确信CFLAGS每次都会是一样的,你可以在脚本中设定它们然后再也不用惦记这个问题了。
你可以通过在命令前放一个赋值操作来为命令导出变量。env命令列出它所知道的有环境变量,所以当你运行:


d4705a76ebb7f93e2fb456b1fda1f9b6d4590d37

你将看到正确的变量及其值。这是为什么shell不让你在等号附近放空格的原因:空格是用来区分命令行中的赋值操作的。

在这个方法中设定和导出变量应该在一行实现。如果你在命令行中执行了上面这条命令,再次运行env | grep PANTS,就会发现PANTS不再是一个被导出的变量了。

只要你愿意,你可以指定任意数量的变量:


9db80cd7e530ab493d65da1426d8f313afd6491e

这个技巧出现在shell规范的“简单命令”描述部分,也就是说赋值必须在一个实际的命令之前。这在你使用非命令的shell构造时很有意义。编写:


cd1577fcaed04beb47c4233042961c5dd03c5865

将失败并伴随一个晦涩的语法错误。正确的方式是:


60d3260b5a4708ab7d34e63d7cac69603543378f

就像前面介绍的那个makefile一样,你可以在makefile的头部设定变量,类似:CFLAGS=…。在makefile中,你可以在等号周围放上空格,这不会引发任何中断。
make将让你在命令行中设定变量,并独立于shell。那么,下面两行基本是对等的:


83d51c13d9fdd672b8de6e6fcce72889c5720b9e

对makefile而言,上面所有这些手段都是相等的;例外之处在于,被make调用的子程序只知道新的环境变量,而不知道任何makefile变量。


7f8a3f2d48da4e40bbdbc1a8061e3f7512e05509

在C代码中,可以用getenv函数来得到环境变量。getenv非常简单易用,在C中快速设定变量时是非常容易的,所以你可以从命令行中尝试设定不同的值。

例1-2是一个打印示例程序,只要用户需要,随时打印一个信息到屏幕。环境变量msg用于设定要打印的信息,而通过reps设定重复的次数。请注意我们是如何设定它们的默认值10次和“Hello.”的,这些默认值一般在调用getenv时返回NULL(典型的含义是这个环境变量没有被设定)。

例1-2 环境变量提供了一个改变程序细节的快速方式(getenv.c)


5180a23b72184af991c5c74d088e5943be818458

就像之前看到的,我们可以用一行命令导出一个变量,这样可以使得向程序发送变量更加方便。用法:


e4de0750e18dd1ac31d08e870a3d34757db4a63f

你可能觉得这个用法很奇怪——程序的输入应该跟在程序名后面才对,真是可恨——先不管这些奇怪的事情,你可以看到程序自身进行了一些设置工作,我们几乎没费什么精力立即就得到了来自命令行的命名参数。

当你的程序能够跑得远一点以后,可以试一下配置getopt来按照通常的方法设定输入参数。
make也提供一些内置的变量。下面是其中一些(POSIX标准)变量的介绍,你可能在随后针对规则的学习中用到。

$@

返回完整的目标文件名。所谓目标(target),我的意思是指需要被生成的文件,比如从一个a.c文件中编译而得到的a.o文件,或者一个通过连接.o文件生成的程序。

$*

不带文件名后缀的输出文件。如果输出文件是prog.o,$就是prog,而$.c就成为prog.c。

$<

触发和制作该目标的文件的名称。如果我们正在制作prog.o,有可能是prog.c文件刚被修改,所以$<就是prog.c。

1.4.2 规则

现在让我们专注地了解一下makefile的执行过程,并了解变量是如何影响这个过程的。

先不讨论变量的事情,来看一下makefile的代码片段,一般有以下形式:


887a747ef2bc4aa94784f0f3ba3dc360c3b1369b

如果目标被调用,通过命令make target,那么dependencies(支持项)将被检查。如果target是一个文件,dependencies也都是文件,并且target是比dependencies时间上更新的文件,那就是说这个文件已经是最新的,所以系统也不会再做什么。否则,针对target的处理将被执行,所有的dependencies将被运行或重新产生,target段落的script(脚本)部分也会被执行。

例如,在本文成书之前,我的博客上贴出了一系列的文章(在http://modelingwithdata.org)。每篇博客都是用HTML和PDF格式上传的,也都是用LaTeX产生的。我忽略了很多这个简单例子里的很多细节(比如latex2html的很多配置选项),但这是一个人们经常编写和运行的makefile。


7160a000ab3323ffd9c186a6a36539056a65d6b5

如果你将这些makefile片段从屏幕或者书本上复制到makefile文件中,不要忘记每行代码开头的留白部分必须是制表符(tab键)而不是空格(space键)。要怪就怪POSIX标准吧。


25048cd031638d2d4b5c4470db941086f60ed17d

我们通过类似export f=tip-make的命令来设定f。然后在命令行输入make的时候,第一个目标all被检测到。就是说,make命令自身相当于make“第一个目标”。这个依赖于html、doc和publish,所以那些目标也被依次调用。如果我知道这还没有准备好递交给这个世界,我可以调用make html doc并仅完成这些步骤。

在之前那个简单的makefile中,我们仅有一组target/dependency/script。例如:


f4774cde3b3ef30af095200de5c48b88fc4a22cb

这个和我的博客里的makefile遵循同样的支持项和脚本执行次序,但是脚本是隐含的。这里,P=domath是被编译的程序,并且依赖于目标文件addition.o和subtraction.o。因为addition.o没有被列出来当作一个处理目标,所以make用一条如下所述的隐含的规则来从.c文件编译.o文件。对subtrction.o和domath.o也是一样的操作(因为GNU make隐含假设domath依赖于这里给出设置的domath.o)。一旦所有的目标文件被建立了,我们没有在$(P) target运行的脚本,那么GNU make填充它的默认脚本来连接.o文件而成一个可执行文件。

POSIX标准的make有一个特殊的从a.c源文件到a.o的编译方法:


714a5a68974997dc65aecaef81d07bb4db3e9fe8

这里$(CC)变量代表你的C编译器;POSIX标准规定一个默认的CC=c99,但是GNU make的当前版本设定为CC=cc,并且一般连接给gcc。在本段最早的那个最小的makefile中,$(CC)被明确设定为c99,$(CFLAGS)被按照之前的选项列表设定,$(LDFLAGS)没有被设定因此没有被任何事物替代。所以如果make认为它必须产生your_program.o,下面就是你需要运行的命令行,在给定的makefile中:


b9d268ac7480c1b9ed703ef4324a350f399ba7a5

当GNU make觉得你需要从目标文件编译出一个可执行文件时,它用下面的方法实现:


3ec41b6dc5fc431cf5812eb9fbfc5ca92f6f3758

如果想起在连接器中的次序问题,那么我将需要两个连接器变量。在前面的例子中,我们需要:


8999dd2fd716353806c5a6e9e626be6780fd871d

作为连接的相关操作。相较这个方法中的正确的汇编命令,我们可以看到我们需要设定LDLIBS=-lbroad –lgeneral。如果我们已经设定了LDFLAGS=-lboard –lgeneral,那么这个方法将产生cc –lbroad –lgenerl specifix,o,这个方法看起来是有问题的。请注意LDFLAGS也经常出现在从.c文件编译到.o文件的过程中。


7150b1a2d8c0d986e78e4d9acfe8e6cdfc71ff0d

如果你想看到你的make内置的全部默认规则和变量的列表,可以尝试:


6d2881f5d9c5791a6654dec23e46659073aa8bba

所以,这个游戏就是:找到合适的变量并把它们设置在makefile中。你还是要探究一下哪个才是正确的选项,但是最少你可以把它们写在makefile中,之后便再也不必去考虑。

如果你用一个IDE,或者CMAKE,或者任何POSIX标准make的替代品,你都可以做这个“找到合适的变量”的游戏。我还会继续讨论前面的最小makefile,在你的IDE中应该不难找到对应的变量。

CFLAGS是根深蒂固的习惯,但是你需要为不同的系统的连接器设定不同的变量。甚至LDLIBS都不是POSIX标准的,但是被GNU make使用。
CFLAGS和LDLIBS变量是我们将用来串起所有的定位和识别库文件的编译器选项。如果你有pkg-config,可以把反引号调用放在这里。例如,我的系统中的makefile,也就是我几乎把Apophenia和Glib用在所有程序中,看起来是这样的:


d38f98275a812aec97c05e52bbe8077f0408208e

或者,手工指定-I、-L和-l变量,如:


3abd30e49a7c105aec4ba4f176504e4abb9e60ba

当你在LIBS和CFLAGS行中添加了一个库的路径,并确定这个配置在你的系统上起了作用时,一般很少有理由去移除这个配置。你真的在乎最终的可执行文件可能会比你为每个程序都做一个个性化makefile所配置的大10KB么?这意味着,你可以把你机器里常用的所有库都总结在一个makefile中,并把它从一个目录复制到另一个目录中而不需要重写。
如果你有第二个(或者更多)C文件,加入类似second.o third.o的内容到makefile OBJECTS段落头部的行中(不要加逗号,名字之间仅用空格)。make将用这些来决定哪个文件需要被制作以及以何种方式制作。
如果你的程序只有一个.c文件,你可能压根就不需要makefile。假设目录中现在有一个erf.c文件且没有makefile,可以用你的shell:


0950df46f80545f5d936d2a9330b379dde09eb50

并可以欣赏make如何用他的C编译知识来做余下的工作。


9899a28ae133ae382955ea32496f4f5352e95b23

说实话,我根本不知道。不同的操作系统中是不同的,无论是类型不同还是年份不同,甚至在同一个系统中,那些规则也经常有点混乱。

不过,我们将在第3章中介绍的工具Libtool知道每个操作系统中的每个共享库的每个制作过程的细节。我建议你花点时间去了解Autotools,那么就可以一举解决共享目标文件的编译问题,而不是花时间在了解每种系统的正确编译器选项和连接过程上。

相关文章
|
2月前
|
Linux 编译器 C语言
Linux应用开发基础知识——Makefile 的使用(二)
Linux应用开发基础知识——Makefile 的使用(二)
40 0
Linux应用开发基础知识——Makefile 的使用(二)
|
6月前
|
自然语言处理 程序员 编译器
C程序设计介绍
C程序设计是一种计算机编程语言,由美国贝尔实验室的Dennis Ritchie在20世纪70年代初开发。C语言是一种通用的高级编程语言,被广泛用于系统软件开发、嵌入式系统、游戏开发等领域。 C语言具有以下特点: 1. 简洁高效:C语言的语法简洁清晰,具有较高的执行效率。它提供了丰富的操作符和控制结构,使得程序员可以更灵活地进行编程。 2. 低级语言特性:C语言提供了对计算机底层硬件的直接访问能力,可以进行位操作、指针操作等。这使得C语言在系统编程和嵌入式开发中非常有用。 3. 可移植性:C语言的标准库提供了丰富的函数和数据类型,可以在不同的操作系统和硬件平台上进行移植。这使得C语言成为
28 0
|
1月前
|
存储 编译器 程序员
C语言调试大作战:与VS编译器共舞,上演一场“捉虫记”的艺术与科学
C语言调试大作战:与VS编译器共舞,上演一场“捉虫记”的艺术与科学
|
9月前
|
编译器 C语言
头歌c语言实训项目-函数(2)
头歌c语言实训项目-函数(2)
292 1
|
存储 人工智能 C语言
2020_883《C程序设计》
简述C语言中标识符的规定;在给变量、数组和函数起名时,应注意什么?
2020_883《C程序设计》
|
存储 自然语言处理 Linux
0基础C语言自学教程——收官之战——第十四节 文件的编译和链接
这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
124 0
0基础C语言自学教程——收官之战——第十四节 文件的编译和链接
|
自然语言处理 Java 编译器
编译基础理论
  最近在读一本编译相关的书《两周自制脚本语言》,书中用Java来设计一种名为Stone的脚本语言。
编译基础理论