C 语言把程序划分成多个文件

本文内容

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

现在应用我们已经知道的关于头文件和源文件的知识来开发一种把一个程序划分成多个文件的简单方法。这里将集中讨论函数,但是同样的规则也适用于外部变量。假设已经设计好程序,换句话说,已经决定程序需要什么函数以及如何把函数分为逻辑相关的组。

下面是处理的方法。把每个函数集合放入一个不同的源文件中(比如用名字 foo.c 来表示一个这样的文件)。另外,创建和源文件同名的头文件,只是扩展名为 .h(在此例中,头文件是 foo.h)。在 foo.h 文件中放置 foo.c 中定义的函数的函数原型。(在 foo.h 文件中不需要也不应该声明只在 foo.c 内部使用的函数。下面的 read_char 函数就是一个这样的例子。)每个需要调用定义在 foo.c 文件中的函数的源文件都应包含 foo.h 文件。此外,foo.c 文件也应包含 foo.h 文件,这是为了编译器可以检查 foo.h 文件中的函数原型是否与 foo.c 文件中的函数定义相一致。

main 函数将出现在某个文件中,这个文件的名字与程序的名字相匹配。如果希望称程序为 bar,那么 main 函数就应该在文件 bar.c 中。main 函数所在的文件中也可以有其他函数,前提是程序中的其他文件不会调用这些函数。

程序 文本格式化

为了说明刚刚论述的方法,现在把它用于一个小型的文本格式化程序 justify。我们用一个名为 quote 的文件作为 justify 的输入样例,quote 文件包含下列(未格式化的)引语,这些引语来自 Dennis M. Ritchie 写的“The Development of the C programming language”一文(参见 History of Programming Language II 一书,由 T. J. Bergin, Jr.和 R. G. Gibson, Jr.编写,第 671~687 页):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
   C     is quirky,  flawed,    and  an
enormous   success.      Although accidents of    history
 surely  helped,   it evidently    satisfied   a    need

    for  a   system  implementation     language     efficient
  enough    to  displace            assembly    language,
    yet sufficiently     abstract     and fluent     to describe
   algorithms   and       interactions      in a    wide    variety
of    environments.
                        --      Dennis       M.         Ritchie

为了在 UNIX 或 Windows 的命令行环境下运行这个程序,输入命令

1
justify <quote

符号 < 告诉操作系统,程序 justify 将从文件 quote 而不是从键盘读取输入。由 UNIX、Windows 和其他操作系统支持的这种特性称为输入重定向。当用给定的文件 quote 作为输入时,程序 justify 将产生下列输出:

1
2
3
4
5
6
C is quirky,  flawed,  and  an  enormous  success.   Although
accidents of history surely helped,  it evidently satisfied a
need for a system implementation language  efficient   enough
to displace assembly language, yet sufficiently abstract  and
fluent to describe algorithms and  interactions   in  a  wide
variety of environments.  -- Dennis M. Ritchie

程序 justify 的输出通常显示在屏幕上,但是也可以利用输出重定向把结果保存到文件中:

1
justify <quote >newquote

程序 justify 的输出将放入到文件 newquote 中。

通常情况下,justify 的输出应该和输入一样,区别仅在于删除了额外的空格和空行,并对代码行做了填充和调整。“填充”行意味着添加单词直到再多加一个单词就会导致行溢出时才停止,“调整”行意味着在单词间添加额外的空格以便于每行有完全相同的长度(60 个字符)。必须进行调整,只有这样一行内单词间的间隔才是相等的(或者几乎是相等的)。对输出的最后一行不进行调整。

假设没有单词的长度超过 20 个字符。(把与单词相邻的标点符号看作单词的一部分。)当然,这样是做了一些限制,不过一旦完成了程序的编写和调试,我们就可以很容易地把这个长度上限增加到一个事实上不可能超越的值。如果程序遇到较长的单词,它需要忽略前 20 个字符后的所有字符,用一个星号替换它们。例如,单词

1
antidisestablishmentarianism

将显示成

1
antidisestablishment*

