第2单元 控制结构本单元教学目标介绍结构化程序设计方法的基本思想以及C++的基本控制结构和控制转移语句。
学习要求通过本单元学习,掌握结构化程序设计方法的基本思想和C++的几种基本控制转移语句,熟悉使用伪代码的编程方法。
授课内容
2.1 程序的基本控制结构我们知道,C++源程序由若干函数构成,而函数又是由语句构成的。对于一个程序员来说,编程序的一个主要内容就是如何将解决一个应用问题所使用的算法用C++的语句和函数来描述。换句话说,也就是如何组织C++程序的结构。在本单元中,首先要介绍的是构成程序的几种基本结构,并进而介绍如何使用这些基本结构编写比较复杂的程序。
结构化设计方法是以模块化设计为中心,将待开发的软件系统划分为若干个相互独立的模块,这样使完成每一个模块的工作变单纯而明确,为设计一些较大的软件打下了良好的基础。由于模块相互独立,因此在设计其中一个模块时,不会受到其它模块的牵连,因而可将原来较为复杂的问题化简为一系列简单模块的设计。模块的独立性还为扩充已有的系统、建立新系统带来了不少的方便,因为我们可以充分利用现有的模块作积木式的扩展。按照结构化设计方法设计出的程序具有结构清晰、可读性好、易于修改和容易验证的优点。C++是一种支持结构化程序设计思想的程序设计语言,使用C++编写程序时,应该遵循结构化程序设计方法。
在结构化程序设计方法中,模块是一个基本概念。一个模块可以是一条语句、一段程序、一个函数等。在流程图中,模块用一个矩形框表示,如图2-1所示。模块的基本特征是其仅有一个入口和一个出口,即要执行该模块的功能,只能从该模块的入口处开始执行 (用图2-1矩形上面的有向线段表示),执行完该模块的功能后,从模块的出口转而执行其他模块的功能 (用图2-1矩形下面的有向线段表示),即使模块中包含多个语句,也不能随意从其他语句开始执行,或提前退出模块。
按照结构化程序设计的观点,任何算法功能都可以通过由程序模块组成的三种基本程序结构的组合,顺序结构、选择构和循环结构来实现。
顺序结构由两个程序模块串接构成,如图2-2左部所示。由图中可以看出,这两个程序模块是顺序执行的,即首先执行<程序模块1>,然后执行<程序模块2>。
顺序结构中的两个程序模块可以合并成一个新的程序模块,即将图2-2中的左边部分整个看成一个新的模块,如图2-2的右部。通过这种方法,可以将许多顺序执行的语句合并成一个比较大的程序模块。但无论怎样合并,生成的新的程序模块仍然是一个整体,只能从模块的顶部 (入口) 进入模块开始执行模块中的语句,执行完模块中的所有语句之后再从模块的底部 (出口) 退出模块。事实上,顺序结构是最常见的程序结构形式,在一般程序中大量存在。但是设想一下,是不是所有程序都可以只使用顺序结构编写呢?显然答案是否定的。在求解实际问题时,常常要根据输入数据的实际情况进行逻辑判断,对不同的结果分别进行不同的处理;或者需要反复执行某些程序段落,以避免多次重复编写结构相似的程序段落带来的程序结构上的臃肿。这就需要在程序中引入选择结构和循环结构。一个结构化程序正是由这三种基本程序结构交替综合而构成的。
选择结构如图2-3所示。从图中可以看出,根据逻辑条件成立与否,分别选择执行 <模块1>或者<模块2>。虽然选择结构比顺序结构稍微复杂了一点,但是仍然可以将其整个作为一个新的程序模块:一个入口 (从顶部进入模块开始判断),一个出口(无论执行了<模块1>还是<模块2>,都应从选择结构框的底部出去)。
在编程实践中,还可能遇到选择结构中的一个分支没有实际操作的情况,如图2-4所示。这种形式的选择结构可以看成是图2-3中的选择结构的特例。
循环结构如图2-5所示。在进入循环结构后首先判断条件是否成立,如果成立则执行<程序模块>,反之则退出循环结构。执行完<程序模块>后再去判断条件,如果条件仍然成立则再次执行内嵌的<程序模块>,循环往复,直至条件不成立时退出循环结构。
与顺序和选择结构相同,循环结构也可以抽象为一个新的模块。图2-5中的循环结构可以描述为“当条件成立时反复执行程序模块”,故又称为while (当) 型循环。除了while型循环以外,还可以构造出其他类型的循环来,如do-while型循环结构,其特点是进入循环结构后首先执行<程序模块>,然后再判断条件是否成立,如果成立则再次执行<程序模块>,直到条件不成立时退出循环结构。do-while型循环结构如图2-6所示。
2.2,自顶向下,逐步求精”的程序设计方法在上面介绍的顺序、选择和循环三种基本程序结构中的程序模块又可以是由这三种基本程序结构抽象而成的新程序模块,因此可以通过组合、嵌套这些基本程序结构来构造更复杂的程序。已经证明,一个合理的程序,总可以化为顺序、选择和循环这三种基本程序结构的某种组合。
结构化程序设计支持“自顶向下,逐步求精”的程序设计方法。自顶向下的程序设计方法从问题本身开始,经过逐步求精,将解决问题的步骤分解为由基本程序结构模块组成的结构化程序框图,据此就很容易编写出结构良好、易于调试的程序来。
[例2-1]“验证”哥德巴赫猜想。哥德巴赫猜想是数论中的一个著名难题,是由法国数学爱好者克里斯蒂安·哥德巴赫于1742年在给著名数学家欧拉的一封信中提出的。“哥德巴赫猜想”可以表述为:任何一个大于等于4的偶数均可以表示为两个素数之和。尽管这个问题看来如此简明清晰,但二百多年来,虽有无数数学家为其呕心沥血、绞尽脑汁,却始终无人能够证明或者证伪这个猜想。
我们将这个问题作为一个练习,在有限的范围内验证哥德巴赫猜想:编写一段程序,验证大于4,小于某一上限M的所有偶数是否都能被分解为两个素数之和。如果一但发现某个偶数不能被分解为两个素数之和,则证实了哥德巴赫猜想是错误的 (果真如此,则可称是数学史上的一大发现!);否则证实哥德巴赫猜想在所给的范围内成立。
算 法,首先画出代表解决该问题的程序模块,见图2-7。
然后根据题意对图2-7中的程序模块进行初步分解,其思路如下:逐个生成由4到M之间的所有偶数,一一验证其是否能够被分解为两个素数之和。具体方法是声明一个变量x,令其初值等于4,然后每次在x上加2,以产生各偶数并验证x是否可以被分解为两个素数之和,直到x不小于M为止。显然,这是一个循环结构,其分解图见图2-8。
在图2-8中的初步分解中用粗线框表示了基本程序结构的嵌套组合情况。 从图中可以看出,最内层的粗线框中是两个顺序排列的程序模块,它们一起构成了循环结构的内嵌程序模块。循环模块又和最前面的语句模块构成了顺序结构。
图2-8中的框图是相当粗糙的,因为如何“验证x 是否能被分解为两个素数之和”并不清楚,因此继续对这个问题进行分解。“验证 x是否能被分解为两个素数之和”的步骤可以这样考虑,首先用x减去最小的素数2,然后看其差是否仍为素数,如果是,则验证结束,可以打印出该偶数的分解表达式。否则,换一个更大的素数,再看x与这个素数的差是否为素数。如果不是则仍进行循环,直到用于检测的素数已经大于x/2而x与其差仍不是素数。这时即可宣布一个伟大的发现,哥德巴赫猜想不成立!(?!)
图2-9给出了图2-8中的程序模块“验证 x是否能被分解为两个素数之和”的进一步分解。这里引入了一个新的变量p,用于存放已经生成的素数。
图2-9 中的模块“生成下一个素数”还可以继续分解。实际上,在大多数程序设计语言中,条件“x–p不是素数?”并不能简单地写成一个表达式,也需要进一步细化分解。在实际编程时,既可以将图2-9直接代入图2-8,编成一个较大的程序,也可以单独将图2-9中的程序模块编写为一个函数,由主函数调用。一般来讲,一个程序模块的大小最好不要超过一页打印纸(约60行左右),这样程序的结构比较清晰。如果程序模块太大则可以进一步分解。
以上过程可以总结如下:
首先从题目本身开始,找出解决问题的基本思路,并将其用结构化框图表示出来。这个框图可能是非常粗糙的,仅仅是一个算法的轮廓,但可以作为进一步分析的基础。接下来就应该对框图中的那些比较抽象的、用文字描述的程序模块作进一步的分析细化,每次细化的结果仍用结构化框图表示。最后,对如何求解问题的所有细节都弄清楚了,就可以根据这些框图直接写出相应的程序代码。这就是所谓的“自顶向下,逐步求精”的程序设计方法。在分析的过程中用结构化框图表示解题思路的优点是框图中的每个程序模块与其他程序模块之间的关系非常简明,每次可以只集中精力分解其中的一个模块而几乎不影响整个程序的结构。
2.3 C++的控制结构
2.3.1 顺序结构在用C++编写程序时,实现顺序结构的方法非常简单,只需将两个语句顺序排列即可。 如例1-1中交换两个整数的值的程序段:
r = p;
p = q;
q = r;
就是顺序结构。
2.3.2 选择结构
C++的选择结构是通过if-else语句实现的。其格式为:
if(<表达式>)
<内嵌语句1>;
else
<内嵌语句2>;
将其与图2-3比较,就会发现“程序模块1”和“程序模块2”分别对应于if-else语句中的“内嵌语句1”和“内嵌语句2”。一般来说,内嵌语句可以是各种可以执行的语句,甚至包括if-else语句和后面要介绍的循环语句。但是如果“程序模块1”和“程序模块2”比较复杂,不能简单地用一条语句实现时怎么办呢? 这时可以使用由一对花括号“{}”括起来的程序段落代替“语句1”和“语句2”,即:
if(<表达式>)
{
…...
}
else
{
…...
}
这种用花括号括起来的程序段落又称为分程序。分程序是C++中的一个重要概念。具体说来,一个分程序具有下述形式
{
<局部数据声明部分>
<执行语句段落>
}
即分程序是由花括号括起来的一组语句。当然,分程序中也可以再嵌套新的分程序。分程序是C++程序的基本单位之一。
分程序在语法上是一个整体,相当于一个语句。因此分程序可以直接和各种控制语句直接结合使用,用以构成C++程序的各种复杂的控制结构。在分程序中声明的变量的作用范围仅限于该分程序内部。
在if语句中用<表达式>的值来判断程序的流向,如果<表达式>的值不为0,表示条件成立,此时执行<内嵌语句1>; 否则 ( 即<表达式>的值等于0 ) 执行<内嵌语句2>。作条件用的表达式中通常含有比较运算符或逻辑运算符,例如
x>y // x大于y则表达式的值非0,否则表达式的值为0
x>=0.0 && x<=1.0 // x的值在0和1之间则表达式的值非0,否则为0
其中的逻辑运算符“&&”表示“并且”。这类表达式在其中的比较或逻辑运算的结果为真时取非0值,为假时取值0,因此正好可以用来在if语句中表示条件。关于这些表达式的使用方法,还要在4.2:“逻辑运算符和逻辑表达式”中做比较详细的介绍。
图2-4中的只有一个分支的选择结构可以使用不含else部分的if语句表示:
if (<表达式>)
<内嵌语句>;
或者
if (<表达式>)
{
…...
}
即如果<表达式>的值不为0时执行<内嵌语句>或分程序,否则直接执行if语句后面的其他语句。
2.3.3 循环结构
while型循环结构可以使用while语句实现:
while (<表达式>)
<循环体>
其中的<循环体>可以是一个语句,也可以是一个分程序:
while(<表达式>)
{
…...
}
while语句的执行过程见图2-5。当表达式的结果不为 0时反复执行其循环体内的语句或者分程序,直到表达式的值为0时退出循环。在设计while型循环时要注意在其循环体内应该有修改<表达式>的部分,确保在执行了一定次数之后可以退出循环,否则就成了“死循环”,一旦程序进入这里,将永远在循环结构中兜圈子而无法结束。
do-while型循环结构可以使用do-while语句实现:
do
{
<循环体>
}while (<表达式>);
do-while型循环和while型循环最大的不同是do-while 循环的循环体最少执行一次,而while型循环的循环体可能一次也不执行。这一点可以很容易地从图2-5和图2-6的比较中看出。
除此而外,C++中还有一种for语句,用来实现一类比较复杂的循环结构。其格式为:
for (<表达式1>; <表达式2>; <表达式3>)
<循环体>
其控制流程见图2-10。
和while语句的情况类似,for语句的循环体也可以是一条语句,或者一个分程序。for语句最常见的用途是构造指定重复次数的循环结构。例如
for (i=0; i<10; i=i+1)
{
…...
}
用于实现重复10次的循环。虽然用while循环也可以构造出这样的循环,但使用for语句更简单、直观。特别是在处理数组时,大多数程序员都喜欢使用for语句。
2.4 伪代码前面介绍了利用流程图实现自顶向下的程序设计方法。实际上,在使用C++的程序员们中还流行着一种利用伪代码进行自顶向下程序设计的方法。伪代码使用C++的几种控制语句和自然语言结合起来描述算法,比画流程图省时省力,且更容易转化为程序。
[例2-2] 编出“验证”哥德巴赫猜想的程序。
算 法:对图2-9中的各程序模块作进一步的分解。这一次采用伪代码表示程序求精分解的结果。“验证”哥德巴赫猜想首先要解决如何生成和测试素数的问题。素数即质数,即除了1和自身之外没有其他因子的正整数。大多数素数都不能简单地根据某个数学表达式计算出来,只能通过逐个检验正整数是否符合素数的定义的方法得出。因此,可以先编写一个生成素数表的函数,用其生成给定范围内所有的素数并存放在一个素数表中,这样“求一个素数”或检查一个数是否素数的工作就可以转化为对素数表的查询了。
我们使用“筛法”来生成素数表,这是生成素数最古老,同时也是最有效的方法,由古希腊的哲学家兼数学家埃拉托色尼提出。因为准备验证的最大偶数小于M,所以用到的最大素数也不会超过M。因此,列出0到M之间的所有自然数:
0,1,2,3,4,5,6,7,8,...,M(1
然后将所有2的倍数,3的倍数,5的倍数,...,直到M/2的倍数均从表中删去 ( 置换为0 ) 即可。
我们用数组PrimeList存放生成的素数。按上法生成的素数表有一个特点,即如果PrimeList[x]≠0,则x为素数,否则x为合数。这样,就可以很方便地根据该素数生成素数,或者判别一个数是否素数了。我们将生成素数表的工作编成一个函数,其原型为:
void CreatePrimeList(int PrimeList[]);
用伪代码描述生成素数表的算法如下:
将PrimeList的各元素设置为2到M-1之间的自然数;
分别从表中去掉已经确定的各素数的倍数 ( 将其置换为0 ) ;
第1步可以用一个for型的循环实现:
for(i=0; i<M; i=i+1)
PrimeList[i] = i;
第2步稍微复杂些。我们使用工作变量i指示出最后确定的素数的位置,在表中将数组元素PrimeList[i]的倍数转换为0:
i = 2;
while(i<M/2)
{
for(j=i+1; j<M; j=j+1)
if(PrimeList[j]≠0且是PrimeList[i]的倍数)
PrimeList[j] = 0;
i = 下一个确定的素数的位置;
}
这里要注意的是,由于在转换的过程中表中会产生一些值为0的元素,所以在计算下一个确定的素数的位置时要跳过这些0:
i = i+1;
while(PrimeList[i]==0)
i = i+1;
将图2-9中的程序模块“生成下一个素数”也编为一个函数:
int NextPrimeNumber(int p,int PrimeList[])
{
…,..
}
其参数为当前的素数p,返回值为素数表PrimeList中的下一个素数。求下一个素数的方法很简单,从PrimeList[p+1]开始向后找到的第一个非0元素,即为所要求的素数。
综合以上分析结果和例2-1中的结构化程序框图,就可以编写程序了:
程 序,
// Example 2-2:"验证"哥德巴赫猜想
#include <iostream.h>
#define M 10001 // 定义验证范围
// 函数 CreatPrimeList(),生成素数表
void CreatPrimeList(int PrimeList[])
{
int i,j;
// 将PrimeList的各元素设置为从2开始的正整数
for(i=0; i<M; i = i+1)
PrimeList[i] = i;
// 分别从表中去掉已经确定的各素数的倍数(将其置为0)
i = 2;
while(i<M/2)
{
for(j=i+1; j<M; j=j+1)
if(PrimeList[j]!=0 && PrimeList[j]%PrimeList[i]==0)
PrimeList[j] = 0;
// 确定下一个素数的位置
i = i+1;
while(PrimeList[i]==0)
i = i+1;
}
}
// 函数 NextPrimeNumber(),求下一个素数
int NextPrimeNumber(int p,int PrimeList[])
{
p = p+1;
while(PrimeList[p]==0)
p = p+1;
return PrimeList[p];
}
// 主函数,在从4到M的范围内验证哥德巴赫猜想
void main()
{
int PrimeList[M]; // 说明存放素数表的数组
int x,p; // 变量x,偶数,p,素数
// 建立素数表
CreatPrimeList(PrimeList);
// 对从4到M的所有偶数验证哥德巴赫猜想
x = 4;
while(x<M)
{
// 检查偶数减去一个素数后的剩余部分是否仍为素数
p = PrimeList[2];
while(p<M/2 && PrimeList[x-p]==0)
p = NextPrimeNumber(p,PrimeList);
// 输出检查结果
if(p>=M/2) // 找到了一个不能分解为两个素数和的偶数
cout<<"Great discovery,Goldbach is wrong!"<<endl;
else // PrimeList[x-p]≠0,分解成功
cout<<"The even number "<<x<<"="<<p<<" + "<<x-p<<endl;
// 检查下一个偶数
x = x+2;
}
}
输 出,The even number 4 = 2 + 2
The even number 6 = 3 + 3
The even number 8 = 3 + 5
The even number 10 = 3 + 7
…,..
The even number 9998 = 31 + 9967
The even number 10000 = 59 + 9941
分 析,上述程序在4到10,001的范围内验证了哥德巴赫猜想,发现在这个范围内的所有偶数均可以分解为两个素数之和。当然,这还不能说明哥德巴赫猜想确实成立。鉴于这种方法只能检验有限个偶数,而偶数的个数是无限的,所以如果想做出一个“伟大的发现”,就只有指望在所检验的范围内能出现一个确实不能被分解为两个素数之和的偶数了 (此时可以宣布哥德巴赫猜想不成立,同样是解决了这个问题)。很遗憾,使用上面的程序,在M=32,000时仍然没能发现一个这样的偶数。超过这个限度,就要对程序的结构进行修改才能继续运行。
自学内容
2.5 结构化程序设计方法简介在计算机问世以后的最初几年,其价格非常昂贵,而运算能力很差,可靠性也很低。由于当时计算机的速度慢、内存小,加之为计算机编写软件主要使用机器指令代码或者汇编语言,所以当时研究程序设计方法的重点是如何通过运用一些编程技巧尽量节约内存空间,提高运算速度。后来虽然也出现了FORTRAN、ALGOL等高级程序设计语言,为提高程序员的算法表达能力和降低劳动强度提供了一定条件,但是由于这一时期计算机的主要任务是进行科学计算,而且程序的规模一般都比较小,因此从程序设计方法上来看并没有发生什么根本的变化。总的来说,程序设计被看成是一种技巧性很强的工作,程序员们大都采用手工工艺式的、精雕细凿的设计方法。
60年代以后,计算机硬件的发展速度异常迅猛,其速度和存储容量不断提高,成本急剧下降。但程序员要解决的问题却变得更加复杂,程序的规模越来越大,出现了一些需要几十甚至上百人年的工作量才能完成的大型软件,远远超出了程序员的个人能力。这类程序必须由多个程序员密切合作才能完成。由于旧的程序设计方法很少考虑程序员之间交流协作的需要,所以不能适应新形势的发展,因此编出的软件中的错误随者软件规模的增大而迅速增加,造成调试时间和成本也迅速上升,甚至许多软件尚未出品便已因故障率太高而宣布报废。当时人们认为,这是由于计算机的效率远远超过人 (程序员) 的效率造成的,而随着技术的发展,计算机的效率还可不断地提高,人的效率却无法有大的改进,因此由人编写的软件的规模和复杂度就会有一个上限,软、硬件之间的效率差别会越来越大,从而会限制计算机的发展。这就是通常所说的“软件危机”。
有危机就会有革命。1968年,E.W.Dijkstra首先提出“goto语句是有害的”,向传统的程序设计方法提出了挑战,从而引起了人们对程序设计方法讨论的普遍重视,许多著名的计算机科学家都参加了这场论战。结构化程序设计方法正是在这种背景下产生的。
结构化程序设计的基本观点是,随着计算机硬件性能的不断提高,程序设计的目标不应再集中于如何充分发挥硬件的效率方面。新的程序设计方法应以能设计出结构清晰、可读性强、易于分工合作编写和调试的程序为其基本目标。
结构化程序设计认为,好的程序具有层次化的结构,应该采用“逐步求精”的方法,只使用顺序、分支和循环等基本程序结构通过组合嵌套来编写(见2.2:“‘自顶向下,逐步求精’的程序设计方法”)。
与此同时,关于程序设计和软件生产的其他研究也在不断深入,例如数据抽象与模块化程序设计、程序正确性证明、程序自动生成以及研究大型软件生产方法的软件工程等。
今天,结构化程序设计方法、面向对象的程序设计方法、第4代程序设计语言、计算机辅助软件工程(CASE)等软件设计和生产技术都已日臻完善,计算机软、硬件技术的发展交相映辉,使计算机的发展和应用达到了前所未有的高度和广度。
2.6 C++的其他控制转移语句
C++提供的控制转移语句,除了授课内容部分所介绍的if-else语句、while语句、do-while语句和for语句以外,还有如下一些控制语句:
2.6.1 switch语句
switch语句用于实现多重分支,其格式为:
switch (<整型表达式>)
{
case <数值1>:
…,..
case <数值2>:
…...
case <数值3>:
…,..
default:
…,..
}
其中default模块也可省略。switch语句的执行过程是,首先计算整型表达式的值,然后将其结果与每一个case后面的数值常量依次进行比较,如果相等则执行该case模块中的语句,然后依次执行其后每一个case模块中的语句,无论整型表达式的值是否与这些case模块的进入值是否相同。如果需要在执行完本case模块以后就跳出switch语句,则可以在case模块的最后加上一个break语句,这样才能实现真正的多路选择。如果整型表达式的值与所有case模块的进入值无一相同,则执行default模块中的语句。带有break语句的switch多分支结构的框图见图2-11。
[例2-3] 编写一个函数,将百分制的学生成绩转换为优秀、良好、中等、及格和不及格的5级制成绩。标准为:
优秀,100-90分;
良好,80-89分;
中等,70-79分;
及格,60-69分;
不及格,60分以下。
算 法,我们使用switch语句构成的多分支结构编写这个函数。switch语句根据具体的数值判断执行的路线,而现在的转换标准是根据分数范围。因此,构造一个整型表达式old_grade/10用于将分数段化为单个整数值。例如对于分数段60-69中的各分数值,上述表达式的值均为6。再配合以在switch语句的各case模块中灵活运用break语句,即可编写出所需转换程序。
程 序:下面是分数转换函数的代码。要验证该函数,还需自行编写一个相应的中函数,并加上文件包含命令。这一工作留作上机作业。
// Example 2-3:将百分制的分数转换为5级制分数
int TranGrade(int old_grade)
{
int new_grade;
switch (old_grade/10)
{
case 10:
case 9:
new_grade = 1;
break;
case 8:
new_grade = 2;
break;
case 7:
new_grade = 3;
break;
case 6:
new_grade = 4;
break;
default:
new_grade = 5;
}
return new_grade;
}
分 析,该函数将百分制的分数值(0~100)转换为5级制成绩,1代表优秀,2代表良好,...,5代表不及格。 请注意switch语句的第1个case模块中没有任何语句(包括break),因此进入该模块时 (原成绩为100分) 将直接转入第2个case模块 (处理原成绩在90~99分之间) 中继续执行。
2.6.2 goto语句和语句标号
C++允许在语句前面放置一个标号,其一般格式为:
<标号>,<语句>;
标号的取名规则和变量名相同,即由下划线、字母和数字组成,第一个字符必须是字母或下划线,例如:
ExitLoop,x = x+1;
End,return x;
在语句前面加上标号主要是为了使用goto语句。goto语句的格式为:
goto <标号>;
其功能是打乱语句执行顺序,转去执行前面有指定标号的语句,而不管其是否排在当前语句之后。C++的goto语句只能在本函数模块内部进行转移,不能由一个函数中转移到另一个函数中去。由于结构化程序设计方法主张尽量限制goto语句的使用范围,因此在这里不对goto语句作过多的讨论,仅举一个例子说明在某些情况下使用goto语句可以简化程序设计,同时又不明显地降低程序的可读性:
while (<条件1>)
{
… …
if(<条件2>)
goto OutLoop;
… …
}
… …
OutLoop,… …
上面这种程序结构当然也可以不用goto语句,而使用“纯粹”的结构化程序模块实现,
t = 1;
while(<条件1> && t≠0)
{
… …
if(<条件2>)
t = 0;
else
{
… …
}
}
if(t≠0)
{
… …
}
OutLoop,…
但这段程序的可读性并不比使用goto语句时好多少 (甚至可能更糟),结构也复杂多了,还引入了一个辅助变量t。这说明,尽管按照结构化程序设计方法的说法滥用goto语句会使程序的结构变坏,但在特殊情况下可能恰恰相反。何时应该使用goto语句,何时应该避免使用goto语句,应该以所设计出的程序模块是否清晰易读为标准,不可胶柱鼓瑟,一概排斥使用goto语句。
2.6.3 break语句和continue语句
break语句的格式为:
break;
用于立即跳出包含该break语句的各种循环语句和switch语句的case模块。 在循环语句中使用的break语句一般应和if语句配合使用,例如:
while(<条件1>)
{
… …
if(<条件2>)
break;
… …
}
以上结构的框图见图2-12。
continue语句用于提前结束循环,可用于while,do-while和for语句中。其格式为:
continue;
continue语句的用法和break语句相似,均应和if语句配合使用。仍以while语句为例:
while(<条件1>)
{
… …
if(<条件2>
continue;
… …
}
其执行框图见图2-13。
其实,break语句和continue语句都是变相的goto语句。恰当地应用这些语句,可以使程序的表达比较清晰,同时仍然满足结构化程序的基本特征,每个程序模块只有一个入口和一个出口,可以自上而下地阅读。
2.6.4 exit()函数和abort()函数
exit()和abort()是C++的两个库函数,作用均为中止整个程序的运行。所不同的是,前者在结束程序的运行前,还会做一些善后工作,如将程序中的变量、对象等占用的内存资源释放掉;关闭所有已打开的文件等。而后者只是单纯地结束程序的运行。
在使用这两个函数时,均要求用一整数参数表示退出的原因。习惯上用0表示正常退出,非0值表示非正常退出。
如果在程序中使用这两个函数,必须在程序首部加上文件包含命令:
#include <stdlib.h>
调试技术
2.7 Developer Studio的文本编辑器
Developer Studio提供了一个优秀的程序文本编辑器,特点是同Developer Studio的其它工具(如调试器)的配合非常好,使应用程序的编辑修改和调试工作混为一体,非常方便。该文本编辑器不仅可编辑程序文本,还可编辑一般的文本文件和HTML Page。
启动文本编辑器非常简单,只要建立一个新文本文件,或打开一个已存在的文本文件,文本编辑器就会自动出现。
在文本编辑器中,用一闪烁的短竖线表示编辑位置,通过键盘输入的文字在此位置插入文本。用鼠标左键点击文本中的某个字符可以改变编辑位置。
文本编辑器的基本操作包括:
→:光标向后移动一个字符。
←:光标向前移动一个字符。
↑:光标向上移动一行。
↓:光标向下移动一行。
Home:光标移动到行首。
End:光标移动到行尾。
Ctrl+Home,光标移动到文件头。
Ctrl+End,光标移动到文件尾。
PgUp:光标向上滚动一屏。
PgDn,光标向下滚动一屏。
Ctrl+Y:删除行。
Del:删除光标右边字符。
Backspace:删除光标左边字符。
Ins:插入/改写方式切换。
Developer Studio的Edit子菜单还提供了一批高级编辑功能,大致可分为以下几类:
Undo和Redo,用于反悔对文本文件所做的修改
Undo:取消最近一次的编辑修改操作。
Redo:取消最近一次的“Undo”操作,即恢复被“undo”命令取消的修改操作。
(2)剪贴、复制、粘贴和删除
Cut:将选定内容复制到剪贴板,然后再从当前活动窗口中删除。“Cut”选项与“Paste”选项联合使用可以移动选定的内容。要选定文本内容,可将鼠标移到选定内容的开始位置,按下鼠标左键,然后拖动鼠标到选定内容的末尾放开鼠标左键。此时整个被选定内容将反白显示(黑底白字)。
Copy:将选定内容复制到剪贴板,但不从当前活动窗口中删除。“Copy”选项与“Paste”选项联合使用可以复制选定的内容。
Paste:将剪贴板中的内容插入到当前编辑位置。注意,需要先剪切或复制选定内容到剪贴板后,才能进行粘贴。
Delete:删除选定内容。删除以后,还可用“Undo”命令来恢复误删的内容。
Select All:选定当前正在编辑的文本的所有内容。
(3)查找和替换
Find:在正在编辑的文本中查找指定的字符串。
Find in Files:在多个文件中查找指定的字符串。
Replace:在正在编辑的文本中替换指定的字符串。
(4)书签
Go to:弹出“Go To”对话框,指定如何将编辑位置移到当前活动窗口的指定位置,如指定的号、地址和书签等。
Bookmarks:弹出“Bookmarks”对话框,以便设置或取消书签。书签用于在文件中做标记。
(5)高级选项
Advance:选择该项将弹出子选单,其中含有用于编辑或修改的高级命令,如增量式搜索、将选定内容全部转为大写或个写、显示或隐藏制表符等。
(6)断点设置
Breakpoints:弹出“Breakpoints”对话框,以便设置、删除和查看断点。断点用来通知调试器应该在何处或何时中断程序的执行过程,以便检查程序代码、变量和寄存器值。“Breakpoints”对话框的“Location”,“Data”和“Messages”选项卡分别用于设置位置断点、数据断点和消息断点。如果要设置条件断点,必须先设置位置断点,然后再设置中断程序执行的条件。
(7)编程指导信息
List Members:输入程序代码时,如果在变量名之后键入“.”或“(>”(即要访问某对象的成员),默认的处理是自动列表显示所有有效的成员名。这时,键入待输入成员的前几个字母即可从列表中选中该成员,按Tab键即可完成成员名的输入;也可通过滚动条找到待输入的成员,然后用鼠标左键双击成员名来完成输入。通过该菜单项也可得到此效果。
Type Info:在编辑源程序代码时,如果将鼠标指向某一标识符,默认的处理是显示所指变量、函数或方法等的语法。通过该菜单项也可得到此效果。
Parameter Info:在编辑源程序代码时,如果在输入函数名之后键入左括号,默认的处理是显示该函数的完整原型,并用黑体字显示其第一个参数。输入第一个参数值之后,接着会以黑体显示第二个参数,直至所有参数输入完毕。通过该菜单项也可得到此效果。
Complete Word,自动完成当前语句其余部分的输入。如果不能自动完成,则给出适当的提示来辅助用户完成。
程序设计举例
[例2-4] 计算,当通项时停止计算。
算 法,声明三个工作变量e,n 和u,分别用于存放已计算出的结果近似值、当前项序号和当前通项值,则伪代码算法为:
e = 1.0; n = 1; u = 1.0;
while (通项u大于等于10-7)
{
计算新的通项值 u = u/n;
将新通项值加到结果近似值上;
准备处理下一项 n = n+1;
}
程 序:
// Example 2-4:计算常数e的值
#include <iostream.h>
#include <math.h>
void main()
{
double e = 1.0;
double u = 1.0;
int n = 1;
while(u >= 1.0e-7)
{
u = u/n;
e = e+u;
n = n+1;
}
cout << "e = " << e << " ( n = " << n << " )" << endl;
}
输 出,e = 2.71828 ( n = 12 )
分 析,根据计算结果同时打印出的项数n,表明该级数收敛相当快,仅计算到前12项其截断误差便已小于10-7。
[例2-5] 使用do-while结构重新编写上题的程序。
程 序,
// Example 2-5:计算常数e的值
#include <iostream.h>
#include <math.h>
void main()
{
double e = 1.0;
double u = 1.0;
int n = 1;
do
{
u = u/n;
e = e+u;
n = n+1;
}while(u>=1.0E-7);
cout << "e = " << e << " ( n = " << n << " )" << endl;
}
输 出,e = 2.71828 ( n = 12 )
[例2-6] 求水仙花数。如果一个三位数的个位数、十位数和百位数的立方和等于该数自身,则称该数为水仙花数。编一程序求出所有的水仙花数。
算 法,显然,水仙花数要在100~999的范围内去找。我们对于这个范围内所有的数一一检验其是否符合水仙花数的定义:
for(n=100; n<=999; n=n+1)
if (n是水仙花数)
打印n的分解形式;
程 序:
// Example 2-6:打印所有的水仙花数
#include <iostream.h>
void main()
{
int n,i,j,k;
for(n=100; n<=999; n=n+1)
{
i = n/100; // 取出n的百位数
j = (n/10)%10; // 取数n的十位数
k = n%10; // 取出n的个位数
if(n==i*i*i+j*j*j+k*k*k)
cout <<n<<" = "<<i<<"^3 + "<<j<<"^3 + "<<k<<"^3"<<endl;
}
}
输 出: 153 = 1^3 + 5^3 + 3^3
370 = 3^3 + 7^3 + 0^3
371 = 3^3 + 7^3 + 1^3
407 = 4^3 + 0^3 + 7^3
分 析,在程序中利用了C++的整数除法和求余运算从一个3位数中分解出其个位、十位和百位数。这些运算符的特点请参看4.1:“算术运算符和算术表达式”。
[例2-7] 编写一个可以打印任何一年的日历的程序。
算法分析,打印日历的算法必需解决两个问题,一是这年是否闰年(闰年的二月份有29天,平年的二月份有28天),二是这年的元旦是星期几。确定某年是否闰年的算法为,如果某年的年份不能被4整除,则是平年。如果能够被4整除,再看其是否能被100整除,不能被100整除的肯定是闰年。在既能被4,也能被100整除的年份中,还能被400整除的是也是闰年,否则是平年。上述算法可以用伪代码表示为:
leap = NO;
if(year能被4整除)
{
leap = YES;
if(year能被100整除)
{
leap = NO;
if(year能被400整除)
leap = YES;
}
}
确定某年的元旦是星期几可以这样考虑:我们知道,平年一年有365天,而一年大约有52个星期。实际上,52个星期共有52×7=364天,这也就是说,如果某年的元旦是星期一,则第二年的元旦整整过了52个星期又一天,因此应该是星期二。当然如果这一年是闰年,则在2月份多了一天,所以下一年的元旦也会顺延到星期三。因此,如果知道了某个基准年份的元旦是星期几,就可以用这种方法推算出其后任何一年的元旦是星期几来。例如,我们恰好知道1900年的元旦是星期一,因此如果year是1900年之后的某个年份,则其元旦是星期几可以按如下方法计算:
计算出year和1900年之间所差的年数,n=year-1900;
因为每年都是52个整星期多1天,因此n年多出的天数还是n天;
在n天上再加上1900年到year年之间因闰年多出来的天数,n=n+闰年数;
将多出来的天数n用7除,求其余数,即星期几:n=n%7;
再加上1900年元旦的星期序号(1),n=n+1;
如果n≥7则再减去7即为year年元旦的星期序号(0表示星期日):n=n%7。
在上面的算法中“闰年数”可以简单地用 (n-1)/4计算。由于2000年正好是闰年(为什么?),所以这个简化公式可以一直用到2099年!
程 序:
// Example 2-7:打印年历
#include <iostream.h>
#define YES 1 // 定义符号常数"是"
#define NO 0 // 定义符号常数"否"
// 函数 isleap(),判断某年是否闰年
int isleap(int year)
{
int leap = NO;
if(year%4==0 && year%100!=0 || year%400==0)
leap = YES;
return leap;
}
// 函数 week_of_newyears_day(),求元旦是星期几
int week_of_newyears_day(int year)
{
int n = year-1900;
n = n+(n-1)/4+1;
n = n%7;
return n;
}
// 主函数,打印年历
void main()
{
int year,month,day,weekday,len_of_month,i;
cout << "Please input year,";
cin >> year;
// 打印年历
cout << endl << year << endl; // 打印年份
weekday = week_of_newyears_day(year); // 求元旦是星期几
for(month=1; month<=12;month=month+1) // 打印12个月的月历
{
cout << endl << month << endl;
cout << "---------------------------------" << endl;
cout << "SUN MON TUE WED THU FRI SET" << endl;
cout << "---------------------------------" << endl;
for(i=0;i<weekday;i=i+1) // 找当月1日的打印位置
cout << " ";
if(month==4 || month== 6 || month==9 || month==11)
len_of_month = 30;
else if(month==2)
{
if(isleap(year))
len_of_month = 29;
else
len_of_month = 28;
}
else
len_of_month = 31;
for(day=1;day<=len_of_month;day=day+1) // 打印当月日期
{
if(day>9)
cout << day << " ";
else
cout << day << " ";
weekday = weekday+1;
if(weekday==7) // 打满一星期应换行
{
weekday = 0;
cout << endl;
}
}
cout << endl; // 打完一月应换行
}
}
输 入:Please input year,2000
输 出:
1
---------------------------------
SUN MON TUE WED THU FRI SET
---------------------------------
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
2
---------------------------------
SUN MON TUE WED THU FRI SET
---------------------------------
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29
( (
12
---------------------------------
SUN MON TUE WED THU FRI SET
---------------------------------
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
分 析,该程序的函数虽相对比较多,但其结构比较简单。主程序中的基本结构可以用伪代码表示为:
输入年份year;
确定该年的元旦是星期几;
for(month=1; month<=12; month=month+1)
打印month月份的月历;
而打印某月份的月历又可以用伪代码表示为:
打印月历头;
确定该月份的天数;
确定当月1日的打印位置;
for(day=1;day<=当月天数;day=day+1)
在适当位置上打印日期;
而所谓在“适当位置”上打印日期则包括打满一行(一星期)后自动换行,以及打印日期时所使用的格式等。
单元上机练习题目
1.编写计算阶乘 n!的程序。
2.编写程序求斐波那契数列的第n项和前n项之和。斐波那契数列是形如
0,1,1,2,3,5,8,13,...
其通项为:
F0 = 0;
F1 = 1;
Fn = Fn(1+Fn(2。
3.编程求 ,其中。
提示:结束条件可用 ,其中u为通项。
4.选做题:用弦截法求一元方程在区间之间的一个根。
提示:考虑当区间足够小,在此区间中方程仅有一个单根的情况,如图2-14所示。
此时如和异号,则可用两点间直线公式求出x2:
然后用x2代入原式求出f(x2),判断f(x2)与f(x1)和f(x0)中的哪一个同号,就用x2和f(x2)代替之,即如果f(x2)和f(x0)同号,就用x2和f(x2)代替x0和f(x0),反之用x2和f(x2)代替x1和f(x1),然后再继续上述过程直至|x1(x0|小于给定的误差控制值。
学习要求通过本单元学习,掌握结构化程序设计方法的基本思想和C++的几种基本控制转移语句,熟悉使用伪代码的编程方法。
授课内容
2.1 程序的基本控制结构我们知道,C++源程序由若干函数构成,而函数又是由语句构成的。对于一个程序员来说,编程序的一个主要内容就是如何将解决一个应用问题所使用的算法用C++的语句和函数来描述。换句话说,也就是如何组织C++程序的结构。在本单元中,首先要介绍的是构成程序的几种基本结构,并进而介绍如何使用这些基本结构编写比较复杂的程序。
结构化设计方法是以模块化设计为中心,将待开发的软件系统划分为若干个相互独立的模块,这样使完成每一个模块的工作变单纯而明确,为设计一些较大的软件打下了良好的基础。由于模块相互独立,因此在设计其中一个模块时,不会受到其它模块的牵连,因而可将原来较为复杂的问题化简为一系列简单模块的设计。模块的独立性还为扩充已有的系统、建立新系统带来了不少的方便,因为我们可以充分利用现有的模块作积木式的扩展。按照结构化设计方法设计出的程序具有结构清晰、可读性好、易于修改和容易验证的优点。C++是一种支持结构化程序设计思想的程序设计语言,使用C++编写程序时,应该遵循结构化程序设计方法。
在结构化程序设计方法中,模块是一个基本概念。一个模块可以是一条语句、一段程序、一个函数等。在流程图中,模块用一个矩形框表示,如图2-1所示。模块的基本特征是其仅有一个入口和一个出口,即要执行该模块的功能,只能从该模块的入口处开始执行 (用图2-1矩形上面的有向线段表示),执行完该模块的功能后,从模块的出口转而执行其他模块的功能 (用图2-1矩形下面的有向线段表示),即使模块中包含多个语句,也不能随意从其他语句开始执行,或提前退出模块。
按照结构化程序设计的观点,任何算法功能都可以通过由程序模块组成的三种基本程序结构的组合,顺序结构、选择构和循环结构来实现。
顺序结构由两个程序模块串接构成,如图2-2左部所示。由图中可以看出,这两个程序模块是顺序执行的,即首先执行<程序模块1>,然后执行<程序模块2>。
顺序结构中的两个程序模块可以合并成一个新的程序模块,即将图2-2中的左边部分整个看成一个新的模块,如图2-2的右部。通过这种方法,可以将许多顺序执行的语句合并成一个比较大的程序模块。但无论怎样合并,生成的新的程序模块仍然是一个整体,只能从模块的顶部 (入口) 进入模块开始执行模块中的语句,执行完模块中的所有语句之后再从模块的底部 (出口) 退出模块。事实上,顺序结构是最常见的程序结构形式,在一般程序中大量存在。但是设想一下,是不是所有程序都可以只使用顺序结构编写呢?显然答案是否定的。在求解实际问题时,常常要根据输入数据的实际情况进行逻辑判断,对不同的结果分别进行不同的处理;或者需要反复执行某些程序段落,以避免多次重复编写结构相似的程序段落带来的程序结构上的臃肿。这就需要在程序中引入选择结构和循环结构。一个结构化程序正是由这三种基本程序结构交替综合而构成的。
选择结构如图2-3所示。从图中可以看出,根据逻辑条件成立与否,分别选择执行 <模块1>或者<模块2>。虽然选择结构比顺序结构稍微复杂了一点,但是仍然可以将其整个作为一个新的程序模块:一个入口 (从顶部进入模块开始判断),一个出口(无论执行了<模块1>还是<模块2>,都应从选择结构框的底部出去)。
在编程实践中,还可能遇到选择结构中的一个分支没有实际操作的情况,如图2-4所示。这种形式的选择结构可以看成是图2-3中的选择结构的特例。
循环结构如图2-5所示。在进入循环结构后首先判断条件是否成立,如果成立则执行<程序模块>,反之则退出循环结构。执行完<程序模块>后再去判断条件,如果条件仍然成立则再次执行内嵌的<程序模块>,循环往复,直至条件不成立时退出循环结构。
与顺序和选择结构相同,循环结构也可以抽象为一个新的模块。图2-5中的循环结构可以描述为“当条件成立时反复执行程序模块”,故又称为while (当) 型循环。除了while型循环以外,还可以构造出其他类型的循环来,如do-while型循环结构,其特点是进入循环结构后首先执行<程序模块>,然后再判断条件是否成立,如果成立则再次执行<程序模块>,直到条件不成立时退出循环结构。do-while型循环结构如图2-6所示。
2.2,自顶向下,逐步求精”的程序设计方法在上面介绍的顺序、选择和循环三种基本程序结构中的程序模块又可以是由这三种基本程序结构抽象而成的新程序模块,因此可以通过组合、嵌套这些基本程序结构来构造更复杂的程序。已经证明,一个合理的程序,总可以化为顺序、选择和循环这三种基本程序结构的某种组合。
结构化程序设计支持“自顶向下,逐步求精”的程序设计方法。自顶向下的程序设计方法从问题本身开始,经过逐步求精,将解决问题的步骤分解为由基本程序结构模块组成的结构化程序框图,据此就很容易编写出结构良好、易于调试的程序来。
[例2-1]“验证”哥德巴赫猜想。哥德巴赫猜想是数论中的一个著名难题,是由法国数学爱好者克里斯蒂安·哥德巴赫于1742年在给著名数学家欧拉的一封信中提出的。“哥德巴赫猜想”可以表述为:任何一个大于等于4的偶数均可以表示为两个素数之和。尽管这个问题看来如此简明清晰,但二百多年来,虽有无数数学家为其呕心沥血、绞尽脑汁,却始终无人能够证明或者证伪这个猜想。
我们将这个问题作为一个练习,在有限的范围内验证哥德巴赫猜想:编写一段程序,验证大于4,小于某一上限M的所有偶数是否都能被分解为两个素数之和。如果一但发现某个偶数不能被分解为两个素数之和,则证实了哥德巴赫猜想是错误的 (果真如此,则可称是数学史上的一大发现!);否则证实哥德巴赫猜想在所给的范围内成立。
算 法,首先画出代表解决该问题的程序模块,见图2-7。
然后根据题意对图2-7中的程序模块进行初步分解,其思路如下:逐个生成由4到M之间的所有偶数,一一验证其是否能够被分解为两个素数之和。具体方法是声明一个变量x,令其初值等于4,然后每次在x上加2,以产生各偶数并验证x是否可以被分解为两个素数之和,直到x不小于M为止。显然,这是一个循环结构,其分解图见图2-8。
在图2-8中的初步分解中用粗线框表示了基本程序结构的嵌套组合情况。 从图中可以看出,最内层的粗线框中是两个顺序排列的程序模块,它们一起构成了循环结构的内嵌程序模块。循环模块又和最前面的语句模块构成了顺序结构。
图2-8中的框图是相当粗糙的,因为如何“验证x 是否能被分解为两个素数之和”并不清楚,因此继续对这个问题进行分解。“验证 x是否能被分解为两个素数之和”的步骤可以这样考虑,首先用x减去最小的素数2,然后看其差是否仍为素数,如果是,则验证结束,可以打印出该偶数的分解表达式。否则,换一个更大的素数,再看x与这个素数的差是否为素数。如果不是则仍进行循环,直到用于检测的素数已经大于x/2而x与其差仍不是素数。这时即可宣布一个伟大的发现,哥德巴赫猜想不成立!(?!)
图2-9给出了图2-8中的程序模块“验证 x是否能被分解为两个素数之和”的进一步分解。这里引入了一个新的变量p,用于存放已经生成的素数。
图2-9 中的模块“生成下一个素数”还可以继续分解。实际上,在大多数程序设计语言中,条件“x–p不是素数?”并不能简单地写成一个表达式,也需要进一步细化分解。在实际编程时,既可以将图2-9直接代入图2-8,编成一个较大的程序,也可以单独将图2-9中的程序模块编写为一个函数,由主函数调用。一般来讲,一个程序模块的大小最好不要超过一页打印纸(约60行左右),这样程序的结构比较清晰。如果程序模块太大则可以进一步分解。
以上过程可以总结如下:
首先从题目本身开始,找出解决问题的基本思路,并将其用结构化框图表示出来。这个框图可能是非常粗糙的,仅仅是一个算法的轮廓,但可以作为进一步分析的基础。接下来就应该对框图中的那些比较抽象的、用文字描述的程序模块作进一步的分析细化,每次细化的结果仍用结构化框图表示。最后,对如何求解问题的所有细节都弄清楚了,就可以根据这些框图直接写出相应的程序代码。这就是所谓的“自顶向下,逐步求精”的程序设计方法。在分析的过程中用结构化框图表示解题思路的优点是框图中的每个程序模块与其他程序模块之间的关系非常简明,每次可以只集中精力分解其中的一个模块而几乎不影响整个程序的结构。
2.3 C++的控制结构
2.3.1 顺序结构在用C++编写程序时,实现顺序结构的方法非常简单,只需将两个语句顺序排列即可。 如例1-1中交换两个整数的值的程序段:
r = p;
p = q;
q = r;
就是顺序结构。
2.3.2 选择结构
C++的选择结构是通过if-else语句实现的。其格式为:
if(<表达式>)
<内嵌语句1>;
else
<内嵌语句2>;
将其与图2-3比较,就会发现“程序模块1”和“程序模块2”分别对应于if-else语句中的“内嵌语句1”和“内嵌语句2”。一般来说,内嵌语句可以是各种可以执行的语句,甚至包括if-else语句和后面要介绍的循环语句。但是如果“程序模块1”和“程序模块2”比较复杂,不能简单地用一条语句实现时怎么办呢? 这时可以使用由一对花括号“{}”括起来的程序段落代替“语句1”和“语句2”,即:
if(<表达式>)
{
…...
}
else
{
…...
}
这种用花括号括起来的程序段落又称为分程序。分程序是C++中的一个重要概念。具体说来,一个分程序具有下述形式
{
<局部数据声明部分>
<执行语句段落>
}
即分程序是由花括号括起来的一组语句。当然,分程序中也可以再嵌套新的分程序。分程序是C++程序的基本单位之一。
分程序在语法上是一个整体,相当于一个语句。因此分程序可以直接和各种控制语句直接结合使用,用以构成C++程序的各种复杂的控制结构。在分程序中声明的变量的作用范围仅限于该分程序内部。
在if语句中用<表达式>的值来判断程序的流向,如果<表达式>的值不为0,表示条件成立,此时执行<内嵌语句1>; 否则 ( 即<表达式>的值等于0 ) 执行<内嵌语句2>。作条件用的表达式中通常含有比较运算符或逻辑运算符,例如
x>y // x大于y则表达式的值非0,否则表达式的值为0
x>=0.0 && x<=1.0 // x的值在0和1之间则表达式的值非0,否则为0
其中的逻辑运算符“&&”表示“并且”。这类表达式在其中的比较或逻辑运算的结果为真时取非0值,为假时取值0,因此正好可以用来在if语句中表示条件。关于这些表达式的使用方法,还要在4.2:“逻辑运算符和逻辑表达式”中做比较详细的介绍。
图2-4中的只有一个分支的选择结构可以使用不含else部分的if语句表示:
if (<表达式>)
<内嵌语句>;
或者
if (<表达式>)
{
…...
}
即如果<表达式>的值不为0时执行<内嵌语句>或分程序,否则直接执行if语句后面的其他语句。
2.3.3 循环结构
while型循环结构可以使用while语句实现:
while (<表达式>)
<循环体>
其中的<循环体>可以是一个语句,也可以是一个分程序:
while(<表达式>)
{
…...
}
while语句的执行过程见图2-5。当表达式的结果不为 0时反复执行其循环体内的语句或者分程序,直到表达式的值为0时退出循环。在设计while型循环时要注意在其循环体内应该有修改<表达式>的部分,确保在执行了一定次数之后可以退出循环,否则就成了“死循环”,一旦程序进入这里,将永远在循环结构中兜圈子而无法结束。
do-while型循环结构可以使用do-while语句实现:
do
{
<循环体>
}while (<表达式>);
do-while型循环和while型循环最大的不同是do-while 循环的循环体最少执行一次,而while型循环的循环体可能一次也不执行。这一点可以很容易地从图2-5和图2-6的比较中看出。
除此而外,C++中还有一种for语句,用来实现一类比较复杂的循环结构。其格式为:
for (<表达式1>; <表达式2>; <表达式3>)
<循环体>
其控制流程见图2-10。
和while语句的情况类似,for语句的循环体也可以是一条语句,或者一个分程序。for语句最常见的用途是构造指定重复次数的循环结构。例如
for (i=0; i<10; i=i+1)
{
…...
}
用于实现重复10次的循环。虽然用while循环也可以构造出这样的循环,但使用for语句更简单、直观。特别是在处理数组时,大多数程序员都喜欢使用for语句。
2.4 伪代码前面介绍了利用流程图实现自顶向下的程序设计方法。实际上,在使用C++的程序员们中还流行着一种利用伪代码进行自顶向下程序设计的方法。伪代码使用C++的几种控制语句和自然语言结合起来描述算法,比画流程图省时省力,且更容易转化为程序。
[例2-2] 编出“验证”哥德巴赫猜想的程序。
算 法:对图2-9中的各程序模块作进一步的分解。这一次采用伪代码表示程序求精分解的结果。“验证”哥德巴赫猜想首先要解决如何生成和测试素数的问题。素数即质数,即除了1和自身之外没有其他因子的正整数。大多数素数都不能简单地根据某个数学表达式计算出来,只能通过逐个检验正整数是否符合素数的定义的方法得出。因此,可以先编写一个生成素数表的函数,用其生成给定范围内所有的素数并存放在一个素数表中,这样“求一个素数”或检查一个数是否素数的工作就可以转化为对素数表的查询了。
我们使用“筛法”来生成素数表,这是生成素数最古老,同时也是最有效的方法,由古希腊的哲学家兼数学家埃拉托色尼提出。因为准备验证的最大偶数小于M,所以用到的最大素数也不会超过M。因此,列出0到M之间的所有自然数:
0,1,2,3,4,5,6,7,8,...,M(1
然后将所有2的倍数,3的倍数,5的倍数,...,直到M/2的倍数均从表中删去 ( 置换为0 ) 即可。
我们用数组PrimeList存放生成的素数。按上法生成的素数表有一个特点,即如果PrimeList[x]≠0,则x为素数,否则x为合数。这样,就可以很方便地根据该素数生成素数,或者判别一个数是否素数了。我们将生成素数表的工作编成一个函数,其原型为:
void CreatePrimeList(int PrimeList[]);
用伪代码描述生成素数表的算法如下:
将PrimeList的各元素设置为2到M-1之间的自然数;
分别从表中去掉已经确定的各素数的倍数 ( 将其置换为0 ) ;
第1步可以用一个for型的循环实现:
for(i=0; i<M; i=i+1)
PrimeList[i] = i;
第2步稍微复杂些。我们使用工作变量i指示出最后确定的素数的位置,在表中将数组元素PrimeList[i]的倍数转换为0:
i = 2;
while(i<M/2)
{
for(j=i+1; j<M; j=j+1)
if(PrimeList[j]≠0且是PrimeList[i]的倍数)
PrimeList[j] = 0;
i = 下一个确定的素数的位置;
}
这里要注意的是,由于在转换的过程中表中会产生一些值为0的元素,所以在计算下一个确定的素数的位置时要跳过这些0:
i = i+1;
while(PrimeList[i]==0)
i = i+1;
将图2-9中的程序模块“生成下一个素数”也编为一个函数:
int NextPrimeNumber(int p,int PrimeList[])
{
…,..
}
其参数为当前的素数p,返回值为素数表PrimeList中的下一个素数。求下一个素数的方法很简单,从PrimeList[p+1]开始向后找到的第一个非0元素,即为所要求的素数。
综合以上分析结果和例2-1中的结构化程序框图,就可以编写程序了:
程 序,
// Example 2-2:"验证"哥德巴赫猜想
#include <iostream.h>
#define M 10001 // 定义验证范围
// 函数 CreatPrimeList(),生成素数表
void CreatPrimeList(int PrimeList[])
{
int i,j;
// 将PrimeList的各元素设置为从2开始的正整数
for(i=0; i<M; i = i+1)
PrimeList[i] = i;
// 分别从表中去掉已经确定的各素数的倍数(将其置为0)
i = 2;
while(i<M/2)
{
for(j=i+1; j<M; j=j+1)
if(PrimeList[j]!=0 && PrimeList[j]%PrimeList[i]==0)
PrimeList[j] = 0;
// 确定下一个素数的位置
i = i+1;
while(PrimeList[i]==0)
i = i+1;
}
}
// 函数 NextPrimeNumber(),求下一个素数
int NextPrimeNumber(int p,int PrimeList[])
{
p = p+1;
while(PrimeList[p]==0)
p = p+1;
return PrimeList[p];
}
// 主函数,在从4到M的范围内验证哥德巴赫猜想
void main()
{
int PrimeList[M]; // 说明存放素数表的数组
int x,p; // 变量x,偶数,p,素数
// 建立素数表
CreatPrimeList(PrimeList);
// 对从4到M的所有偶数验证哥德巴赫猜想
x = 4;
while(x<M)
{
// 检查偶数减去一个素数后的剩余部分是否仍为素数
p = PrimeList[2];
while(p<M/2 && PrimeList[x-p]==0)
p = NextPrimeNumber(p,PrimeList);
// 输出检查结果
if(p>=M/2) // 找到了一个不能分解为两个素数和的偶数
cout<<"Great discovery,Goldbach is wrong!"<<endl;
else // PrimeList[x-p]≠0,分解成功
cout<<"The even number "<<x<<"="<<p<<" + "<<x-p<<endl;
// 检查下一个偶数
x = x+2;
}
}
输 出,The even number 4 = 2 + 2
The even number 6 = 3 + 3
The even number 8 = 3 + 5
The even number 10 = 3 + 7
…,..
The even number 9998 = 31 + 9967
The even number 10000 = 59 + 9941
分 析,上述程序在4到10,001的范围内验证了哥德巴赫猜想,发现在这个范围内的所有偶数均可以分解为两个素数之和。当然,这还不能说明哥德巴赫猜想确实成立。鉴于这种方法只能检验有限个偶数,而偶数的个数是无限的,所以如果想做出一个“伟大的发现”,就只有指望在所检验的范围内能出现一个确实不能被分解为两个素数之和的偶数了 (此时可以宣布哥德巴赫猜想不成立,同样是解决了这个问题)。很遗憾,使用上面的程序,在M=32,000时仍然没能发现一个这样的偶数。超过这个限度,就要对程序的结构进行修改才能继续运行。
自学内容
2.5 结构化程序设计方法简介在计算机问世以后的最初几年,其价格非常昂贵,而运算能力很差,可靠性也很低。由于当时计算机的速度慢、内存小,加之为计算机编写软件主要使用机器指令代码或者汇编语言,所以当时研究程序设计方法的重点是如何通过运用一些编程技巧尽量节约内存空间,提高运算速度。后来虽然也出现了FORTRAN、ALGOL等高级程序设计语言,为提高程序员的算法表达能力和降低劳动强度提供了一定条件,但是由于这一时期计算机的主要任务是进行科学计算,而且程序的规模一般都比较小,因此从程序设计方法上来看并没有发生什么根本的变化。总的来说,程序设计被看成是一种技巧性很强的工作,程序员们大都采用手工工艺式的、精雕细凿的设计方法。
60年代以后,计算机硬件的发展速度异常迅猛,其速度和存储容量不断提高,成本急剧下降。但程序员要解决的问题却变得更加复杂,程序的规模越来越大,出现了一些需要几十甚至上百人年的工作量才能完成的大型软件,远远超出了程序员的个人能力。这类程序必须由多个程序员密切合作才能完成。由于旧的程序设计方法很少考虑程序员之间交流协作的需要,所以不能适应新形势的发展,因此编出的软件中的错误随者软件规模的增大而迅速增加,造成调试时间和成本也迅速上升,甚至许多软件尚未出品便已因故障率太高而宣布报废。当时人们认为,这是由于计算机的效率远远超过人 (程序员) 的效率造成的,而随着技术的发展,计算机的效率还可不断地提高,人的效率却无法有大的改进,因此由人编写的软件的规模和复杂度就会有一个上限,软、硬件之间的效率差别会越来越大,从而会限制计算机的发展。这就是通常所说的“软件危机”。
有危机就会有革命。1968年,E.W.Dijkstra首先提出“goto语句是有害的”,向传统的程序设计方法提出了挑战,从而引起了人们对程序设计方法讨论的普遍重视,许多著名的计算机科学家都参加了这场论战。结构化程序设计方法正是在这种背景下产生的。
结构化程序设计的基本观点是,随着计算机硬件性能的不断提高,程序设计的目标不应再集中于如何充分发挥硬件的效率方面。新的程序设计方法应以能设计出结构清晰、可读性强、易于分工合作编写和调试的程序为其基本目标。
结构化程序设计认为,好的程序具有层次化的结构,应该采用“逐步求精”的方法,只使用顺序、分支和循环等基本程序结构通过组合嵌套来编写(见2.2:“‘自顶向下,逐步求精’的程序设计方法”)。
与此同时,关于程序设计和软件生产的其他研究也在不断深入,例如数据抽象与模块化程序设计、程序正确性证明、程序自动生成以及研究大型软件生产方法的软件工程等。
今天,结构化程序设计方法、面向对象的程序设计方法、第4代程序设计语言、计算机辅助软件工程(CASE)等软件设计和生产技术都已日臻完善,计算机软、硬件技术的发展交相映辉,使计算机的发展和应用达到了前所未有的高度和广度。
2.6 C++的其他控制转移语句
C++提供的控制转移语句,除了授课内容部分所介绍的if-else语句、while语句、do-while语句和for语句以外,还有如下一些控制语句:
2.6.1 switch语句
switch语句用于实现多重分支,其格式为:
switch (<整型表达式>)
{
case <数值1>:
…,..
case <数值2>:
…...
case <数值3>:
…,..
default:
…,..
}
其中default模块也可省略。switch语句的执行过程是,首先计算整型表达式的值,然后将其结果与每一个case后面的数值常量依次进行比较,如果相等则执行该case模块中的语句,然后依次执行其后每一个case模块中的语句,无论整型表达式的值是否与这些case模块的进入值是否相同。如果需要在执行完本case模块以后就跳出switch语句,则可以在case模块的最后加上一个break语句,这样才能实现真正的多路选择。如果整型表达式的值与所有case模块的进入值无一相同,则执行default模块中的语句。带有break语句的switch多分支结构的框图见图2-11。
[例2-3] 编写一个函数,将百分制的学生成绩转换为优秀、良好、中等、及格和不及格的5级制成绩。标准为:
优秀,100-90分;
良好,80-89分;
中等,70-79分;
及格,60-69分;
不及格,60分以下。
算 法,我们使用switch语句构成的多分支结构编写这个函数。switch语句根据具体的数值判断执行的路线,而现在的转换标准是根据分数范围。因此,构造一个整型表达式old_grade/10用于将分数段化为单个整数值。例如对于分数段60-69中的各分数值,上述表达式的值均为6。再配合以在switch语句的各case模块中灵活运用break语句,即可编写出所需转换程序。
程 序:下面是分数转换函数的代码。要验证该函数,还需自行编写一个相应的中函数,并加上文件包含命令。这一工作留作上机作业。
// Example 2-3:将百分制的分数转换为5级制分数
int TranGrade(int old_grade)
{
int new_grade;
switch (old_grade/10)
{
case 10:
case 9:
new_grade = 1;
break;
case 8:
new_grade = 2;
break;
case 7:
new_grade = 3;
break;
case 6:
new_grade = 4;
break;
default:
new_grade = 5;
}
return new_grade;
}
分 析,该函数将百分制的分数值(0~100)转换为5级制成绩,1代表优秀,2代表良好,...,5代表不及格。 请注意switch语句的第1个case模块中没有任何语句(包括break),因此进入该模块时 (原成绩为100分) 将直接转入第2个case模块 (处理原成绩在90~99分之间) 中继续执行。
2.6.2 goto语句和语句标号
C++允许在语句前面放置一个标号,其一般格式为:
<标号>,<语句>;
标号的取名规则和变量名相同,即由下划线、字母和数字组成,第一个字符必须是字母或下划线,例如:
ExitLoop,x = x+1;
End,return x;
在语句前面加上标号主要是为了使用goto语句。goto语句的格式为:
goto <标号>;
其功能是打乱语句执行顺序,转去执行前面有指定标号的语句,而不管其是否排在当前语句之后。C++的goto语句只能在本函数模块内部进行转移,不能由一个函数中转移到另一个函数中去。由于结构化程序设计方法主张尽量限制goto语句的使用范围,因此在这里不对goto语句作过多的讨论,仅举一个例子说明在某些情况下使用goto语句可以简化程序设计,同时又不明显地降低程序的可读性:
while (<条件1>)
{
… …
if(<条件2>)
goto OutLoop;
… …
}
… …
OutLoop,… …
上面这种程序结构当然也可以不用goto语句,而使用“纯粹”的结构化程序模块实现,
t = 1;
while(<条件1> && t≠0)
{
… …
if(<条件2>)
t = 0;
else
{
… …
}
}
if(t≠0)
{
… …
}
OutLoop,…
但这段程序的可读性并不比使用goto语句时好多少 (甚至可能更糟),结构也复杂多了,还引入了一个辅助变量t。这说明,尽管按照结构化程序设计方法的说法滥用goto语句会使程序的结构变坏,但在特殊情况下可能恰恰相反。何时应该使用goto语句,何时应该避免使用goto语句,应该以所设计出的程序模块是否清晰易读为标准,不可胶柱鼓瑟,一概排斥使用goto语句。
2.6.3 break语句和continue语句
break语句的格式为:
break;
用于立即跳出包含该break语句的各种循环语句和switch语句的case模块。 在循环语句中使用的break语句一般应和if语句配合使用,例如:
while(<条件1>)
{
… …
if(<条件2>)
break;
… …
}
以上结构的框图见图2-12。
continue语句用于提前结束循环,可用于while,do-while和for语句中。其格式为:
continue;
continue语句的用法和break语句相似,均应和if语句配合使用。仍以while语句为例:
while(<条件1>)
{
… …
if(<条件2>
continue;
… …
}
其执行框图见图2-13。
其实,break语句和continue语句都是变相的goto语句。恰当地应用这些语句,可以使程序的表达比较清晰,同时仍然满足结构化程序的基本特征,每个程序模块只有一个入口和一个出口,可以自上而下地阅读。
2.6.4 exit()函数和abort()函数
exit()和abort()是C++的两个库函数,作用均为中止整个程序的运行。所不同的是,前者在结束程序的运行前,还会做一些善后工作,如将程序中的变量、对象等占用的内存资源释放掉;关闭所有已打开的文件等。而后者只是单纯地结束程序的运行。
在使用这两个函数时,均要求用一整数参数表示退出的原因。习惯上用0表示正常退出,非0值表示非正常退出。
如果在程序中使用这两个函数,必须在程序首部加上文件包含命令:
#include <stdlib.h>
调试技术
2.7 Developer Studio的文本编辑器
Developer Studio提供了一个优秀的程序文本编辑器,特点是同Developer Studio的其它工具(如调试器)的配合非常好,使应用程序的编辑修改和调试工作混为一体,非常方便。该文本编辑器不仅可编辑程序文本,还可编辑一般的文本文件和HTML Page。
启动文本编辑器非常简单,只要建立一个新文本文件,或打开一个已存在的文本文件,文本编辑器就会自动出现。
在文本编辑器中,用一闪烁的短竖线表示编辑位置,通过键盘输入的文字在此位置插入文本。用鼠标左键点击文本中的某个字符可以改变编辑位置。
文本编辑器的基本操作包括:
→:光标向后移动一个字符。
←:光标向前移动一个字符。
↑:光标向上移动一行。
↓:光标向下移动一行。
Home:光标移动到行首。
End:光标移动到行尾。
Ctrl+Home,光标移动到文件头。
Ctrl+End,光标移动到文件尾。
PgUp:光标向上滚动一屏。
PgDn,光标向下滚动一屏。
Ctrl+Y:删除行。
Del:删除光标右边字符。
Backspace:删除光标左边字符。
Ins:插入/改写方式切换。
Developer Studio的Edit子菜单还提供了一批高级编辑功能,大致可分为以下几类:
Undo和Redo,用于反悔对文本文件所做的修改
Undo:取消最近一次的编辑修改操作。
Redo:取消最近一次的“Undo”操作,即恢复被“undo”命令取消的修改操作。
(2)剪贴、复制、粘贴和删除
Cut:将选定内容复制到剪贴板,然后再从当前活动窗口中删除。“Cut”选项与“Paste”选项联合使用可以移动选定的内容。要选定文本内容,可将鼠标移到选定内容的开始位置,按下鼠标左键,然后拖动鼠标到选定内容的末尾放开鼠标左键。此时整个被选定内容将反白显示(黑底白字)。
Copy:将选定内容复制到剪贴板,但不从当前活动窗口中删除。“Copy”选项与“Paste”选项联合使用可以复制选定的内容。
Paste:将剪贴板中的内容插入到当前编辑位置。注意,需要先剪切或复制选定内容到剪贴板后,才能进行粘贴。
Delete:删除选定内容。删除以后,还可用“Undo”命令来恢复误删的内容。
Select All:选定当前正在编辑的文本的所有内容。
(3)查找和替换
Find:在正在编辑的文本中查找指定的字符串。
Find in Files:在多个文件中查找指定的字符串。
Replace:在正在编辑的文本中替换指定的字符串。
(4)书签
Go to:弹出“Go To”对话框,指定如何将编辑位置移到当前活动窗口的指定位置,如指定的号、地址和书签等。
Bookmarks:弹出“Bookmarks”对话框,以便设置或取消书签。书签用于在文件中做标记。
(5)高级选项
Advance:选择该项将弹出子选单,其中含有用于编辑或修改的高级命令,如增量式搜索、将选定内容全部转为大写或个写、显示或隐藏制表符等。
(6)断点设置
Breakpoints:弹出“Breakpoints”对话框,以便设置、删除和查看断点。断点用来通知调试器应该在何处或何时中断程序的执行过程,以便检查程序代码、变量和寄存器值。“Breakpoints”对话框的“Location”,“Data”和“Messages”选项卡分别用于设置位置断点、数据断点和消息断点。如果要设置条件断点,必须先设置位置断点,然后再设置中断程序执行的条件。
(7)编程指导信息
List Members:输入程序代码时,如果在变量名之后键入“.”或“(>”(即要访问某对象的成员),默认的处理是自动列表显示所有有效的成员名。这时,键入待输入成员的前几个字母即可从列表中选中该成员,按Tab键即可完成成员名的输入;也可通过滚动条找到待输入的成员,然后用鼠标左键双击成员名来完成输入。通过该菜单项也可得到此效果。
Type Info:在编辑源程序代码时,如果将鼠标指向某一标识符,默认的处理是显示所指变量、函数或方法等的语法。通过该菜单项也可得到此效果。
Parameter Info:在编辑源程序代码时,如果在输入函数名之后键入左括号,默认的处理是显示该函数的完整原型,并用黑体字显示其第一个参数。输入第一个参数值之后,接着会以黑体显示第二个参数,直至所有参数输入完毕。通过该菜单项也可得到此效果。
Complete Word,自动完成当前语句其余部分的输入。如果不能自动完成,则给出适当的提示来辅助用户完成。
程序设计举例
[例2-4] 计算,当通项时停止计算。
算 法,声明三个工作变量e,n 和u,分别用于存放已计算出的结果近似值、当前项序号和当前通项值,则伪代码算法为:
e = 1.0; n = 1; u = 1.0;
while (通项u大于等于10-7)
{
计算新的通项值 u = u/n;
将新通项值加到结果近似值上;
准备处理下一项 n = n+1;
}
程 序:
// Example 2-4:计算常数e的值
#include <iostream.h>
#include <math.h>
void main()
{
double e = 1.0;
double u = 1.0;
int n = 1;
while(u >= 1.0e-7)
{
u = u/n;
e = e+u;
n = n+1;
}
cout << "e = " << e << " ( n = " << n << " )" << endl;
}
输 出,e = 2.71828 ( n = 12 )
分 析,根据计算结果同时打印出的项数n,表明该级数收敛相当快,仅计算到前12项其截断误差便已小于10-7。
[例2-5] 使用do-while结构重新编写上题的程序。
程 序,
// Example 2-5:计算常数e的值
#include <iostream.h>
#include <math.h>
void main()
{
double e = 1.0;
double u = 1.0;
int n = 1;
do
{
u = u/n;
e = e+u;
n = n+1;
}while(u>=1.0E-7);
cout << "e = " << e << " ( n = " << n << " )" << endl;
}
输 出,e = 2.71828 ( n = 12 )
[例2-6] 求水仙花数。如果一个三位数的个位数、十位数和百位数的立方和等于该数自身,则称该数为水仙花数。编一程序求出所有的水仙花数。
算 法,显然,水仙花数要在100~999的范围内去找。我们对于这个范围内所有的数一一检验其是否符合水仙花数的定义:
for(n=100; n<=999; n=n+1)
if (n是水仙花数)
打印n的分解形式;
程 序:
// Example 2-6:打印所有的水仙花数
#include <iostream.h>
void main()
{
int n,i,j,k;
for(n=100; n<=999; n=n+1)
{
i = n/100; // 取出n的百位数
j = (n/10)%10; // 取数n的十位数
k = n%10; // 取出n的个位数
if(n==i*i*i+j*j*j+k*k*k)
cout <<n<<" = "<<i<<"^3 + "<<j<<"^3 + "<<k<<"^3"<<endl;
}
}
输 出: 153 = 1^3 + 5^3 + 3^3
370 = 3^3 + 7^3 + 0^3
371 = 3^3 + 7^3 + 1^3
407 = 4^3 + 0^3 + 7^3
分 析,在程序中利用了C++的整数除法和求余运算从一个3位数中分解出其个位、十位和百位数。这些运算符的特点请参看4.1:“算术运算符和算术表达式”。
[例2-7] 编写一个可以打印任何一年的日历的程序。
算法分析,打印日历的算法必需解决两个问题,一是这年是否闰年(闰年的二月份有29天,平年的二月份有28天),二是这年的元旦是星期几。确定某年是否闰年的算法为,如果某年的年份不能被4整除,则是平年。如果能够被4整除,再看其是否能被100整除,不能被100整除的肯定是闰年。在既能被4,也能被100整除的年份中,还能被400整除的是也是闰年,否则是平年。上述算法可以用伪代码表示为:
leap = NO;
if(year能被4整除)
{
leap = YES;
if(year能被100整除)
{
leap = NO;
if(year能被400整除)
leap = YES;
}
}
确定某年的元旦是星期几可以这样考虑:我们知道,平年一年有365天,而一年大约有52个星期。实际上,52个星期共有52×7=364天,这也就是说,如果某年的元旦是星期一,则第二年的元旦整整过了52个星期又一天,因此应该是星期二。当然如果这一年是闰年,则在2月份多了一天,所以下一年的元旦也会顺延到星期三。因此,如果知道了某个基准年份的元旦是星期几,就可以用这种方法推算出其后任何一年的元旦是星期几来。例如,我们恰好知道1900年的元旦是星期一,因此如果year是1900年之后的某个年份,则其元旦是星期几可以按如下方法计算:
计算出year和1900年之间所差的年数,n=year-1900;
因为每年都是52个整星期多1天,因此n年多出的天数还是n天;
在n天上再加上1900年到year年之间因闰年多出来的天数,n=n+闰年数;
将多出来的天数n用7除,求其余数,即星期几:n=n%7;
再加上1900年元旦的星期序号(1),n=n+1;
如果n≥7则再减去7即为year年元旦的星期序号(0表示星期日):n=n%7。
在上面的算法中“闰年数”可以简单地用 (n-1)/4计算。由于2000年正好是闰年(为什么?),所以这个简化公式可以一直用到2099年!
程 序:
// Example 2-7:打印年历
#include <iostream.h>
#define YES 1 // 定义符号常数"是"
#define NO 0 // 定义符号常数"否"
// 函数 isleap(),判断某年是否闰年
int isleap(int year)
{
int leap = NO;
if(year%4==0 && year%100!=0 || year%400==0)
leap = YES;
return leap;
}
// 函数 week_of_newyears_day(),求元旦是星期几
int week_of_newyears_day(int year)
{
int n = year-1900;
n = n+(n-1)/4+1;
n = n%7;
return n;
}
// 主函数,打印年历
void main()
{
int year,month,day,weekday,len_of_month,i;
cout << "Please input year,";
cin >> year;
// 打印年历
cout << endl << year << endl; // 打印年份
weekday = week_of_newyears_day(year); // 求元旦是星期几
for(month=1; month<=12;month=month+1) // 打印12个月的月历
{
cout << endl << month << endl;
cout << "---------------------------------" << endl;
cout << "SUN MON TUE WED THU FRI SET" << endl;
cout << "---------------------------------" << endl;
for(i=0;i<weekday;i=i+1) // 找当月1日的打印位置
cout << " ";
if(month==4 || month== 6 || month==9 || month==11)
len_of_month = 30;
else if(month==2)
{
if(isleap(year))
len_of_month = 29;
else
len_of_month = 28;
}
else
len_of_month = 31;
for(day=1;day<=len_of_month;day=day+1) // 打印当月日期
{
if(day>9)
cout << day << " ";
else
cout << day << " ";
weekday = weekday+1;
if(weekday==7) // 打满一星期应换行
{
weekday = 0;
cout << endl;
}
}
cout << endl; // 打完一月应换行
}
}
输 入:Please input year,2000
输 出:
1
---------------------------------
SUN MON TUE WED THU FRI SET
---------------------------------
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
2
---------------------------------
SUN MON TUE WED THU FRI SET
---------------------------------
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29
( (
12
---------------------------------
SUN MON TUE WED THU FRI SET
---------------------------------
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
分 析,该程序的函数虽相对比较多,但其结构比较简单。主程序中的基本结构可以用伪代码表示为:
输入年份year;
确定该年的元旦是星期几;
for(month=1; month<=12; month=month+1)
打印month月份的月历;
而打印某月份的月历又可以用伪代码表示为:
打印月历头;
确定该月份的天数;
确定当月1日的打印位置;
for(day=1;day<=当月天数;day=day+1)
在适当位置上打印日期;
而所谓在“适当位置”上打印日期则包括打满一行(一星期)后自动换行,以及打印日期时所使用的格式等。
单元上机练习题目
1.编写计算阶乘 n!的程序。
2.编写程序求斐波那契数列的第n项和前n项之和。斐波那契数列是形如
0,1,1,2,3,5,8,13,...
其通项为:
F0 = 0;
F1 = 1;
Fn = Fn(1+Fn(2。
3.编程求 ,其中。
提示:结束条件可用 ,其中u为通项。
4.选做题:用弦截法求一元方程在区间之间的一个根。
提示:考虑当区间足够小,在此区间中方程仅有一个单根的情况,如图2-14所示。
此时如和异号,则可用两点间直线公式求出x2:
然后用x2代入原式求出f(x2),判断f(x2)与f(x1)和f(x0)中的哪一个同号,就用x2和f(x2)代替之,即如果f(x2)和f(x0)同号,就用x2和f(x2)代替x0和f(x0),反之用x2和f(x2)代替x1和f(x1),然后再继续上述过程直至|x1(x0|小于给定的误差控制值。