C 语言构建多文件程序

目录汇总:C 语言零基础入门教程

编写一个简单的 C 程序 中,我们研究了对单个文件的程序进行编译和链接的过程。现在将把这种讨论推广到由多个文件构成的程序中。构建大型程序和构建小程序所需的基本步骤相同。

  • 编译。必须对程序中的每个源文件分别进行编译。(不需要编译头文件。编译包含头文件的源文件时会自动编译头文件的内容。)对于每个源文件,编译器会产生一个包含目标代码的文件。这些文件称为目标文件(object file),在 UNIX 系统中的扩展名为 .o,在 Windows 系统中的扩展名为 .obj。
  • 链接。链接器把上一步产生的目标文件和库函数的代码结合在一起生成可执行的程序。链接器的一个职责是解决编译器遗留的外部引用问题。(外部引用发生在一个文件中的函数调用另一个文件中定义的函数或者访问另一个文件中定义的变量时。)

大多数编译器允许一步构建程序。例如,对于 GCC 来说,可以使用下列命令行来构建 C 语言把程序划分成多个文件 中的 justify 程序:

1
gcc –o justify justify.c line.c word.c

首先把三个源文件编译成目标代码,然后自动把这些目标文件传递给链接器,链接器会把它们结合成一个文件。选项 -o 表明我们希望可执行文件的名字是 justify。

一、makefile

把所有源文件的名字放在命令行中很快变得枯燥乏味。更糟糕的是,如果重新编译所有源文件而不仅仅是最近修改过的源文件,重新构建程序的过程中可能会浪费大量的时间。

为了更易于构建大型程序,UNIX 系统发明了 makefile 的概念,这个文件包含构建程序的必要信息。makefile 不仅列出了作为程序的一部分的那些文件,而且还描述了文件之间的依赖性。假设文件 foo.c 包含文件 bar.h,那么就说 foo.c“依赖于”bar.h,因为修改 bar.h 之后将需要重新编译 foo.c。

下面是针对程序 justify 而设的 UNIX 系统的 makefile,它用 GCC 进行编译和链接:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
justify: justify.o word.o line.o
        gcc -o justify justify.o word.o line.o

justify.o: justify.c word.h line.h
        gcc -c justify.c

word.o: word.c word.h
        gcc -c word.c

line.o: line.c line.h
        gcc -c line.c

这里有 4 组代码行,每组称为一条规则。每条规则的第一行给出了目标文件,跟在后边的是它所依赖的文件。第二行是待执行的命令(当目标文件所依赖的文件发生改变时,需要重新构建目标文件,此时执行第二行的命令)。下面看一下前两条规则,后两条类似。

在第一条规则中,justify(可执行程序)是目标文件:

1
2
justify: justify.o word.o line.o
        gcc -o justify justify.o word.o line.o

第一行说明 justify 依赖于 justify.o、word.o 和 line.o 这三个文件。在程序的上一次构建完成之后,只要这三个文件中有一个发生改变,justify 都需要重新构建。下一行信息说明如何重新构建 justify(通过使用 gcc 命令链接三个目标文件)。

在第二条规则中,justify.o 是目标文件:

1
2
justify.o: justify.c word.h line.h
        gcc -c justify.c

第一行说明,如果 justify.c、word.h 或 line.h 文件发生改变,那么 justify.o 需要重新构建。(提及 word.h 和 line.h 的理由是,justify.c 包含这两个文件,它们的改变都可能会对 justify.c 产生影响。)下一行信息说明如何更新 justify.o(通过重新编译 justify.c)。选项 -c 通知编译器把 justify.c 编译为目标文件,但是不要试图链接它。

一旦为程序创造了 makefile,就能使用 make 实用程序来构建(或重新构建)该程序了。通过检查与程序中每个文件相关的时间和日期,make 可以确定哪个文件是过期的。然后,它会调用必要的命令来重新构建程序。

如果你想试试 make,下面是一些需要了解的细节。

  • makefile 中的每个命令前面都必须有一个制表符,不是一串空格。(在我们的例子中,命令看似缩进了 8 个空格,但实际上是一个制表符。)

  • makefile 通常存储在一个名为 Makefile(或 makefile)的文件中。使用 make 实用程序时,它会自动在当前目录下搜索具有这些名字的文件。

    用下面的命令调用 make:

    1
    
    make 目标
    

    其中目标是列在 makefile 中的目标文件之一。为了用我们的 makefile 构建 justify 可执行程序,可以使用命令

    1
    
    make justify
    
  • 如果在调用 make 时没有指定目标文件,则将构建第一条规则中的目标文件。例如,命令

    1
    
    make
    

    将构建 justify 可执行程序,因为 justify 是我们的 makefile 中的第一个目标文件。除了第一条规则的这一特殊性质外,makefile 中规则的顺序是任意的。

make 非常复杂,复杂到可以用整本书来介绍,所以这里不打算深入研究它的复杂性。真正的 makefile 通常不像我们的示例那样容易理解。有很多方法可以减少 makefile 中的冗余,使它们更容易修改。但是,这些技术同时也极大地降低了它们的可读性。

顺便说一句,不是每个人都用 makefile 的。其他一些程序维护工具也很流行,包括一些集成开发环境支持的“工程文件”。

二、链接期间的错误

一些在编译期间无法发现的错误会在链接期间被发现。尤其是如果程序中丢失了函数定义或变量定义,那么链接器将无法解析外部引用,从而导致出现类似“undefined symbol”或“undefined reference”的消息。

