[推荐]GNU make 指南
时间:2006-06-19 18:34:16 来源:本站搜集整理 作者:Eric
发信人: joviqq (smtp projecting&freeradius), 信区: CPlusPlus 标 题: GNU make 指南 发信站: BBS 水木清华站 (Wed Mar 17 16:12:49 2004), 转信 翻译: 哈少 译者按: 本文是一篇介绍 GNU Make 的文章,读完后读者应该基本掌握了 make 的用法。 而 make 是所有想在 Unix (当然也包括 Linux )系统上编程的用户必须掌握的工具。如 果你写的程序中没有用到 make ,则说明你写的程序只是个人的练习程序,不具有任何实用 的价值。也许这么说有点 儿偏激,但 make 实在是应该用在任何稍具规模的程序中的。希 望本文可以为中国的 Unix 编程初学者提供一点儿有用的资料。中国的 Linux 用户除了学 会安装红帽子以外, 实在应该尝试写一些有用的程序。个人想法,大家参考。 -------------------------------------------------------------------------------- C-Scene 题目 #2 多文件项目和 GNU Make 工具 作者: 乔治富特 (Goerge Foot) 电子邮件: george.foot@merton.ox.ac.uk Occupation: Student at Merton College, Oxford University, England 职业:学生,默尔顿学院,牛津城大学,英格兰 IRC匿名: gfoot -------------------------------------------------------------------------------- 拒绝承诺:作者对于任何因此而对任何事物造成的所有损害(你所拥有或不 拥有的实际的 ,抽象的,或者虚拟的)。所有的损坏都是你自己的责任,而 与我无关。 所有权: “多文件项目”部分属于作者的财产,版权归乔治富特1997年 五月至七月。 其它部分属 CScene 财产,版权 CScene 1997年,保留所有 版权。本 CScene 文章的 分发,部分或全部,应依照所有其它 CScene 的文章 的条件来处理。 0) 介绍 ~~~~~~~~~~~~~~~ 本文将首先介绍为什么要将你的C源代码分离成几个合理的独立档案,什么时 候需要分, 怎么才能分的好。然后将会告诉你 GNU Make 怎样使你的编译和连 接步骤自动化。对于其 它 Make 工具的用户来说,虽然在用其它类似工具时要 做适当的调整,本文的内容仍然是 非常有用的。如果对你自己的编程工具有怀 疑,可以实际的试一试,但请先阅读用户手册 。 1) 多文件项目 ~~~~~~~~~~~~~~~~~~~~~~ 1.1为什么使用它们? 首先,多文件项目的好处在那里呢? 它们看起来把事情弄的复杂无比。又要 header 文件,又要 extern 声明,而且如果需要查 找一个文件,你要在更多的文件里搜索。 但其实我们有很有力的理由支持我们把一个项目分解成小块。当你改动一行代码,编译器需 要全部重新编译来生成一个新的可执行文件。但如果你的项目是分开在几个小文件里,当你 改动其中一个文件的时候,别的源文件的目标文件(object files)已经存在,所以没有什么 原因去重新编译它们。你所需要做的只是重现编译被改动过的那个文件,然后重新连接所有 的目标文件罢了。在大型的项目中,这意味着从很长的(几分钟到几小时)重新编译缩短为 十几,二十几秒的简单调整。 只要通过基本的规划,将一个项目分解成多个小文件可使你更加容易的找到一段代码。很简 单,你根据代码的作用把你的代码分解到不同的文件里。当你要看一段代码时,你可以准确 的知道在那个文件中去寻找它。 从很多目标文件生成一个程序包 (Library)比从一个单一的大目标文件生成要好的多。当然 实际上这是否真是一个优势则是由你所用的系统来决定的。但是当使用 gcc/ld (一个 GNU C 编译/连接器) 把一个程序包连接到一个程序时,在连接的过程中,它会尝试不去连接没 有使用到的部分。但它每次只能从程序包中把一个完整的目标文件排除在外。因此如果你参 考一个程序包中某一个目标档中任何一个符号的话,那么这个目标文件整个都会被连接进来 。要是一个程序包被非常充分的分解了的话,那么经连接后,得到的可执行文件会比从一个 大目标文件组成的程序包连接得到的文件小得多。 又因为你的程序是很模块化的,文件之间的共享部分被减到最少,那就有很多好处——可以 很容易的追踪到臭虫,这些模块经常是可以用在其它的项目里的,同时别人也可以更容易的 理解你的一段代码是干 什么的。当然此外还有许多别的好处…… 1.2 何时分解你的项目 很明显,把任何东西都分解是不合理的。象“世界,你们好”这样的简单程序根本就不能分 ,因为实在也没什么可分的。把用于测试用的小程序分解也是没什么意思的。但一般来说, 当分解项目有助于布局、发展和易读性的时候,我都会采取它。在大多数的情况下,这都是 适用的。(所谓“世界,你们好”,既 'hello world' ,只是一个介绍一种编程语言时惯 用的范例程序,它会在屏幕上显示一行 'hello world' 。是最简单的程序。) 如果你需要开发一个相当大的项目,在开始前,应该考虑一下你将如何实现它,并且生成几 个文件(用适当的名字)来放你的代码。当然,在你的项目开发的过程中,你可以建立新的 文件,但如果你这么做的话,说明你可能改变了当初的想法,你应该想想是否需要对整体结 构也进行相应的调整。 对于中型的项目,你当然也可以采用上述技巧,但你也可以就那么开始输入你的代码,当你 的码多到难以管理的时候再把它们分解成不同的档案。但以我的经验来说,开始时在脑子里 形成一个大概的方案,并且尽量遵从它,或在开发过程中,随着程序的需要而修改,会使开 发变得更加容易。 1.3 怎样分解项目 先说明,这完全是我个人的意见,你可以(也许你真的会?)用别的方式来做。这会触动到 有关编码风格的问题,而大家从来就没有停止过在这个问题上的争论。在这里我只是给出我 自己喜欢的做法(同时也给出这么做的原因): i) 不要用一个 header 文件指向多个源码文件(例外:程序包 的 header 文件)。用一个 header定义一个源码文件的方式 会更有效,也更容易查寻。否则改变一个源文件的结构( 并且 它的 header 文件)就必须重新编译好几个文件。 ii) 如果可以的话,完全可以用超过一个的 header 文件来指向同 一个源码文件。有时将 不可公开调用的函数原型,类型定义 等等,从它们的C源码文件中分离出来是非常有用的 。使用一 个 header 文件装公开符号,用另一个装私人符号意味着如果 你改变了这个源码 文件的内部结构,你可以只是重新编译它而 不需要重新编译那些使用它的公开 header 文 件的其它的源文 件。 iii) 不要在多个 header 文件中重复定义信息。 如果需要, 在其中一个 header 文件里 #include 另一个,但 是不要重复输入相同的 header 信息两次。原因是如果你以后改 变 了这个信息,你只需要把它改变一次,不用搜索并改变另外一 个重复的信息。 iv) 在每一个源码文件里, #include 那些声明了源码文件中的符 号的所有 header 文件 。这样一来,你在源码文件和 header 文件对某些函数做出的矛盾声明可以比较容易的被编 译器发现。 1.4 对于常见错误的注释 a) 定义符 (Identifier) 在源码文件中的矛盾:在C里,变量和函数的缺 省状态是公用的 。因此,任何C源码档案都可以引用存在于其它源 码档中的通用 (global) 函数和通用变 量,既使这个档案没有那个变 量或函数的声明或原型。因此你必须保证在不同的两个档案 里不能 用同一个符号名称,否则会有连接错误或者在编译时会有警告。 一种避免这种错误的方法是在公用的符号前加上跟其所在源文件有 关的前缀。比如:所有 在 gfx.c 里的函数都加上前缀“gfx_”。如果 你很小心的分解你的程序,使用有意义的函 数名称,并且不是过分 使用通用变量,当然这根本就不是问题。 要防止一个符号在它被定义的源文件以外被看到,可在它的定义前 加上关键字“static” 。这对只在一个档案内部使用,其它档案都 都不会用到的简单函数是很有用的。 b) 多次定义的符号: header 档会被逐字的替换到你源文件里 #include 的位置的。因此 ,如果 header 档被 #include 到一个以上的源文件 里,这个 header 档中所有的定义就 会出现在每一个有关的源码文件 里。这会使它们里的符号被定义一次以上,从而出现连接 错误(见 上)。 解决方法: 不要在 header 档里定义变量。你只需要在 header 档里声明它们然后在适当 的C源码文件(应该 #include 那个 header 档的那个)里定义它们(一次)。对于初学者 来说,定义和声明是 很容易混淆的。声明的作用是告诉编译器其所声明的符号应该存在, 并且要有所指定的类型。但是,它并不会使编译器分配贮存空间。 而定义的做用是要求编 译器分配贮存空间。当做一个声明而不是做 定义的时候,在声明前放一个关键字“extern ”。 例如,我们有一个叫“counter”的变量,如果想让它成为公用的, 我们在一个源码程序( 只在一个里面)的开始定义它:“int counter;”,再在相关的 header 档里声明它:“ extern int counter;”。 函数原型里隐含着 extern 的意思,所以不需顾虑这个问题。 c) 重复定义,重复声明,矛盾类型: 请考虑如果在一个C源码文件中 #include 两个档 a.h 和 b.h, 而 a.h 又 #include 了 b.h 档(原因是 b.h 档定义了一些 a.h 需要的类型),会发生什么事呢?这时该C源码文 件 #include 了 b.h 两次。因此每一个在 b.h 中的 #define 都发生了两次,每一 个声明 发生了两次,等等。理论上,因为它们是完全一样的拷贝, 所以应该不会有什么问题,但 在实际应用上,这是不符合C的语法 的,可能在编译时出现错误,或至少是警告。 解决的方法是要确定每一个 header 档在任一个源码文件中只被包 含了一次。我们一般是 用预处理器来达到这个目的的。当我们进入 每一个 header 档时,我们为这个 header 档 #define 一个巨集 指令。只有在这个巨集指令没有被定义的前提下,我们才真正使用 该 header 档的主体。在实际应用上,我们只要简单的把下面一段 码放在每一个 header 档的 开始部分: #ifndef FILENAME_H #define FILENAME_H 然后把下面一行码放在最后: #endif 用 header 档的档名(大写的)代替上面的 FILENAME_H,用底线 代替档名中的点。有些人 喜欢在 #endif 加上注释来提醒他们这个 #endif 指的是什么。例如: #endif /* #ifndef FILENAME_H */ 我个人没有这个习惯,因为这其实是很明显的。当然这只是各人的 风格不同,无伤大雅。 你只需要在那些有编译错误的 header 档中加入这个技巧,但在所 有的 header 档中都加 入也没什么损失,到底这是个好习惯。 1.5 重新编译一个多文件项目 清楚的区别编译和连接是很重要的。编译器使用源码文件来产生某种 形式的目标文件 (object files)。在这个过程中,外部的符号参考并 没有被解释或替换。然后我们使用连 接器来连接这些目标文件和一些 标准的程序包再加你指定的程序包,最后连接生成一个可 执行程序。 在这个阶段,一个目标文件中对别的文件中的符号的参考被解释,并 报告不能 被解释的参考,一般是以错误信息的形式报告出来。 基本的步骤就应该是,把你的源码文件一个一个的编译成目标文件的格 式,最后把所有的 目标文件加上需要的程序包连接成一个可执行文件。 具体怎么做是由你的编译器决定的。 这里我只给出 gcc (GNU C 编译 器)的有关命令,这些有可能对你的非 gcc 编译器也适 用。 gcc 是一个多目标的工具。它在需要的时候呼叫其它的元件(预处理 程序,编译器,组合 程序,连接器)。具体的哪些元件被呼叫取决于 输入文件的类型和你传递给它的开关。 一般来说,如果你只给它C源码文件,它将预处理,编译,组合所有 的文件,然后把所得 的目标文件连接成一个可执行文件(一般生成的 文件被命名为 a.out )。你当然可以这么 做,但这会破坏很多我们 把一个项目分解成多个文件所得到的好处。 如果你给它一个 -c 开关,gcc 只把给它的文件编译成目标文件, 用源码文件的文件名命 名但把其后缀由“.c”或“.cc”变成“.o”。 如果你给它的是一列目标文件, gcc 会把 它们连接成可执行文件, 缺省文件名是 a.out 。你可以改变缺省名,用开关 -o 后跟你指 定 的文件名。 因此,当你改变了一个源码文件后,你需要重新编译它: 'gcc -c filename.c' 然后重新 连接你的项目: 'gcc -o exec_filename *.o'。 如果你改变了一个 header 档,你需要重 新编译所有 #include 过 这个档的源码文件,你可以用 'gcc -c file1.c file2.c file3.c' 然后象上边一样连接。 当然这么做是很繁琐的,幸亏我们有些工具使这个步骤变得简单。 本文的第二部分就是介 绍其中的一件工具:GNU Make 工具。 (好家伙,现在才开始见真章。您学到点儿东西没?) |
文章评论
共有 0 位网友发表了评论 此处只显示部分留言 点击查看完整评论页面