《C语言程序设计现代方法》note-11 构建大型程序时如何编排文件

助记提要

  1. 源文件和头文件;
  2. #include指令的两种格式;
  3. 使用头文件共享信息;
  4. 嵌套包含如何避免多次编译;
  5. 如何避免包含头文件;
  6. 程序分为多个文件的方法;
  7. 程序构建的基本步骤;
  8. makefile的格式和规则;
  9. makefile的使用;
  10. 自动重新构建的原理;
  11. 程序外操作宏;

15章 编写大型程序

15.1 源文件

源文件是指程序员编写的全部文件。
程序可以分割为任意数量的源文件,每个源文件包含程序的部分内容。
其中一个源文件必须包含名为main的函数,做为程序的起始点。

程序分为多个源文件的优点:

  • 相关的函数和变量放在一起,使程序结构清晰。
  • 可以分开对每个源文件进行编译。在程序规模很大且需要频繁改变时很节约时间。
  • 函数分别放在不同的源文件中更易复用。

15.2 头文件

#include指令告诉预处理器打开指定的文件,并把文件的内容插入到当前文件中。
如果想让几个源文件访问相同的信息,可以把这个信息放到一个文件中,然后用#include把该信息带到每个文件中。
这样的文件称为头文件。头文件的扩展名一般为.h

15.2.1 #include指令

书写格式
#include指令有两种书写格式,编译器对它们分别有不同的判定规则。

// 编译器在系统头文件所在目录搜索文件
#include <文件名>
// 编译器先在当前目录搜索,然后去系统头文件所在目录搜索
#include "文件名"

C标准不要求<>内是文件名,这种格式的指令有可能直接在编译器内部处理。

提高可移植性
不要在#include指令中使用绝对路径:

// 不要使用如下的绝对路径
#include "d:\cprogs\include\utils.h"
// 使用相对路径
#include "..\include\utils.h"

或者使用宏定义文件名,再由预处理器替换为正确的文件名:

#define FILE_PATH "..\include\utils.h"

#include FILE_PATH

预处理器不会把#include指令之后的字符串当做字面串处理,因此路径中的\不会被转义。

15.2.2 使用头文件的情况

以下内容一般整理在头文件中:
共享宏定义和类型定义
大型程序中多个源文件共享的宏定义和类型定义应该放在头文件中。
任何需要这些宏或类型的地方只要包含这个头文件即可。

这样做更节约时间,更易于修改,并且不用担心源文件包含相同宏或类型定义出现的矛盾。

共享函数原型
调用函数前没有函数定义或函数原型,编译器会嘉定返回类型是int型,并假定形参的数量和函数实参是匹配的。这样的假定可能是错误的。且编译器一次只编译一个文件,因此也无法做检查。

在每个调用该函数的地方都做一次声明可以解决问题,但是难以维护。

解决方法是把函数的原型放在一个头文件中,然后在所有调用它的地方包含这个头文件。
定义这个函数的地方也要包含这个头文件,用来验证函数原型是否和函数定义一致。

共享变量声明
通过类似int i;的格式声明变量时,编译器会为i预留空间。

当多个文件都需要使用变量i时,仅需要预留一次空间即可。
其余的地方只做声明而不分配空间的方式是使用extern关键字:

extern int i;

extern告诉编译器变量已在其它地方定义,不需要预留空间。

为了避免和共享函数原型一样的问题,通常把共享变量的声明放在头文件中,再由其它源文件包含这个头文件。
该变量定义的文件也要包含这个头文件,用于确保声明和定义匹配。

共享变量的做法是有缺陷的,尽量设计不需要共享变量的程序。

#include指令可以也直接包含源文件,但是不好。被多个文件使用的函数,在链接时会出现有多个目标代码的情况。

15.2.3 嵌套包含

嵌套包含是指头文件中也能使用#include指令。

源文件包含某个头文件两次时,如果这个头文件包含类型定义的话就会导致编译错误。
嵌套包含经常会出现这个问题。

为了防止头文件被多次包含,可以用#inndef#endif来封闭文件中的内容:

#ifndef BOOLEAN_H
#define BOOLEAN_H

#define TRUE 1
#define FALSE 0
typedef int Bool;

#endif

BOOLEAN_H是专门用来防止多次包含头文件而创建的。

15.2.4 避免包含

#error指令经常放在头文件中,用于检查不应该包含该头文件的条件。

// 存在__STDC__宏的C标准版本可以包含该头文件
#ifndef __STDC__
#error This header requires a Standard C compiler
#endif

15.3 程序如何分到多个文件

15.3.1 划分方法
  1. 把每个函数集合放在一个源文件中。
  2. 创建和源文件同名的头文件,其中放置源文件中定义的函数的原型。(只在源文件使用的函数不需要在头文件声明。)
  3. 源文件中也要包含对应的头文件。
  4. main函数会出现在某个文件中,这个文件的名字和程序的名字相匹配。