链接器检查到的错误通常很容易修改。下面是一些最常见的错误起因。

  • 拼写错误。如果变量名或函数名拼写错误,那么链接器将进行缺失报告。例如,如果在程序中定义了函数 read_char,但调用时把它写为 read_cahr,那么链接器将报告说缺失 read_cahr 函数。
  • 缺失文件。如果链接器不能找到文件 foo.c 中的函数,那么它可能不知道存在此文件。这时就要检查 makefile 或工程文件来确保 foo.c 文件是列出了的。
  • 缺失库。链接器不可能找到程序中用到的全部库函数。UNIX 系统中有一个使用了 <math.h> 的经典例子。在程序中简单地包含该头可能是不够的,很多 UNIX 版本要求在链接程序时指明选项 -lm,这会导致链接器去搜索一个包含 <math.h> 函数的编译版本的系统文件。不使用这个选项可能会在链接时导致出现“undefined reference”消息。

三、重新构建程序

在程序开发期间,极少需要编译全部文件。大多数时候,我们会测试程序,进行修改,然后再次构建程序。为了节约时间,重新构建的过程应该只对那些可能受到上一次修改影响的文件进行重新编译。

假设按照 C 语言把程序划分成多个文件 中的框架方法设计了程序,并对每一个源文件都使用了头文件。为了判断修改后需要重新编译的文件的数量,我们需要考虑两种可能性。

第一种可能性是修改只影响一个源文件。这种情况下,只有此文件需要重新编译。(当然,在此之后整个程序将需要重新链接。)思考程序 justify。假设要精简 word.c 中的函数 read_char(修改过的地方用粗体标注):

int read_char(void)
{
  int ch = getchar();

  return (ch == '\n'  || ch == '\t') ? '  '  : ch;
}

这种改变没有影响 word.h,所以只需要重新编译 word.c 并且重新链接程序就行了。

第二种可能性是修改会影响头文件。这种情况下,应该重新编译包含此头文件的所有文件,因为它们都可能潜在地受到这种修改的影响。(有些文件可能不会受到影响,但是保守一点是值得的。)

作为示例,思考一下程序 justify 中的函数 read_word。注意,为了确定刚读入的单词的长度,main 函数在调用 read_word 函数后立刻调用 strlen。因为 read_word 函数已经知道了单词的长度(read_word 函数的变量 pos 负责跟踪长度),所以使用 strlen 就显得多余了。修改 read_word 函数来返回单词的长度是很容易的。首先,改变 word.h 文件中 read_word 函数的原型:

/***********************************************************
 * read_word: Reads the next word from the input and       *
 *            stores it in word. Makes word empty if no    *
 *            word could be read because of end-of-file.   *
 *            Truncates the word if its length exceeds     *
 *            len. Returns the number of characters        *
 *            stored.                                      *
 ***********************************************************/
int read_word(char *word, int len);

当然,要仔细修改 read_word 函数的注释。接下来,修改 word.c 文件中 read_word 函数的定义:

int read_word(char *word, int len)
{
  int ch, pos = 0;

  while ((ch = read_char()) == ' ')
    ;
  while (ch != ' ' && ch != EOF)  {
    if (pos < len)
      word[pos++] = ch;
    ch = read_char();
  }
  word[pos] = '\0';
  return pos;
}

最后,再来修改 justify.c,方法是删除对 <string.h> 的包含,并按如下方式修改 main 函数:

int main(void)
{
  char word[MAX_WORD_LEN+2];
  int word_len;

  clear_line();
  for (;;) {
    word_len = read_word (word, MAX_WORD_LEN+1);
    if (word_len == 0) {
      flush_line();
      return 0;
    }
    if (word_len > MAX_WORD_LEN)
      word[MAX WORD LEN] = '*';
    if (word_len + 1 > space_remaining()) {
      write_line();
      clear_line();
    }
    add_word(word);
  }
}

一旦做了上述这些修改,就需要重新构建程序 justify,方法是重新编译 word.c 和 justify.c,然后再重新链接。不需要重新编译 line.c,因为它不包含 word.h,所以也就不会受到 word.h 改变的影响。对于 GCC,可以使用下列命令来重新构建程序:

1
gcc –o justify justify.c word.c line.o

注意,这里用的是 line.o 而不是 line.c。

使用 makefile 的好处之一就是可以自动重新构建。通过检查每个文件的日期,make 实用程序可以确定程序上一次构建之后哪些文件发生了改变。然后,它会把那些改变的文件和直接或间接依赖于它们的全部文件一起重新编译。例如,如果我们对 word.h、word.c 和 justify.c 进行了修改,并重新构建了 justify 程序,那么 make 将执行如下操作。

(1) 编译 justify.c 以构建 justify.o(因为修改了 justify.c 和 word.c)。

(2) 编译 word.c 以构建 word.o(因为修改了 word.c 和 word.h)。

(3) 链接 justify.o、word.o 和 line.o 以构建 justify(因为修改了 justify.o 和 word.o)。

四、在程序外定义宏

在编译程序时,C 语言编译器通常会提供一种指定宏的值的方法。这种能力使我们很容易对宏的值进行修改,而不需要编辑程序的任何文件。当利用 makefile 自动构建程序时这种能力尤其有价值。

大多数编译器(包括 GCC)支持 -D 选项,此选项允许用命令行来指定宏的值:

1
gcc –DDEBUG=1 foo.c

在这个例子中,定义宏 DEBUG 在程序 foo.c 中的值为 1,其效果相当于在 foo.c 的开始处这样写:

1
#define DEBUG 1

如果 -D 选项命名的宏没有指定值,那么这个值被设为 1。

许多编译器也支持 -U 选项,这个选项用于删除宏的定义,效果相当于 #undef。我们可以使用 -U 选项来删除预定义宏(C 语言宏定义简介)或之前在命令行方式下用 -D 选项定义的宏的定义。

请参阅

(完)

comments powered by Disqus