数据结构与程序设计——C++语言描述(Data Structures & Program Design in C++) by Robert L.Kruse & Alexander J.Ryba

来源:互联网 发布:js对象属性访问错误 编辑:程序博客网 时间:2024/06/05 02:34
第1章程序设计原理
  率章首先慨述良好程序设计的重要原理,特别是它们在大型项目中的应用;然后介绍用
于发现有救算法的方法,如面向对象的设计和白顶向下的设计。在此过程中,我们提出将在
后继章节中论述的程序设计和数据存储方法方面的问题,并通过使用c'+编写程序,复习一
下这门语言的一些基本特性。
1 1简介
  编写大型计算机程序的最大困难不在于确定此程序的目标是什么,也不在于找出达到
此目标的方法。某个企业的总裁可能会说:“让我们用一台计算机来记录所有的存货信息、
账目报告和人事文件,并让它告诉我们何时需要再次订购存货,以及何时花费超出了预算
线,并且还可以让它处理工资单。”花上充足的时间和精力,系统分析和程序设计员就可以确
定企业的各个职员现在是如何完成这些任务的,然后就编写程序用同样的方式来完成这些
工作,
…大型程序的问题
    然而,这种方法几乎必定是灾难性的失败。当系统分析员去谛』查职员时,他会发现某
些工作可必轻易地放Iijj算机七去执行,于是他就这样做了。然后,当他们将其他工作移
到计算机上时,往往发现它依赖于最初的那些任务。遗憾的是,这些f务的输出却或多或
少地没有采用正确的格式,因此他们需要更多的程序设计,以将数椐从一个任务给定的格
式转换成另一个任务需要的格式。这个程序设计项目开始变得像是一床由各色布片拼缀
成的被子,其中一些布片较牢,而另一些布片较弱;一些布片被仔细地与邻接的布片缝合,
另一些却是勉强带上。如果程序设计员幸运的话,他们的作品ri-以足够好地连在一起,在
大多数时间完成大多数的例行工作。但如果一旦必须做出任何修改,则整个系统将会有
不可预测的结果。随后,又会出现新的需求,或者是一个出乎意外的问题,或许甚#.是一
个紧急事件,r而这些程序设计员的成果所具有的功效就好比是用一张拼凑的被单作为从
高楼跳下的人的安全网。
    本书的主要目ft是描述程序设计的方法和工具,这些方法和工具证明对现实规模的项
目是有效的,这些程序比用来示范初级程序设计特征的那些普通程序大得多。由于将杂碎
的方法用于解决大型问题注定会失败,因此我们首先必须采用一种一致的、统一的和逻辑的
方法,也必须仔细遵守程序设计的重要原理。这些原理有时候在编写小程序时被忽略,但对
大型项目,忽略它们将证明是灾难性的。
…问题说明
    处理大型问题时,首个主要障碍就是准确地判断这t问题是什么-有必要将模糊的目
标、矛盾的要求和可熊未明确说明的需求转换成能够进行编翟的、精确规划的项日。人们以
前使用的方法或对工作的划分未必最适于机器使用。因此我们的方法必须确定总体目标’
C++数据结构与程序设计
但却是准确的目标,然后渐渐地将工作划分成更小的问题,直到它们达到可管理的规模。
…程序设计
    许多程序设计员奉行的准则是:“首先让你的程序运行起柬,然后使它变得优美。”这一
准则对小型程序可能是有效的,但却不适用于大型程序。一个大型程序的每一部分都必须
得到妥善地组织浦晰地书写和全面地理解,否则它的结构将被遗忘,也不再能够在以后某
个时间连接到项目的其他部分,而那些部分可能由另一个程序设计员来完成。因此我们不
把风格从程序设计的其他部分分离出来,但是从一开始我们就必须小心,养成良好的习惯。
…数据结构的选择
    即使对非常大的项目,困难通常不在于未能找到一种解决方案,而是在于有太多可用的
不同方法和算法,导致我们难以确定其中哪一个晟佳、哪一个将导致程序设计困难或者哪一
个可能效率低得让人绝望。算法设计中可变性的晟大余地通常在于存储程序的数据的
办法:
    ·它们彼此如何被安排。
    ·哪些数据保存在内存巾。
    ·哪些在需要时被计算。
    ·哪些保存在文件中,这些文件卫如何安排。
    因此,本书的第二个目的是为数据组织和操作提+一牡一流的但实质上简单的思想。
表、栈和队列是我们最先学习的3种结构。随后,我们将为数据处理昀重要任务设计一些强
大的算法,如排序和查找。
…算法分析
    当有若干不同的方法可以用来组织数据和鹾计算法时,开发一个用来推荐选择的标准
就变得很重要了。因此我们将专心分析各种条件下的算法行为。
…测试和验*
  调试程序的困难比程序规模的增长要快得多。即,如果一个程序的规模是另_AI程序
的两倍,则不一定花两倍的时问就能调试它,l而很可能会是四倩的时间。许多大型程序(如
操作系统)在交付使用时,仍然包含程序设计员无望找山的错误,因为这些困难看起来是难
以克服的。有时候已耗时数年努力的项目由于无法发现为何不能工作而废弃。如果我们不
希望自己的项目遭受这种命运的话,就必须使片j满足以下性质的方法:
…程序t确性
    ·减少错误数目,使得更易于发现那些剩余的错误。
    ·能够预先验证我们的算法是正确的。
    ·能够提供测试程序的方法,使得我们有理由确信这些程序不会行为失常。
    这些方法的设计是我们的另一个目的,但它却并非完全在我们的能力可及的范围之内。
…维护
  即使在一个程序开发完成、全部调试井交付使用后.仍然需要大量的工作来维护此程序
