300 likes | 463 Views
第 8 章 程序开发过程. 学习目的: ① 理解软件开发的一般过程; ② 了解软件测试方法; ③ 应用 VC++ 的 IDE 熟练调试程序。. 8.1 软件开发方法概述 8.2 软件设计 8.3 软件编码 8.4 软件测试与调试 8.5 程序运行效率. 8.1 软件开发方法概述.
E N D
第8章 程序开发过程 学习目的: ① 理解软件开发的一般过程; ② 了解软件测试方法; ③ 应用VC++的IDE熟练调试程序。 8.1软件开发方法概述 8.2软件设计 8.3软件编码 8.4软件测试与调试 8.5程序运行效率
8.1 软件开发方法概述 自从1968首次提出“软件工程”的概念以来,软件的生产过程一直是一个热门话题,“软件”从最原始的单纯的程序、发展到由程序和设计说明组成的所谓程序系统、乃至后来的软件工程(程序+文档+数据)。其间,无论是开发任务、程序设计语言,还是软件的规模和开发技术与手段都有了质的飞跃,人们越来越认识到一个成功的计算机软件的研制不是一蹴而就的,软件开发需要科学的理论、先进的技术和规范化的管理。时至今日,程序开发已经远远不是传统意义上的程序设计,而变成了涵盖程序设计、文档编制、多种先进开发技术与手段,以及现代软件管理技术的软件工程。 8.1.1软件生存周期 8.1.2软件开发方法
8.1.1软件生存周期 软件的研制,从问题的提出,经过开发、使用、维护、修订,直到最后终止使用或被另一软件取代,如同生命体从孕育、出生、成长,到最后消亡,软件整个状态变化的过程称为生命周期(或生存期)。 软件工程国家标准—计算机软件开发规范(GB8566-88)中将软件生命周期划分为8个阶段: 可行性研究与计划 需求分析 概要设计 详细设计 实现(包括单元测试) 组装测试(集成测试) 确认测试 使用和维护
问题定义 计划阶段 可行性研究 需求分析 总体设计 开发阶段 详细设计 编 码 测 试 维护阶段 运行维护 8.1.2软件开发方法 开发过程依照固定顺序进行,各阶段的任务与工作结果如图所示。该模型适用于需求明确、开发技术比较成熟、工程管理严格的场合,其缺点是由于任务顺序固定,软件研制周期长,前一阶段工作中造成的差错越到后期越大,而且纠正前期错误的代价高。 1 瀑布模型
需求分析 软件设计与编码 软件运行与测试 问题描述 NO 用户满意否 交用户使用 8.1.2软件开发方法 从一组简单的基本用户需求出发,首先建立一个满足基本要求的原型系统。通过测试和运行原型系统,有用户提出进一步细致的需求,然后修改和完善原型系统,反复进行这个过程直到用户满意为止。该模型适合于开发初期用户需求不甚明确,相关技术和理论需要不断研究、反复实验,以及开发过程需要经常与用户交互的场合,学习或研究类软件的开发常用此法。由于用户在整个软件开发过程中都直接参与,因此最终的软件产品能够很好地满足用户的需求。 2 渐进模型
演 化 维 护 确 认 实 现 设 计 分析 8.1.2软件开发方法 该模型主要用于面向对象软件技术开发项目,其特点是各项活动之间没有明显的界限,由于面向对象技术的优点,该模型软件开发过程与开发者对问题认识和理解的深化过程同步。该模型重视软件研发工作的重复与渐进,通过相关对象的反复迭代并在迭代中充实扩展,实现了开发工作的迭代和无间隙,该开发过程分为:分析、设计、实现、确认、维护、演化。 3 喷泉模型
8.2 软件设计 设计阶段是软件开发的重要环节,其主要任务是对软件总体结构、算法及其具体实现进行描述,由概要设计(亦称为总体设计或结构设计)和详细设计(亦称程序设计)组成 。 8.2.1 概要设计 8.2.2 详细设计
8.2.1 概要设计 概要设计决定软件系统的层次结构,最终成果为概要设计说明书,该说明书描述软件系统的基本处理流程、组织结构、功能分配、模块划分、接口设计、运行设计、数据结构设计和出错处理设计等方面的规定。 1 模块化 将软件分解成若干个可以单独命名和编址的组成部分,这些组成部分设计完成后,将它们以某种方式相互连接可形成满足设计需求的软件系统。 可分解性 复杂软件系统模块化的基础和前提。将复杂系统划分成不同模块,这些模块的复杂程度降低,可再继续划分为更简单的模块,如此下去直至各模块足够简单,易于实现。 可组合性 划分得到的模块实现后至少应能按照某种方式组合在一起,形成其上一层模块,最终构成整个软件系统,否则模块化就无意义。从长期考虑,划分出的模块应具某种通用性,以至于还能由它们构造出其它软件系统。 可理解性 模块的可理解性的重要性是显然的,只有可理解才能便于实现,只有可理解才能便于重用,只有可理解才能便于维护。
A B B B C D E G H D D D F F F D 8.2.1 概要设计 软件系统模块化应按某种够符合人类认知习惯的规则进行,得到的模块及其间的相互联系将反映出这种划分规则,模块及联系以图示方式表示出来,可以给出直观形象的软件系统内部各组成部分和层次结构。 2 概要设计 进行软件层次结构设计的方法很多,常见的有SD(Structured Design,结构化设计方法)、SC(Structure Chart,程序结构图)、Jackson方法等,它们都给出相对规范的设计步骤、描述方法等。
8.2.2 详细设计 在概要设计阶段得到软件系统的总体结构、各模块功能及其相互关系后,接下来的软件设计工作就是需要考虑如何实现每个模块的具体功能,这个工作阶段称为软件的详细设计阶段,通常也称为程序设计。详细设计阶段软件开发的主要任务就是对概要设计中每个模块的功能进行分析,建立每一个待实现功能的数学模型,将实际问题转化为数学问题,然后选择或制定解决相应数学问题的算法,并将该算法以适当的方式(比如流程图)描述出来,通过具体的描述形式直观、明确地反映程序设计思想,以待进入编码阶段。 详细设计阶段的成果主要为软件系统的详细设计说明书,该说明书描述了软件系统各层次中每一个程序(模块、函数等)的设计细节,包括功能描述、模块性能、实现算法及其逻辑流程、接口定义、存储分配、测试计划、待解决问题等等各方面的信息。关于软件详细设计的主要思想、原理、步骤和过程等方面的内容第1章中做了详细讲解,此处不再重复。
8.3 软件编码 8.3.1 程序设计方法 8.3.2 程序设计语言 8.3.3 编码风格
8.3.1 程序设计方法 目前流行的程序设计方法有很多,但真正具有广泛意义的是结构化程序设计、面向对象程序设计以及20世纪90年代后逐渐发展起来的基于构件的程序设计方法。这些方法各有自己的特点,面向对象技术和构件技术是目前程序设计领域的热点,但是,仅从程序编码的角度讲,它们主要的特长在于程序的组织结构、信息封装以及软件重用方面,因此它们对于规模足够小模块的编码并没使人得到更多教益,反而是结构化程序设计方法对面向对象程序设计中每个小模块(成员函数)的设计起到关键作用。
8.3.2 程序设计语言 从理论上讲,对于设计阶段的输出,无论采用哪一种风格的设计方法,都可以用任何一种程序设计语言来编码实现,但实际上对于具体的任务和设计风格,我们总可以在众多的编程语言中挑选出一种最适合的,使用它能够在程序运行效率、开发效率、软件可维护性等方面达到令人满意的折衷。
8.3.3 编码风格 易理解 1 关于名字 2 关于注释 int MyFunction(char *pName, int Mark) { // Function : Demonstrate how to write comment // Parameter : pName, the name of a student // Mark, the score of student pName // Global : xxxx // Algorithm : yyyyyyy // Writer : Wangming // Date : 2002-8-9 // note : xxxx … … }
8.3.3 编码风格 缩进可以显示出程序的层次,空行可以表示出程序的段落感。同一结构的多条语句间保持同样的缩进,不同结构的语句模块之间隔以空行等以提高程序的可读性。 while(…) { if(…) { … } else if(…) { … } else { … } … } 3 空行与缩进 4 语句结构 语句中表达式的书写要符合人们的习惯,必要时添加空格或括号,提高表达式易读的易读性。 if( ((x-8) & y) || 0x16) 不要写成 if(x-8&y||0x16)
8.4 软件测试与调试 8.4.1 调试工具及使用 8.4.2 调试过程 8.4.3 错误类型 8.4.4 异常处理* 8.4.5 软件测试
8.4.1 调试工具及使用 (略)
连编 调试 设断点 进入语句 跳出函数 工作区 查看 栈 反汇编 停止连编 停止调试 删除断点 单步执行 执行至 输出窗口 寄存器窗口 内存窗口 8.4.2 调试过程 结合VC++环境讲解 ! 或留给实验课。
8.4.3 错误类型 1. 关系运算中误用赋值符 C++允许在if、for等语句的条件判断中使用赋值符,因为赋值表达式具有值,非0可表示true,0表示false,所以编译并不对此报错,由于 = 和 == 的语义不同,人们经常说“如果xx等于xxx”,所以这种错误十分常见。通常编译时会产生一个警告 "warning C4706: assignment within conditional expression",对于编译给出的警告要给予充分注意,它们都说明程序中存在产生错误的隐患。 2. 指针或变量未赋值 未经赋值就直接引用,尤其是指针变量未赋初值。见下面两条语句: int *p; *p=100; //错误 char* p; cin>>p; //错误,p未分配存储空间 *p=new char[30]; //错误,应该是p=new char[30];
8.4.3 错误类型 3. 动态分配内存 p=new char[30]; p=“sdkjfds” //可能错误 p=new char[30]; if(p==0) { cout<<"No enough space!"<<endl; abort(); } p="sdkjfds" 4. 内存泄漏 int* p=new int[512]; p+=255; delete p; 利用Visual C++6.0所提供的调试功能检测内存泄漏。
8.4.3 错误类型 5. 顺序性错误 顺序错误是由于编程者对运算符的优先级、结合性的理解有误或疏忽导致的语义理解或语句书写错误。例如 a=b<c?d=e:f=g,这种复杂的表述很难不出语义错误,最好将它用适当的括号明确地表示出来。 6. 边界错误 #include "iostream.h" void A(int a, int b) { int a1=0x11; a=a1+b; } void main() { int n1=1; char cc[4]; int n2=2; cin>>cc; //input 1234567 cout<<"n1: "<<n1<<endl; cout<<"cc: "<<cc<<endl; cout<<"n2: "<<n2<<endl; } 调试运行此程序
8.4.4 异常处理* 异常:由于程序顺序控制之外的原因导致程序的非正常运行,比如资源不足、I/O错误等等。 异常处理:程序出现异常时能够产生(或抛出throw)异常事件,由错误处理函数将之俘获(catch),根据所俘获具体异常事件进行相应处理。 异常是现代程序设计语言中提供的除三种基本控制结构外的一种重要的控制结构,除用于错误处理外,还可用于被调函数与调用者之间的数据信息交互。 提供异常处理的意义:程序运行过程中出现错误是不可避免的,因此研究如何发现错误和定位错误十分必要。软件重用使错误处理变得复杂,库函数开发者能够发现程序中何时发生错误,但对于其中的很多错误他不知道函数的使用者将如何处理;而库函数使用者知道函数调用出现错误将如何进行处理,但是他却无法直接侦知函数何时出现错误。
8.4.4 异常处理* try 模块try程序块标出了程序中将被监督的代码快,对于不属于某个try后面复合语句中的其它语句发生的异常事件不予监视。 catch模块catch模块用来俘获异常事件,进行相应处理。catch模块仅随try模块之后,每个try模块可有多个catch模块用于俘获try模块中抛出的不同异常事件。catch后括号中异常说明表明该语句处理异常的类型,catch后复合语句只能处理所说明类型的异常事件,如写成 catch(…) 的形式,说明该函数处理所有类型的异常事件。 抛出异常事件 根据程序运行情况,在程序中任何地方抛出异常事件。 C++异常处理结构:
[例8.1] 异常处理实例,分析下述程序的运行结果。 #include <iostream.h> #include "string.h" void MyFunc( void ); class CTest { public: CTest(int nCause) { m_nCause=nCause; }; ~CTest(){}; void ShowReason() const { cout<<"CTest exception caused by m_nCause = "<<m_nCause<<endl; } private: int m_nCause; }; void MyFunc1() { cout<< "Throwing CTest exception." << endl; throw CTest(1); } void MyFunc2(char* pStr, int n) { int l=strlen(pStr); if(l<n) return; cout<< "Throwing char* exception" << endl; throw "Exception is caused by out of range"; } } int main() { cout << "In main." << endl; try { cout << "In try block, calling MyFunc1()." << endl; MyFunc1(); cout << "In try block, calling MyFunc2()." << endl; MyFunc2("Test for exception handling", 5); } catch( CTest E ) { cout << "In catch handler." << endl; cout << "Caught CTest exception type: "; E.ShowReason(); } catch( char *str ) { cout << "Caught char* exception: " << str << endl; } cout << "Back in main. Execution resumes here." << endl; return 0; }
a==2 a==3 a==1 b==0 while(c>0) process(c) x==2; x==2; x==4; 8.4.5 软件测试 白盒测试又称为结构化方法或结构测试。在白盒测试中,参照程序的具体实现过程,根据程序的结构,选择测试数据,其目标是选择测试数据使程序的每条可执行路径都能够被覆盖。 1 白盒测试 switch a { case 1: x=3; break; case 2: if(b==0) x=2; else x=4; break; case 3 while(c>0) process(c); break; }
8.4.5 软件测试 对于大程序,通常并不关心实现细节,只在意提供一些数据,程序是否正确输出,测试时选择一些代表性数据,对于测试者而言,程序就像是一个黑盒子,看不到内部结构,称这种测试方法为黑盒测试法。 在黑盒测试法中,测试数据来自对所解决问题的详细描述,而不考虑程序实现。编程人员在进行测试时应该注意下述方面: 2 黑盒测试 简单值调试程序应该以简单的数值开始,从程序的最简单的运行情况开始。 典型的、有实际意义的数值 就程序被使用的最一般情况给出测试数据,当然,这些数据应该尽可能简单,以便于程序出错时对程序的调试。 边界值程序出错可能性最大的情况是程序运行在其应用范围边界上。 非法值非法值也是一类测试数据,输入非法值后,程序的运行不应发生异常。
8.5 程序运行效率 程序的效率包括两个方面的内容,即程序运行的时间代价和系统资源的占用情况。泛泛而言,这两个方面是一对矛盾,可以牺牲程序的运行速度来减少程序对内存占用,也可以通过对内存的消耗提高运行速度,例如:可以通过适当的压缩算法对数据进行压缩,从而提高内存和磁盘的利用率,但解压缩和压缩都需要有时间开销,必然增加程序的运行时间。 因此,归根结底提高程序的运行效率最终将是在速度和内存之间进行一种权衡,但这只是当程序本身各方面已经处于理想状态时人们所能做的,实际上在达到理想程序之前人们还有许多有关程序优化方面的工作可做。 8.5.1 适当的算法 8.5.2 选择快速运算 8.5.3 函数
8.5.1 适当的算法 算法一:power(2, n) 提高程序运行效率的根本途径是选择一个好算法。下面的几个实例说明了这一点: 求2n (n<15)。 算法二:int GetPower(n) { int a=1; for(int i=0; i<n; i++) a+=a; return a; } 算法三:2<<n 算法四: unsigned long GetPower(unsigned long n) { __asm{ xor eax, eax mov ebx, n; bts eax, bx mov n, eax } return n; }
8.5.2 选择快速运算 1. 加1减1运算 x=x+1的运算效率往往低于x++ 。 2. 其他它运算 2*n的效率低于n+n,因为乘法运算速度低于加法计算n2时, pow(n, 2)的效率低于n*n。 必要时重新实现sin()、cos()等,列表查值方式可提高运行速度。 一般来说,库函数的开发都是针对一般应用场合中带有普遍性的问题提供支持,一个库函数可以满足多种情况下的应用,如果编程时仅仅以极特殊的形式调用某个库函数,而且这种调用位于程序运行的关键路径上,那么就有必要为这个特殊的操作提供一个“量身定制”的函数,这通常是可行的,因为通用的运行效率一般比专用的要低。
8.5.3 函数 1. 函数的使用 调用函数需要进行参数传递、保存CEP及CIP等多个额外操作,一个极简单的函数,其运行时间开销可能是用户描述语句的几倍,如果函数处于关键路径上,则函数的运行效率可能成为程序运行效率的瓶颈,因此对于一些简单的由一两个简单操作组成的函数不防代之以内联函数或者宏定义。 在使用类等语法进行编程时,应该熟练掌握构造函数、赋值函数等的调用规律,提供必要的构造函数、赋值函数、类型转换函数等提高运行效率。 2. 函数参数及返回值 函数的参数可以是对象、结构,函数的返回值也可以是对象,但这样的函数在参数传递上效率低,尤其是当结构和类比较复杂时,应尽量以指针、引用等作为函数参数或函数的返回值,这样可以提高函数的数据传递效率 。