英文原版:P349
一个典型的程序由多个源文件(.c
)和一些头文件(.h
)组成。
- 源文件包含函数的定义,外部变量等。
- 头文件包含被多个源文件共享的信息。
本章的主要内容:
- 15.1节介绍源文件。
- 15.2节介绍头文件。
- 15.3节通过实例来介绍如何将一个程序分成源文件和头文件。
- 15.4节介绍如何构建由多个文件组成的程序,及当该程序的部分发生变化时,如何重新构建该程序。
15.1 源文件
C语言对源文件的规定:
- 源文件的拓展名为
.c
; - 每个源文件包含程序的一部分,主要是函数和变量的定义;
-
必须有一个源文件包含
main
函数,作为程序的入口;
例1 假设要编写一个计算器程序:该程序对用户输入逆波兰记号形式的整数表达式求值,其中在逆波兰表达式里,操作符跟在操作数后面。
示例输入:30 5 - 7 *
示例输出:175
程序设计思路:
- 使用栈来记录中间结果。
- 如果程序读到一个数,则将这个数入栈。
- 如果程序读到一个操作符,则将从栈中弹出两个数,执行该操作,并将结果压入栈中。
- 当程序读取输入完成,则表达式的结果就保存在栈里。
如何将该程序分成多个源文件?
- 将读取
tokens
的函数放源文件token.c
; - 将诸如
push
、make_empty
、is_empty
、is_full
等栈操作相关的函数,及表示栈的变量等放入源文件stack.c
; - 将
main
函数放入源文件calc.c
;
如何共享函数原型?
由于源文件calc.c
要调用源文件stack.c
里的函数,所以需要将源文件stack.c
中的相关函数的原型放入头文件stack.h
中,并在源文件calc.c
和stack.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语言对头文件的规定:
- 头文件的拓展名是
.h
; - 头文件里包含的是被多个源文件共享的信息,比如函数原型、宏定义、类型定义等;
- 使用
#include
指令来引入头文件;
当把一个程序分成多个源文件时会出现如下几个问题:
- 在一个文件里的函数如何调用定义在另一个文件里的函数?
- 一个函数如何访问定义在另一个文件里的外部extern变量?
- 两个文件如何共享同一个宏定义、类型定义?
解决办法:使用预处理指令#include
,该指令使得在多个源文件之间共享函数原型、宏定义、类型定义等信息成为可能。
#include
指令
功能描述:
- 告诉预处理器打开一个具体的文件,将该文件的内容插入到当前文件中。
用途:
- 可实现在多个源文件之间共享函数原型、宏定义、类型定义等信息。
有三种使用形式
-
#inlcude <filename>
:会搜索系统头文件目录,比如/usr/include
等。 -
#include "filename"
:首先会搜索当前目录,然后搜索系统头文件目录。 -
#include tokens
:对tokens
进行扫描,并用宏定义来替换tokens
;替换后的#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 共享宏定义和类型定义
假设正在编写一个使用宏BOOL
、TRUE
、FALSE
的程序。
文件file1.c
、file2.c
、file3.c
都需要这3个宏定义。
不是在源文件file1.c
、file2.c
、file3.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;
}
如何共享函数原型?
-
调用定义在另一个文件里的函数
f
时,一定要确保在调用f前让编译器遇到过函数f
的原型。 -
务必在包含函数
f
定义的文件里的包含有函数f
原型的头文件,因为如果在程序中函数f的调用跟函数f
的定义不匹配,且没有在包含函数f
定义的文件里的包含有函数f
原型的头文件的话,则编译器就不会检查函数f
的定义是否跟函数f的原型相匹配,会导致很难查找的bug。
例1 为什么要将共享函数的原型放入头文件?
假设源文件file5.c
例包含定义在foo.c
里的函数f
。
首先,调用一个没有声明的函数f
是有风险的。
如果函数f
没有可依赖的原型,则编译器会强制假设函数f
的返回值类型是int
,调用函数f的实参的个数要跟形参的个数相匹配。而且,实参会根据默认的实参类型提升规则来对实参进行类型的自动转换。编译器的默认假设可能是错的,但是编译器却没办法检查出来,因为编译器一次只能编译一个文件。如果假设是错的,则程序将会运行异常,且不知道为什么会这样。
方法一:在调用函数f
的文件里声明函数f
。
这种方法可以解决上面的问题,但带来了新的问题。比如
- 有50个文件要调用函数
f
,如何保证所有50个文件里的函数f
的原型都是一样的? - 如何保证
foo.c
里的定义能匹配所有50个文件里的函数f
的原型? - 如果后续要修改函数
f
,如何查找所有使用函数f的文件?
方法2:将函数f
的原型放入一个头文件,让所有调用函数f
的源文件里包含该头文件。
因为函数f
是在foo.c
中定义的,所以命名头文件为foo.h
。
除了在调用函数f的源文件里包含foo.h
外,还需要在foo.c
里也包含foo.h
,这样可让编译器能够检查在foo.c
中的函数f
的定义是否跟在foo.h
中声明的函数原型相匹配。
共享变量声明
如何在多个源文件之间共享变量i
?
方法一:
- 首先,将变量i的定义放入一个源文件中;如果需要初始化,可以在这里完成;
- 然后在其他需要使用变量i的源文件中包含变量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实现格式化输出的功能。
- 假设所有词的长度都小于等于20个字符;如果一个词的长度超过20,则使用一个星号
*
来代替剩余的字符; - 输出内容跟输入一样,但要删除额外的空格和空行;
- 保证每行都被填满:在每行中添加词直到再添加一个或者多个词就导致溢出;
- 对每行进行校正:在词与词之间添加额外的空格,保证每行的长度是一样的,比如60个字符;保证词与词之间的空格数一样的;最后一行可以不做校正。
程序设计思路:
首先,不能读一个单词,写一个单词。需要将单词读入到行缓冲区里,直到该缓冲区填满一行。
然后,主程序使用循环来实现,比如
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 如何构建多个文件的程序
什么时候会出现外部引用?
- 当文件a里的函数调用了一个定义在文件b里的函数时;
- 当文件a里的函数访问了一个定义在文件b里的变量时;
构建一个大型程序,需要两步:
- 编译
在程序里的每个源文件必须被单独地编译;
头文件不需要编译,当包含该头文件的源文件被编译时自动编译的;
编译器对每个源文件都生成目标文件,比如在UNIX是.o
文件,在Windows上是.obj
文件等; - 链接
链接器将编译步骤中创建的目标文件和库函数代码综合起来生成可执行文件;
链接器负责解析编译器没做的外部引用;
网友评论