的有效性。在对此程序有新的要求的时候,它的运行环境会改变,必须l使之适合新的需求。
基于此固,务必使一个大型项日编写得尽可能简单以利于理解和修改-
…('+
  程序设计语言c++是表达我们将面临的算法的一种f分便利的选择. BJarn‘
第1章程序设计原理
Stroustrup在20世纪80年代早期设计了这门语言,将它作为通俗C语言的一种扩展。
Stroustrup在C++巾并人的大多数新特性有利于理解和实现数据结构。在c-+晟重要的特
性巾,用于我们学习数据结构的有:
…要点
    ·C++允i数据抽象:这意味着程序设计员能创建新的类型来表达方便其应用程序的
    任何数据集合。
    ·c++支持面向对象的设计,在面向对象的设计巾,程序设计员定义的类型在算法实
    现中起到巾心作用。
    ·重要的是,正如允许面向对象的方法,c十十允许使用自顶向下的方法,这也是c程序
    设计员所熟悉的。
    ·C++便于代码重用及通用目的库的构造。这门语言包含了一个扩展的、有效的和方
    便的标准库。
    ·C++改进了c语言几个麻烦和危险的方面。
    ·C++保持了作为c语言特点的效率。
    灵活性、通用性和高效性的结合,使得c++成为目前程序设计员晟流行的选择之一。
    我们将发现,c++的数据抽象和面向对象的特性自然地实现了作为数据结J设计基础
的普遍原理。因此,我们将仔细解释如何使用c++的这些方面并当它们首次在本书中出现
时简要地概述它们的语法(语法规则)。这样,我们将阐明和描述c++中许多不与c重叠的
特性,关于(y+语法的精确细节,请参考c++程序设计方面的谋本——我口j在本章最后的
参考书目中推荐了几本这样的书。
1 2  Life游戏
  冒昧地引用一句古老的格言;实践出真知。
…案例研究
  贯穿本章,我们将集中学习一个案例。虽然按现实标准来看它并不大,但它阐明了程序
设计的原理和我们应当学习避免的易犯的错误。有时候这个例子会带出通用的原理'有时
候我们首先进行综合讨论,但我们的目的始终是发现能在实际应用范围中证明其价值的那
些通用原理。在后继章节中,我们将对大型项目使用类似的方法。
    我们将使用的例子称为Life游戏,由英国数学家J H Conway在1970年提出。
1.2.1   Life游戏的规则
…定义    卜无边界的矩形网格
  Lif。游戏实际上是一种模拟,并不是游戏者之间的游戏。它在-’
上进行,这个矩形网格中的每个单兀可被一个有机体占据,或者不被占据。被占据的单元称
为活的,来被占据的单元称为死的。哪一个单元是活的要根据其周围活的邻居单元的数目
而一代代地发生变化,规则如下:
C+十数据结构与程序设计
…转换规Ⅲ
    (l)给定单元的邻居是与它在垂直、水平或对角方向上相接的8个单元。
    (2)如果个单元是活的,但没有邻居单元是活的,或者仅有- .个邻居单元是活的,则
    在下一代,此单IC就会用孤独而死亡。
    (3)如果一个单元是活的,日有4个或4个以上的邻居单元也是活的,则在下一代,此单
    元会闰拥塞而死亡。
    (4) -个括的单7L,如果具有2个或3个活的邻居单元,则此单元在下一代仍是活的。
    (5)如果一个单元是死的,则在下一代,如果它不多不少刚好有3个邻居单元是活的,
    则此单兀变成活的。所有其他死的单元在下一代仍是死的。
    (6)所有的出牛和死r都刚好在同时间发生,冈此正在死亡的单元有助于*-个单
    元的出生,但它不能通过减少拥塞而阻止其他单元的死f;正在出生的单元也不能
    保护或杀死上一代中活着的单元。
…配置
  在一个网格中,活的和死的单元的一种特定的安排称为配置(configurac“ion)。前述的规
则解释了种配置在每一代如何变换戚¨种配置。
1.2.2例子
作为第一个例子,考虑下面这个配置
┏━┳━┳━━━┳━┳━┳━┓
┃  ┃  ┃      ┃  ┃  ┃  ┃
┣━╋━╋━━━╋━╋━╋━┫
┃  ┃  ┃  .  ┃  ┃  ┃  ┃
┣━╋━╋━━━╋━╋━╋━┫
┃  ┃  ┃      ┃  ┃  ┃  ┃
┗━┻━┻━━━┻━┻━┻━┛
每个单元活的邻居的计数如F
┏━┳━┳━━┳━━┳━┳━┓
┃  ┃  ┃    ┃    ┃  ┃  ┃
┣━╋━╋━━╋━━╋━╋━┫
┃  ┃  ┃    ┃    ┃  ┃  ┃
┣━╋━╋━━╋━━╋━╋━┫
┃  ┃  ┃.1 ┃'1  ┃  ┃  ┃
┣━╋━╋━━╋━━╋━╋━┫
┃  ┃  ┃    ┃    ┃  ┃  ┃
┣━╋━╋━━╋━━╋━╋━┫
┃  ┃  ┃    ┃    ┃  ┃  ┃
┗━┻━┻━━┻━━┻━┻━┛
…垂死的例子
    由规则(2),在接下来的一代,两个活的单元都将死产,而且规则(5)丧明没有单元会变
成活的,因此这个配置灭绝。
…稳定性
    另一方面,下面的配置拥有所示的邻居计数。每个活的单元的邻居单元计数为3,闪此
将保持活的,但死的单元都只有2个或更少的邻居汁数,斟此z们当中不会有单元变Tt。
第1章程序设计原理
┏━┳━┳━━┳━━┳━┳━━┓
┃  ┃  ┃    ┃    ┃  ┃。  ┃
┣━╋━╋━━╋━━╋━┻━━┫
┃  ┃  ┃    ┃    ┃        ┃
┣━╋━╋━━╋━━╋━━━━┫
┃  ┃  ┃.3 ┃.3 ┃ 2  0   ┃
┣━╋━╋━━╋━━╋━┳━━┫
┃  ┃  ┃.3 ┃.3 ┃  ┃    ┃
┣━╋━╋━━╋━━╋━╋━━┫
┃  ┃  ┃    ┃    ┃  ┃    ┃
┣━╋━╋━━╋━━╋━╋━━┫
┃  ┃  ┃    ┃    ┃  ┃    ┃
┗━┻━┻━━┻━━┻━┻━━┛
…交替
  下面这两个配置将按代交替出现,如配置中给出的邻居计数所指示
┏━┳━━┳━━┳━━┳━┓
┃  ┃    ┃    ┃    ┃  ┃
┣━╋━━╋━━╋━━╋━┫
┃  ┃    ┃    ┃    ┃  ┃
┣━╋━━╋━━╋━━╋━┫
┃  ┃.1 ┃.2 ┃.1 ┃  ┃
┣━╋━━╋━━╋━━╋━┫
┃  ┃    ┃    ┃    ┃  ┃
┣━╋━━╋━━╋━━╋━┫
┃  ┃    ┃    ┃    ┃  ┃
┗━┻━━┻━━┻━━┻━┛

┏━┳━┳━━┳━┳━┓
┃  ┃  ┃    ┃  ┃  ┃
┣━╋━╋━━╋━╋━┫
┃  ┃  ┃.1 ┃  ┃  ┃
┣━╋━╋━━╋━╋━┫
┃  ┃  ┃.2 ┃  ┃  ┃
┣━╋━╋━━╋━╋━┫
┃  ┃  ┃.1 ┃  ┃  ┃
┣━╋━╋━━╋━╋━┫
┃  ┃  ┃    ┃  ┃  ┃
┗━┻━┻━━┻━┻━┛
…*$样性
  这是一个令人惊讶的事实,从十分简单的初始配置,持续许多代后,能发展成Life配置
的相当复杂进程,而且按代进步时会发生什么变化通常并不明显.一些很小的初始配置会
成长为较大的配置,其他的会慢慢灭绝,许多配置会达到一种不变的状态或者每隔几代即经
历一种重复模式的状态。
…流行
    在Life游戏发明后不久,Martin Gardner在Scien£ific A卅encarz的专栏巾对它进行了
讨论,从那时起,它已使许多人为之着迷,以至于甚至有一本新闻季刊有好几年都致力于相
关的论题。它为家用微型计算机提供了理想的显示。
    当然,我w J的首要目的是编写一个程序来显示一种初始配置如何逐代变化。
1.2.3解决方案:类、对象和方法
  概括地说,运行L.fe游戏的程序具有这样的形式+
…算法
  创建【.fc配置作为活的和死的单元的一种初始安排
  打印Life配置。
  当用户希望看到下一代时:
    通过应用J.ife游戏的规则更新配置。
    打印当前配置。
c++数据结构与程序设计
…对象
    在此算法中我们要研究的一件重要的事情是Life配置。在c++中,我口j使用类(class)
来组合数据和用于访问或修改这些数据的方法,数据和方法的这样的组合称为属于给定类
的对象。对Life游戏来说,我们称这个类为Iife,因此配置configuration成为Li[e的一个
对象。我们然后对这个对象使用3个方法:lnltlalize0将创建具有活的和死的单元的初始配
置;print0将打印出当前配置;update()将作出从一代迁移到下代时发生的所有变化。
…C++类
…方法
    实际上,每个C+十类由表示变量或函数的成员所组成。表示变量的成员称为数据成员,
这些成员用于存储数据值。表示某个类的函数的成员称为方法或成员函数,类的方法一般
用丁访问或改变数据成员。
…客P程序
  客户程序即访问某个特定类的用户程序,它能声明并操作那个类的对象,因此在Lif游
戏中,我们声明一个Life对象:Life configuratinn;。
…成员选择运算符
  现在可以使用c++运算符.(成员选择运算符)来访问configuration的方法。例如,可以
如下方式打印出configuration中的数据:configuration,print();,
…信息隐藏
  在书写客户程序时,只要我们知道一个C++粪的每个方法的说明,即每个方法功能的精
确描述,就可以使用这个类,意识到这一点很重要。我们不必知道数据实际上是如何存储的
以发方法实际上是如何编程实现的。例如,为了使用I.ife的对象,我们不必确切地知道此对
象是如何存储的,或者类Life的方法如何完成它们的工作。我们的首个例子反映了一条重
要的程序设计策略,此策略称为信息隐藏。
…私有和套用
  当开始实现类I,.fe时,我们发现在幕后还有更多事情:需要决定如何存储数据,并且需
要变量和函数来操作这些数据。然而,所有这些变量和甬数是这个类私有的( private);客户
程序不必知道它们是什么,它们是如何设计的或者对它们有任何访问。客户程序仅需要这
个类说明和定义的公用(public)方法。
  Z本书中,我们将如下区分方法和而数,即使它们实际的语法(程序设计屠法)是一
样的:
第1章程序设计原理
1.2.4   Life:主程序
前面给出的Life游戏的算法大纲可转化成下面的c++程序
/*Pre :    "he _ 'er s Je                 : - r "_ cor f/CU ra t i . r o-. i - .._rcr ce一-
   POSt:   The pr:vran: onn rs a sciruence or- c_ :tures  showira  tn_  c.ianqes  i -  rnc c_r -. a-
             ura I on  of l i v_nq c:e_ _ -. a cc'or.7 l n:J  r o  -.he ru/ es  ro r the口ane Of L_ te .
   uses :   n:-. c_a -.s 9 i fe anci i ts meLbjds _n二 rial i ze ( ).  pi : nF { ).  anci updal_e ( ) .
L.fC con figuration;
instruct.ons { );
cor f jqurat.on . initializ e r ):
cor.figuration.print ( i;
cout《 .. continue viewing… CJene.ation s ? "《  endl ;
while  { user_says_yes ( l )  L
  configuration . update t );
   con figuratiOn . p.int ( );
   cout " " continue viewing new generations ? " "  endl
一实用程序包
  此程序以包含文件开始,使得它能够使用类I。.fe和标准C++的输人和输出库。实用函
数user_savs_yes()在utdity. h巾声明,我们稍后讨论它。对于Life程序,我们所需要的关
于。tility.h的仅有的其他信息是它吼如下的指令开始:
    # includeflostream>
    using namespace std;
    这条指令允许我们使用标准wJC++输入和输出流,如cin和c。ut。(在较老的编译器上,
一条替代的指令# include <iustream.h>具有同样的效果。)
    程序设计技术规则;;ij二]
    编写的每个程序、函数和方法要包含准确的前置条件和后
…函数
  程序说明的第三部分是它所使用的类和函数的列表,每个程序、函数或方法都应包含
个类似的列表。
第1章程序设计原理
棋跳盘J-创建这个Life配置,用一种颜色的棋子代表当前一代中活的单元,并用第二种颜色
的棋子标记那些将在下一代出生或死去的单元。)
a)    、k)
    图1_l简单的Life配置
1 3程序设计风格
  在着手实现Life游戏的类和雨数前,我们先暂停一下,呲考虑在程序设计中应该注意使
用的几个原理。
1.3.1名称
  在创世故事中(《圣经》创世纪2:19),上帝带着所有的动物来看亚当能绐它mj取什么
名字。按照旧的犹太传统,只有当亚当命名一种动物后,这种动物才会有生命。这个战事
蛤计算机程序设计提出了一条重要的道德标准:尉l使数据和算法事先已存在,只有当它们
有了有意义的名称,并且它们在程序中的位置能被恰当地识别和理解,它们才能获得自己
的生命。
***审慎命名的目的
    为了使程序完善地运行,最重要的是确切地知道每个类和变量代表什么以及每个函数
做什么,吲此一般应该包括对类、变量和函数进行解释的文档。我们应该审慎地选择粪、变
量和函数的名称,以明确、简洁地识au-.mwj意义。找到好名称通常并非一件易事'但它却
掣鳅∞帮黥
c++数据结构与程序设计
足够重要,我们将它挑出来作为第二个程序设计技术规则
  c++在这条技术规则的实施方面有相当的进展,它要求变量声明,而且给予我们几乎无
限制的自由来标识名称。在不同的地方使用的常量应赋予名称,不同的数据类型也应如此,
所以编译器能够捕获那些本来难以发现的错误。
  我们会发现类型和类在C++程序中起着根本的作用,并且特别重要的是,它们应浚被突
出呈现给程序的阅读者。因此我们采用一种大写约定,这点已在Life程序中应用:我们对
任何类或程序设计员定义的类型标识符的起始字母使用人写。相反,对函数、变量或常蛀的
标识符则使用小写字母。
  名称的审慎选择在阐明程序和避免打印与常见错误方面大有帮助,指导原则如下:
…指导原则
  (l)特别审慎地选择在程序的不同部分中使用的类、函数、常量和所有全局变量的名
    称。这些名称应该是有意义的,并且应该能够明确地指示这个类、函数、变量等的
    目的。
  (2)对仅是暂时、局部使用的变量,保持其名称简单。数学家通常使用单个字母代表一
    个变量,因此有时候当我们编写数学程序时,有可能对数学变量使用单个字母的名
    称。然而,即使是x控制for循环的变量,也经常有可能找到一个比较短但却较有
    意史的单词,它能更好地描述此变量的用途。
  (3)使用通用的前缀或后缀来关联同一常规娄别的名称。例如,在个程序中使用B
    文件可以被称为:
    lnputflle  transact_on一fne  t.tal file   out_flle   re-ec‘flle
    (4)避免采用故意的误拼和无意义的后缀来获得不同的名称.在下列所有名称中
    indexindx    ndeyindexlndex2index3
    只有一个(第一个)应被正常使用。当你试图引入这种类型的多个名称时,用它作
    为一个警J<,告诉你自己应该再仔细想想并设计出更能描述其使用意图的名称。
  (5)避免选择那些本身意义与问题毫无关系或只有很少关系的漂亮的名称。下面的语
    句可能非常有趣,但它们是低劣的程序设计!
    dO f
    study{).
    】Nhik (TV.in_hock¨)j
    if(!  sle.pyl piail¨
    else nap("
    (6)避免选择拼写互相接近或者其他方面易于混淆的名称a
    <7)应小心使用字母“l”(小写的cLD."O'(大写的。h)和“。”(零)。在词或数中,通常可
    以根据上下文区分出它们而不会带来问题,但“l”和“(”决不应被单独用作名称。
    考虑下面的例子:
第1章程序设计原理
L 3.2文档和格式
…文档的目的
  最初大多数学生认为文档是程序结束后必须忍受的繁琐之事,只是保证标注者和教师
能阅读它,爿而不会因为含糊而损失学分。小型程序的作者确实能在头脑中记住所有的细
节,因此仅需要文档来向其他人解释这个程序。对于大型程序(及数月过后的小型程序),不
可能记住每十细节如何与其他细节相关,因此为了编写大型程序,与程序的每个较小部分一
起准备合适的文档是必要的。一种良好的习惯是在编写程序的同时准备文档,但如同我们
随后将看到的,一种更好的习惯是在开始书写程序之前就准备部分文档。
  并非所有的文档都是合适的。与很少有文档或仅有含义模糊的注释的程序一样常见的
是那些带有冗长文档的程序,但这些文档却很少有助干增加对程序的理解。因此我们的第
三个程序设计技术规则是:
  文档的风格同所有的书写风格一样,是高度个人化的,许多不同的风格都证明是有效
的。尽管如此,还是应当遵守一些普遍接受的指导原则:
…指导原Ⅻ
i.在每个函数的开始处放上序言,包括:
  (1)身份证明(程序设计员的名称、日期和版本号)。①
  (2)所用函数和算法的目的的说明。
  (3)函数所做的修改及其所使用的数据。
  (4)对程序外部更多文裆的引用。
2当定义每个变量、常量或类时,解释清楚它是什么及如何使用。如果从名称上就能明显
  看出这些信息则更好。
3对程序的每个重要的片断(段落或函数),用一句注释简要随明它的目的或动作。
4.如果每个重要片断的结束不明显,则加以指示。
5.避免机械模仿代码功能的注释,{:
    count++;    //将counter加1。
    或者使用无意义的行话,如
    ,/Horse string lengthlnto correctitude.
    (这个例子直接取自一个系统程序。)
6.对任何使用了技巧或意义不清楚的语句加以解释,如果能避免使用这样的语句则更好。
  ①为节省空间,车书中打印的程序不包青身份证明行或者序言的
经蛤出了必要的信息。
雌其他部分,崮为周同的文本已
12
(1++数据结构与程序设计
7代码本身应解释程序足如何工作的。文档应解释它为什么工作及它做州么。
8无论何时修改个程序,确信文档得到了相应的修改。
…格式
    程序中的空格、空白行和缩排是一种重要的文档形式。它们使程序易于阅读,使我们能
一眼看出程序的哪些部分彼此相关、哪里出现r主要中断以及每个循环中或者条件语句的
每个选择分支当中分别包含哪衅语句。有许多系统(有些是自动的)可用于缩排和调整空
格,它们的日的都是为了更容易地确定程序的结构。
** * prettyprlntmg
    preityprmter是一个系统实用程序,它读人一个C++程序,在行间移动文本并调整窄
行,以此改进程序的外观并使其结构更清晰。如果在你的系统中有prettyprlnter的话,你u『
以用它做试验,看看它是否有助于改进程序的外观。
…一致性
    由于程序的良好格式的重要性,需要为空格和缩排设置一些合理的规则并在所书写的
所有程序中一致地使用这些规则。如果要使格式化系统在阅读程序中有用,则一致性是必
要的。许多专业程序设计团体会选定一种统一的体系并坚持他们所编写的所有程序都符台
它。一些班级或学生程序设计小组也同样这样做。这样,一个程序设计员就更窖易阅读和
理解别的程序设计员的作品。
1.3.3细化和模块化
  程序设计技术规则
的时间比编写程序的时间多得多
  使阅读更容易。
——
…月题求解
…再分
    计算机不求解问题,而人会求解。通常求解过程的最重要的部分是将问题划分成更小
的问题,这些小问题能够被更详细地理解。如果这些问题仍然太困难,则再次细分它们,如
此等等。在任何大型组织中,最高管理部门不必担心每个活动的每个细节,他们必须将精力
集中在总体且标和问题上,而将具体的责任委派给下属。另一方面,中层管理者不必处理所
有事情:他们必、再次划分1作并将它交给其他人。计算机程序设i-r亦是如此。即使当项
目小得用一个人就能自始至终完成它,对这个工作进行划分还是最重要的。从对问题的全
面理解开始,将它再分成子问题,然后依次处理每个子问题而不用考虑其他子问题a
  让我们用一句经典的格言来重新描述这个原理:
  程序设计技术规则
不要只见树木不见森林
第1章程序设计原理
…自顶向下的细化
…说明
  这条原理称为自顶向下的细化,是编写能运行的大型程序的真正关键。这原理隐
含着详细的考虑可吼延迟,但精确性和严格性并不能延迟。它不意味主程序变成某种模
糊的、难以描述任务的实体。相反,主程序会将几乎所有的工作分派给各种类、数据结构
和雨数,并且当我们编写主程序时(这是我们首先要写的),我们确切地知道工作是如何划
分的。然后在后面开发某个特定的类或函数时,我们就能在开始之前确切地知道它们将
来做什么。
  通常很难准确地确定如何将工作划分成类和函数,而且有时候曾经做出的决定必须在
以后被修改。虽然如此,还足有一些指导原则能有助于决定如何划分一项工作:
  例如,我们的Life程序确实是处理Life游戏,因此创建个类Life来模拟这个游戏。
我们经常可以通过用文字描述任务并对所用的不同名词各指派一个类,从而挑选出应用程
序巾的重要的类,我们所使用的动词经常意味着重要的函数。
  也就是说,我们应该能够简洁地描述一AI函数的目的。如果你发现自己写了很长的一
段话去说明一个函数的前置条件或后置条件,那么或者你给出了太多的细节(即,你提前写
出了函数),或者你应该重新考虑工作的划分。函数本身毫元疑问会包含许多细节,但这些
细节应该直到下一阶段的细化时才出现。
    程序设计技术规则——]
    每个类或雨数应该隐藏某些东西,________    j
  大型公司w中层管理部门不会将部门的所有信息都传递给领导,他们首先概括、比较并
清除信息,自己处理其中的许多请求,然后仅将需要的东西E报给上级领导-类似地,管理
者不会将来自更高层管理部门的所有东西都传达给自己的下属,他们仅向下属传达开展工
作所需要的那些东西。我们所编写的类和函数也应该一样。换言之,我们应该实施信息
隐藏。
…拳数
    细化过程最重要的部分之僦星确切地确定每个函数的任务,精确地说明它的前置条
件和后置条件。也就是说,它的输入是什么,而且它将产生的结果又是什么。这些说明中的
错误是最常发生的错误故障之一,也是最难阻查找的错误之一。首先,必须对函数中使用的
参数进行精确地说明。这些数据有3种基本类型:
    ·输人参数由函数使用但并不被函数修改。在c++中,输人参数通常是通过值传递口
14
C++数据结构与程序没计
    (例外:大对象应通过引用传递:D,这样避免丁建立本地削本所需要的空间和时间。
    然而,当我们通过引用传递-个输人参数时,应该在它的前而用关键字const加以声
    明。类型修改符const的使片j是很重要的,因为它让阅读者知道我们是在使用一个
    输入参数,并允许编译器检测对这个参数的意外修改,有时候也允许编译器优化我
    们的代码。)
    ·输出参数包含两数的计算结果。在本书中,输m参数使用引用变量。相反,c群序设
    计员需要通过传递变量地址来模拟引用变量以利用输Il参数。当然,C的方法在
    c++中仍然可用,但我们应避免使用。
    ·输入输出参数既用作输入,也片j作输出,丽数使用参数的初始值并随后修改它,我
    们应通过引用来传递输入输出参数。
…变量
  除了参数外,函数使片j的其他数据对象通常属于下列类别之一:
  ·局部变量在函数内部定义,仅当甬数运行时存在。它“J在函数Jr始前不会被初始
    化,并且当雨数结束时就废弃。
…副作月
    ·全局变量用于函数巾但并不在函数中定义。在函数中使用全局变量往往很危险,吲
    为在甬数编写完毕后,程序作者可能完全忘了曾用过哪些全局变量以及如何使用它
    们。如果主程序在以后被修改的话,则这个函敬可能会神秘地变得行为异常。如果
    函数政变一个全局变量的值,这种情况会引起副作用。剐作用比使用全局变量作为
    函数的输入更危险,因为副作用可能改变其他函数的性能,从而在程序员调试程序
    时,经常将他误导到程序中已经正确的部分。
『一——一    程序设计技术规则
    保持连接简单。尽可能避免使用全局变量
程序设计技术规则
    只要能够避免,切勿引起副作用。
如果必须使用全局变量作为输人,则详细地将它们写入
  尽管这些自顶向下的设计原理看起来几乎是不言而喻的,但全面学习它们的1唯一方法
是通过实践。凼此贯穿本书我们都仔细地将它们应用到所编写的大型程序中,m且现在就
适合返回去讨论我们的第一个例子项目。
练习1.3
El.在实现下列项目中,适合定义什么类?每个类将拥有什么方法7
  (l)存储电话号码的程序。
①参考(斗+课本巾关于通过引用进行调川和引川变鼍的列沧
第1章程序设计原理
  (z)玩垄断游戏的程序。
  (3)玩井字游戏的程序。
  (4)模拟在带有红绿灯的十字路口等待的汽车队列的程序。
E2.下面的类应该模拟一幅扑克牌,重写它的定义,使它符合风格原理。
        claia a {            /:  a dack of ca…d5
int x; thinq Yl [52] ; /'X .S tha Iocation of t/w top card ir the deck. Y_ l__ts the
 cards . ' l puUic: a ( };
 v口id  Shuffle l L                   i l    ShU ffle  randomly arranoes  the cards .
 thing dm                   il   deals the top card off thC deck
E3.给定下面声明:
    int arnl【nl,1,];
  这里n是一个常量。判断下面的语句的功能,并且重写这些语句,用非技巧的方法达到
  同样的效果。
  for  (i-0;i(n; i++】
    for{j-O; j<n; j++)
    a【11【]】=¨1+1)“]+1))+“j+1)/(1+1));
E4.重写下面的函数,用非技巧的办法实现同样的结果。
    void  doe8   somethinS(int&f ir st,int&second)
    first= second - first;
    second= second - first;
    first= second+ fi.str
E5.判断下列每个函数的功能。用有意义的变量名、较好的格式重写每个函数,并去掉其中
    不必要的变量和语句。
    (l) int calculate (int apple. int orangel
    t int peach, lemon;
    peach-。;lemon-。,if( apple< orangel
    peach-orange;e1盹if(o.ange' -apple)
    peach-apple*.1¨{peach-17;
    lemon-19;)
    ntutn (peach);
(2)假设声ah typedef that vector [maxl "
       flo_t ficjure {vector vectorl)
      { int loopl. loop4; float loop2. loop3;
     loopl--0 :  loop2 -.vectorl L ioopi l ;  loop3 -.0 . O
      loop4-.loopl;  for  ( loop4-0 ;
       loop4 <max;  loop4 ++)   { loopl-.loopl+l ;
      loop2-vectorl L locpl-l ] ;
       loop 3 -. ioop2 + loop :. ;    } iOOpl _ loopl- l ;
16
C++数据结构与程序设计
    loop2 -looplj
    return( loop2--loop3 iloop2)j l
    (3) int quest.on lint  &al7. int &StUffl
    {  int  another,  yetan Other,s tillonemore;
    anothe yetano+he…tillonemo.e- al 7,
    yetanother- stuff  anothar- stil lomamo ra;
    al 7  - yetanother; tillonemore-yetanother;
    StUf f-anOther  anothe r-yetanotherj
    yetanother-stuff,}
    (4) int mysterylint apple.  int o.ange. int peach)
    (if C apple)o.ancje  if( apple>pearh) if
    f peach>o.angel raturn( peach);  else if( appiY oranga)
    raturn f applel; alsa return( orarule); elsa return( apple)j  else
    if( peach> apple)  if( peach>orange) raturn I o.ange)j  else
    retuni(peach}i  else returnf applel;}
E6T面设计的语句是用米检查3个整数的相对大小,假设这3个整数彼此不同:
    if lx<z) if( x<y) if ly<zl c-lj  else  C-2j  alsa
    Lf( y<z) C-3;  else  C-4;  eise if( x<y>
    if lx<z) c-5j  else一6j else if(y<z)c=7;else
    if f z<x) if( z<yi C-8j  else  c-9;  elae  c-1 0;
    (l)以易于阅读的格式重写这些语句。
    (2)由于3个整数仅有6种可能的顺序,因此实际上仅会发生10种情况中的6种。找
    出那些决不会发生的情况,并消除其中儿余的检查。
  (3)编写更简单、更简短的语句来实现同样的结果。
…立方根
E7.下面的C+4_函数计算一个浮点数的立方根(通过牛顿近似法),使用的原理是:如果y是z
  的立方根的一个近似值,则z一2T +l/y2是一个更接近的近似值。
float  funCtion fcn( float  stuf引
{ float april. tim. tiny, shadow, tom. tam. square; int flag;
tim-stuff;  tam-stuffj  tny-0 0 0 0 0l;
if (stuff.=O)do f shadow-.tim+tim; square-tim* tim:
tom-(shadow+stuf=isquare)j  aprii--tom/30i
if( april*april* ap.il- tam> tiny}  if( april* april* ap.il - tam
    <tiny) flag-l: olse flaq-0; else f_ag-0;
if ff lag--0) tim-april; else  tim- tam;) whila ff lag'21);
if (stuff--O) ratuni (stuff); elae return lapril);}
(l)用有意义的变量名重写这AI函数,去掉其中无益于理解的多余的变量,使它有更好
  的布局,而且没有冗余的和无用的语句。
(2)编写一个函数从数学公式直接计算:jI上的立方根,以赋值y—r开始.然后重复
    _=(2”y+(z/(y 4y))),73直到abs(y”y“y-r)‘0. 00001.
(3)这些任务中的哪一个更容易’
第1章程序设计原理
…‰计
E8.一个数序列的均值是这个序列中的所有数的和除以数的个数。这个序列的(总体)方差
  是序列中所有数的平方的均值,再减去序列中的数的均值的平方。标准方差是方差的
  平方根。编写个结构良好的C++函数束计算有”个浮点数的一个数序列的标准方
  差,这里n是一个常量并且这些浮点数在数组中的下标是从o到n-l,”和数组作为蛹
  数的参数。使用并随后编写辅助函数来计算均值和方差。
…标目
蜘设计一个程序在~上标绘出给定的点集。此程序的输入是一个文本文件,文件的每一
  行包含两个数,它们分别是要标绘的点的I和y坐标。此程序将使用一个函数来标绘
  一个这样的坐标对,这个甬数的细节涉及到标绘的特定方法,由于它们依赖于标绘设备
  的要求,而我们并不知道这些要求,因而未能写出这个函数的细节。程序在标绘点之前
  需要知道其输入文件中上和y的最大值、最小值。因此程序要使用另一个函数bounds
  来i人整个文件并判断这4个最大值、最小值。其后,程序将使用另一个函数来画出并
  标注数轴,然后重置输入文件并标绘各个点。
  (1)编写主程序,不包括函数。
  (2)编写函数bounds。
  (3)写出剩余函数的前置条件和后置条件,同时加上合适的文裆,说明函数的目的和
    需求。
1 4编码、测试和进一步细化
  本节标题中的3个过程足联合并进的,必须一起进行。然而我们在思想上还是让它们
保持独立,因为每个过程都需要自己的步骤和方法。当然,编码是用一门计算机语F1如时+
的正确语法(语法规则)编写算法的过程;测试是用所选的样本数据运行程序以发现所出现
的错误的过程。在进一步细化过程中,我们着手处理尚未编写的函数并重复这些步骤。
1.4 1占位函数
…及早调试和测试
    在主程序的编码完成后,大多数程序设计员希望尽早完成所需要的类和函数的书写和
编码,以查看整个项目是否能正常工作。对于像Life游戏这样小的项目来说,这种方法。f能
是有效的。但对更大型的项目,书写和编码会是一项很艰巨的工作,当它完成的时候,在早
期所写的主程序和类及函数的细节可能都被忘记了。实际上,可能是由不同的入编写不同
的函数,而其中一些启动这个项目的入可能在所有函数编写完成前就离开了项目组。在脑
千里比较新鲜的项目会更易于理解和调试。因此,对大型项目来说,当每个类和函数一编写
完就进行调试和测试比等到项目全部编码完成后再进行调试和测试更加有效。
    即使对较小的项目,也有理由对类和函数一状一个地进行渊斌。例如,我们可能不能确
定特在程序中几处位置上出现的某个c++语法。如果能分别编译每个函数,贼我们能{艮‘汰
地学会在后继函数的语法中避免类似错误。作为第二个例子,假定我们已确定程序的主要
18
(I++数据结构与程序设计
步骤应按某种顺序进行,如果我们一编写完主程序就测试它,则叮能会发现有时候主要的步
骤是按错误的顺序进行的,那么我们就能很快地改正程序,这比等到主要步骤可能被它所包
含的许多细节所遮掩的时候再来测试它要更加容易。
…{位日数
  为正确地编译主程序,在用到的每个函数的位置上应该有点东西,因此我们必须放上简
短的哑函数,叉称为占位函数。最简单的占位函数只做很少工作或完全不做任何事情:
V0id instruct.ons ( ) t l
 bool  u sersaysye s ( )  l return (true}
  注意在编写占位函数时,我们至少必须约束它们的相关参数和返回类型。例如,在为
user_says_yes()设计占位函数时,我们决定它应该返回一个简单的t…或false,这意味着
我们应该将此函数的返回类型定为布尔型( bool)。类型bool只是在最近才被加到c++中,
一些较老的编译器不识别它,但我们通常可以用下面的语句采模拟它——按照惯例,如果需
要的话,可以将它放在实用程序包中:
typedef int bool:
eonst bool false- O
eonst h001 true- l ;
    除了占位函数以外,我们的程序巾也需要粪Life的占位定义。例如,在文件life.h中
我们应该如下定义这个类,不必带数据成员:
    class Lifef
    public:
    void initialize();
    vaid print("
    .roid  update¨i
    )j
在life.c中我们也要为这个类的方法提供下列占位函数
.roid  L.fe . initializ.
void Life::print <)  { J
、mid Lifa :: update ( )  {
  注意这些方法定义必须使用(>+的作用域解析运算符::①以指示它们属于类Life的作
用域。
  即使是用这些最小的占位函数,我们至少能编译程序并确信类型和变量的定义在语法
上是正确的。然而,通常每个占位函数应该打印一条说明此函数被调用的信息。当我们执
行程序时,我们发现它进入个无限循环,因为函数user_savs_yes()总是返回true值。但
是主程序毕竟编译通过而且能够运行了,那些我们可以继续细化这些占位函数。对于像
L。f。游戏这样的小项目,我们完全可“依次编写每个类或函数,用它替换对应的占位,并观
察程序执行的结果。
①参考C__课奉中剥作用域解析运算符和类方法语法的讨论
第1章程序设计原理
l_4.2类Life的定义
…l:活的单元
…O:死的单元
  每个Life对象需要包含一个矩形数组①,我们称之为gird,用来存储Life配置。在数组
grid中,我们用整数1表示活的单元,用。表示死的单元。因此为r对某个特定单元的活的
邻居计数,只要累加其邻居单元的值即可。实际上,在更新Life配置时,我们要重复计算此
配置中各个单元活的邻居的数目。因此,类Life应包含一个成员函数neighbor_count来完
成这项任务。另外,由于客户程序的代码并不需要成员neighbor_count,因此我们将这个函
数的可见性定为私有(private)。相反,前面的Life函数都必须拥有公用(public)可见性。最
后,必须决定Life配置所承载的矩形数组的维数。我们将这些维作为全局常量编码,因此进
行一下简单的修改就可以按我们的需要重新设置程序中网格的大小。注意常量可以安全地
放在.h文件中。
C∞8t lnt……=2 0,…C0l;6 0j 7  9………一5
clasa Lifef
public:
  void  initialize(),
  void print()j
  void update();
private:
int  cjrid imax -.Ow+2 l  l maxcol+2 l ;
                                 ;:    -.. . /c.ws i c-r
int nejqYo:or_count lint row.  int col);
  找们不用编写成员甬数,通过一同使用前面的占位函数和这个私有函数ne—ghb。’
count的占位函数,就可以测试这个定义。
1.4.3对邻居计数
…函数neighbor_count
…栅栏
  现在进一步细化我们的程序。对坐标为row. col的单元的邻居进行计数的函数要求查
看它的8个邻接单元。我们需要使用一对for循环来完成它,一层循环从row -1运行到
row+l,另一层循环从。。1-1到col+l.需要小心的是,当row. col在网格的边界上时'只查
看网格中合法的单元。我们不是采用几个if语句米确认有没有跑出网格,而是在网格周围
引入栅栏(hedge):通过增加额外的两行,其中一行是在网格的第一个实行前,另一行在最后
一个实行之后,也增加了额外的两列,其中一列是在网格的第一个实列之前,另一列是在最
后一个实列之后,从而扩大了网格。在对类Life的定义中,我们事先已考虑到栅栏'将成员
①具有两个下标的数组称为矩形,第一个下标确定数组中的行,第二个确定列
20    C++数据结构与程序设计
Rrid定义为一个具有maxrnw+2行和maxc01+2列的数组。栅栏行和列中的单元始终是死
的,因此完全不会影响活的邻居的计数。但它们的出现意味着对邻居计数的fo,循环不用再
区分F,格边界上的行或J与其他的行或列。参见图1.2中的例子。
    栅栏
    maxrow
maow+l
    田1.2带栅栏的Life田格
…监视哨
  另一个经常使用的替代栅栏的术语是监视哨( sent.nel):监视哨是教人数据结构中的额
外的元素,因而不用再将边界条件作为一种特殊情况对待。
int Life:: neighbor_count{int row.  int col)
/+胁:  The Life obiect contains confiquration,and the -oordinates row and col
    define cell inside its hedge
  Po  .t: The number of n ving neig肌耶of the speci fied ce// is returned.,/
int i. j;
int  count-0 :
fOr ri-row-i; i<-.row+l; i++)
   for (j-col-l; i<-coi+i; jH)
      count十-grid [ i ]  [ j l ;     7    increase  the coun t  u neiqhoor is al i ve .
count--g.id t rowJ [ coIJ ;
                                     U   RedUce courJ t . since -.eii is not its own ne.ghbcr
r~turn  count ;
1.4.4曼新网格
…方法update
    用来更新Life配置的方法的动作简单明了。首先使用存储在配置中的数据来计算名为
new_grid的矩形数组的各个元素.new_grid记录了更新后的配置。然后逐个元素地将new
_grid复制回Life对象的grid成员中。
  为创建new_grid,我们用一个在行row和列∞l上的嵌套的循环,在矩形数组grid中的
非栅栏元索上运行一遍。这些嵌套的循环体由多路选择语句switch组成。函数neighbor_
第1章程序设计原理
count(row. col)的返回值是。,l,…,8中的某一个,对其中的每种情况,可以采取一种不
同的动作,或者就像我们的应用程序中,几种情况可能导致同一种动作。应当检查每种情况
中所规定的动作正确对应到了i.2 1节中的规则2,3,4,5。
.roid L.fe :: update ( )
I * Pre :   The I. i fe  ob7 ec t co-. . t._ z.s a  co n r i qu r a n on .
   Post : "-he T . fe obi ect con tains tne next q?nera t ioi of con_iqura t ion .  *l
int row. col;
int  new  grid I maxrow+2 ]  imaxcol+2 l ;
for ( row-l ;  row<-.max.ow;  row++ }
   for  ( col- l ;   col< -maxe ol;   c ol+ + I
         sNitch  l neighbor  count t row.  coll )   {
       casu 2.
         new qridtrowl Lcoll - cjridirowl icoll ; iT  Status stays the san.e
        break ;
      cale  3 :
         new  grid L rowl l coll --l ;    .V   Ce//  is now a. _ve .
        break ;
       difault :
         new  cirid E row l  l coll -.O ;     ii    C~//  is now deai.
for   l row- l _  row<-.maxrow .  row+ + )
    for  ( col- l ;  coU-max.ol ;  col++ )
      grid L row] l colI -new_grid E rowl l col
1.4.5榆入和输出
…审慎地输入和输出
  现在用函数user_says_yes()和instructi。ns()来作为程序的输入和输出后,仅剩下编写
类L1{。的.rut.al.。。()和pr.。t()方法。在设计供许多人使用的计算机程序中,执行输入和输
出的程序经常是最长的。程序中必须对输人进行充分检查,确信它是有效的和一致的,并且
必须处理输入中的错误以避免灾难性的失败或产生荒谬的结果。程序中也必须仔细地组织
输出和编排输出格式,充分考虑什么内容应该打印,什么内容不应该打印,要准备各种选择
以满足不同的1睛况。
r一————    程序设计技术规则夏;;三;;三_]
    将输入和缝兰堡为独立的函数,使得它们易于修改并能定制修改‘
    喊护黪
    勰1薹鼢
    撩
    流仅¨
    鼬罐付
    雠枢翔
    R t2 0
    ㈤雌曲
    娜一一一一
    ~篙一~
c++数据结构与程序设计
考c++的课本
 void instructions t )
 I * Pre :   rrone .
    POSt : ins t ruc ti ons for usi ncr  the  L_ f.  progran: ha \re been pn n ted.   */
cout《 -- WeICome to Conway ' s gzune o f Life . "《 endl;
cout《 " This game uses a qrid of size '
《 ma x r ow《 -- by " ~   maxco        " in  wbi c h        en dl ;
cout《 " each cell  can either be occupied by an organism or not . "《 endl
cout《 -- The occupied cells  change from generation to generation "
《endl;
coutc< " according to the numbe r of neighboring cells which are alive . "
《endi;
2 initialization
…输^方法
  Life的initialization()方法必须完成的任务是设置初始配置。为初始化Life对象,我们
可以分别考虑每个可能的坐标对并要求用户指出此单元是否被占据。过种方法要求用户输
入maxrow+ maxcol-20* 60-1200个记录项,这个数太大了。因此,我们仅输入对应于最
初被占据单元的那些坐标对。
void  L.fe :: initialize ( >
i . Pra :  uone .
   Post: The  Life  ob7 c.ct contai:.s a conf~ouration speci fied by the urer.  *!
 int .ow. col;
 for  l row- O ;  row<-.maxrow+ I ;  row++ )
   far  ( col--0 ;  coU-.maxcol+l _  col++ )
      grid irowl [colJ -O;
 cout《 .. List the coordinate_  for living .ells . " " endl
 cout《 -- Terminate the  list with the the  special pair- l
 cin》.ow》 col;
 ,thih irowl--l U col' --l) {
    if ( row>-.l & & row-.maxrow)
        if ( col>-.l && col<-.maxCol}
         gndirowl icoll-l;
           cout" " Column" " co      .. is out of range . " " endl .
        .out《 -- Row .-《 row《 -- is out o f range . "《endl ;
    cin》 row》 col;
㈣榆女
  对输出方法print(),我们采用简单的方法,输出每一代的整个矩形数组,其中被占据的
单元用*表示,空单元用空格表示。
void Life :: print
第1章程序设计原理
   Post : n.e c-..r. fi cru_a t i or    s  v H f te月  fo r -.he  us_r .   . .
int row. col;
CO lt《 " \ nThe  current  Li fe con tiguration is : " q endl
for  l row--l ;  row<-maxrow;  row++ ;   f
  for ( col-l;  cok-maxcol;  col十+ l
       if tgndErowncol] --l) Cout“ ' * ' ;
      else cout《 - ' ;
     .out《 endl;
…m P响应
  最后来看函数user_says_yes(),它判断用户是否要继续计算下一代。user_savs_yes()
的任务是请求用户响应“是”或者“否”。为使程序对输入具有更强的容错能力,我们将此请
求救在一个f自环巾,它一直重复直到用户的响应可“接受。在我们的函数中,我们使用标准
的输入函数g。t()来每次处理一个输八字符.在c_+中,函数get()实际上仅是类istream的
一个方法:在我们的应用程序中,应用的方法cin. get()属于isrream的对象cln。
 int c;
 bool  initial_response-true ;
                                                       Ii   Loop u~_ i :r; app_ joru r e _ npC L  _S recei;ed
 d0 {
    if ( iriitial response>
        cout《 -- ( y. n) ?"《flush;
    else
        cout《 -- Respond with eithe. y or n : "《 flush;
    垩此,我们已经有了供模拟Life用的所有函数,现在是暂停一下井榆查它是否能正常t
作的时候了。
  未m m
  ~一~一
  銎一
    列破是鼬
    §E吏出
    科瓤州辅
    良琶  个
    射射氯这
    霪
    一一一一
    酶∽椎√
    勰  触一一一一一
    驱  例槲断粕稍删
    ;  。£}自E*
    ¨  川8酣啡”稀
    L  *  的调在却
C++数据结构与程序设计
一vⅢ动《序
    涮试和测试单个函数的种方法是写个较短的辅助程序来为此两数提供必要的输
入、捌用此函数并评价函数执行的结果.这样的辅助函数称为函数的驱动器。通过使用驱动
器,可以将每个丽数分离出来逐个研究,因此经常能很快地发现错误。
  作为一个例子,我们为i.i矗项目的两数编写驱动器。首先考虑neighbor_count()方法。
在我们的程序中,使用了这个函数的输出,但并未将邑直接显不出来以供检查,因此我们对
这个甬数是否止确毫无信心。为J-测试neighbor count(),需要提供I.ife的对象
configuration,对configuration的每个单元调用ncighbor_count,然后输出结果。最后形成
的驱动器使用configuration. initialize( )Ai昔对象,而且与麒柬的主程序有几分相似。为
了从驱动器中调用neighbor—count(),需要在姿i.ifr中将函数的圳见性暂时改为pubiic。
int  main  ( )                          i i J ! : ..p :  r~'   -..e _o! . b  J  -. _ .iI . .  U
   Post :作!』『_u—baf    qh… -.   CCT r: { ) L c r _._ nr.  :h._ :-. r_ -. c _  vai uc._
Life -.onfiguration;
.onfiqu ration . _nitiali ze l i;
for  ( row- l ;  row< -n_axrow ;  row+ + )  {
    for  I col -. l :  col< -rnaxrow ;  co l+ + )
       cout《 -.oi r iqurat i on . ne _ ghbor coun t l ro i .  co i )         "
    .out《 encii;
  有时候两个函数可以用于互相检盎。例如,检沓LLk的方法inituializc()和print()的最
简单的方法足用个驱动器,这个驱动器的动作部分是:
    conf.qurat】onlnlt.al】ze();
    …fjquration p…t(),
通过运行这个驱动器J}确信打Em来的配置与作为输入的配置是一样的,可以同时测试这
两个方法。
1.4.7程序跟踪
…十组讨论
  在将函数组装成完整的程序后,就口r以检查整个程序了。发现潜在缺陷最有效的方法
之一称为结构化预排(。t.。州t.rcd walkthr。Wh)。在预排时,程序设计员向其他程序设计员
或一个f序设j十员小组展示所完成的程序i准确地bu以解释:首先解释主程序,接下来逐个
介绍各个函数。有3个原因袁明结构化预排足有用的:首先,对实际代码并不熟悉的程序设
i+员经常能发现原程序设计员Ffr;aa略的bug和概念上的错误。其次,别人所问的问题有助
于澄清思想并发现自己的错误。最后,结构化预排经常会提议‘些测试思想,这些测试在以
唇的软件产品阶段往往证明是有用的。
…调试用打印语句
  大型程序首次作为整体执行就能正确运行的情况是比较罕见的,如果它未正确运行_则
第1章程序设计原理
要准确判断错误所在并不容易。许多系统上都有复杂的跟踪工具可以只j束跟踪鬲数调用和
变量修改等。然而,一种简单而有效的调试r具是通过在丰程序的关键位置插入打印语句
而得到程序执行的快照(snapshot),这种策略也经常作为可用调试器的一个选项。lir以在
函数每次被调用时都打印一条信息,也可以在函数捌用之前或调异j之后打印出重要变量的
值。这种快照能帮助程序设计者快速聚焦予错误发生的特定位置。
…临时支架
  支架( scaffolding)是另一个术语,经常用来描述为辅助调试而捕入到一个程序中的代
码。在书写程序时不要犹豫向程序中驶入支架,当不再需要时删除它们也很简单,rTIJ咀在调
试过程它可能会使我们避免失败。
    当程序巾出现了某个完全无法定位的神秘的错误时,向主程序巾做人支架打印出重
要变量的值是非常有用的方法。应该将支架放在主程序中--或两个主要划分点的位置
上,(如果体写了一个很长的程序而没有将它的工作再分为几个主要部分,则你已经存程
序设计结构中犯下了严重的错误,因而必须加以改正。)利用主要划分点的扣印输出,就能
够判断程序的哪一部分行为异常。然后我们就可以将精力放存那个部分,在它的于刘分
中再引入支架。
…防御式程序设计
    另一种重要的检错方法是实施防御式程序设汁。在函数开始处放上-f语句检查是否确
实满足前置条件,如果不满足,则打印一条错误信息。这样,当想像中的不会发生的情况确
实出现时,就可以尽早得到警告。而如果不出现这种情况,则i此处的错误检查对用户来说是
完全不可见的。当然,当函数的输入足来自用rl .文件或者程序外部wJ其他来源u,特别重
要的是要检查前置条件能否满足。然而令人惊奇的是,无论多么经常地检查酊置条件,也往
往会发现有错,甚至是在那些已确信-切正常的地方发现错误。
…静态分析器
    对大型程序有时还会用到另一个工具,它就是静态分析器。静态分析器对源程序(例如
用c++编写)进行检查以发现其中未曾初始化或未被使用过的变量、决不可能到达的代码段
或者其他的可能错误的事故。
1.4.8程序测试原理
…选择测试数据
  迄今为止,我们尚未时论如何选择用T测试程序和函数的数据n当然,这种选择密切依
赖于正在开技的项目,因此我们只能给出一些一般说明。首先注意以下技术规则:
    程序设计技术规则
    测试数据的质量比数量更
重要。
    .________________————J
  多次用同样的案例做同样的计算而进行的抽样逗行
有效。
其测试结果并不比一次运行更bⅡ
c++数据结构与程序设计
    程序设计技术规则
程序测试可用于说明bLw的存在,而不能说明其不存在
有可能在多次抽样运行后仍然还有从未被测试到的其他案例。对任何真正复杂的程序,
做穷尽测试是不H『能的,而测试数据的精心选择则可以提供对程序的实质信心。例如,每
个人对典型的计算机能够正确地做两个浮点数的加法抱有极大的信心,但这种信心当然
并非基于用所有可能的浮点数做加法来测试计算机。如果一个双精度浮苣数占64位,则
共有0-zs个不同的数对要相加。这是一个天文数字:迄今为止制造的所有计算机一同工
作,也只能执行其中很小的一部分加法。我们关于计算机iF确做加法的信心是基于分别
测试每种成分,即,通过检查每个64位数对能被正确地相加,并H从一处搬到) -处时也
能被正确地相加。
…测试方法
  至少有3种通用的思想可以用于选择测试数据:
1黑盒方法
  大型程序的多数用户对程序的功能细节并不感兴趣·他f仪希望得到答案,也就是说,
他们希望将此程序作为一}-黑盒(Black-box)对待。类似地,测试数据是用来检查程序能甭
正确运行,因此在选择测试数据时,应该按照问题的说明,rfu不用关心程序的内部细节。测
试数据至少要用下列方式选择:
…敬据选择
  (l)简易值。应当用易于检查的数据来调试程序。许多学生只用复杂的数据测试程序,
    而当教师尝试简单的样倒时,虽然程序能完善地运行,但仍然会使这些学生感到局
    促不安。
  (2)典型的逼真的值。总是选择那些能展示如何使用此l)~的数据来测试程序。ijrLt
    数据应该足够简单,因而能够对结果进行手工检查。
  (3)极值。许多程序在其应用范围的界限处出错。计数器或数组边界就很容易会被
    突破。
    (4)非法值。“废料A,废料出”是计算机行业巾一句不应被注意的古老谚语。当个良
    好的程序用废料输入时,其输出至少应当是·甸敏感的错误信息。实际上,程序应
    陵对输^中可能的错误提供一些指示,并且在忽略错误的输入后仍可能执行一些
    计算。