现在明白了程序应该完成的内容,接下来该考虑如何设计了。首先发现程序不能像读单词一样一个一个地写单词,而必须把单词存储在一个“行缓冲区”中,直到足够填满一行。在进一步思考之后,我们决定程序的核心将是如下所示的循环:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
for (;;) {
  读单词;
  if (不能读单词) {
     输出行缓冲区的内容,不进行调整;
     终止程序;
  }

  if (行缓冲区已经填满){
    输出行缓冲区的内容,进行调整;
    清除行缓冲区;
  }
  往行缓冲区中添加单词;
}

因为我们需要函数处理单词,并且还需要函数处理行缓冲区,所以把程序划分为 3 个源文件。把所有和单词相关的函数放在一个文件(word.c)中,把所有和行缓冲区相关的函数放在另一个文件(line.c)中,第 3 个文件(fmt.c)将包含 main 函数。除了上述这些文件,还需要两个头文件 word.h 和 line.h。头文件 word.h 将包含 word.c 文件中函数的原型,而头文件 line.h 将包含 line.c 文件中函数的原型。

通过检查主循环可以发现,我们只需要一个和单词相关的函数——read_word。(如果 read_word 函数因为到了输入文件末尾而不能读入单词,那么将通过假装读取“空”单词的方法通知主循环。)因此,文件 word.h 是一个短小的文件:

word.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#ifndef WORD_H
#define WORD_H

/************************************************************
 * 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.                                          *
 ************************************************************/
void read_word(char *word, int len);

#endif

注意宏 WORD_H 是如何防止多次包含 word.h 文件的。虽然 word.h 文件不是真的需要它,但是以这种方式保护所有头文件是一个很好的习惯。

文件 line.h 不会像 word.h 那样短小。主循环的轮廓显示了需要执行下列操作的函数。

  • 输出行缓冲区的内容,不进行调整。
  • 检查行缓冲区中还剩多少字符。
  • 输出行缓冲区的内容,进行调整。
  • 清除行缓冲区。
  • 往行缓冲区中添加单词。

我们将要调用下面这些函数:flush_linespace_remainingwrite_lineclear_lineadd_word。下面是头文件 line.h 的内容。

line.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#ifndef LINE_H
#define LINE_H

/**********************************************************
 * clear_line: Clears the current line.                   *
 **********************************************************/
void clear_line(void);

/**********************************************************
 * add_word: Adds word to the end of the current line.    *
 *           If this is not the first word on the line,   *
 *           puts one space before word.                  *
 **********************************************************/
void add_word(const char *word);

/**********************************************************
 * space_remaining: Returns the number of characters left *
 *                  in the current line.                  *
 **********************************************************/
int space_remaining(void);

/**********************************************************
 * write_line: Writes the current line with               *
 *             justification.                             *
 **********************************************************/
void write_line(void);

/**********************************************************
 * flush_line: Writes the current line without            *
 *             justification. If the line is empty, does  *
 *             nothing.                                   *
 **********************************************************/
void flush_line(void);

#endif

在编写文件 word.c 和文件 line.c 之前,可以用在头文件 word.h 和头文件 line.h 中声明的函数来编写主程序 justify.c。编写这个文件的主要工作是把原始的循环设计翻译成 C 语言。

justify.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* Formats a file of text */

#include <string.h>
#include "line.h"
#include "word.h"

#define MAX_WORD_LEN 20

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

  clear_line();
  for (;;) {
    read_word(word, MAX_WORD_LEN+1);
    word_len = strlen(word);
    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);
  }
}

包含 line.h 和 word.h 可以使编译器在编译 justify.c 时能够访问到这两个文件中的函数原型。

main 函数用了一个技巧来处理超过 20 个字符的单词。在调用 read_word 函数时,main 函数告诉 read_word 截短任何超过 21 个字符的单词。当 read_word 函数返回后,main 函数检查 word 包含的字符串长度是否超过 20 个字符。如果超过了,那么读入的单词必须至少是 21 个字符长(在截短前),所以 main 函数会用星号来替换第 21 个字符。

