第2章程序设计的基本方法
对于初学者来说,写出一个满足题目要求的程序并不是一件简单的事情。明明已经了解和掌握了C语言中各种语句的语法和语义以及C程序的基本结构,对题目的要求似乎也都清楚,但就是不知道怎样写出一个满足题目要求的程序:或者是程序运行所产生的结果不对,或者是程序一运行就崩溃,或者有时感觉根本就无从下手。
出现这种情况是很正常的。编程是用程序设计语言描述一种可以让计算机准确执行的计算过程,以期完成所需的计算。这里涉及内容和表达两个方面。所谓内容就是要有明确的解决问题的思路和方案,所谓表达就是使用程序设计语言对问题的解决方案,包括计算的过程和步骤、所采用的算法和数据结构等,进行准确的描述。大部分初学者在程序设计的学习过程中首先把注意力集中在对程序设计语言本身的学习上,需要了解和掌握程序设计语言的基本要素、熟记各种关键字和各种语句的语法、含意和基本使用方法,因此还没有足够的时间和精力去学习和掌握使用这些语句去编写程序的方法和技巧,更难以关注如何从任务的要求入手,构思一个合理的解决方案,以及如何准确有效地实现这一方案,保证所完成的程序正确可靠地运行。这是学习过程中的一个必然阶段,就好像人们首先要学习和掌握写字和造句,然后才能练习写文章一样。但是,如果注意掌握正确的学习方法,在学习程序设计语言的同时注意学习程序设计的方法和对程序设计语言的运用,则可以收到事半功倍的效果。
和学习写作需要掌握遣词造句、布局谋篇、起承转合相类似,学习程序设计也要掌握一些专门的方法。与使用自然语言写作相比,程序设计语言的词汇和语法都要简单得多,写程序的方法和步骤也更加规范和易于掌握。因此,经过一定的学习和练习,编写符合题目要求的程序将不再是一件很困难的事情。
2.1 程序设计的基本过程
和解决任何其他问题一样,在进行程序设计时,需要首先明确的是需要解决的问题和已知的条件。只有在这两者都明确的情况下,才有可能到从出发点通向目标的正确道路。在程序设计中,需要首先考虑的问题是,程序需要完成的任务是什么,已知的条件和数据有哪些,从哪里获得这些数据、在计算过程中有哪些限制。在明确了这些基本要素之后,才能开始寻实现目标的方法,选择和确定适当的算法和数据结构,并且考虑如何检验和证明所实现的程序是否符合设计目标的各项要求。在这些问题都弄清之后,才能进一步考虑使用什么样的语句进行编码,把上述思想转化为程序。根据这些要点,程序设计的基本过程可以分为问题分析、设计、编码、调试和测试等几个阶段。
问题分析阶段的主要工作是明确程序所要完成的任务目标及其工作环境和限制。设计阶段主要是明确问题求解的基本思路和步骤,将任务目标进一步细化成为对于程序的具体要求,并在此基础上确定实现技术的基本要素,如数据结构、算法、程序结构等。编码过程是程序的具体实现,是将问题求解过
程和步骤的描述由自然语言或其他不能由计算机执行的表达方式转换为计算机所能理解的形式,将由形式化或非形式化方式完成的设计转换成用编程语言完成的对计算过程的步骤和细节进行具体描述的代码。调试阶段的任务主要是发现和改正编码过程中的错误,保证编码过程,也就是从设计到程序的转换工作的正确性,保证程序能够正常运行。测试阶段的工作目的是检查程序的功能和性能是否符合目标的要求,能否满足所规定的各项指标。
尽管所有的程序设计工作基本上都要经过这几个阶段,但不同规模的程序在每一个阶段所需完成的任务的复杂程度是有很大差别的。例如在大型的软件中,与问题分析工作相对应的工
作被称为需求分析,在需求分析结束后需要产生一份详细的需求分析报告。对于更复杂的系统的需求分析已经创造出了一个新的术语:需求工程。在大型软件中,设计工作被进一步细化分解为概要设计和详细设计,测试也被划分为单元测试、模块测试、功能测试、性能测试、回归测试等很多种。这些在软件工程领域都有专门的论著和教科书,进行深入的讨论和分析。对于几十行、上百行或是再稍大一些的程序来说,事情远不需要这么复杂,各个阶段的工作都要简单得多。很多时候,这些阶段之间的分界并不是非常明确的,很多工作有可能是交叉进行的。但是,即使对于一个不大的程序来说,也仍然有许多问题需要仔细地考虑,分阶段地处理。因此,把程序设计的过程分成上述这些阶段是很必要的。对于初学者来说,这有利于掌握有条有理、按部就班地分析和解决问题的方法,养成良好的习惯。
在程序设计前期的分析和设计阶段,特别需要注意的是阶段性的工作结果一定要完整、细致、具体。所谓完整、细致和具体,主要的判断标准有三项:第一是工作的结果能够满足其前导阶段的要求。例如,对功能和性能的分析结果应当能够与题目的各项要求一一对应,设计方案应当能够实现分析结果中对功能和性能的要求。第二是工作结果能够为后续阶段工作提供具体的指导。例如,分析结果应当明确地列举所有需要实现的功能和性能指标,以便在方案设计时有所依据;方案设计应当清晰完整,对程序的结构以及算法和数据结构的描述应当准确、完整,以便编码时可以一一对照,而不需要在这些方面再重新构思。第三是工作结果能够为后续阶段工作提供具体的检验标准,也就是说,我们可以根据前期工作的结果,逐项检查后续阶段的工作是否满足要求、是否合格。例如,我们可以根据设计方案中关于解题思路、计算步骤、算法以及数据结构的描述来检查编码的内容是否符合要求,是否完整地实现了设计方案所规定的各项任务,也可以根据分析结果来检查设计方案是否完整地体现了对功能和性能的各项要求。我们还应该可以根据分析结果来确定测试数据的构造原则,并据此来构造相应的测试数据,检验整个程序的功能和性能,并能确保在通过了这些测试后,程序就可以满足题目所规定的各项任务和要求。所有上述各项,都是概念含混、用词模糊、叙述笼统的分析和设计结果所无法实现的。
有些人编程时往往不对问题进行认真的分析,不对解题思路进行认真的思考,也没有仔细的方案设计,而是直接使用编程语言思考如何进行编码。他们首先考虑的往往不是程序的第一步应该做什么,
第二步应该做什么,而是第一个语句应该怎么写,第二个语句应该怎么写。这种缺少对于问题宏观把握的做法混淆了不同阶段的任务,不仅使得编程人员需要同时面对很多不同性质的问题,而且需要使用他们还不很熟悉的编程语言来进行思考和做出决策。这种做法对于简单的小问题还可以应付,对于稍微复杂一点的问题就会很难把握,往往事倍功半。为避免出现此类情况,应该在开始学习程序设计时就重视编程工作的阶段性,养成踏踏实实、循序渐进的工作习惯。在编码之前的各个阶段,应当使用中文这种我们最熟悉的自然语言思考,以保证对问题理解准确、描述清楚,为后续阶段的工作打下坚实的基础。
2.2 问题分析怎样写代码 自己做编程
问题分析是程序设计的第一步,其目的是理解题目的要求,明确程序的运行环境和方式,以及相关的限制条件。问题分析的基本内容包括确定程序的功能和性能、程序的输入输出数据的来源、去向、内容、范围及其格式,程序的使用者、调用方式、人机交互要求,与其他程序的关系和交互方式,对通用性的要求和扩展的可能,以及性能和其他对程序的特殊要求和限制,如程序所占用系统资源的数量、对输入命令的响应速度等。在进行问题分析时需要注意的是,不但要理解题目字面的意思,更要深入分析题目字面中隐含的内容,要准确、完整、全面地理解题目的要求。
2.2.1 对程序功能的要求
对于一般的程序,特别是对于练习题一类的小程序来说,程序的功能要求会在程序的任务说明中很明确地给出。对于为解决某项实际任务而设计的程序,其主要功能可能会明确地给出,也可能会隐含地给出,但辅助功能以及具体要求的细节往往需要通过对实际问题的具体分析才能获得。有时程序的主要功能比较复杂,只凭文字的描述还不足以准确地界定,这就需要通过一些示例来进一步阐述。这时,编程人员就需要通过对问题的描述以及相关的示例分析来明确任务对程序主要功能的要求。对程序主要功能的理解是否全面、准确、具体,是后续工作是否正确和顺利的关键。所谓全面,就是要尽量考虑到问题涉及到的所有方面,以及所有可能出现的情况。在对主要功能进行描述时,要尽量避免使用“主要是”、“基本上”、“等等”这样一些不精确的词句,而要将程序所应具备的所有功能,无论大小,一律一一列出,勿使遗漏。此外,还应考虑到对程序隐含的和潜在的要求。例如,在实际问题中,除了要考虑到任务本身直接的要求外,还应该考虑到程序在使用过程中可能出现的各种要求以及可能遇到的情况,包括用户的使用方式、可能的运行环境、潜在的对程序升级和扩展的要求等。对于练习题,则除了认真考虑题目中给出的每一个条件字面上的意思之外,还需要考虑其中隐含的内容。所谓理解准确,就是要避免理解上的误差。这其中特别要注意的是一些需求的细节和边界条件。例如取值的范围是开区间还是闭区间、所要求的解是否是唯一的、对于多解的问题,需要只生成一个任意的解还是生成全部的解、以及输入/输出数据的准确格式和要求等。所谓具体,就是要尽量避免过于笼统含混的说法,尽量将程序的主要功能用具有一定限制和可操作性的方式进行描述,以便于后续的工作。例如,―获取系统运行状态‖就是过于笼统的说法,而―获取当前CPU的使用率和内存的占用率‖
就是更加具体的描述。除了定性的要求之外,应当尽量使用定量的要求。例如,给出参数的取值范围以及对计算结果的精度要求就比仅仅说明参数和结果的数据类型要更加明确,说明―数据吞吐量不低于2MB/s‖要比―要有很高的数据吞吐量‖更加具体。
2.2.2 对程序性能的要求
对于程序性能的要求,可以用对系统资源的占用来衡量。程序运行所需要的最基本的系统资源是CPU时间和存储空间。有一些任务对于程序性能有着明确的要求,例如,要求程序运行的时间不超过1秒钟,占用内存空间不超过32MB。而多数小型编程任务,特别是练习题一类的程序,因为程序任务简单,运行时所占用的资源微不足道,一般都没有给出对程序性能的明确要求。当然,有时一些看似很简单的问题,有可能需要占用很多的系统资源。也有些问题在使用一些效率不高的算法时,所需要占用的系统资源明显超过了合理的范围,使得程序或者无法运行,或者在有限的时间内无法得出计算结果。面对这类问题时,对程序性能的考虑就成为对问题分析的一项重要工作了。
对系统资源的占用,受两方面因素的影响。一个因素是问题的规模,另一个是程序设计和实现时所选择的算法、数据结构以及代码的结构。一般而言,一个程序对系统资源的占用随问题规模的增大而增加。例如,对一个大的图像进行压缩所需要的计算时间和存储空间一般都要大于较小的图象。而对系统资源占用随问题规模增加的速度,取决于所选择的算法和数据结构。用算法分析的术语来讲,就是
说不同的算法具有不同的计算时间复杂度和存储空间复杂度。例如,如果一个算法具有O(n)的时间复杂度,也就是说其计算所需的时间与问题规模n成正比,当问题的规模增加到原来的10倍时,它所需要的计算时间约等于原来的10倍。而如果一个算法具有O(2n)的时间复杂度,当问题的规模增加到原来的10倍时,它所需要的计算时间约为原来的1024倍以上。我们无法改变需要求解的问题的规模,但是有可能设计和选择合适的、具有更高效率的算法,使程序满足题目的要求。因此,对程序性能的要求实际上是对所选择的算法提出
了要求和限制。
2.2.3 程序的使用方式和环境
使用方式是问题分析的一项重要内容,涉及人机界面、输入/输出数据及格式、与其他系统的交互、以及使用环境和人员等方面。在问题分析阶段,需要明确程序的基本使用方式,例如程序是在字符终端上通过字符界面的命令方式被使用,通过图形界面的方式被使用,还是作为后台服务程序被使用;输入数据是通过命令行参数传递,还是由程序通过文件或图形界面上的控件读入,抑或是使用给定的库函数获取;输出数据是写到标准输出上,还是写到指定的文件中;输入/输出数据的编码是二进制方式的还是正文的,数据的类型是整型数还是浮点数;以及数据的组织方式,数据之间的分隔符等。程序的使用者以及程序的生命周期对于程序的分析和设计,以及其他阶段的工作也有很大的影响。程序的
生命周期是指从任务的提出到程序不再被使用和维护的全部时间。不同程序的生命周期是不同的。例如,课程作业习题的生命周期到程序提交并评测完毕,取得了分数就结束了;一个自己常用的工具程序可能会跟随自己几周、几个月、或者几年的时间;而一个商业性的程序,其生命周期可能会持续更长的时间。程序生命周期的长短直接关系到设计者需要对它在测试和优化上所花费的资源和精力。为大量非专业的使用者设计的、具有很长的生命周期、需要经常维护和升级的程序,在功能、性能、程序的可靠性、可扩展性、对错误的处理等工作的复杂和细致程度方面都会与由设计者本人或者少数专业人员使用的程序、或者只是作为练习题的答案或者临时使用的工具的程序有很大的不同。此外,还需要考虑程序运行所在的计算平台,包括硬件系统和操作系统,以及它们所能提供的系统支持。尽管一般的应用程序基本上不与操作系统直接打交道,但是至少一些基本数据类型的实际长度会受CPU结构的影响,数据的输入/输出会涉及到操作系统所提供的文件格式、以及对文件的基本访问和控制功能。当需要使用一些比较复杂的系统功能,例如多进程/多线程的创建、进程间的通信、内存的管理和设备的控制等等时,就更是需要直接和操作系统进行交互。从功能方面看,不同操作系统所提供的对一些常用功能,例如对文件的打开和关闭、对数据的读写等的支持比较类似,有些已经被封装在C语言的库函数中。但是即使是对这些功能的支持,不同的系统在实现的细节上仍然可能有一些差别,并且这些差别有可能影响到程序执行的正确性。例如,对于ASCII文件中的换行符,在Unix/Linux上的表示方法就与Windows/Dos不同。对更复杂的功能来说,差异可能就更大了。因此,当程序有可能在不同的平台上运行时,必须要考虑不同系统的这些差异,并采取相应的处理方法。
尽管使用方式和环境不是对程序功能要求的主要部分,但在程序的实现中,有时其所占的比重会大于程序的主要功能本身。这一点在一些通用的实用程序中表现得特别明显。有些时候,实现程序的主要功能的代码所占的比重远远小于为适应不同的使用环境和使用方式而产生的代码所占的比重。例如,一个最简单的可以完成基本功能的Web服务器,在仅使用标准库函数的情况下,其所需要的代码量不过几百行。然而,一个功能完善,可以适用于多种使用环境、满足不同要求、具有灵活的可配置功能的Web服务器的代码量会远远超过这一规模。广泛应用的开源Web服务器Apache,其2.0版的源代码包括.c和.件共670个,总共约有260000行代码,这其中有相当一部分是用于处理不同应用环境和使用方式的需求以及各种辅助功能及其配置的。
2.2.4 程序的错误处理
错误处理是指程序在运行时遇到各种不正常的情况时所应做出的反应。程序运行时常见的错误包含下面三大类:一是用户在使用中造成的错误,二是程序运行环境中出现的错误,三是程序设计或实现中的漏洞所导致的程序运行错误。我们不能假设程序始终运行在正确的环境下,不能假设使用者在使用程序时会不犯错误,也不能假设我们的程序自身没有一点错误。例如,
网络连接可能由于网络设备的故障而无法建立,用户可能会在调用程序时给出错误的参数,指定的输入文件可能不存在或者打不开,输入文件中数据的类型或数值可能是错误的,程序在输出数据时可能
会由于权限问题或存储空间问题而无法打开指定的文件或无法写入数据,程序在运行时可能会由于地址越界问题而访问了不存在的存储空间,等等。所有设计规范的软件都会对已知的或未知的错误做出适当的处理和防范。例如,当用户试图使用编辑工具打开一个不存在的文件时,程序会报告该文件不存在,并允许用户重新输入文件名。这就是对已知类型的使用错误的一种处理方式。著名的Windows系统的蓝屏问题则是对系统自身产生的未知类型、且无法正常恢复的运行错误的一种处理方法。错误处理所需要做的就是在出现错误的情况下做出恰当的反应。完善的错误处理机制是一个相当复杂的问题,有时甚至连一些比较成熟的软件在这方面也未必做得很完美。例如,很多编译系统对于用户程序中一般的语法错误可以给出比较准确的定位,但是对于象括号不匹配这样的错误就往往给出不正确的错误信息,有些甚至会停止运行。
对错误处理的复杂和完善程度取决于程序的性质、规模、重要性、及其用户。对于初学者来说,重要的首先是要有对程序在运行时有可能出现错误的预期和防范意识,其次需要知道什么是对于不同性质的错误的恰当处理,然后,需要根据任务的要求和程序的重要性,在一些关键的地方进行错误的检测和响应。对于比较简单的程序,包括练习题和在小范围内使用的简单工具等,首先需要对使用中最容易出现的错误,特别是程序的被调用方式和输入的数据的正确性进行检测,在错误出现时进行适当的处理,并且向使用者报告提示信息。这是对任何一个程序,哪怕仅仅是为自己使用的小型工具程序都应当做的。例如,假设一个程序在运行时需要从命令行上读入一个给定范围内的整数,程序应该在
一开始就检查这个参数是否在命令行上给出了,这个参数是否是一个整数,以及这个整数的值是否在规定的范围内。如果发现其中有任何一项错误,则程序需要输出错误提示信息,告诉用户程序的正确调用方式以及参数的类型和范围,然后结束运行。其次,程序应当对运行中可能出现的错误,如网络连接无法建立、输入文件无法打开、计算结果溢出、地址越界、除数为0等进行适当的检查和防范,并尽可能地自动对发现的错误进行处理。在无法自动处理时,也需要向用户报告出错信息,以方便对程序的维护和修改。应当尽量避免程序在没有任何提示的情况下就停止运行甚至崩溃,并因此导致数据丢失、服务中断,而且没有为程序的维护和更新留下任何有用的信息。
2.2.5 程序的测试
在对问题的分析完成之后,除了对问题本身的理解之外,还需要考虑对程序的测试。虽然程序只有在完成后才能进行测试,但关于程序测试的考虑应该在问题分析阶段就开始。对于小的程序,我们可以只考虑如何对程序的整体进行测试。对于功能和结构较为复杂的程序,则在整体测试之前还需要考虑如何根据程序的功能和结构,对程序的各个部分进行单独的测试。认真深入地考虑对程序的测试有助于检验和促进对问题的分析工作。实际上,只有认真思考过如何对程序进行测试,才能更深刻、更全面地理解题目或任务中各项要求的含义。在问题分析阶段对测试的考虑主要集中在四个方面,即程序测试的内容、测试的方法、测试所使用的数据、以及测试所使用的工具和环境。测试内容应当覆盖对程序功能和性能的全部要求。这些内容应当全面地体现在测试方法和测试数据中。测试的方法取决于
程序的类型和规模。对于小型的面向字符终端的计算和数据处理型程序,在基本测试中多采用“黑盒测试”的模式,不考虑程序的内部结构和实现方法,只根据对程序功能和性能的要求,设计测试数据和测试方法。测试数据是测试工作中的重要工具,对测试数据应规定其类型、规模和生成方法,说明预期结果的生成方法、结果正确性的判断方法、以及测试过程的具体操作流程。测试数据应该完整全面,应该包括各种典型的正常输入数据、可能出现的极限数据、以及应该处理或做出响应的错误数据。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。