2玻璃盒方法
…路径测试
  选择测试数据的第二种方法是以下面的观察为出发点,即如果一个程序实际上有某些
部分的代码未被执行到,则不能认为此程序经过了全面的测试c在玻璃盒测试方法(lJ18
t;lass-Box Method)中,要检查程序的逻辑结构,对其中出现的每个分支,都要设计测试数据
通往那个分支。因此要仔细选择数据来检查每个switch语句中的每种可能、每个if语刊中
的每个分句以及每个循环的终止条件。如杲程序中有数个选择或迭代语句,则要求使用不
第1章程序设计原理
同组合的测试数据来检查所有可能的路径。图1 3说明了一个短程序段的可能的执行
路径。
switch a {
  case  i: x - 3.
          break;
  case  2: if (b - O)
             x-2;
          ejse
                        x -. 4;
          break;
  case 3: wMe (e >O)
             process (O;
          break;

路径1    路径2    路径3    路径4
>0、
(c)k;
    囝1.3贯穿一个程序段的执行路径
  对大型程序,玻璃盒方法显然是不可行的,但对单个的小模块,它是一种极好的调试和
测试手段。在一个设计良好的程序中,每个模块都包含少数循环和分支,因此仅用一些精。b
选择的测试案例就足以独立地测试每个模块。
…模块测试
  在玻璃盒测试中,模块化程序设计的好处就表现得很明显。我们考虑一个包含50个函
数的项目的典型例子,每个函数包含5个不唰的情况或分支。如果我们打算将整个程序合
一测试,则需要5。。个测试案例才能确认每个分支都被测试到。而每个模块分别只需要5个
(更简单的)测试案例,总共是5×50-250个。因此对大型程序,玻璃盒测试就将规模从难
以承受缩减成相当遣度。
…}B较
x *接口错误
    在你得出玻璃盒测试总是更可取的方法这一结论之前,我们将说明,实践中'黑盒测试
通常在友现错误方面更加有效。或许一个原闻就是大多数微妙的程序设计错误经常不是发
生在一个函数内部,而是发生在函数间的接口中,误解了函数间信息交换的准确条件和标
准。因此对大型项目来说,条合理的测试思想就是在编写每个小模块时就对它应用玻璃
盒方法进行测试,而当程序完成时就使用黑盒测试数据来测试较大盼部分口
j■一■
    。    晕
|k'F
叫R,≈j|
—k j
c++数据结构与程序设计
3 Ticking-Box方法
  为r总结1书节,让我们再说明一条程序测试思想。遗憾的是,这思想的使用相当广
泛.它或许能够被称为t.cking-box方法。此方法在项目完成了相当完善的调试后完全水再
做任何测试,取fⅢ代之的是将程序移交给客户来试用和认可。当然,其结果是一颗定时
炸弹。
练习1.4
 E1如果你怀疑Life程序中包含错误,主程序巾的何处是插入支架的较好位置t应该打印
    出什么信息,
E2提出对1 3节中练习E9(设|f程序来标绘个点集)的解决方案,并指出在需要的情况
  下插入支架的较好位置。
E3为n自海个丽数找出合适的黑盒测试数据:
    (1)此函数返回3个参数中的最大者,其参数为浮点数。
    (2)此函数返回一个浮点数的平方根。
    (3)此函数返回其两个参数的最小公倍数,参数必须是正整数。(最小公倍数是两个参
    数最小的公共整数倍数,例如:4和6的最小公倍数是12,3和9的最小公倍数是9,
    5和7的最小公倍数是35。)
  ( /l)此函数将3个作为参数给出的整数按升序排序。
  (5)此雨数将F标从O到变量n的整数数组a按升序排序,这里a和n均为参数。
E4为下面每小题找出合适的玻璃盒测试数据:
  (l)语句
if <atb)  if ( c<d) x-l;  alse if ( c--d) X-2 ;
alse X-. 3 ;  else if ( a--b)  X-4 ;  alse if r…d)  x-5
else x- 6 ;
(2) Life VJ方法 neighbor_count(ro,v .  col) .
程序设计项目1.4
Pl在计算机上输A本章的J_ife程序并确信7占能正确运行。
P2用图1 1中所示的例子测试Life程序。
P3用同l_4中所不的J始配置运行Life程序,它们巾有几个需要经历许多次变化,才能达
  到保持同样配置或者具有可预测(FJiJ为。
第1章程序设计原理
螺旋条状纹
病毒
收割桃
┏━━━━━━━━━┳━━━━┳━━━━┳━━━━━┳━━┳━━━━┳━━┳━━━┳━┓
┃+} - - - - - -    ┃—L -   ┃i|i     ┃—r_ - -  ┃廿  ┃        ┃    ┃      ┃  ┃
┣━━━━━━━━━╋━━━━╋━━━━╋━━━━━╋━━╋━━━━╋━━╋━━━╋━┫
┃                  ┃  —L 1 ┃        ┃          ┃    ┃        ┃    ┃      ┃  ┃
┃  l l             ┃1.     ┃        ┃          ┃    ┃.1     ┃    ┃      ┃  ┃
┃  l刊             ┃        ┃        ┃          ┃    ┃        ┃    ┃      ┃  ┃
┃                  ┃        ┃        ┃          ┃    ┃…      ┃    ┃      ┃  ┃
┃  登:一:::二  ┃半i     ┃嘉::  ┃+卜 - -   ┃    ┃…      ┃    ┃      ┃  ┃
┃                  ┃        ┃        ┃          ┃    ┃…      ┃    ┃      ┃  ┃
┣━━━━━━━━━╋━━━━╋━━━━╋━━━━━╋━━╋━━━━╋━━╋━━━╋━┫
┃#i兰              ┃丰:    ┃丰二:  ┃#:二     ┃#   ┃牛二二  ┃牛  ┃丰干  ┃  ┃
┗━━━━━━━━━┻━━━━┻━━━━┻━━━━━┻━━┻━━━━┻━━┻━━━┻━┛
    滑膛枪
    图1.4 J,ife配置
