9.编写大型程序
在编写一些大型的程序时,我们经常需要把程序分为多个源文件的方式,这样可以使程序的结构清晰。当把程序分割为几个源文件时,问题也随之产生了:某文件中的函数如何调用定义在其他文件中的函数呢?函数如何访问其他文件中的外部变量呢?两个文件如何共享同一个宏定义或类型定义呢?答案取决于#include指令,此指令使得任意数量的源文件中共享信息成为可能,这些信息可以是函数原型、宏定义、类型定义等。
include指令告诉预处理器打开指定的文件,并且把此文件的内容插入到当前文件中。因此,如果想让几个源文件可以访问相同的信息,可以把此信息放入一个文件中,然后利用#include指令把该文件的内容带进每个源文件中。把按照这种方式包含的文件称为头文件(有时也称为包含文件)。头文件的扩展名为.h。
#include指令
include指令主要有两种书写格式,这两种格式间的细微差异在于编译器定位头文件的方式:
-
include <文件名>
用于属于C语言自身库的头文件。编译器会搜寻系统头文件所在的目录。例如unix系统中,通常把系统头文件保存在目录/usr/include中。
-
include "文件名"
用于所有其他头文件,也包含任何自己编写的文件。编译器会先搜寻当前目录,然后搜寻系统头文件所在的目录。#include指令中的文件名也可以含有帮助定位文件的信息,比如目录的路径或驱动器号:例如unix上的#include "/cprogs/utils.h"和Windows上的#include "C:\cprogs\utils.h"。但是为了可移植性,通常最好的做法是在#include指令中不包含路径或驱动器的信息。不然当把程序转移到其他机器上,或者更糟的情况是转移到其他操作系统上时,这类信息会使编译器变的很困难。如果使用相对路径会好一些,例如#include "..\include\utils.h"。
如果源文件包含同一个头文件两次,那么可能产生编译错误。例如file1.h包含file3.h,file2.h包含file3.h,而prog.c同时包含file1.h和file2.h,那么在编译prog.c时,file3.h就会被编译两次。两次包含同一个头文件不总是会导致编译错误。如果文件只包含宏定义、函数原型和/或变量声明,那么将不会有任何困难。然而,如果文件包含类型定义,则会带来编译错误。
为了防止头文件多次包含,用#ifndef和#endif指令来封闭文件的内容,例如:
#ifndef BOOLEAN_H
#define BOOLEAN_H
#endif
在首次包含这个文件时,没有定义宏BOOLEAN_H,所以预处理器允许保留#ifndef和#endif之间的多行内容。但是如果再次包含此文件,那么预处理器将把#ifndef和#endif之间的多行内容删除。
模块
设计C程序时,最好将它看作是一些独立的模块。模块是一组服务的集合,其中一些服务可以被程序的其他部分(称为客户)使用。每个模块都有一个接口来描述所提供的服务。模块的细节(包括这些服务自身的源代码)都包含在模块的实现中。
在C语言环境下,这些服务就是函数。模块的接口就是头文件,头文件中包含那些可以被程序中其他文件调用的函数的原型。模块的实现就是包含该模块中函数的定义的源文件。
C库本身就是一些模块的集合。库中每个头都是一个模块的接口。例如,
将程序分割成模块有一系列好处:
-
抽象
如果模块设计合理,我们可以把他们作为抽象对待。我们知道模块会做什么,但是不需要知道这些功能的实现细节。因为抽象的存在,我们不必为了修改部分程序而了解整个程序是如何工作的。同时,抽象让一个团队的多个程序员共同开发一个程序更容易。一旦对模块的接口达成一致,实现每一个模块的责任可以被分派到各个成员身上。团队的成员可以更大程度上相互独立的工作。
-
可复用性
任何一个提供服务的模块都有可能在其他程序中复用。
-
可维护性
将程序模块化后,程序中的错误通常只会影响一个模块的实现(然后重新链接整个程序)即可。更广泛的说,为了提高性能或将程序移植到另一个平台上,我们甚至可以替换整个模块的实现。维护一个程序就像维护一辆汽车一样,修理轮胎应该不需要同时检修引擎。
好的模块接口并不是声明的随意集合。在设计良好的程序中,模块应该具有下面两个性质:
-
高内聚性
模块中的元素应该彼此紧密相关。我们可以认为它们是为了同一目标而相互合作的。高内聚性会使模块更易于使用,同时程序更容易理解。
-
低耦合性
模块之间应该尽可能相互独立。低耦合性可以使程序更便于修改,并方便以后复用模块。
- 邮箱 :charon.chui@gmail.com
- Good Luck!