6. 基于记录的应用程序设计
6.1 使用数据库的重要性
目标是开发一个应用程序,提供程序化教学,使不具备编程技能的老师可以提出问题、接受答案并根据前后对照的信息使程序按照适当的顺序进行。
为了达到这个目标,可以将程序设计为一个通用的工具,该工具可以从文件中读入与程序化教学相关的信息和数据。
只要提供不同的数据文件,同一个程序就可以支持不同的课程了。
6.2 问题的框架
当程序运行时,基本的操作是重复执行以下几步操作:
(1) 提出当前的问题。一个问题可以由一行或多行文本组成,这些文本可以用字符串表示。
(2) 从学生处获取答案。答案也可以由字符串表示。
(3)查看为该问题准备的一系列答案。
如果学生的答案在上述答案中列出,则参考数据结构来选择新的问题。
如果学生的答案与提供的答案不符,则告知学生该结果并提供另一次机会回答该问题。
为使该应用程序拥有较好的可移植性,关于某门课程的所有信息必须存入一个数据文件中,而不是直接将它们写入程序。
程序的任务是读取数据文件,在内部数据结构中存储信息,然后用本节开始所描述的方法处理该结构。
因此,下一个任务是设计一个适当的数据结构,这样就可以为整个程序的编写提供一个背景。
设计数据结构的过程包括两个步骤。
首先,需要设计一个供程序使用的内部结构。内部数据结构由类型定义组成,其中结合了数组和记录,因而可以反映真实世界中信息的组成方式。
其次,需要设计能够反映数据文件中信息如何存储的外部结构。
这两个步骤是紧密关联的,主要是因为它们表示同样的信息。但是这两种结构是为了不同目的而设计的。
内部结构应便于程序员使用,而外部结构需要为课程设计者服务,不会在程序操作方面遇到太多困难。
6.3 设计内部表示
第一步需要设计一个包含了必需信息的数据结构。
数据库的设计中有一个重要概念—封装(encapsulation),这一步将相关的信息结合起来放入结构中,并作为整体处理。
对于一个大型数据库来讲,封装的过程是有层次的,且必须在每一个层上考虑细节。
在最高层,需要将整个数据库作为一个变量考虑,它包含了所需的全部信息。
因此,下图将数据库表示为指向某结构的指针,该结构的内容留待后面考虑:
![](https://i-blog.csdnimg.cn/blog_migrate/076a08c41ce80138519d4e078f3eeab3.png)
当你需要将整个数据库传递给函数或过程时,你只需传递变量db即可,这是一个易于操作的指针,利用它可以访问其他数据。
只有当函数或过程需要对数据库中个别字段进行操作时,才需要查看结构中的细节。
整个数据库应该包含一系列的问题,以及其他的一些信息,如课程名称等。
这些问题应该是一个数组,但其中数组元素的类型仍未定义。因此,在目前层次上的结构分解应该如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/cee6681d85a41b7e73855cbaabb1747e.png)
已知其他字段的类型,因此可以写出一个恰当的记录定义,如:
typedef struct {
string title;
questionT questions[MaxQuestion+1];
} *courseDB;
常量MaxQuestions为程序所允许的问题数目的最大值。
courseDB的定义使我们可以声明变量db来存放整个数据库,定义如下:
courseDB db;
但是这个声明仅为指针保留存储空间,因此是与前面所示的虚线框部分相应的。
当在程序中创建结构时,需要为该结构分配空间,代码如下:
db=New(courseDB);
关于questionT的定义,问题由文本组成,其中包括多行文件以及一系列可能的答案。
这两个结构都可由数组表示。
问题的文本是字符串的数组,每一个字符串包含了一行的内容。
而答案存储在一个结构较为复杂的数组中。
目前,我们可以将答案声明为一个类型answerT的数组,其细节将在下一步完善。
通常情况下,我们需要提供一个机制来追踪数组的有效大小,因为它通常比所分配的空间小一些。
记录有效大小可以利用下面两种方式:
- 在数组的最后一个值后面增加一个标记值。
- 将元素的个数存入一个整型变量,并将其作为记录的一部分。
究竟采用哪种方式取决于数组的使用方式,以及是否可以选择一个合适的标记值。
为了介绍这两种模式的操作,示例程序采用了两种定义questionT的方式。
组成问题文本的行存入了一个由字符串组成的数组。
对于指针数据来讲,常量NULL是一个最佳的标记值。在此例中更适合用一个变量存储答案的数量。
一个问题的结构可用下图表示:
![](https://i-blog.csdnimg.cn/blog_migrate/ca729de1dcf42c1eb3ec05b6bc38e6b8.png)
因为这个结构比较复杂,而且需要将其考虑为一个整体,因此可以将此结构定义为一个指针类型,如下所示:
typedef struct {
string qtext[MaxlinesPerQuestion+1];
answerT answers[MaxAnswersPerQuestion];
int nAnswers;
} *questionT;
将此结构声明为一个指针意味着必须调用New来为每个问题分配空间。
设计数据结构的最后一步是定义类型answerT。
一个答案通常由下面的内容组成:标准答案以及相应的下一个问题。
标准答案为一个字符串,而下一个问题可以由存放了问题编号的变量表示。
因此,答案的结构应该如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/5c58b8e92574655cc9dfb2705fb9e405.png)
相应的结构应该为由一个字符串和一个整型值组成的记录:
typedef struct{
string ans;
int nextq;
} answerT;
在这里,此结构足够小,以便可以将其定义为记录而不用定义为指针。
这样就可以用下图表示完整的数据结构了:
![](https://i-blog.csdnimg.cn/blog_migrate/317b735fc8ed35d8ee7d5eb38208a121.png)
6.4 设计外部结构
在对数据的内部结构进行定义之后,就要决定如何在数据文件中表示相同的信息。
在此例中,最简单的方法是依次写出每个问题,以及可能的答案。
为了使计算机能够将每个问题区分开来,需要定义某些规则来区别每个问答单元。在此例中最好使用一个空行。
一个问答单元都包含哪些内容呢?
首先,包含问题的文本,文件中的若干行构成了一个问题。
我们同样也需要用某种方式表示问题结束,最简单的方法是定义一个标记值。在这个程序中,可以选定用五个“-”来表示文件结束。
另外,程序同样需要允许课程设计者指明答案/下一问题的配对。在此,我们选择如下的方式:
在一个数据行中先列出答案的文本,然后用冒号分隔,再跟随下一问题的序号。
因此,文件中的每条问题都是这样的:
![](https://i-blog.csdnimg.cn/blog_migrate/b11178e442de8f930211ff10faac7e8a.png)
如何将问题序号与每一个问题联系起来?
更好的办法是由程序编写者给每个问题编一个序号。
例如,如果示例中关于地球和太阳的问题是1号的话,它的文件可以包含该问题的编号,如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/a9ebfddb462de0f07312a4fa80e02ce8.png)
参考
《C语言的科学和艺术》 —— 16 记录