1 5程序维护
  作为练习或示范而编写的小程序往往在运行几次后就被丢弃r,但大型程序的保存就
  鞭群一群
c++数据结构与程序设计
完全不同丁。有实际价值的程序通常会由许多不同的人运行许多次,而程序的编写和调试
仅仅标志着使用的开始,同时也仅仅标志着使程序有用并保持’e-直有用的工作的开始。
因此有必要对程序进行复查和分析,以确保它满足指定的要求,使它能够适应不断变化的环
境,并对它进行修改以使它更好地满足用户的需要。
  计算机程序的维护( malntenance)包含在程宁完全稠试、测试并投人使用后对程序所做
的所有J作。随着时间和经验的积累,人们对这个计算机程序的期望通常会发生变化。运
行和硬件环境会发生变化,用户的需求和期望会发生变化,与软件系统其他部分的接U也会
发生变化。因此,如果要使程序继续有用,那么必须对它保持注意力,使它保持最新。实际
上,有调查表明:
    程序设计技术规则    一————]
对一个大型且重要的程序,超过·半的工作是在
它巳被完全调试、测试并投人使用后,来白于维护阶段
1.5.1程序评价
  程序维护的第一步是开始复查、分析和评价这一莲续的过群。我们可以对任何程序提
出几个有用的问题,其中第一组问题涉及程序的使用和输出(因此是黑盒测试工作的继续)。
i.程序是否解决丁所要求的问题?是否准确地遵循了问题说叫?
2程序在所有条件下都能正确工作吗’
3程序是否有良好的用户接口。是否能让用户方便简单地进行输入々程序的输L+是否清
  晰、有用且界面漂亮?是否提供替代选择或町选特性以方便使用7是否为用户包含r明
  确且足够的指令和其他信息?
  其余的问题涉及程序的结构(是玻璃盒测试过程的继续)。
