C Programming: A Modern Approach

第15章 编写大型程序

2020-03-09  本文已影响0人  橡树人

英文原版:P349

一个典型的程序由多个源文件(.c)和一些头文件(.h)组成。

本章的主要内容:

15.1 源文件

C语言对源文件的规定:

例1 假设要编写一个计算器程序:该程序对用户输入逆波兰记号形式的整数表达式求值,其中在逆波兰表达式里,操作符跟在操作数后面。

示例输入:30 5 - 7 *
示例输出:175

程序设计思路:

如何将该程序分成多个源文件?

如何共享函数原型?
由于源文件calc.c要调用源文件stack.c里的函数,所以需要将源文件stack.c中的相关函数的原型放入头文件stack.h中,并在源文件calc.cstack.c中包含该头文件stack.h

源文件stack.h

#include <stdbool.h>

void make_empty(void);
bool is_empty(void);
bool is_full(void);
void push(int i);
int pop(void);

源文件stack.c

#include "stack.h"
#include <stdbool.h>

#define STACK_SIZE 100

//全局变量
int contents[STACK_SIZE];
int top = 0;

void make_empty(vid)
{
    top = 0;
}

bool is_empty(void)
{
    return top == 0;
}

bool is_full(void)
{
    return top == STACK_SIZE;
}

void push(int i)
{
    if (is_full())
    {
        stack_overflow();
    }
    else 
    {
        contents[top++] = i;
    }
}

int pop(void)
{
    if (is_empty())
    {
        stack_overflow();
    }
    else 
    {
        return contents[--top];
    }
}

源文件calc.c

#include "stack.h"

int main(void)
{
  make_empty();

  return 0;
}

15.2 头文件

C语言对头文件的规定:

当把一个程序分成多个源文件时会出现如下几个问题:

解决办法:使用预处理指令#include,该指令使得在多个源文件之间共享函数原型、宏定义、类型定义等信息成为可能。

#include指令

功能描述:

用途:

有三种使用形式

例1 使用宏来定义文件名,不用在#include中硬编码文件名

#if defined(IA32)
  #define CPU_FILE "ia32.h"
#elif defined(IA64)
  #define CPU_FILE "ia64.h"
#elif defined(AMD64)
  #define CPU_FILE "amd64.h"
#endif

#include CPU_FILE

如何共享宏定义和类型定义?

许多大型程序都存在被多个源文件共享的宏定义和类型定义。

被多个源文件共享的宏定义和类型定义应该被放入头文件.h

共享宏定义和类型定义有哪些优点?

例1 共享宏定义和类型定义
假设正在编写一个使用宏BOOLTRUEFALSE的程序。
文件file1.cfile2.cfile3.c都需要这3个宏定义。

不是在源文件file1.cfile2.cfile3.c里重复定义这3个宏,而是将这几个宏的定义放入头文件boolean.h中。

源文件boolean.h

#define BOOL int
#define TRUE 1
#define FALSE 0

源文件file1.c

#include "boolean.h"

int main(void)
{
    /* code */
    BOOL is_full;

    is_full = 0;

    return 0;
}

预处理后的文件:file1.i

int main(void)
{

 int is_full;

 is_full = 0;

 return 0;
}

源文件file2.c

#include "boolean.h"

int main(void)
{
    /* code */
    int a = 10;
    if ((a > 9) == TRUE) {

    }
    return 0;
}

预处理后的文件:file2.i

int main(void)
{

 int a = 10;
 if ((a > 9) == 1) {

 }
 return 0;
}

源文件file3.c

#include "boolean.h"

int main(void)
{
    /* code */
    int a = 10;
    if ((a > 11) == FALSE) {

    }
    return 0;
}

预处理后的文件:file3.i

int main(void)
{

 int a = 10;
 if ((a > 11) == 0) {

 }
 return 0;
}

源文件file4.c

#include <stdio.h>
#include "boolean.h"

int main(void)
{

    Bool a;

    a = 10;
    printf("%d\n", a);

    return 0;
}