注意 每个源文件(.c结尾)都要有自己的头文件(.h结尾)。

由一个大的头文件包含所有的宏定义、类型定义、函数原型是合法的。但是这样做不能为阅读程序的人提供有用的分类信息。并且包含信息过多,这个文件可能需要频繁修改,每次改变时,依赖于它的源文件都需要重新编译。

15.3.2 示例

格式化文本的程序。要求:

  • 将格式散乱的文本重排,重新输出。
  • 除最后一行外都需要左右对齐。
  • 每个单词最多20字符,超出的部分以*表示。
  • 每行最多60字符。

word.h处理单词相关的函数:

#ifndef WORD_H
#define WORD_H

// 读取输入的下一个单词,存在word中。文件读完后清空word。
// 单词长度超过len后,做截断操作。
void read_word(char *word, int len);

#endif

line.h操作行相关的函数:

#ifndef LINE_H
#define LINE_H

// 清除当前行
void clear_line(void);

// 添加单词到行尾
// 如果单词不是行首单词,前面加空格
void add_word(const char *word);

// 返回当前行的剩余字符数
int space_remaining(void);

// 写当前行,并对齐
void write_line(void);

// 写当前行,不对齐;空行不做任何事情
void flush_line(void);

#endif

justify.c主程序:

/* 格式化文本文件 */

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

#define MAX_WORD_LEN 20

int main(void)
{
  // 第22位为空,21位为*
  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);
  }
}

word.c读取单词:

#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()) == ' ')
    ;
  // 读取非空格字符写到word
  while (ch != ' ' && ch != EOF){
    // 超出长度的不保存
    if (pos < len)
      word[pos++] = ch;
    ch = read_char();
  }
  // 最大pos=len=21,将第22位写为空
  word[pos] = '\0';
}

line.c存储和输出行:

#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(word)
{
  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(' ');
      // 行末时,需填补空格为0
      extra_spaces -= spaces_to_insert;
      num_words--;
    }
  }
  putchar('\n');
}

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

15.4 构建多文件程序

15.4.1 构建大型程序的基本步骤
  1. 编译。
    分别编译每个源文件。
    每个源文件产生一个包含目标代码的文件。(后缀为.o.obj)。
  2. 链接。
    把目标文件和库函数的代码结合在一起,生成可执行的程序。
    链接器要解决编译器遗留的外部引用问题。

头文件中的库函数很多,但是大多数链接器只会链接程序实际需要用到的函数。

gcc构建程序的命令:

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

-o表示定义程序的名字。
执行这个命令,三个源文件先编译为目标代码,然后自动传递给链接器,再结合为一个文件。

15.4.2 makefile

命令行操作在源文件数量很多的时候不不适用。
因为写命令很乏味,需要重新构建时也很浪费时间。

makefile是包含构建程序的必要信息的文件。
它内部不仅包括了构建程序需要的文件,还描述了这些文件之间的依赖性。

makefile的规则
构建justify程序的makefile

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

文件中的每组代码行称为一条规则。
每条规则的第一行给出结果文件和它所依赖的文件,第二行是待执行的命令。
结果文件依赖的文件发生改变时,就需要重新执行命令来构建结果文件。

makefile中每条命令的前面都是一个制表符,而不是多个空格。

makefile的使用
makefile一般存储在一个名为Makefilemakefile的文件中。
当用make程序时,它会自动在当前目录下搜索这两个名字的文件。

通过make 结果文件形式的命令,调用make程序构建makefile中出现过的结果文件。不指定结果文件,会构建第一条规则中的结果文件。

有很多方法可以减少makefile中的冗余,使其易于修改,但是可读性会降低。
make是标准的UNIX系统自带的程序。
除了makefile外,也可以使用其他的工具配合工程文件构建程序。

makefile自动重新构建
makefile的好处是可以自动重新构建。
make程序会检查每个文件的日期,确认上一次构建后哪些文件发生了改变。然后把改变了的文件和直接或间接依赖于它们的全部文件都重新编译。

15.4.3 链接期间的常见错误
  • 拼写错误
    变量名或函数名拼写错误,链接器会做缺失报告。
  • 缺失文件
    链接器找不到某个文件中的函数,可能是该文件没有在makefile或工程文件中列出。
  • 缺失库
    链接器找不到程序中的全部库函数。
    很多UNIX版本要求在链接程序时指明-lm选项,让链接器搜索一个包含<math.h>的编译版本的系统文件。不使用该选项会在链接时导致“undefined reference”。
15.4.4 程序外定义宏

大多编译器支持-D选项,允许在命令行指定宏:

gcc -DDEBGU=1 foo.c

这种方式易于对宏值进行修改,而不需要重新编辑任何文件。

-D选项不指定值时,值会被设为1。

也有-U选项,用于删除宏定义,相当于#undef-U可以删除预定义宏或由-D选项定义的宏。


更多相关内容参考: 《C语言程序设计:现代方法》笔记

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值