4程序的编写是否清晰且符合逻辑7是否有适于完成逻辑任务的方便的类和短函数?类
  中的数据结构是否正确地反映丁程序的需要7
5程序是否有良好的文档?变量、函数、类型和方法的名称是否正确地反映了它们的意义?
  是胥合适地给出了精确的前置条件和后置条件,是r对代码中的主要部分我任何特殊
  或困难的代码给出了解释?
6程序是否有效地利用了时间和空间?通过修改潜在的算法,程序的性能能否得到改进’
  我们将对所编写的程序详细地研究其中的一些标准,对其他的准则不于明确地说明,但
这并不表明它们不重要。而如果在程序设计的每一阶段都投人足够的思考和努力的话,这
些标准能够自动被满足。我们希望所研究的例子能揭示这个道理。
1.5.2复查Life程序
  让我们通过重新考虑L。.f。游戏的程序来详细说明这些程序评价标准a这样做在一定程
度上实在是矫枉过正,因为像LLfe游戏这样酌一个玩具项目本身并不值得这种努力。然而,
第1章程序设计原理
在此过程中,我们应该考虑对许多其他应用来说都十分重要的程序设计方法。我们将依次
考虑前述的每个问题。
问题说明
…月题:边界
  如果我们返回去复查1.2 1节中Life游戏的规则,就会发现实际上我们并未像最初所