预处理后的文件:file4.i

int main(void)
{

 Bool a;

 a = 10;
 printf("%d\n", a);

 return 0;
}

如何共享函数原型?

例1 为什么要将共享函数的原型放入头文件?

假设源文件file5.c例包含定义在foo.c里的函数f

首先,调用一个没有声明的函数f是有风险的。
如果函数f没有可依赖的原型,则编译器会强制假设函数f的返回值类型是int,调用函数f的实参的个数要跟形参的个数相匹配。而且,实参会根据默认的实参类型提升规则来对实参进行类型的自动转换。编译器的默认假设可能是错的,但是编译器却没办法检查出来,因为编译器一次只能编译一个文件。如果假设是错的,则程序将会运行异常,且不知道为什么会这样。

方法一:在调用函数f的文件里声明函数f
这种方法可以解决上面的问题,但带来了新的问题。比如

方法2:将函数f的原型放入一个头文件,让所有调用函数f的源文件里包含该头文件
因为函数f是在foo.c中定义的,所以命名头文件为foo.h
除了在调用函数f的源文件里包含foo.h外,还需要在foo.c里也包含foo.h,这样可让编译器能够检查在foo.c中的函数f的定义是否跟在foo.h中声明的函数原型相匹配。

共享变量声明

如何在多个源文件之间共享变量i

方法一:

方法二:

如何保证同一个头文件只被包含一次?

首先,在一个源文件里包含两次同一个头文件,会报编译错误,这个很容易发现。
其次,当一个头文件里包含了其他头文件时,也会报编译错误,但这个很难发现。

例1 假设file1.h中包含了file3.h,file2.h中包含了file3.h,prog.c中包含了file1.h和file2.h。

如果不做特殊处理,当编译prog.c时,file3.h就会被编译两次。当file3.h中包含函数定义时,就会报编译时错误。

务必要保护所有的头文件不被多次包含。

源文件file3.h

#include <stdio.h>

#define LEN 10
tydef int Bool

extern int i;

void f(void);

void g(void)
{
  printf("test header file multiple inclusion in g()\n");
}

15.3 示例:将一个程序分解成多个文件

样例输入文件quote

     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 en vironments. 
                                     - - Dennis M. Ritchie

样例输出文件newquote

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 iyet sufficiently abstract and
fluent to describe algorithms and interactions in a wide
variety of environments. --  Dennis  M.  Ritchie

编写程序justify实现格式化输出的功能。

程序设计思路:
首先,不能读一个单词,写一个单词。需要将单词读入到行缓冲区里,直到该缓冲区填满一行。
然后,主程序使用循环来实现,比如

for (;;){
  读单词;
  if (没有单词可读了) {
    就将不用校正行缓冲区里的内容,直接写入文件;
    程序终止;
  }
  
  if (行缓冲区满了) {
    对行缓冲区里的内容进行校正,然后写入文件;
    清空行缓冲区;
  }
  将单词添加到行缓冲区中;
}

源文件justify.c

#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);
    }

    return 0;
}

源文件word.h

#ifndef WORD_H
#define WORD_H

void read_word(char *word, int len);

#endif

源文件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()) == ' ') {
        ;
    }
    while (ch != ' ' && ch != EOF) {
        if (pos < len) {
            word[pos++] = ch;
        }
        ch = read_char();
    }
    word[pos] = '\0';
}

源文件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

源文件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;

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

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

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

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++;
}

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

编译justify程序

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

15.5 如何构建多个文件的程序

什么时候会出现外部引用?

构建一个大型程序,需要两步:

  1. 编译
    在程序里的每个源文件必须被单独地编译;
    头文件不需要编译,当包含该头文件的源文件被编译时自动编译的;
    编译器对每个源文件都生成目标文件,比如在UNIX是.o文件,在Windows上是.obj文件等;
  2. 链接
    链接器将编译步骤中创建的目标文件和库函数代码综合起来生成可执行文件
    链接器负责解析编译器没做的外部引用;
上一篇 下一篇

猜你喜欢

热点阅读