第十章程序开发
本章通过程序实例,总结本书各方面内容,包括:•C 语言里的一些主要机制;
•写程序的各种基本技术;
•在编写较大程序时需要考虑的问题;•同时介绍一些有用的编程技术。
程序复杂,源程序文件变大,处理更加困难。
软件通常是许多人共同工作的结果。多个人无法共用一个源文件工作。
实际开发过程提出了许多难解决的困难。需要考虑用多个源文件建立一个程序的方式。
C 语言设计考虑了这个问题,提供了支持机制。下面用一个程序实例,介绍这方面的情况。
10.1 分别编译和C 程序的分块开发
C 语言源程序文件
编译
没有预处理命令的C 语言源程序文件
预处理连接
函数库
目标程序文件
可执行程序
图5.5 C 语言程序的加工过
分块开发的问题和方法
以多个源文件的方式开发程序的过程称为分块开发。
分块开发中最重要工作就是程序结构的“物理”组织。在C 语言里做分块开发,要借助C 系统的预处理功能,以对源程序的适当物理划分,并设法保证程序的不同部分之间的一致性,使编译之后的目标代码模块能组合成一个具有内在一致性的完整的可执行程序。
在一般性地讨论分块开发中的问题前,先看一个实例。
程序实例:学生成绩处理
本例也作为后面许多讨论的公用实例。
做一些扩充,假定成绩文件给出每个学生一门课程的期中考试成绩,平时成绩和期末考试成绩。程序需要按一定比例计算出课程最终成绩,并能在此基础上做统计,画学生成绩分布直方图,做排序输出等。在考虑分块开发前,先给一个合理实现。首先为这一工作起名字stu ,作为主文件名。假定成绩文件中数据形式如下(每行一个):02001014 zhangshan 86 80 9102001016lisi 77 90 69
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <ctype.h>
enum{MAXNUM = 400,
MIDDLE=20, EXECISE=20, FINAL=50, /*成绩比例*/ HISTLEN = 60,      /* 最长行的长度(字符数)*/ SEGLEN = 5,          /* 分段长度*/
SEGNUM = 100/SEGLEN+1 /* 分段数,自动算出*/ };
typedef struct{/* 公用类型的定义*/
unsigned long num;
char name[20];
double mid, exe, final, score;
}StuRec;
StuRec students[MAXNUM]; /* 全局性数据对象*/主函数main处理学生记录文件的打开关闭:
int main(void) {
FILE *fp;
char fn[128];
do { /* 交互式获取文件名*/
getnstr("Student record file: ", 128, fn);
if ((fp=fopen(fn, "r")) == NULL)
printf("Can't open file: %s\n", fn);
else {
commander(fp, fn);
fclose(fp);
}
} while (next("file"));
return 0;
}
commander首先将文件里的学生成绩记录读入,而后转入交互命令的处理:
void commander (FILE*fp, char *fn) {
int n,cmd;
n =readSRecs(stdin, MAXNUM, students);
if (n <= 1) {
printf("File %s: too few data items.\n", fn);
return;
}
printf("File %s: %d records read.\n", fn, n);
do {
cmd=getcmd("1,Statistics\n;2,Histogram;\n"
"3,Sort and store to file\n",1,3);
switch (cmd) {
case 1: statistics(n, students);break;
case 2: histogram(n, students, SEGLEN); break;
case 3:sortoutput(n, students); break;
}
} while (next("command"));
}需定义取得用户命令的getcmd,它显示信息串,要求一定范围里的数。可参考getnstr和getnumber,读数后应该丢掉行中其余东西。下面是原型:
int getcmd(char *prompt,int c1,int cn);
int next(char s[]);
void getnstr(char prompt[],int lim, char bf[]);完成统计和生成直方图的函数需要做少许修改,以处理目前所采用的结构数组。
实现排序输出功能需要定义函数sortoutput:void sortoutput(int n,StuRec tb[]) {
char fn[128];
FILE *fp;
do { /* 交互式获取文件名*/
getnstr("File name for saving: ", 128, fn);
if ((fp=fopen(fn, "w")) == NULL)
printf("Can't open file: %s\n", fn);
else {
qsort(tb, n,sizeof(StuRec),scrcmp);
printSRec(fp, n,tb);
fclose(fp);
break;
}
} while (next("Really want to save?"));
}学生记录存入结构数组里,每个人的成绩有三项,读入文件的函数需要修改,读入时计算:
static int chk(double x)
{    return x >= 0.0 && x <= 100.0; }
int readrec(FILE*fp,StuRec*stp) {
char s[256]; /* 一次读入一行*/
if(fgets(s, 256,fp) == NULL) return EOF;
if(sscanf(s, "%lu%s%lf%lf%lf", &stp->num,stp->name, &stp->mid, &stp->exe, &stp->final) == 5) if(chk(stp->mid)&&chk(stp->exe)&&chk(stp->final)){ stp->score =(stp->mid*MIDDLE +stp->exe*EXECISE +
stp->final*FINAL) / 100;
return 1;
}
return 0;
}
int readSRecs(FILE *fp,int limit,StuRec tb[]) { int i = 0, line = 1, n;
double x;
while (i<limit && (n =readrec(fp,&tb[i]))!=EOF){ if (n == 0)
printf("Data error, line %d\n", line);
else ++i;
++line;
}
……
return i;
}
完成上面函数,适当排列或加入原型,程序就完成了。逻辑上说,程序由一批函数定义组成,#include了几个标准头文件,定义几个常量和一个全局结构数组。#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <ctype.h>
enum{MAXNUM = 400, …};
typedef struct{ …} StuRec;
StuRec students[MAXNUM]; /* 全局性数据对象*/
int getcmd(char *prompt,int c1,int cn) { …} int next(char s[]) { …}
void getnstr(char prompt[],int lim, char bf[])…void commander (FILE*fp, char *fn) { …}
int readrec(FILE*fp,StuRec*stp) { …}
int readSRecs(FILE *fp,int limit,StuRec tb[]) { …}
……
void sortoutput(int n,StuRec tb[]) { …}
int main(void) { …}
分块重整
现在研究如何将较大程序分为一组物理上独立的块(程序文件),又保证文件间的正确逻辑联系和正确编译结果。先考虑上面程序的物理划分。
做好的程序为什么还去整理它,去分块?是马后炮?有时需要!程序常由于新情况和需求而发展。不断变化中程序规模会扩大。整理好物理结构,将使它能更好地适应修改和扩充。
合理划分需要分析情况,提出可能性。下面要做各种分析,看划分过程中可能遇到的问题,及如何选择处理。该程序是典型的较简单的C程序:包含若干标准头文件,定义若干公用类型(StuRec)和一些函数,一些全局
变量或常变量,主函数控制程序执行。
一种可能方式是这个程序分为4个部分:
•main和实现命令循环的commander是高层控制。•完成输入输出的一组函数;
•完成对成绩记录的各种处理的函数;
•辅助性的功能函数,如next、getnstr等。
考虑按上述功能划分将程序分为4个源程序文件,使它
们可以分别编译,并保证最终连接为一个有机整体。
各部分并不相互独立:主函数部分用到其他许多函数,许多其他部分依赖公用结构StuRec。
可以在每个文件前加上结构StuRec的定义、有关函数原型定义等等,使它们可以独立编译。
危险:如果修改某个文件,其他文件将得不到信息,如果修改破坏了一致性,编译也不能帮助检查。
必须贯彻前面的原则:使同一程序对象的定义点和所有使用点都能参照同一个描述。达到这一目标的一种方式是将所有类型定义、常量定义放入一个文件,把定义和使用出现在不同文件的函数原型也列入这个公用信息文件,所有源文件都参考这个文件(#include它)。按习惯,为此创建的文件称为头文件,常用.h作为文件名后缀,其作用是为其他文件提供信息。将这个头文件命名为stu.h,其内容如下:
#include <stdio.h>
... ...
enum{/* 程序的公用常量定义*/
MAXNUM = 400, ... ...
};
typedef struct{/* 公用类型定义*/
unsigned long num;
.
.. ...
}StuRec;
/* 全局性数据对象的外部说明,此时不必给出数组长度*/ extern StuRec students[];
/* 在某一文件里定义,其他文件里使用的全部函数的原型*/ int readSRecs(FILE *fp,int limit,StuRec tb[]); int printSRec(FILE *fp,int limit,StuRec tb[]); ... ...
这里只需要列出在某个源文件里定义,在其他部分使用的函数的原型。
下面看各个源程序文件。首先是主程序文件,这里略去了不必要的细节:
/* file stu.c, 程序stu 的主源程序文件*/
#include "stu.h"
/* 全局数据对象定义在主文件里,或按归属原则定义在某文件里*/ StuRec students[MAXNUM];
void commander (FILE*fp, char *fn) { ... ... }
int main(void) { ... ... }
由于包含了"stu.h",因此可以得到所有必须信息。完成输入输出的文件:
/* file stu_io.c, 程序stu 的输入输出源程序文件*/
#include "stu.h"
static int check(double x) { ... ... }
static int readrec(FILE*fp,StuRec*stp) { ...}
int readSRecs(FILE *fp,int limit,StuRec tb[]){ ...}
int printSRec(FILE *fp,int limit,StuRec tb[]){ ...} readrec和check只在本文件内部用,故原型不写进公共信息文件,并将其定义为static函数,使函数名局部化,防止与其他源文件里的全局名字冲突(万一开发其他部分的人也定义了同名函数)。
/* file stu_fun.c, 程序stu 的处理部分源程序文件*/
#include "stu.h"
void statistics(int n,StuRec tb[]) { ...}
static void prtHH(int n) { ...}
void histogram(int n,StuRec tb[],int high) { ...} static int scrcmp(const void* vp1, const void* vp2){...} void sortoutput(int n,StuRec tb[]) { ...}
/* file utilities.c, stu使用的功能函数源程序文件*/
#include "stu.h"
int next(char s[]) { ...}
void getnstr(char *prompt, int lim, char bf[]){ ...} int getcmd(char *prompt,int c1,int cn) { ...} stu.h是公共信息通道,各源文件由它得到所需信息,保证文件间的一致性。如某函数的原型修改,编译程序就会发现头文件中的函数原型与函数定义不一致。如果修改函数原型,信息就会反应到使用函数的源文件里。
/* file stu.h, 程序 stu 的公共头文件 */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <ctype.h>
enum {
MAXNUM = 400,
MIDDLE = 20, EXECISE = 30, FINAL = 50, /* 成绩比例 */
... ...
};
typedef struct { ... } StuRec;
......
/* file stu.c 程序stu的主源程序文件 */ */
#include "stu.h"
StuRec students[MAXNUM];
void commander (FILE* fp, char *fn) { ... ... }
int main(void) { ... ... }
/* file stu_io.c, 程序 stu 的输入输出源程序文件 */
#include "stu.h"
static int check(double x) {  }
static int readrec(FILE* fp, StuRec *stp) { ... ... }
int readSRecs(FILE *fp, int limit, StuRec tb[]) { ... ... }
int printSRec(FILE *fp, int limit, StuRec tb[]) { ... ... }
/* file stu_fun.c, 程序 stu 的处理部分源程序文件 */
#include "stu.h"
void statistics(int n, StuRec tb[]) { ... ... }
static void prtHH(int n) { ... ... }
void histogram(int n, StuRec tb[], int high) { ... ... }
static int scrcmp(const void *vp1, const void *vp2) { ... ... }
void sortoutput(int n, StuRec tb[]) { ... ... }
图10.1分块开发的程序及其关联结构
/* file utilities.c, 程序 stu 使用的功能函数源程序文件*/
#include "stu.h"
int next(char s[]) { ... ... }
void getnstr (char prompt[], int lim, char bf[])
{ ... ... }
int getcmd (char *prompt, int c1, int cn)
{ ... ... }
其他安排和考虑
同一程序可采用不同的物理组织结构,用同样结构时内容分配也可不同,同时又能保证程序语义和各部分之间信息畅通。现考虑另一种可能。printf函数原型在什么头文件里
前面的stu.h包含了程序所需的所有标准头文件,使所有源文件得到标准库信息。大程序可能用到许多标准库功能,但并非每个源文件都需要用所有功能,具体文件可能只用很少一部分,甚至完全没用任何标准库功能。
统一包含所有标准库头文件,编译每个源文件时都要处理所有标准头文件。对大系统可能造成很大时间浪费。为避免这个问题,可以换种方式,让各源文件包含自己所需的标准头文件。对上面例子将头文件改为:
/* file stu.h 程序stu的公共头文件*/
enum{ MAXNUM = 400, ... ... };
typedef struct{
unsigned long num;
... ...
} StuRec;
extern StuRec students[];
int readSRecs(FILE *fp,int limit,StuRec tb[]); int printSRec(FILE *fp,int limit,StuRec tb[]); ... ...
主程序文件变成:
/* file stu.c 程序stu的主源程序文件*/ */#include <stdio.h>#include "stu.h"
StuRec students[MAXNUM];
void commander (FILE*fp, char *fn) { ... ... }int main(int argc, char**argv) { ... ... }
其他文件类似。这样写每个源文件时多费点事,编译加工效率可能提高。虽然各文件内容有所调整,这
组文件仍组成一个有机整体,满足前面原则:同一程序对象的定义点和所有使用点都能参照同一个描述。一般将标准头文件的包含命令写在前面。
一般原则
头文件应成为保证程序各部分间一致性的信息桥梁,连接程序对象的定义和使用的纽带。
定义好头文件是保证程序开发顺利进行的最重要环节。几个人共同开发一个系统需要有些约定。若一人定义的东西供别人用,就要写出适当头文件,建立联系。程序开发最早可能成型一批头文件,形成“标准”。若所做修改不影响共用头文件,就不会影响程序其他部分和其他人的工作。修改头文件时就需要与其他人联系,保证修改的一致性。个人用分块方式写程序时情况类似。实现源文件功能时可能要定义内部使用的辅助函数和变量。定义为static 就不会与其他文件(其他人)的定义冲突。静态函数和静态机制在开发大程序中很常用。
采用多文件方式开发程序,许多外部对象(函数、外部变量等)的定义和使用处在不同文件里。
C 对许多情况不检查,若没有提供足够类型信息,编译会按默认假定工作下去,不发现矛盾就不认为程序有错误。连接程序只检查需要的东西有没有,到后就把它们连接起来。连接时不做任何类型一致性检查。为最终产生连接正确的程序,关键在于保证编译的正确进行。要保证编译正确,最重要的就是给
编译程序提供有关外部程序对象正确完全的信息。函数原型说明、变量外部说明、类型定义、系统头文件包含等等,其作用都是为编译程序提供信息。
写程序时必须认真写好这些,这对提高工作效率、减少程序中隐含错误极其重要,也是本书反复强调的。
物理组织的合理原则
按惯例,C 程序开发者把程序的源文件分成两类,一类是包含实际代码的程序文件,另一类是为基本程序文件提供信息的辅助文件。基本程序文件通常以.c 为扩展名,称源程序文件或源代码文件;为提供信息的文件以.h 为扩展名,称为头文件、head 文件,或h 文件。若决定采用多文件方式实现程序,该把哪些东西放进头文件,哪些放在程序文件?
语言没有规定,人们在长期编程实践提出了一些合理方式。下面介绍一套比较合理的方式。
如果程序由多个源文件组成,一些是头文件,一些是程序文件。内容安排应遵循下面规则:
•在各程序文件里定义所有的外部变量和函数,以及那些只在一个文件中用的变量、类型、函数等等。•只用#include 包含头文件,不用它包含程序文件。•通过头文件解决文件间信息传递。与多个文件有关的函数的完整原型、全局变量的完整外部说明写入头文件。定义和使用它们的程序文件都包含该头文件,
就能保证编译程序的一致性检查。连接将保证有关函数的调用和外部变量使用的能实现。
•头文件只写不生成代码、不分配存储的描述:包含头文件的预处理命令;公共类型定义;结构/联合/枚举说明;函数原型说明;变量extern 说明;公用宏定义(尽量少用宏)。建议把与多个程序文件有关的结构等都定义为类型)。
具体考虑
面对具体问题,如何处理程序物理结构组织?这里提出一些方法,具体情况具体分析。大致工作步骤:估计程序的大小,据此考虑源文件应分为几块。在开发过程中,初始划分也可能调整。例如发现一个文件膨胀得很大,就应考虑是否将它划分为几个部分。又会出现如何划分和信息的组织管理问题,应坚持同样原则。每部分中的东西互相间应有较密切逻辑联系,如提供类似功能,完成同类工作,调用关系密切等。相关的东西可考虑放在一个程序文件里。例如与输入输出有关功能可考虑放在一起,若输入和输出都很复杂,也可以考虑各建立一个文件。主函数通常单独建一个文件,其中也可包含少数关系密切的其他函数定义。