描述的那样解决Life游戏。规则中并未提到包含单元的网榷的边界,而在我们的程序中,当
一个移动的群体充分靠近边界时,则邻居的空间就消失了,由于边界的出现而使得此群体产
生失真。这一点并不是规则所假定的,因此我们的程序违反了规则。
  在任何计算机模拟中可能出现的值都有绝对的边界,这一点当然正确,但在我们的程序
中使用一个20乘60的网格的确太受限、太武断丁。我们有可能编写一个不限制网榷大小
的L.fe程序旭在这样做之前,我们必须设计几个复杂的数据结构。只有当我们实现这些数
据结构以后,我们才能在9 9节编写一个通用的不带网格大小限制的Life程序。
  然而,我们在首次尝试中,对要解决的问题加以限制是完全有道理的,因此,暂时让我们
继续研究受限大小的网格上的L.fc。尽管如此,准确地说出我们要做的事情仍然非常重要:
2程序正确性
    由于程序测试只能说明错误的存在,而不能说明没有错误,因此我们需要其他的方洼来
不容置疑地证明程序的正确性。构造正规有效的证据来证明程序的正确l经常存在困难,
但有时候还是能够做到的,就像我们要在后继章节中对所设计的一些复杂算法所做的证明。
对Lif。游戏,我们暂且还是满足于用更加非正规的理由来证明程序为何是正确的。
    首先,考虑程序的哪些部分需要验证。I』fe配置仅由update方法修改,并且只有