现在开始编写 word.c 程序。虽然头文件 word.h 只有一个 read_word 函数的原型,但是如果需要,我们可以在 word.c 中放置更多的函数。不难看出,如果添加一个小的“辅助”函数 read_char,函数 read_word 的编写就容易一些了。read_char 函数的任务就是读取一个字符,如果是换行符或制表符则将其转换为空格。在 read_word 函数中调用 read_char 函数而不是 getchar 函数,就解决了把换行符和制表符视为空格的问题。

下面是文件 word.c:

word.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include "word.h"

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

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

void 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';
}

在讨论 read_word 函数之前,先对 read_char 函数中的 getchar 函数的使用讲两点。第一,getchar 函数实际上返回的是 int 类型值而不是 char 类型值,因此 read_char 函数中把变量 ch 声明为 int 类型,而且 read_char 函数的返回类型也是 int。第二,当不能继续读入时(通常因为读到了输入文件的末尾),getchar 的返回值为 EOF

read_word 函数由两个循环构成。第一个循环跳过空格,在遇到第一个非空白字符时停止。(EOF 不是空白,所以循环在到达输入文件的末尾时停止。)第二个循环读字符直到遇到空格或 EOF 时停止。循环体把字符存储到 word 中直到达到 len 的限制时停止。在这之后,循环继续读入字符,但是不再存储这些字符。read_word 函数中的最后一个语句以空字符结束单词,从而构成字符串。如果 read_word 在找到非空白字符前遇到 EOFpos 将为 0,从而使得 word 为空字符串。

唯一剩下的文件是 line.c。这个文件提供在文件 line.h 中声明的函数的定义。line.c 文件也会需要变量来跟踪行缓冲区的状态。一个变量 line 将存储当前行的字符。严格地讲,line 是我们需要的唯一变量。然而,出于对速度和便利性的考虑,还将用到另外两个变量:line_len(当前行的字符数量)和 num_words(当前行的单词数量)。

下面是文件 line.c:

line.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <stdio.h>
#include <string.h>
#include "line.h"

#define MAX_LINE_LEN 60

char line[MAX_LINE_LEN+1];
int line_len = 0;
int num_words = 0;

void clear_line(void)
{
  line[0] = '\0';
  line_len = 0;
  num_words = 0;
}

void add_word(const char *word)
{
  if (num_words > 0) {
    line[line_len] = ' ';
    line[line_len+1] = '\0';
    line_len++;
  }
  strcat(line, word);
  line_len += strlen(word);
  num_words++;
}

int space_remaining(void)
{
  return MAX_LINE_LEN - line_len;
}

void write_line(void)
{
  int extra_spaces, spaces_to_insert, i, j;

  extra_spaces = MAX_LINE_LEN - line_len;
  for (i = 0; i < line_len; i++) {
    if (line[i] != ' ')
      putchar(line[i]);
    else {
      spaces_to_insert = extra_spaces / (num_words - 1);
      for (j = 1; j <= spaces_to_insert + 1; j++)
        putchar(' ');
      extra_spaces -= spaces_to_insert;
      num_words--;
    }
  }
  putchar('\n');
}

void flush_line(void)
{
  if (line_len > 0)
    puts(line);
}

文件 line.c 中大多数函数很容易编写,唯一需要技巧的函数是 write_line。这个函数用来输出一行内容并进行调整。函数 write_lineline 中一个一个地写字符,如果需要添加额外的空格,那么就在每对单词之间停顿。额外空格的数量存储在变量 spaces_to_insert 中,这个变量的值由 extra_spaces / (num_words -1) 确定,其中 extra_spaces 初始值是最大行长度和当前行长度的差。因为在打印每个单词之后 extra_spacesnum_words 都发生变化,所以 spcaes_to_insert 也将变化。如果 extra_spaces 初始值为 10,并且 num_words 初始值为 5,那么第 1 个单词之后将有两个额外的空格,第 2 个单词之后将有两个额外的空格,第 3 个单词之后将有 3 个额外的空格,第 4 个单词之后将有 3 个额外的空格。

请参阅

(完)

comments powered by Disqus

本文内容