跳转至

第十九章 程序设计

虽然C语言不是专门用来编写大规模程序的,但许多大规模程序的确是用C语言编写的。相对于小型程序,编写一个大规模的程序需要更仔细的设计和更详细的计划。


模块

当设计一个C程序(或其他任何语言的程序)时,最好将它看作是一些独立的模块。模块是一组功能(服务)的集合,其中一些功能可以被程序的其他部分(称为客户)使用。每个模块都有一个接口来描述所提供的功能。模块的细节,包括这些功能自身的源代码,都包含在模块的实现中。

在C语言环境下,这些“功能”就是函数,模块的接口就是头文件,头文件中包含那些可以被其他文件调用的函数的原型。模块的实现就是包含该模块中函数的定义的源文件。

将程序分割成模块有一系列好处:

  • 抽象。我们知道模块会做什么,但不需要知道这些功能是如何被实现的。因为抽象的存在,使我们不必为了修改部分程序而了解整个程序是如何工作的。

  • 可复用性。每一个提供一定功能的模块,都有可能在另一个程序中复用。

  • 可维护性。将程序模块化后,程序中的错误通常只会影响一个模块,因为更容易找到并解决错误。在解决了错误后,重新编译程序只需要将该模块的实现进行编译即可。

一旦我们已经认同了模块化程序设计是正确的方向,接下来的问题就是设计程序的过程中究竟应该定义哪些模块。

内聚性与耦合性

一个好的模块并不是随意的一组声明。好的模块应该具有下面两个性质:

  • 高内聚性。模块中的元素应该相互紧密相关。

  • 低耦合性。模块之间应该尽可能相互独立。低耦合性可以使程序更便于修改,并方便以后复用模块。

模块的类型

由于需要高内聚性、低耦合性,模块通常会属于下面几类:

  • 数据池。表示一些相关变量或常量的集合。通常这类模块是一些头文件。

  • 库。库是一组相关函数的集合。

  • 抽象对象。一个抽象对象是指对于隐藏的数据结构进行操作的一组函数的集合。

  • 抽象数据类型。将具体数据的实现方式隐藏起来的数据类型称为抽象数据类型。作为客户的模块可以使用该类型来声明变量,但不会知道这些变量的具体数据结构。如果客户模块需要对变量进行操作,则必须调用抽象数据类型所提供的函数。

信息隐藏

一个设计良好的模块经常对它的客户隐藏一些信息。例如我们的栈模块的使用者就不需要知道究竟栈是用数组实现的还是用链表。信息隐藏有两大优点:

  • 安全性。数据必须通过模块自身提供的函数来操作,而这些函数都是经过测试的。

  • 灵活性。无论对模块的内部机制进行多大的改动,都不会很复杂。不需要改变模块的接口。

在C语言中,可以用于强行信息隐藏的工具是 static 存储类型。将一个函数声明成 static 类型可以使函数内部链接,从而阻止其他文件(包括模块的客户)调用这个函数。将一个带文件作用域的变量声明成 static 类型可以达到类似的效果,使该变量只能被同一个文件中的其他函数访问。

抽象数据类型

对于作为抽象对象的模块,有一个缺点:不可能对同一个对象有多个实例。为了达到这个目的,需要进一步创建一个新的类型。这就是抽象数据类型。然后模块的接口函数需要传入这个类型对象的指针对其进行操作。

但C语言不提供封装的功能,客户可以访问抽象数据类型的成员。确实有技巧可以达到类似的目的,但使用起来笨拙。

实现封装的最佳方法是使用C++语言。实际上,C++语言产生的原因之一就是因为C语言不能很好的支持抽象数据类型。

C++语言

略。