updaie和neighbor_count涉及可能出错的任何计算。因此应当集中考虑这两个方法的正
确性。
…neighbor_count的正确性
    neighbor_count方法只查看作为参数给出的单元及其邻居。此单元及其邻居的状惫只
有有限的几种可能性,因此通过为neighbor_count构造一个驱动器,对这些可能性进行玻璃
盒测试是可行的。这种测试会很快使我们确信n日ghb。re。un‘的正确性。
*** update的正确性
    对update方法,首先我们应当检查switch语句中的情况u确信萁动作正确地对应
1 2 1节中的规则。接下来,我们能够注意到每个单元的动作仅仅依赖于这个单元的状态
及其邻居计数。因此,就像对neighbor_count那样,我们能够构造有限集合的玻璃盒测试数
据,验征。pd。t。在每种可能的情况下执行了正确的动作。
c++数据结构与程序设计
3用户接口
…月题:输^
    在运行I。if程序时,可能会发现用于输入初始配置的低劣方法足主要的不便之处。让
人来计算并键人每个活的单元的数字坐标的做法显得很不自然。输人格式应该反映出与打
印的配置同样的视觉效果。至少,程序应该允许用户像输入由空白(用于死的单元)和非空
白字符(活的单元)组成的行那样输入配置的每一行。
…文件输入和输出
    Life配置可以相当复杂,为了更简单地输人,程序应该能从一个文件中读人初始配置。
为允许中断程序并在以后再继续运行,程序也应该能够将最后的配置存人文件,以后可以从
这个文件中再次读取配置。
…编辑
    另一项选择是允许用户编辑任意一代的配置。
…输女改进
  程序的输出也能够得到改进。应该使用直接的光标定位米仅仅修改那些已经改变状态
的单元,而不是重新输出每一代的整个配置。可以使用颜色或其他特性来使输出更漂亮更
有用。例如,最近变活的单元可以用一种颜色,而那些继续活着的单元可以按其存活的时间
长度赋予其他颜色。
…帮助窗口
  为了使程序更加完备,选择性地显示I.ife游戏的简要描述及其规则也是有用的,或许可
以采用一个弹出窗口来显示这些内容。
  总的来说,设计的程序要有吸引用户的外舰,这一电非常重要。在大型程序设计中,将
用户接口看得非常重要,甚至超过程序中所有其他部分的组合。
    程序设计技术规则
    殳计用户接口,程序的成功很大程度上是靠它的吸引塑!兰里兰二    一J
4模块化和结构
  我们已经在原始设计中谈论过这些问题,已做的结论仍然适用
5文档
  同样,我们在前面也谈论过文档的问题,不必再在此处重复。
6效率
  Lif。程序的哪一部分花费的运行时间最多。当然不会是在输人阶段,因为输入只做一
次。输出通常也相当有效率。大部分计算发生在update方法及其调用的neighbor_。oun‘
方法中。
  在每一代,。pd。t。重新计算每个可能单元的邻居计数。在典型的配置中,可能只有5个
第1章程序设计原理
单元是恬的,并H它们经常位于网格的某个区域中。因此update将大量时问花费在艰*地
证实许多死的且没有活的邻居的单元,这些单元实际上凼为其邻居单元¨数为O而在下一
代仍是死的。如果有95蹦的单元是死的,那么在计算机时间的使用方面效率就相当低下。
…低效率
  但这种效率低下的影响重要吗々通常情况下并不重要,因为对用户来说计算是这样‘陕t
每·代好像瞬间就出现了。另一方面,如果在一台非常慢的计算机上或者在繁. rl-的分时系
统上运行这个Life程序,就可能会发现程序的速度有点令人失望,在打EIJ一代和开始打印下
一代之间有明显的停顿。既然这样,就值得试着节省计算机时间。但一般来说,像Life这样
的程序,即使效率很低,也不必对其进行优化。
    程序设计技术规则
    除非必要,不要优化代码。
    在代码完善和正确之前,不要开始优化代码。
大多数程序将90%的时间花在10%的指令上,找出这1。%,集中精力提高它的效率
  在程序开始优化前进行仔细思考的另一个原因是优化经常生成更复杂的代码,这种代
码会更难于进行调试和必要的修改。
    程序设计技术规则EYJj;;二]
    尽你所能保持算法简单。当犹豫不决时,选择简单的,
L 5.3程戽修正和重新开发
  当我们继续评价程序,斟酌它是否满足目标和用户需求时,可能会接连发现它在当前设
计中的不足和能够使它变得更为有用的新特性,因此程序复查自然导致程序修正和重新
(y+数据结构与程序设计
练习L 5
El有时用户可能希望在小丁20×60的网格上运行Life程序。判断怎样可能将m…ow
  和maxcol作为变量,使用户在程序运行时能设置它们。试着尽可能在程序中少做
  修改。
EZ.对函数Life::neighbor_count(row,  col)进行改进的一种构想是从数组grid和new—
  grid中删去栅栏(始终是死单元的额外的行和列)。然后,当单元在边界卜时,neighbor
  count检查的单元数会少于8个,因为其中有一此已在网格的边界之外。为了这样做,
  函数需要判断单元(row,col)是否在边界上,但可以将它放存嵌套循环之外来完成,即
  在循环开始前判断此循环的上下边界。例如,如果row与所允许的值同样小,则行循环
  的下边界为row;否则,它就是row-l。按照网格的大小判断,neighbor_count的原版本
  和新版本各约执行多少语句。值得做出本练习所提议的修改吗?
程序设计项目1.5
Pl.修改Life的initialize函数,使它将空白和x构成的个序列作为适当行中的被占据位
  嚣输入,从而设置初始的Life::glrd配置,衙不是要求以数字坐标对来输入被占据位置。
P2+向函数initialize中加入功能部件,使它能够根据用户的选择,从键盘或者从文件读人初
  始配置。其中文件的第一行是注释,给出九比配置的名称,文件中剩余的每一行对应配
  置中的一行。每一行中活的位置包含一个“∥,而每个jEb/Ji置是。个空白。
P3向L.fe程序中加入功能部件,使得它在中断时能将最后配置写入一个文件,文件格式能
  够被用户编辑,能被读人以重新启动此程序(使用练习EZ的功能部件)。
P4向Lif。程序中加入功能部件,使得在任意一代,用户能够通过插入新的活的单元或删除
  活的单元来编辑当前配置。
P5向I.ife程序中加入功能部件,使得在任意一代,如果用户希望的话+它日B显示_AI帮助
  窗口,给出Life游戏的规则并解释如何使用此程序。
P6向L.f。程序中加入单步模式,使得它能解释从-t到下一代所做的每次变化。
P7用直接光标定位(一种依赖于系统的特性)让I.ife的print方法更新配置,而不是在每 -
  代完全重写它。
P8在Life的输出中使用不同的颜色来显示哪些单元在当代发生了变化,哪一些没有变化。
1 6结论和复习
  本章对大量话题做了综述,但主要是概况。其中的一些主题将在后继章节做更深的论
述,~些将放到更高级的课程中学习,j有一些通过实践学习则效果最好。
  本节扼要重述并展开一些已经学过的原理c
第1章程序设计原理
1.6.1软件工程
  软件工程( sc)fiware engineering)是对右助于大型软件系统的建设和维护的方法的研究
和实践。虽然本章所研究的例子按现实标准来看显得太小,但它阐述了软件工程的许多方
面的内容。
  软件工程始于人们意识到需要很长的过程来获得好软件。它在程序编码之前开始,并
持续到程序投入使用后多年的维护。这一持续的过程称为软件的生命期,它能分成如下几
个阶段:
…生命期阶段
1准确全面地分析问题,确信仔细地规定了所有必要的用户接口。
2建立原型并用其试验,直到最后定下所有的说明。
3使用数据结构和其他已知功能的算法工具来设计算法。
4验证算法是正确的,或者使得它尽量简单而无需证明其止确性。
j分析算法以判断它的要求并确信它满足说明。
6用合适的程序设计语言对算法进行编码。
7.用精心选择的数据来测试和评价程序。
8必要时细化附加的类和函数并重复前进步骤,直到软件完成并具有全音B功能。
9.优化代码以改进性能,但仅在必要时进行。
iO.维护程序,使匕能够满足用户不断变化的需要。
    我们在本章和前章的许多篇幅中讨论过其中的大多数主题并加以举例说明,但再次对
第一阶段的问题分析和说明给出一些更多的对论还是适宜的。
  件系统有哪些’
8预期的扩展或其他维护是什么7以前对软硬件修改的历史是什么?
    算用    幔
    计使    啸
    比于    衅
    上随    雌
    念习    刚
    概员    疗
    在计
    题设    萄
    问序    ,兼
    际程    船懒
    实和
    为户    接稚
    因用
良  *  学同
L    *    科不L置薯“置豇一
(、++数据结构与程序设计
1.6.3需求说明
    对大型项目来说,问题分析和试验阶段将最终产生项目需求的正式说明。这个说明成
为用户和软件工程师尝试互相理解的主要方式,并且建立r评判最终项目的标准。此说明
会包含以下内容:
l系统的功能需求:它将要做什么以及用户可用的命令是什么’
2系统的假定和限制:系统将使用什么硬件,必须采取什么样的输人格式,输入的最大长度
  是多少,最大用户数是多少,等等。
3维护需求;系统预期的扩展,硬件的变化,用户接fi的变化。
4文档需求:对哪类用户需要什么种娄的解释材料。
  需求说明规定软件要做什么,而不是如何做。这些说明对用户和程序设计员都应该是
能够理解的。如果精心准备的话,它们能够成为后继设计、编码、测试和维护阶段的基础。
1.6.4编码
…说明完成
  在大型软件项目中,有必要在合适的时间开始编码]‘作,不官太早也不宜太迸。多数程
序设计员易犯的错误是过早地开始编码。如果在需求说明弄准确之前就进行编码,则在编
码时不町避免地会对说明做出尤根据的假设,这些假设会使不同的类和丽数互不兼容,或者
使程序设汁任务变得更为困难。
    程序设计技术规则
在需求说叫准确和完善前不要进行编码
    程序设计技术规则
匆匆行劫,慢慢后悔。匆匆编程,常常调试
…自顶向下的编码
  另一方面,可能会将编码延期太长,f但实际上这种情况未必会发生。就像我们进行白顶
向下的设计,我们应该进行自顶向下的编码。一日顶层的说明完成且准确t则应该对这些层
巾的类和函数进行编码并通过加入合适的占位函数来测试它们。如果我们随后发现它的世
计中带有缺陷,则可以直接修改它,而不必在术用的低层甬数卜付出过高的代价。
    同样的想法可以稍微表达得更确定些:
程序设计技术规则
  重新开始经常比给一个旧程序打补丁更简单。
  -条好的经验法则是,如果程序有超过10必的部分必须修改,耶么就到J完全重写稃序
的时候了。对一个大型程序重复打补丁,会使bug的数量趋于一个常数。因为补丁根复杂,
第1章程序设计原理
每个补丁可能引入的新错误与它所改正的错误同样多。
  一个极好的避免从头开始重写大型程序的办法是在开始时就计划书写两个版本。在程
序正在运行时,有可能知道设计的哪些部分会引起困难或需要修改哪些功能部件以满足用
户的要求。r程师们多年以来一直知道不可能直接从制图板上建设大型项目。对大型项
目,工程师们总是建立原型:即能被研究、测试甚至在某些时候可必用于受限目的的接比例
缩小的模型。例如人们在风洞中研究和测试桥梁模型,在装配线上尝试使用新技术之前也
会构造试验工场。
…软件原型
    原型法对计算机软件特别有用,因为它减轻了用户和设计者在项目早期的通信,因此减
少了误解并有助于使每个人对设计满意。在建立软件原型时,设计者能利用已经写就的输
入输出、排序或其他通用需求的程序,但在用构造块装配能完成预期任务的工作模型时要尽
可能地少用新的程序设计。即使原型可能不能有效地运转或完成最终系统的每项功能,但
是它却为用户和设计者提供了一个极好的实验室来试验可供选择的最终设计思想。
程序设计项目1.6
P1幻方是一个方形整数数组,它每行的和、每列的和及两条对角线上的和全部相等。罔
  1 6给出了两个幻方。①
和- 34

    和=65
    圉1.5两个幻方
(1)编写程序读A-个正方形整数数组,并判断它是否是一个幻方a
(2)编写程序用下面的方法生成一个幻方,此方法仅当正方形大小是奇数时可用。开始
  时将1放在顶行的中间位置,将后续整数2.3,…分别写入前个数字所在位置的上
  行偏右一格。当到达顶行时(比如1就放在顶行),返回到底行继续进行,就好像底
  行是直接在顶行上面一样。当到达最右列时,继续到最左列,就好像最左列直接在
  ①这里所示的左边E的幻方
1514年。
出现在Alh,。。ht Du阳·的铜版画Melancolia中,注意包含此铜版画的日期
38
C++数据结构与程序设计
    最右列的右边。当到达的位置已被占据时,就直接在前一个数的下面位置处插入新
    数。按这种方法构造的5X 5幻方如图i.5巾所示。
P2. -维Life发生在一条直线上而非矩形网格内,它的每个单元葙4个邻居位置:它每侧距
  离为1或2的单元。一维Life的规则类似丁一维Iife,除了规则(1)死的单元如果有2
  或3个活的邻居,则它将在下一代变成活的和(2)活的单元如果有O、1或3个活的邻居
  则将死去。(因此有O、l或4个活的邻居的死的单元仍然是此的;有2或4个活的邻居
  的活的单元仍然是活的。)在图i.6中显不了样本群落的发展。为一维Life设计、编写
  并测试程序。
II■IIIII_rTTI
    {
lIII可ET-LIIII
IIIiII币II工工II
lrrr]可1币■rrri
Tr几I可F阿III
    {
工工I_r下T_研IIII
工IIII工日]王Ⅲ
滑向右侧
工工工正巨回卫丑]工工
    {
工工正田]Ⅱ工正I
    }
T下T_F冈可丌丁下rr
T丁IIIIIIII
IT_几可丌IF几_『T
T_厂同1丌IIIII[
    {
Tr_几可可下F几_rT
固1.6一维Life配置
每6代重复
编写一个程序打印出当年的日历。
修改此程序,使得它佳读人一个年号并打印出那一年的年历。女I果此年号是4的倍
数并且不是世纪年(是100的倍数),除非年号能够被400整除,则此年是闰年(二月
有29天,而不是28天)。因此1900年不是闰年,而2000年是闰年。
修改此程序,使得它能接受任何日期(日、月、年)并fTEP出此日期是星期儿。
修改此程序,使得它能馥八两个日期并打印出这两个日期之间的天数。
一+一十一
一十一十一
第1章程序设计原理
(5)使用闰年规则,证明日历序列确实每400年重复一次。
(6) -个月的13日是星期五的概率是多步(按400年的周期)々为什么一个月的13日
  更可能是星期五,而不是一周中的任何其他一天?编写程序计算本世纪有多少个星
  期五且为13日。
启示和易犯的错误
1.为改进程穿,复查它的逻辑。不要基于低劣的算珐优化代码。
2在程序正确且能够运行之前决不要优化它。
3.除非绝对必要,不要优化程序。
4.保持函数简短,任何函数很少超过一页长。
5.开始编码前确信算法是正确的。
6.验证算法中复杂的部分。
7.保持简单逻辑。
8.确信在决定如何求解问题之前理解7毛。
9.确信在开始程序设计前理解算珐的方法。
iO如果困难,则将问题划分成多块并分别考虑每一部分。
¨在描述问题时产生的名词为问题的解答提议了有用的类,动词则提议了有用的函数。
12.在编写每个函数时包含细致的文档(如出现在i. 3 2节中的)。
13.确信为每个函数写下准确的前置条件和后置条件。
14在函数开始时包含错误检查以证明前置条件确实成立。
10.在每次使用函数前,自问为何知道其前置条件会被满足7
16.使用占位程序和驱动器、黑盒和玻璃盒测试来简化调试。
17便用大量的支架来帮助定位错误。
18.在带数组的程序设计中,谨慎对待比1小的下标值,总是使用极值测试来检查使用数组
  的程序。
19.在编写程序时保持怠好的格式一它会使凋试更加简单。
20保持文档与代码一致,而且在阅读程序时确信是在调试代码而不仅仅是注释。
21向其他人解释程序,这样散会有助于自己更好地理解程序a
复习题
  本书大多数章节在结束时都设计了帮助复习本章主要思想的问题集。对这些同题'可
“由书中的讨论直接回答;如果对答案不确定的话,请参考合适的章节。
L 3
 L何时适合使用单字母的变量名?
2.指出4类应包含在程序文档中的信息。
3外部和内部文档的区别是什么’
.4什么是前置条件和后置条件?
c++数据结构与程序设计
5指出三类参数.c++中如何处理它们?
6为什么应避免函数的副作用?
1.4
 7什么是程序占位?
8占位程序和驱动器之间的区别是什么,分别在何时使用它们7
 9什么是结构化预排7
10什么是程序中的支架9何时使用支架,
ll指出一种实施防御式程序设计的方法。
12.给出两种测试程序的方法,并讨论应在何时使用哪一种?
13如果不能立即描绘出解决问题所需要的所有细节,那么应该如何处理此问题’
14子程序的前置条件和后置条件是什么,
15.何时应该在函数间分配任务’
1.6
16编码应该延缓多长时间?
17什么是程序维护?
18什么是原型?
19指出软件生命期至少6个阶段并说明每个阶段是什么状态。
20定义软件工程。
21.什么是程序的需求说明’
进阶参考书目
  程序设计语言C++由Bjarne Stroustrup发明,他最初在1984年发表了c++的描述。标
准参考手册是H Stroustrup,The C++  Programminl Lang uage,third edition. Addison
Weslcy. Reading. Mass., 1997
  有许多好的课本对C++提供了更通俗的描述,这类书太多,再此就不一一列出了,这堂
课本也提供了许多例子和应用。
  对已经知道这门语言的程序设计者,推荐一本关于如何有效地使用c++的有趣的书:
Scott Meyers,Effective C++, second edition. Addison-WesleV. Rcading. Mass.,  1997
程序设计原理
  有两本书包含关于程序设计风格和正确性方面许多有用的提示,也包含正确的和不良
的实践的例子,分别是:Brian Kernighan andP J Plauger, The Elements of Programming
S“J如,second edition McGraw-Hill, New York, 1978, 168 pages. Dennie Van Tassel,
Progrom Style,Design, E'fficiency,Debugging,and Testing,second edition.  Prentice
Hall, Englcwood Cliffs.N.J.,1978, 323 pages.
第1章程序设计原理
    Edsger W.T)ijkstra是钋构化程序设计的先驱,他强调在程序i#ll和程序编写时采用精
心组织的白顶向下的方法。在1968年3月,他斟为在Commurtzcatz07uor the ACM(vol.
il,第147-148贞)上发表题为“(.o To Staternent Considercd Harmful”(Go To语句是有害
的)的信件而引起一些震惊。自那呲后,Dijkstra已发表了数篇论文和书籍,它们是程序设计
方法中最有启发性的。特别重要的一本书是:Edsger W Dijkstra,A Discipline of
Progranuning, Prentuice Hall. Englewood Cliffs.N J,1976. 217 pages.
    全面介绍面向对象程序设计的一本书是:Gradv Booch,Object-Oriented Analysis and
DeAign unith Applications,Benjamin/Cummings, Redwood Citv. Calif.,  1994
Life游戏
  杰出的英国数学家J H. Conway在有限单群、逻辑和组合数学理沧的多1-学科做出了
许多独创的贡猷。他从先前的关于元胞自动机( cellular automata)的技术研究开始设计
L.f。游戏,然后设计繁殖规则使得配置难以无边界地生长,并使得许多配置能经历有趣的演
进。然而,Conwav自己并没有发表他的观察结果,而足将它告知了Martin Cardner。当这
个游戏在下面的文章中被讨论后,它快速流行起来。
Martn Gardner. "Mathematical Games’’( reguLar column), Scientni fic Arnerican 223.No.
4 (October 1970), 120 -123; 224, no.2 (February 1971), 112 -117
    1.2节和1 4节结尾处的例子来自这些专栏,这些专栏和更多的相关的成果被重新再版
于:Martin Gardner,WheeLs,Li fe arui Other MathenuzticaZ Am“』ements,W.H.Freema“,
New York and San Francisco.  1983. pp. 214 - 257
    这本书也包含一篇关于L.fe的文章的参考书日。甚至当时有几年还出了份名为
L。fel,ne的新闻季刊,它跟踪实际爱好者在Life和相关话题有面的最新的研究进展。
软件工程
  下而这本书对结构化程序设计的许多方面进行了全面的讨论:Edward Yourd。“,
Technzques。/Program Struaure arul De』79”,Prentice_Hall, Engle-wood Cliffs.N J'
197"..:364 pagcs
  下面这本书提供了关于大型软件系统构造中产生的许多问题的细致讨论(这本书也是
一本有趣的读物)Frederce P  Brooks, JR.,The Wbzhical Man-Month  Pisays
So tiware Engineering, Addison-Wesley. Reading, Ma.s., 1975. 195 pages
    关于软件工程的一本好课本是:lan SommerviIle. Softuxzre E'ngineering,Addison。
Wesley. Wokingharn, England, 1985. 334 pages
…茸法验*
    两本关于证明程序并使用断言和不变式来设计算法的忙是:David Gries. The Science
., Programmin9. Springer-Verlag, New York, 1981, 366 pages. Suad Alagit and
 Michael A.Arbib.  The Designnr Well-structureLi anci C.orrect Progroms'  Sprmger
 Verlag. hlew York. 1978. 292 pages
c++数据结构与程序设计
    保持程序简单以能够证明它们的正确性并非易事,但这一点却很重要。C A R Hoare
(他发明了我们将在第8章中学习的快速排序算法)写道:“有两种构造软件设计的方法:一
种方法是使它非常简单而明显没有缺陷,另一种方法是使它如此复杂而没有明显的缺陷,第
一种方法远远比第一种方法困难。”这句引语来自他1980年的图灵奖讲演:“The emperor's
old clothes.”Cummunications of the ACM, 24(1981), 75 -83.
…R题求解
    两本关于问题求解方法的书是:George Polya,How to Solven,second edition,
Doubledav, Garden City,N Y,1957, 253 pages. Wavne A Wickelgren. How加Solve
Probtems. W. H. F'reeman. San Francisco. 1974, 262 pages
    一维Life程序设计项目取自Jonarhan K Miller.“One-dimensional Life,”B.We3
(Decemher. 1978), 68-74
原创粉丝点击