您好,欢迎来到纷纭教育。
搜索
您的当前位置:首页《C++程序设计》(第2版)教学资源 教师用书 C++教学指导书第二版

《C++程序设计》(第2版)教学资源 教师用书 C++教学指导书第二版

来源:纷纭教育
《C++程序设计》教学指导书 1

《C++程序设计》(第二版)教学指导书

——供任课教师使用

通常认为C++是一个非纯粹的面向对象的程序设计语言,因为它是从面向过程的C发展而来的,对它有种种诟病。然而编者认为符合ISO14882标准的C++语言是一种先进的面向对象与参数化程序设计语言。因而本教材从面向对象和参数化程序设计两个方面来展开C++程序设计的教学,这在编者所见过的教材中是仅见的(其他的教材是从面向过程和面向对象两个方面来展开教学),这也是本教材的教学体系先进所在。

本教材的出发点是:与时俱进和实事求是。与时俱进是指教学内容要跟上计算机技术的最新发展;实事求是是指教学要切合当今大学生的实际情况,切合本课程在大一同时开设的课程中的地位。

现有的C++的教材的教学体系大致可分两类:

第一类是经典的,按语法顺序讲授基本知识、面向过程的程序设计、基于对象的程序设计和面向对象的程序设计。

第二类是尽早进入面向对象的程序设计的讲授。侧重于面向对象。 本教材可属于第二类,但对教学体系做了全面的改革,力求建立全新的面向对象与参数化程序设计的C++教学体系。将最新发展的知识传授给学生,教学内容的选定以ISO14882 C++语言标准为基础。抛弃传统的C++教学面向对象内容以语法为主的教学模式,突出面向对象和参数化程序设计关键技术的教学,让学生获得面向对象C++程序设计的真实本领。这一全新的教学体系经过4年大范围的教学实践已经成熟。

本教材是通用教材,可以用于对计算机知识要求相当深入的专业,包括电类、机电一体化、计算机专业等等。尽管随后续课程组织不同,教学侧重有些不同,但本教材均可适用。

课程特色

第一,突出面向对象与参数化程序设计关键技术的教学:

强调类对象个性实现的关键技术——多态,包括重载(编译时的多态,包括函数与运算符重载)、层次结构中的同名覆盖与超载(运行时的多态)。在介绍函数时就引入函数重载,在教学刚涉及类对象时就引入运算符的重载。引入的越早应用的机会越多。普遍使用这些技术是面向对象的C++的标志。

强调参数化程序设计,突出模板相关内容的教学。不是将模板作为一种语法现象,而是作为一个有力的工具用于本教材所涉及的全部数据结构基本知识,包括顺序表、链表、栈、队、二叉树(可选)以及查找排序算法。同时介绍标准模板库的简单使用方法。

提倡完善的类对象封装,不仅封装数据和对数据的操作,而且封装资源(尤其是内存)的动态分配与释放,形成一个完备的子系统。在一个有层次

《C++程序设计》教学指导书 2

结构的类体系中内存的动态分配与释放最好封装在成员对象(聚合)中,如同使用标准的string字符串类那样。

介绍怎样在面向对象的程序设计中使用异常处理技术来处理一些很难查找甚至是无法避免的运行时错误和异常。

总之,不是泛泛介绍面向对象的C++的语法和框架,而是突出实用技术,包括完善的封装、派生、多态和模板,在构造函数中动态分配资源、在析构函数中释放资源和异常处理,这是面向对象的C++程序设计的精髓。

本教材要求学生能熟练应用多态(重载和超载),熟练应用模板,熟练应用派生。习惯在构造函数中动态分配资源、在析构函数中释放资源和异常处理的方式。

第二,强调算法,注意介绍有关于任何特定编程语言的算法概念和结构,即突出程序设计而不是语法。强调算法,不是忽视语法,而是不要繁琐的钻牛角尖的语法,我们要的是基本的常用的语法,但更多的是模仿。不是知道的语法越多,程序编得越好,而是自己动手编程越多,程序编得越好。

第三,培养面向对象程序设计能力。掌握怎样从客观事物中抽象出类来的方法。基础教学与实践教学相结合。在基础教学中采用Windows平台下的控制台方式(命令行方式)以突出编程能力的培养。在实践部分比较全面地学习标准的Windows图形界面编程。采用研究型学习进行课程设计。

本教材第二版做了大幅改动。首先例题和习题解答主体按三个熟练应用和一个习惯的要求,进行改编与重组,更加突出了面向对象C++程序设计的实用技术;采用统一的规范的编程模式,不再体现编程的多样性,降低了学习难度。其次是调整章节安排与精简教学内容。第三,为了更好地配合精讲多练,安排了二十九个(其中两个选做)与教学内容紧密相关的同步实验。第四,更新部分不完全符合ISO14882 C++语言标准的内容。

性质与任务

程序设计课程与数学、物理、外语等一样,是大学生的通识教育课程,也是从技术角度学习计算机的主要基础课,包括面向对象程序设计及最基本的数据结构和软件工程的知识。其任务是培养学生的面向对象的编程能力,也锻炼大学生的逻辑思维能力(计算机思维方式)。

学时安排

本教程建议授课时数48学时,习题课8学时,上机实验56学时(含课外上机)。另有课程设计(小型软件设计的实践环节),16学时加上机实验32学时(含课外上机)。

课堂教学内容

分四部分,共十二章。

第一部分,包括第一到第三章,是基础部分。介绍C++程序的基本组成和编写方法,函数的递归以及最简单的常用算法。这一阶段代码的编写方法,应该是课堂教学的重点。

《C++程序设计》教学指导书 3

第二部分,包括第四到第五章,是面向对象的入门部分。介绍类对象的组成,构造函数和析构函数的编写,运算符的重载,还有数组与指针的概念和两者之间的对应关系。本阶段逐步过渡到以算法为重点,逐渐淡化代码的编写细节。

第三部分,包括第六到第八章,是核心部分。介绍通用算法的编写方法,特别是数据结构的基本算法,和软件的再利用。第六第七两章采用模板技术,第八章采用继承与派生。一般来讲,本阶段以算法为重点,不再细讲代码的编写。

第四部分,包括第九到第十一章(标准模板库选读),是扩展部分。包括:输入输出重载、在构造函数中动态分配资源、在析构函数中释放资源和异常处理、以及标准模板库。本阶段主要学习面向对象的C++程序框架构造知识。

考虑到各专业相关课程配置不同,生源不同,授课内容可有筛选。选读内容,可以作为学有余力的同学的自学内容,做到因材施教。其实讲哪些内容,不讲哪些内容,教师完全可以自主掌握,关键是要学生掌握课程特色中所言的三个熟练应用和一个习惯。。

教学方法

现实情况是:第一,在大一的基础课程中,大学生将程序设计排在数学、外语和物理之后,不可能投入过多的精力,大学生如果要放弃某门课以保证集中精力通过其他课时,最不熟悉的本课程列在首位;第二,大学生对这门课学习的期望值很高,但对学习时可能遇到的困难估计不足;第三,本科生总学时数下降,尽管计算机课程重要性上升,但总学时不可能增加;第四,大学生上机实践的条件大大改善,特别有利于贯彻先进的精讲多练的教学思想。所以在学时有限和学生所投精力有限的条件下,围绕课程特色中提到的面向对象关键技术进行精讲多练是第一条。为此安排了29个同步实验。

一年级大学生认为应试教育天经地义,大学应延续中学的应试教育,不懂得主动学习。要培养大学生的自学能力。预习是本课程对学生的基本要求。

整个教学强调过程,知识积累的过程,能力培养的过程,使学生能快乐地学习。(但绝不是自学,实践经验指出,自学出来的学生实际应用时编出来的程序往往效率低,错误百出,而且别人看不懂,无法交流,原因就在不会规范化地编程。)一定要避免应试教育。教学要求高于考试要求,必须给学生真本事。考试可以多样化,面要广,难度要适中。切忌题目过偏过难,切忌出弯弯绕的语法题,要考学生编程的实际能力,这是大学的第一门计算机课,不要把多数学生的自信心毁了。

授课时要采用案例教学法,从实例引出语法,尽管书中多数地方为了阅读方便是将语法条文单列,甚至放在前面,但上课时不要顺序往下讲。

现在提供的电子教案只是参考,修改和重编的过程也是熟悉教学内容和发现问题的过程。要求每位老师自己做有自己个性的教案。

面向对象程序设计的原代码通常比较庞大,原因是数据与数据的操作封装在一起,原则上包含的操作要全面,正是众多的成员函数使学生认为自己面对的是一个庞然大物,吓也吓蒙了。教师应该指出成员函数是一个个的操作,每一个成员函数都是简单的。可以给学生讲讲庖丁解牛的故事,要

《C++程序设计》教学指导书 4

求学生做到目无全牛,也就是面向对象的程序要一个一个函数来编。

习题与实验都是学生的实践机会,具体指导和讲评是必要的。特别是在学生尚未入门时具体指导尤其重要,最好是在实验室里配大屏幕显示,学生与教师演示同步操作,或用同步操作的教学软件。如无此条件,至少教师在第二章和第三章课堂教学时多做控制台应用程序设计全过程演示。在辅导实验时只可能解决少数学生的少数问题,提倡上机时学生互相讨论互相帮助。做习题,也提倡较难的题可以同学之间先讨论再完成。也可以把部分习题的参先发给学生作为参考,毕竟我们要求规范化编程,主要是灵活应用通用算法,不是创造别人看不懂的算法,初级阶段主要是模仿,见多识广后就能编出好的程序。

讲评是在学生做完习题和实验之后,针对学生实际发生的最常见的错误进行的,也可以介绍一些同学的好作业,这是一个总结提高的过程。

面向对象C++程序设计的习题基本是在一个C++的类或类模板定义中添加一些成员函数。建议可以给学生提供完整的类定义和需添加的成员函数的声明,以及检测其功能的主函数代码,同时给出需添加的成员函数的思路或提示,仅让学生编写需添加的成员函数,可能效果会更好。建议将给教师使用的习题参,删去要求编写的函数,添加所需的说明,再发给同学去做习题,这样即降低了难度也突出了重点,便于学生调通程序,同时提高了学生的信心和学习的效率。

实验也可做类似的考虑。

该教学体系培养的学生所编的程序给人的第一影响应该是:这是规范的面向对象的程序。

对于需要计算机知识较多的专业,程序设计课程应考虑后续课程的需要。尤其是电气电子信息类专业的学生的后续课程中需要大量的面向过程的程序设计的基础知识,包括汇编语言的编程,单片机、嵌入式系统和DSP的C语言编程。面向对象的程序设计其实与面向过程的程序设计是密不可分的。在本课程中,算法的描述实际上是面向过程的,而面向对象是一种包装,它使程序的整体组织更合理,使用起来更方便。教学中应该合理地将两个方面有机地结合起来,即细节上算法的编程和程序总体上的把握并重。

在前三章的教学中对算法的描述,必须侧重于采用C++语言编程的细节的讲解,即培养学生对算法的编程能力。要求学生学会先分析算法,再画UML的活动图或普通的流程图,最后进行编程。特别是在“基本控制结构程序设计”和“函数”这两章的教学中要严格贯彻这一要求。

“类与对象”和“数组与指针”两章是过渡阶段,对编程的细节的讲授随教学推进而逐渐淡化,对程序的整体掌握的要求逐渐加强,使学生的编程能力上一个台阶。

从第六章起,算法表述的细节基本留给学生自己看,教师重点讲解脱离具体C++语言的算法本身的描述和程序整体的构造。一方面提高学生的自学能力,另一方面引导学生的编程的大局观。

要引导学生,不要做学生的尾巴,片面地降低要求。

《C++程序设计》教学指导书 5

目录

第一章 C++基础知识

第二章 基本控制结构程序设计 第三章 函数

第四章 类与对象 第五章 数组与指针 第六章 模板与数据结构

第七章 动态内存分配与数据结构 第八章 类的继承与派生 第九章 输入/输出流类库 第十章 异常处理

第十一章 标准模板库 (选读) 同步实验部分 课程设计部分 UML活动图 UML类图

疑难问题解答索引

C++类的构造函数的作用的讨论 const引用

forward与向后移动

if语句和switch语句的对应关系 Node类和List类的复制构造函数讨论 this指针的应用 try块应用

VC++的随机数

VC++与标准名字空间 编程中附加条件的安排 标准流文件结束符的使用 迟缓错误检测

重载提取和插入运算符 初学编程常见错误

传统运行库和新的标准库中的输入输出流的差异 传值调用与引用调用讨论 递归算法的讨论 调用树的使用 动态分配的讨论

数组与多级指针 构造函数与虚函数 关键字索引

函数返回值与返回引用

《C++程序设计》教学指导书 6

基类中protected成员的使用 静态变量

聚合的复制构造函数 矩形类讨论

类模板的插入和提取运算符的重载 类模板的派生的讨论 类型的转换

流类库中的控制枚举常量的定义 模板编译模式

模板与派生在实现通用性上的对比 默认参数函数 默认的构造函数 内存分配图 派生常见错误

派生类的插入和提取运算符的重载 使用多态完成定积分的通用性 矢量类的边界

输出运算符(插入运算符<<)的讨论 数字与数字串

顺序队列转化为循环队列的算法讨论 图解法阅读程序

图解法分析算法——Fibonacci级数 文件中的数据格式 下标运算符重载 前向引用声明

循环控制变量使用要点 虚函数、同名覆盖与重载 异常处理匹配原则

一个流文件一次只能与一个磁盘文件建立联系 引用的讨论 优先级与结合性

约瑟夫问题求解过程

状态字state及其使用技巧 指向类成员的指针 指针与数组的差异

《C++程序设计》教学指导书 7

第一章 C++基础知识

教学目的

使大学生了解C++数据和运算的表述方法,简单的输入输出流类,引入广义的数据类型的概念。

建议学时安排

授课5学时

初识C++程序,关键字与标识符,数据类型,变量与常量。2学时。 运算符、表达式和语句,数组。2学时。 简单的C++输入输出。1学时。

教学方法提示

从形式上看本章与面向过程的教材差异不大,但教学上要从面向对象的思维方式出发:数据类型总是与相关的算法封装在一起。不要说成算法是的。某种算法可以用于那些数据,这是面向过程的说法,并不切合实际。实际上不同数据类型是重载了运算符。例如加法,对于整型数,执行的是定点运算;而浮点数,首先要对阶。再如除法,对于整型数,执行的是整除,商为整数,另有求余数的运算;而浮点数,商为浮点数,没有求余数的运算。从不同数据有不同算法出发,很容易理解为什么要将函数附属于数据类型。这样考虑问题,学生易于接受面向对象的思想。

在本章中,存储方式的概念要建立起来,每一个变量总是在内存中分配有一块内存单元,不仅本章中的左值、表达式、字面常量和sizeof()与之有关,后面的算法的分析、函数的传值等等全与之有关。介绍数据的存储方式,并在教学的过程中经常利用它们,也是本书的教学特点之一。

基本数据类型,相关的算术、关系和逻辑运算,是本章最基本的知识点。 有关运算符的结合性是决定同一优先级的运算符组合在一起时的运算次序,同一优先级的运算符有相同的结合性。如+、-的结合性是从左到右(左结合),则a+b+c-d的运算次序为:

((a+b)+c)-d //先算a+b,再加c,最后减d

赋值号=的结合性是从右到左(右结合),则a=b=c+d的运算次序为:

a=(b=(c+d)) //算出c+d的和,赋给b,再将b赋给a

又如前++和单目负-的结合性是从右到左(右结合),则-++a的运算次序为:

-(++a) //先做++a,再取相反数(加负号)

这对算法描述是必须的,每一步做什么必须确定。

运算符++,学生往往没有真正理解,普通+的结果放在暂存器(累加器)中,而++的值要返回给变量,变量的值变了。画一个演示图应该好一些。要讲清楚a++与a+1的不同。在第五章的运算符重载教学中,++无返回值,+=有返回值,前者友元方式时参数要使用引用,学生不理解,说看不懂,细问发现不懂在++的运算过程。

《C++程序设计》教学指导书 8

简单的数组概念的建立对算法的描述是很重要的,所以提前在这里引入。强调3点:第一,数组下标从0开始,这与简化大量的算法有关,不从0开始,相关的典型算法往往是错的;第二,数组不能作为整体处理,只能按元素处理;第三,数组的初始化方法。

字符数组与字符串现在是要求能用,因为它的应用实在太广了。本章中只能讲清组织方式。特别是串结束符,有了它,串的处理方式与数组大为不同。教师必须在这里给学生点明,输出串是输出串内容,输出数组是输出数组首地址。

输入输出方式简介,强调会用。

输入方面:第一,提醒学生字符输入容易出错,特别是数字与字符混合输入时,回车符的吸收问题要交待清楚。第二要讲清cin和cin.get()的差异,cin会自动跳过所有空白符(包括空格、制表、回车等等与格式有关的符号),cin.get()不会。第三,输入一系列数字时,用空格或回车分隔,不可以用逗号分隔。第四,输入字符串使用cin.getline()的方法,提醒同学:它提取回车符但不保存回车符,而是加一个串结束符。

其他错误解答请参考本指导书第9章。

输出方面:格式控制演示交代清楚,学生就会理解。字符输出要提醒同学,左值输出的才是字符,表达式输出的是ASCII码值,因为在表达式中已转为整数。

第二版中,C++程序改用标准库头文件,无后缀.h,并加using namespace std。教师应作简单说明。

注意新标准要求main()函数必须返回int型,0为正常,否则返回其他整数。这样可以取代exit()等函数(exit()库函数通常在程序出错时用来退回到操作系统,详细见教材4.3.2节)。一般函数返回为int的也不可省略,否则编译器发出警告。

疑难解答

优先级与结合性

优先级高的运算先做怎样理解?请参见下式: a*b+(-d) &&++c 等效为:

(a*b+(-d))&&(++c) 完全按优先级,先做(-d),第2步做(++c),第3步作a*b,第4步做(a*b+(-d)),第5步做(a*b+(-d))&&(++c)。这是人工的做法。

计算机是从最低优先级开始,先处理最低优先级的“与”运算符左边的(因为与运算符的结合性是左→右)操作数(a*b+(-d)),括号内先做a*b,再做(-d),然后将两项相加;其次处理“与”运算符右边的操作数(++c);最后完成“与”操作。如果“与”操作符的左操作数为0,则直接完成与操作。即第1步做a*b,第2步做(-d),第3步做(a*b+(-d)),第4步做(++c),第5步做(a*b+(-d))&&(++c)。如果第3步结果为0,则第4步不做。 只要交换律成立,这两种处理方法是等效的。计算机处理表达式的优先级和结合性是借用操作数栈和运算符栈来完成的,参见教材 栈的应用。最后的运算次序就是首先将最低优先级运算符各操作数项按该优先级的结合

《C++程序设计》教学指导书 9

性依次处理,如果某操作数项为表达式,可同样按上面的规律处理。

设有表达式:a+++b,等效于(a++)+b还是a+(++b)?应该是前者,因为编译器在解释运算符时优先取尽可能长的符号。

而a++b是错误的表达式,改为a+(+b)才是正确的。

++a++,后++优先级高,等效于++(a++),是错的,因为(a++)后++未做,不是左值,不能进行前++。

而(++a)++是正确的,(++a)前++已完成,是左值,后++可以进行。 同样++++a是正确的,而a++++是错误的。

++-a是错的,因为第3优先级结合性是左→右,先做++,但-a不是左值。++(-a)也是错的,尽管(-a)优先级高,但结果是右值。

-++a,等效-(++a),是对的。---a,等效--(-a)是错的。而+++a,等效++(+a),表面是错的,但至少VC++中单目正被忽略,所以是对的。

(-a)++是错的,因为(-a)先做,(-a)不是左值。但-a++是对的,后++优先级高先做,a是左值。

既然后++是在表达式运算结束后做++,那么该运算符的优先级有何意义?并且比前++高一级?从前面的例子可见优先级是有意义的,而且前++优先级定为第3级是合理的,由于后++结合性是左→右,与前++不同,不可同列第3级,列第4级或单列一个级别也不合适,所以归入第2级。

VC++的随机数

VC++输出的随机数,并非随机数,而是特定值,如整型数,为0xcccccccc (-8593460),float型为-1.07374e+008,duoble型为-9.25596e+061。

关键字索引

and 运算符 代替&&; and_eq 运算符 代替&=; asm 运算符 用于标识混编的C++源程序中的汇编语言代码; auto 说明符 说明变量为自动变量,见主教材(下同)P.84; bitand 运算符 代替&; bitor 运算符 代替 |; bool 说明符 用于说明布尔型变量,见P.7; break 语句 结束循环语句或开关语句的执行,见P.50; case 标号 用于开关语句,见P.40; catch 语句 用于异常处理中捕获异常子句,见P.343; char 说明符 用于说明字符型变量,见P.6; class 说明符 用于说明类类型,见P.106; compl 运算符 代替~; const 、136、165、193和200; const_cast 运算符 常量强制类型转换运算符,详见后文; continue 语句 用于循环语句,结束本次循环,见P.52; default 标号 用于开关语句,表示其他情况,见P.40; delete 运算符 用于回收动态存储空间,见P.220; do 语句 用于循环语句,实现直到型循环,见P.44; double 说明符 用于说明双精度实

dynamic_cast else enum explicit export extern false float for friend goto if inline int; long ; mutable namespace; new not not_eq operator or or_eq private 267;protected public register reinterpret_cast return sizeof short; signed static static_cast struct switch template this throw true; try typedef typeid typename

《C++程序设计》教学指导书 10

运算符

动态强制类型转换运算符,详见后文;; 语句 用于组成双分支结构条件语句,见P.34; 说明符 用于说明枚举类型,见P.59;

说明符 取消构造函数隐式数据类型转换的功能,见P.357; 说明符 用于模板分离编译模式,见本指导书第六章模板; 说明符 说明变量为外部变量,见P.84; 逻辑值 对应0,表示逻辑假,见P.7;

语句 用于一种当型循环语句,见P.46;

说明符 用于说明友元函数和友元类,见P.135和139; 语句 用于无条件转移程序执行次序,见P.52; 语句 用于组成双分支结构条件语句,见P.34; 说明符

用于说明内联函数,见P.96和110;

说明符 使const对象和成员函数的数据成员可修改,见后文; 运算符 用于分配动态存储空间,见P.220; 运算符 代替 !; 运算符 代替 !=;

说明符 用于定义运算符重载函数的函数名,见P.130; 运算符 代替 ||; 运算符 代替 |=;

说明符 访问限定或派生方式和267; 说明符 访问限定或派生方式和267;

说明符 说明变量为寄存器变量,见P.84;

运算符 重解释强制类型转换运算符,详见后文; 语句 作为函数返回语句,见P.2和69;

运算符 计算类型或变量占用内存的字节数,见P.19; 说明符 用于说明有符号整型变量,见P.7; 说明符 说明静态变量或类的静态成员和139; 运算符 静态强制类型转换运算符,详见后文; 说明符 用于说明结构类型,见P.142;

语句 用于组成多分支结构开关语句,见P.40; 说明符 用于参数化程序设计的模板构成,见P.186; 说明符 指向具体类对象本身的指针,见P.166; 说明符 用于异常处理中抛出异常,见P.342; 语句 用于异常处理测试程序块,见P.342; 说明符 用于类型定义,见P.173;

运算符 运行时类型信息运算符,详见后文;

说明符

用于模板定义时类型参数的说明,见P.186;

《C++程序设计》教学指导书 11

union unsigned using virtual void volatile wchar_t while xor xor_eq 说明符 说明符 说明符 说明符 说明符 说明符 说明符 语句 运算符 运算符 用于说明联合类型,见P.144; 用于说明无符号整型变量,见P.7;

用于using声明或using指示符,见P.145; 用于说明虚基类或虚函数,见P.278或2; 用于说明无值类型,见P.7;

修饰函数,表示表示该函数不做编译优化,见P.356; 宽字符类型,见P.6;

用于循环语句,见P.43和44; 代替 ^; 代替 ^=;

强制类型转换及其他运算符说明如下: static_cast 静态强制数据类型转换。格式如下:

double a;

int k;

k=static_cast(a);//将双精度型的a的值转换为整型赋给k

特别注意a本身的类型不变,仍是双精度,所有转换都是数值而非变量的转换。允许在两种类型之间进行标准转换(如void*转换为char*,int转换为float等等),包括允许基类指针到派生类指针的向下类型转换。但不能在const类型和非const类型转换之间转换,不能在非公有派生的基类和派生类的指针(引用)间转换,不能转换为无相应构造函数或转换运算符的类型。

const_cast 专用于转换const或volatile类型。格式如下: char *string_copy(char*);

const char *p_str; ……

char *pc=string_copy(const_cast( p_str))//取消p_str的const属性

注意:const_cast不能用于强制转换常变量的常量性。

mutable 在const对象和成员函数中,用mutable修饰的数据成员也

可以被修改。使用格式如下: class student{

int id; mutable string name;//使name可以被修改 public: void setname(string Name)const{name=Name;};//合法 …… };

int main(){

const student stu1; …… }

《C++程序设计》教学指导书 12

reinterpret_cast C++利用重解释转换进行非标准转换(void*转换为int*、int*

转换为char*等等)。重解释转换不可用于标准转换,如duoble到int的转换。该转换可能导致严重的运行时错误。

dynamic_cast 动态强制类型转换运算符用于运行时多态,也就是说用于派

生体系,而且基类至少包含一个虚函数。它可以沿继承树向上(向基类)或向下(向派生类)进行类型转换,改变指针(引用)的类型。使用时包含。 class base{

protected:

int b; base(int x=0){b=x;} virtual void vertFunc(){} void show(){cout<<\"基类:b=\"<class derv:public base{ int d; public: derv(int x=0,int y=0):base(x){d=y;} void show(){cout<<\"派生类:d=\"<int main(){

base* pb;

derv* pd=new derv(23,29); pb=dynamic_cast(pd);

pb->show();//输出 基类:b=23 转换遵从赋值兼容规则 pb=new derv(31,37);

pd=dynamic_cast(pb); pd->show();

return 0;//输出 派生类:b=31,d=37 }

它可以将指向某派生类对象的基类指针转换为该派生类指针,如类型有误,返回NULL,可进而判断对象属于哪一种派生类。 在VC++中需要使用运行时类型识别(RTTI)必须选择Project|setting项,单击标签C/C++,在Category列表框中选择C++ Language,选中“Enable Run-Time Type Information”,单击OK。

typeid 作为运行时类型信息运算符可获得未知对象类型的信息。这

些信息放在type_info类中(其确切定义与编译器有关): class type_info{

type_info(const type_info&);//不再有默认构造函数 type_info& operator=(const type_info&); public: virtual ~type_info();

《C++程序设计》教学指导书 13

}

type_info& operator==(const type_info&) const; type_info& operator!=(const type_info&) const;

const char* name()const;//返回type_info对象所表示类型的名字

用户不能在自己的程序中定义type_info对象。在程序中创建type_info对象的唯一途径是使用typeid运算符。如:

employee *pe=new manager;

cout<使用时包含。选中“Enable Run-Time Type Information”。

第二章 基本控制结构程序设计

教学目的

使大学生掌握基本控制结构程序设计的C++语言实现和常用算法(递推、迭代、穷举、筛选等)。

建议学时安排

授课6学时,习题课2学时

分支结构程序设计:if 语句,条件运算符“?:”,switch语句。2学时。 循环结构程序设计:while语句,do…while语句,for语句,循环的嵌套;转向语句。2学时。

常用算法应用实例。枚举类型定义,输入输出文件简介。2学时。

教学方法提示

本章是用C++语言来描述程序的3种基本结构和介绍常用算法,这是学生编程的入门,也是重点。必须给学生指明,算法就是对数据的操作方法,让学生始终处于面向对象程序设计的氛围中。

在本章教学中要求学生养成先分析算法,再画流程图或UML的活动图,最后进行编程的良好习惯。在本章和下一章的教学中要严格贯彻这一要求。现在不严格要求,形成随意直接编程的坏习惯,要改也难了。

不要指望在本章中让学生入门,他们还是在门口徘徊,要有几个反复。到第4章学完,站得高了,回过头来看,才能比较深刻地理解,才会初步入门。千万不要在这里堆砌过多的学时,学生总会说不懂,要跟他说大胆的往前走。实际上后面的教学内容不断反复使用3种基本结构,在这个过程中学生会逐步熟练起来的。

教师在第二章和第三章课堂教学时应多做控制台应用程序设计全过程演示。学生调试程序的能力主要在这时培养,不仅是C++,而且以后“微机系统”等后续课程用Debug调试各种程序(汇编,C51,DSP的C语言编程)的基本功都是在这里打下的。辅导上机,主要指导学生怎样跟踪程序的运行,怎样设置被监视的变量,怎样设置断点,怎样单步运行,怎样使用Debug

《C++程序设计》教学指导书 14

工具。授之以鱼,不如授之以渔。帮同学调通一个程序,不如教会同学怎样去调程序。

讲解3种基本结构时不要按书上的顺序一个一个地讲,建议把相关的语句对比起来讲解。比如3种循环语句对比起来讲,效率和效果都要好得多。这样学时是绰绰有余。而有关switch语句,后讲不加break的例2.9。

注意不要按C语言的习惯,在for语句括号表达式中定义控制变量。教师首先不要那样做。理由在下一章介绍。

本章的例题非常多,并不是要题题全讲,那样学时是不够的,能讲70%就很多了。编书的出发点是多给学生一些各种编程类型的样板。

学习编程与算法有些象外语,模仿是重要的第一步。有同学特别希望有编程题的标准答案,这是错误的,标准答案是不存在的。但有一个规范化编程样板,对初学者却是非常重要的。大学生往往忽视了这一点,教师应该时时提醒。比如实验四中的约瑟夫算法,由于学生不愿模仿,把数组0号元素放弃,从1号开始,结果书上提供的算法就全错了,陷入死循环。学生不善于模仿,不要去批评他,碰了钉子,学生才有更深的印象。例题源程序有主教材例题和同步实验范例两类,课堂教学完全可以任意选择。非精讲的程序要求学生一定要自己读懂,见多识广,水平才能提高;正如外语只抠精讲部分,不看泛读,考试能对付,水平肯定高不了。

本教材强调算法,不是忽视语法,而是不要繁琐的转牛角尖的语法,不要让这种语法把学生的时间占光了,我们要的是基本的常用的语法,更多的是要模仿。

不是知道的语法越多,程序编得越好,而是自己动手编程越多,程序编得越好。从本章起,开始有大量的实验,一定要抓紧。

为了突出算法,本书主要采用命令行程序设计,教师在本章和下一章课堂教学时应多做控制台应用程序设计全过程演示,否则学生做实验时会卡在工程文件的建立上,而且错误是五花八门不胜枚举,使学生信心大减。

输入输出文件简介,放在这儿是一个创新。就是让学生先用起来,知道文件可以保存数据,并依样使用文件。文件输入输出用两个例子分别对同一个文件进行写入和读取,让同学切实体会到程序退出后信息确实没有丢失。

注意:教师认为文件引入很自然,但学生还没这个概念,认为屏显示与文件保存没什么两样,“数据丢失,大不了再算一遍”,请教师务必讲清楚。

疑难解答

初学编程常见错误

最常见的初级错误有:赋值号(=)与比较运算的相等号(==)混淆,循环语句中两个分号误为逗号,使用了数学符号而不是运算符等等。

在条件语句和循环语句中最常见的错误之一是不会使用复合语句。如:

if(a<=b) temp=a;a=b;b=temp;

其目的是当a<=b时,交换a、b两变量,学生误认为将实现交换的三条语句放在同一行就可以了。但if语句后面跟的仅是一条语句,必须将三条语句用花括号括起来。而且学生发现的错误是有时输出的b是随机数,他始终想

《C++程序设计》教学指导书 15

不到是错在未用复合语句。

再如,被改为:

int i=0;

ifstream baiji;

baiji.open(\"baiji.txt\"); do{ baiji.get(a[i]); i++;

}while(a[i]!='\\n');

其原意是从baiji文件中读取一行文字。当读完一行,a[i]中放了换行符,但随后i++,判断循环条件时,比较的是数组的下一个元素,陷入死循环。而原例题判断是在i++之前。学生不认为这里会错,而奇怪我的步骤没错,怎么文件读不出来?

常用算法应用实例是一个总结提高,有利于开拓学生的思路、提高编程的能力。这几道题都是经典的。

注意:文本文件阅读可以用的程序很多,常见的有,记事本、写字板、Word、Explorer、Visual Sdutio等文本编辑器,其中记事本对程序生成的没有回车的文本往往无法读出。学生往往只用记事本读程序生成的文本文件,有时发现只有黑方块,但用自编的程序读出却完全正确,于是用了大量时间去查错,不知是记事本的要求不同。

注意:当文本文件中既有文字又有数字时,请在文字后加回车(或endl),否则用自编程序读文件时可能会出错,因为文本文件中数字是用数字串存储的,提取数据时系统自动将串转换为数,有回车时从哪里开始转换十分清楚。

if语句和switch语句的对应关系

if语句和switch语句有一个呼应关系。if语句的嵌套方式对应于有break的switch语句,它实现互不相干的分段,每次仅处理一个分段;而并列的if语句对应没有break的switch语句,它依次处理各个分段,但各分段的方法不同。前者例子很多,后者见书上例2.9和习题2.4。

习题 编程计算个人所得税。个人所得税率表如下:月收入1200元起征,超过起征点500元以内部分税率5%,超过500元到2,000元部分税率10%,超过2,000元到5,000元部分税率15%,超过5,000元到20,000元部分税率20%,超过20,000元到40,000元部分税率25%,超过40,000元到60,000元部分税率30%,超过60,000元到80,000元部分税率35%,超过80,000元到100,000元部分税率40%,超过100,000元部分税率45%。

#include using namespace std; int main(){ double income,tax=0; int k; cout<<\"请输入个人月收入:\"<>income; if(income<=1200){ cout<<\"免征个人所得税\"<《C++程序设计》教学指导书 16

}

return 0; }

else income-=1200;

if(income>20000){//收入超过起征点20000以上各段,使用不包含break的switch语句 k=income/20000; switch(k){ default: tax+=(income-100000)*0.45;income=100000; case 4: tax+=(income-80000)*0.40;income=80000; case 3: tax+=(income-60000)*0.35;income=60000; case 2: tax+=(income-40000)*0.30;income=40000; case 1: tax+=(income-20000)*0.25;income=20000; } }

if(income>5000){ //收入超过起征点20000以下各段,使用并列的if语句 tax+=(income-5000)*0.20; income=5000; }

if(income>2000){ tax+=(income-2000)*0.15; income=2000; }

if(income>500){ tax+=(income-500)*0.10; income=500; }

tax+=income*0.05;

cout<<\"应征所得税:\"<程序从最高税率段开始计算,分段叠加。先算两万元以上各段,每两万为一档,采用没有break的switch语句。后面各低收入段,用并列的if语句,这两种方法是对应的。第二要注意计算的入口处,收入减去该段的下限,进行计算,以后各段都是完整的段,计算十分简单。

循环控制变量使用要点

循环语句中的循环控制变量最好不要参加运算,即使参加也只能参加不改变其本身值的运算,如习题2.11求完全数解答中的求某数的因子的运算不会改变原数的值。这是规范的做法。除前面常见错误小节中举的读文件的例子外,下面再看同步实验练习5.2求水仙花数学生的解法:

#include using namespace std; int main(){ int i,j,k,m,a[3]; for(i=100;i<=999;i++){ k=0;

《C++程序设计》教学指导书 17

}

for(j=0;j<=2;j++){ //将该数分解为各位数字,并求各位数字的立方和 a[j]=i%10; i=i/10; k=k+a[j]*a[j]*a[j]; } if(i==k) cout<return 0;

成了死循环,原因是循环变量参加的运算改变了循环变量本身的值。应该添加一个变量m来代替i参加将该数分解为各位数字的运算。

#include using namespace std; int main(){ int i,j,k,m,a[3]; for(i=100;i<=999;i++){ m=i; //用m代替i参加运算 k=0; for(j=0;j<=2;j++){ //将该数分解为各位数字,并求各位数字的立方和 a[j]=m%10; m=m/10; k=k+a[j]*a[j]*a[j]; } if(i==k) cout<编程中附加条件的安排

学生在初学编程时,对题目提出的各种条件应如何实现心中无数,必须多加指点。如穷举法采用循环,对须剔除的情况,应在循环体内用条件语句实现,并使用continue语句,不可用break。有同学将附加条件放在循环条件中,当然出错。例如:0到4五个数字,组成五位数,每个数字用一次,但十位和百位不能为3(当然万位不能为0),输出所有可能的五位数。

#include using namespace std; int main(){ int i,j,k,l,m,n,count=0; for(i=1;i<=4;i++){ for(j=0;j<=4;j++){ if(j==i) continue; for(k=0;k<=4;k++){ if(k==3||k==i||k==j) continue; for(l=0;l<=4;l++){ if(l==3||l==i||l==j||l==k) continue; for(m=0;m<=4;m++){ if(m==i||m==j||m==k||m==l) continue;

n=i*10000+j*1000+k*100+l*10+m; cout<//或cout <}

《C++程序设计》教学指导书 18

} } }

}

return 0; }

学生往往会把排除条件放在循环条件中,不知道应放在循环体中,并用continue跳过,不再输出。如(为了简单,不考虑每个数只用一次):

int main(){

int i,j,k,l,m,n,count=0; for(i=1;i<=4;i++) for(j=0;j<=4;j++)

for(k=0;k<=4&&k<>3;k++) //k到3循环就停止,4没做 for(l=0;l<=4&&l<>3,l++) //同上 for(m=0;m<=4,m++){

n=i*10000+j*1000+k*100+l*10+m; cout<//或cout<if(count%5==0) cout<return 0; }

标准流文件结束符的使用

例2.13采用函数 int cin.get(),当键盘输入Ctrl+z时它可以返回文件结束符EOF,即-1,其后输入的内容全部忽略。但是Ctrl+z必须在的一行的开头输入,否则Ctrl+z被忽略。而函数cin.get(字符变量)中的字符变量(实参)不能取得文件结束符,并发生输入流出错,以后跳过该语句,进入死循环(参见第9章)。但可以如下使用:

cout<<\"输入一段文本(无空行):\"<while(cin.get(ch)){ //ch必须为字符型 if(ch=='\\n') nline++; //遇换行符行数+1 if(ch!=' '&& ch!='\'&&ch!='\\n'&&ch!=EOF){ //读到非间隔符 if(!isword) nword++; //在单词的起始处给单词数+1 nch++; //字符数加+1 isword=1; } else isword=0; //读到间隔符 }; //读到文本结束符为止

函数cin.get(字符变量)的返回值是该输入流对象cin的引用(参见第九章),该函数作为循环条件放在while语句中,实际是以函数的返回值流对象cin的引用作为循环条件,当键盘输入Ctrl_z后,标准输入流结束,循环条件为假,循环结束。详见本指导书第九章的解释。

实践教材实验四范例2也可同样处理:

while(cin.get(c)){ //c必须为字符型 switch (c){ case '0': case '1': case '2' :case '3' :case '4': case '5': case '6': case '7' :case '8': case '9':

《C++程序设计》教学指导书 19

}

}

nDigit++; break;

case ' ': case '\\n': case '\': nWhite++; break; default: nOther++; break;

这两题的源代码在教学资源例题源代码Ex_2_13_1和同步实验范例源

代码Exp_4_2_1中。

图解法阅读程序 程序设计能力的培养包括两个方面:编写程序能力和阅读程序能力。图解法在读懂程序方面可以带来很大方便,跟踪程序执行,实际上是填写变量表格完成的。参照的解答。

图解法分析算法——Fibonacci级数

是已知Fibonacci级数的公式来编程的,如果不知道,可以用图解法。安排四个变量:兔子总数s,成年兔数a,一月兔数b,初生兔数c;则由下图可以得到具体算法。下图每一个变量的内存图中列出的数据从右到左按月变化,绘制时也可以从左到右按月变化,比变量表格方便。

321100a:成年兔211010b:一月兔321101c:初生兔853211d:兔子总数a=a+b;//本月成年兔为上月一月兔与成年兔之和b=c; //本月一月兔数量为上月初生兔c=a; //本月初生兔数量为本月成年兔(每对生一对)s=a+b+c; //本月兔总数代入初值:a=0; b=0; c=1; 就可以进行递推。

第二种考虑,安排三个变量:本月兔子总数i,上个月兔子总数j,前个月兔子总数k,前个月兔子总数恰好是本月可生育兔子的数量,即本月新生兔子数量,所以本月兔子数等于上月兔子数加前月兔子数之和。列出递推算法,即:

k:前月总数321100j:上月总数532110i:本月总数853211 k=j; //新的一个月来临,应将原前个月的兔子总数更新为原上个月的总数

《C++程序设计》教学指导书 20

j=i; //原上个月的兔子总数更新为原本月的总数

i=j+k; //新的一个月最终兔子总数为上月兔子数加前月兔子数之和

与Fibonacci级数的公式一样。

第二种考虑方式,也可以安排两个变量:本月兔子总数m,上个月兔子总数n。进入新的一月,新本月兔子数等于原本月兔子数m加原上月(可生育)兔子数n之和,新本月兔子总数放入临时变量temp,更替上月兔子数为原本月兔子数,再用temp更新本月兔子数。列出递推算法,即:

temp=m+n;n=m;m=temp;

以上3种方式,列在前面的更加易读易懂。

数字与数字串

例2.23为什么要用字符输入二进制序列,不用数字,学生不理解。教师应加说明,对应数字,cin输入数字串是自动转换为数字的,也就是本例题的算法已经包含在cin对象中。

也可以输入数字,变成字符保存,参见下例:请输入一组数字,它们是一字符串的ASCII码,将它们转换为字符串保存。

#include #include using namespace std; int main(){ int i,k; char a[]; for(i=0;i<;i++){ cin>>k; //必须用整型变量读入ASCII码 a[i]=k; //赋入字符型变量,则强制转换为字符 if(k<32) break; } a[i]='\\0'; cout<当输入84 104 105 115 32 105 115 32 97 32 98 111 111 107 46 ,输出及文件为: This is a book.

约瑟夫问题求解过程 n 个人围坐成一圈,从 1 开始顺序编号;游戏开始,从第一个人开始由 1 到 m 循环报数,报到 m 的人退出圈外,问最后留下的那个人原来的序号。

《C++程序设计》教学指导书 21

1Jose[8]1数组元素值为1代表在局182以数组取代11环形队列11731011数组下标取115代序列编号111i=(i+1)%800出局

参见上图,本题首先要定义一个数组,其元素个数为n(n定义为常变量,以便定义数组)。数组下标代表参加者编号,由0开始。数组元素的值标识该人是否出局,1在圈内,0出局。值为0的元素不参加报数。

数组是线性排列的,而人是围成圈的,用数组表示要有一种从数组尾部跳到其头部的技巧,即下标加1除以n求余数。

可用一个整型数k做计数器,采用倒计数,记录留下的人数。

上图n=8,m=4,i为数组下标,代表参加者编号。数组右二图表示2人出局,左图3人出局,注意下标i加到8因求余运算自动回到0。

#include using namespace std; const int n=8; const int m=4; int main(){ int jose[n],k,i,j; for(i=0;i0){ j=1;//报数由1开始 while(j<=m){// A if(j==m) {//下一个计数为m if(jose[i]==1) {//如果数组元素代表在局,元素清0,计数加1 jose[i]=0; j++; cout<11100110《C++程序设计》教学指导书 22

}

else i=(i+1)%n; //否则仅下标加1,寻找下一个有效数组元素 } } k--; }

cout<<\"最后留下的是第\"<循环语句A是关键,该代码比较复杂,原因是一般报数和出局的算法差异大,表述困难。可用break语句简化。

while(1){ if(jose[i]==1)//元素值为1,该员在局 if(j==m){//报数到m,元素清0,退出 jose[i]=0; cout<如果计数从0开始则程序稍有变化。

for(k=n;k>=1;k--){ j=0; while(1){ if(jose[i]==1){ j++; if(j==m){ jose[i]=0; cout<第三章 函数

教学目的

函数是作为C++程序的基本模块出现,使大学生具备有关函数较全面的知识,对变量的作用域和存储方式有深刻理解,重点与难点是递归算法。了解和使用多文件组织、工程文件及运行库函数。

《C++程序设计》教学指导书 23

建议学时安排

授课5学时,习题1学时

函数定义与调用,函数的参数传递,返回值及函数声明,全局变量与局部变量,函数调用机制。2学时。

函数的递归调用。2学时。

函数重载,默认变元,内联函数。1学时。

作用域,变量的存贮类型,生命期与可见性。1学时。

教学方法提示

要求讲出面向对象的特色,即对数据的操作总是封装在函数中,一个函数描述一种操作。不要完全按模块思想讲。也是让学生处于面向对象程序设计的氛围中。这样第4章的教学会顺利一些。

本章有两个重点:函数的基本使用方法和函数的递归调用。

函数的基本使用方法要求熟练掌握,特别是传值、在函数中重新分配内存的思想,一定要讲透彻。教材中画了不少堆栈内存分配图,就是为了帮助学生理解。这个思想有了,局部变量,生命期等等非常重要的思想都很容易理解。当然在教学中,后者也进一步帮助学生理解在栈中重新分配内存的思想,相辅相成。

其实在面向对象的程序设计中对函数参数的传递的要求大大降低,绝大多数情况下,直接访问数据,如同在第2章main()函数中描述算法。

全局变量与局部变量、作用域与存储类型是对函数的深入理解,这些知识对函数的安全应用是关键的,对第4章封装性的理解也是重要的基础。

关于块域有一点需要说明:在VC++中,只认花括号,for语句括号表达式中定义的变量在for语句之外仍有效,如在for语句外加花括号,则只在for语句之内有效。所以不要按C语言的习惯,在for语句括号表达式中定义控制变量。

递归调用是一个难点,学生学习起来会有困难。其表述则非常简单,写出来的程序易读易懂。但学生自己编就会错误百出,还是不善于模仿,授课时要让同学多模仿。有错不怕,只有遇到挫折,解决了,才会有成就感,就会信心大增。有些问题用递归设计算法非常简单,用其他方法则十分困难甚至现在编不出来。但也要指出递归消耗了大量的内存空间和运行时间,可用Fibonacii例演示,把它改成计算前40项,就非常明显,耗时按指数上升。并与第二章的递推法Fibonacii例运行相比较。要指出:递归法中循环是隐式的,递推法中循环是显式的。

函数的一些高级议题是为第4章服务的,仅选用了与之相关的内容。 函数重载与第4章中的运算符重载是面向对象程序设计的关键技术之一。在教学次序上现在是尽可能前移,原因之一是模板(参数化程序设计)教学已经大大前移,而模板必须使用重载。

《C++程序设计》教学指导书 24

默认变元(参数)在构造函数中应用非常频繁,其他方面也是常用的。一个参数只能在一个文件被指定一次默认实参,习惯上,默认参数在公共头文件包含的函数声明中指定,否则默认实参只能用于包含该函数定义的文件中的函数调用。 内联函数的本意是:在源程序中原地扩展。在普通的面向过程程序设计中原地扩展是调用处有一份副本,好像宏一样;在第4章类对象定义处可看作原来使用指针指向共享的一份成员函数,现在是原地扩展,自己有一份的副本。但内联只是一个建议,编译器有权决定是否这样做。

头文件与多文件结构可简单地讲一下,学生自学为主,称为泛读。应用是很多的,主要是在后面的应用中掌握。

本书中有两个呼应,课堂教学与实验呼应,实验滞后一些,同时起复习巩固作用;另一个就是课本中前后内容呼应,不指望学习一次完成。这样对学生效果要好一些,教师要辛苦一些。

疑难解答

静态变量

静态局部变量何时建立的是一个有趣的问题。实际上在生命期的教学内容中已经指出:静态生命期(Static extent或Static storage duration)指的是标识符从程序开始运行时就存在,具有内存空间,到程序运行结束时消亡,释放内存空间。所以与全局变量一样是程序开始运行时建立,如有显式初始化,则在第一次进入该静态局部变量所在局部域时进行唯一的一次初始化。

内存分配图 使用内存分配图来分析函数调用的结果是很有效的,参见下面的例子: .6 设有函数说明如下:

int f(int x, int y){ return x%y+1; }

假定a=10,b=4,c=5,下列语句的执行结果分别是 (1) 和 (2) 。 (1) cout<(1)答案:4

amain()域:10xf(a,b)域:10xf(a,c)域:10bc5f(a,b)返回值3f(a,c)返回值14y4return x%y+1;y5return x%y+1;

(2)答案:5

《C++程序设计》教学指导书 25

amain()域:10bc5xf(f(a+c,b),f(b,c))返回值:5yreturn x%y+1;54f(f(a+c,b),f(b,c))域:xf(a+c,b)域:xf(b,c)域:415y44return x%y+1;y5return x%y+1;

.7下列程序的输出结果分别为 (1) 和 (2) 。 (1)

#include using namespace std; int a,b;

void f(int j){ static int i=a; //注意静态局部变量 int m,n; m=i+j; i++; j++; n=i*j; a++; cout<<\"i=\"<int main(){ a=1; b=2; f(b); f(a); cout<<\"a=\"<解:

《C++程序设计》教学指导书 26

a全局域:main()域:jf(b)域:jf(a)域:232123bi静态f()域:1232mni*j=2*3=6ni*j=3*3=9

3i+j=1+2=3mi+j=2+2=4

答案:

i=2 j=3 m=3 n=6 (f(b)域和静态f()域:对应蓝色) i=3 j=3 m=4 n=9 (f(a)域和静态f()域:对应绿色) a=3 b=2 (2)

#include using namespace std;

float sqr(float a){return a*a;} float p(float x,int n){ cout<<\"in-process:\"<<\"x=\"<int main(){ cout<图解递归,共五层,返回值是回归时产生:

《C++程序设计》教学指导书 27

main()域:p(2.0,13)8192x*sqr(p(x,n/2)第1层()域:n13x2.0p(2.0,6)sqr(p(x,n/2)第2层()域:n6x2.0p(2.0,3)x*sqr(p(x,n/2)第3层()域:n3x2.0p(2.0,1)x*sqr(p(x,n/2)第4层()域:n1x2.0p(2.0,0)128第5层()域:n0x2.0

答案:

in-process:x=2 n=13 in-process:x=2 n=6 in-process:x=2 n=3 in-process:x=2 n=1 in-process:x=2 n=0 8192

调用树的使用

用递归法求解Fibonacii数列。递归调用过程用内存分配图表示是困难的,因为这使用了两分支的调用,如下图,随层数的递增,内存分配在翻倍。画到Fib(4)已经很困难。

fib4域:fib:2+1=3n:4fib3域:fib:1+1=2fib2域:fib:1+0=1fib:1fib1n:1n:2fib:0fib0n:0n:3fib1域:fib:1n:1fib2域:fib:1+0=1fib1域:fib:1n:1n:2fib0域:fib:0n:0

《C++程序设计》教学指导书 28

而教材中采用调用树描述则明了得多。这里画到了fib(5)。

fib(1) 1 0 fib(0)fib(2) 1 1 fib(1)fib(1) 1 1 0 fib(0)fib(1) 1 0 fib(0) 2 fib(3)fib(2)fib(2) 1 1 fib(1)fib(4) 3 2 fib(3) 5 fib(5)

图中的带箭头的曲线表示程序运行的过程,向上是调用(递推),向下是返回(回归),函数名旁标的数字是函数返回值,填写时从树的末端开始,那里的值是确定的,不再调用自己(递归终止),逐步向根部填写,即数值是在返回时产生。

,那里的加圈的数字表示打印的次序,排序是由带箭头的曲线,即程序运行的过程决定的:

3.15 下面递归函数执行结果是什么?

1) void p1(int w){

int i; if(w>0){

for(i=0;i调用p1(4)。

答:用调用树来解答,如下图,注意打印是在递归调用之前:

P1(0)按照打印语句:

for(i=0;i4P1(1)可得输出为: ① 4 4 4 4 //第1次W=4 ② 3 3 3 //第2次W=3 ③ 2 2 //第3次W=2 ④ 1 //第4次W=1

2) void p2(int w){

3P1(2)2P1(3)1P1(4)《C++程序设计》教学指导书 29

int i; if(w>0){ p2(w-1);

for(i=0;i调用p2(4)。

答:用调用树来解答,如下图,注意打印是在两次递归调用之间:

P2(0)1P2(0)P2(1)2P2(0)3P2(0)P2(1)P2(0)5P2(0)P2(1)6P2(0)7P2(0)P2(1)P2(0)9P2(0)P2(1)10P2(0)11P2(0)P2(0)13P2(0)P2(0)15P2(0)P2(1)P2(2)P2(1)14P2(1)P2(2)P2(2)P2(2)412P2(3)P2(3)8P2(4)

按照次序和打印语句,可得输出为: ① 1 ② 2 2 ③ 1 ④ 3 3 3 ⑤ 1 ⑥ 2 2 ⑦ 1 ⑧ 4 4 4 4 ⑨ 1 ⑩ 2 2 ⑾ 1 ⑿ 3 3 3 ⒀ 1 ⒁ 2 2 ⒂ 1

3) void p3(int w){ int i; if(w>0){ for(i=0;i《C++程序设计》教学指导书 30

}

cout<调用p3(4)。

答:用调用树来解答,如下图,注意打印是在两次递归调用之前:

P3(0)4P3(1)3P3(2)P3(-1)P3(0)P3(0)5P3(1)P3(-1)P3(0)7P3(1)P3(-1)P3(0)2P3(3)6P3(2)1P3(4)

按照次序和打印语句,可得输出为: ① 4 4 4 4 ② 3 3 3 ③ 2 2 ④ 1 ⑤ 1 ⑥ 2 2 ⑦ 1

4) void p4(int w){ int i; if(w>0){ for(i=0;icout<for(i=0;icout<P4(0)4P4(1)53P4(2)62P4(3)7调用p4(4)。

答:如下图用调用树解答,注意打印是在递归调用之前和之后各一次:

1P4(4)8 按照次序和打印语句,可得输出为:

《C++程序设计》教学指导书 31

① 4 4 4 4 ② 3 3 3 ③ 2 2 ④ 1 ⑤ 1 ⑥ 2 2 ⑦ 3 3 3 ⑧ 4 4 4 4

输出运算符(插入运算符<<)的讨论

参见教材例3.12:

cout<<”\\n4!=”<输出:

4 3 2 4!=24

1

1

2

6

24

为什么“4!=”后出现?

插入运算符是左移运算符重载而来,是左结合。插入运算符可以连续使用是因为其返回的是输出流本身。虽然输出时是从左往右依次输出,但内部的计算次序是另一回事。编译时确定从最左边开始调用和计算,全部算完,再输出。执行时是先算函数值,然后再从左到右输出各表达式的值。所以例4.12是先算阶乘,后输出“4!=24”。

但不能分成两句: cout<<”\\n4!=”; cout<这样输出则与教材上完全不同。

计算是先右后左。请看下一条输出语句:

cout<<”\\n4!=”<先算fac(3), 后算fac(4),先右后左。但输出还是从左到右。 输出:

3 2 1 4 3 2 4!=24 3!=6

1 1

2 1

6 2

6

24

或者说,先从右到左的次序把表达式算出来,放到待输出的位置上,再从左到右依次输出。参看下例:

int main(){

int a=5;

cout<<++a<<’\’<输出: 6 5 6

为什么第一次a已变为6,第2次却还是5? 该例子先安置a(计算a,a也是表达式),后计算++a;如果++a与a换位,

《C++程序设计》教学指导书 32

似乎应是输出:5 6 6,实际是:6 6 6。

再参看一例:

int a=2,b=3,c=4; a=b=c=5;

结果a,b,c全为5,而不是a为3,b为4,c为5。道理是一样的:从右开始计算。

默认参数函数

一个参数只能在一个文件中被指定一次默认实参,习惯上,默认参数在公共头文件包含的函数声明中指定,否则默认实参只能用于包含该函数定义的文件中的函数调用。

注意,默认实参可以在定义中,也可以在声明中制定:

#include using namespace std; void delay(int=1000); void delay(int loops){ //延时函数,默认延时1000个时间单位

for (; loops>0; loops--); }

int main(){

delay(100);

cout<<\"延时100个时间单位\"<也可以:

#include using namespace std; void delay(int );

void delay(int loops=1000){ //延时函数,默认延时1000个时间单位

for (; loops>0; loops--); }

int main(){

delay(100);

cout<<\"延时100个时间单位\"<但有两点:第一,先说明后引用,通常声明在使用之前,定义在使用之后,所以往往在声明中指定;第二,不能在两处指定,即使完全一样也认为重复指定,出错;但是声明与定义在不同域中(如声明在头文件中),可以也应该重新指定。

默认实参可以分数次完成,但一个参数也只能指定一次默认实参,并且

《C++程序设计》教学指导书 33

是从右向左添加:

int fun2 (int, int , int =20);

……//A域,使用fun2()含一个默认实参 int fun2 (int, int =10, int);

……//B域,使用fun2()含两个默认实参 int fun2 (int=5, int , int);

……//C域,使用fun2()含三个默认实参

默认实参并不一定是常量表达式,可以是任意表达式,甚至可以通过函数调用给出。如果默认实参是任意表达式,则函数每次被调用时该表达式被重新求值。但表达式必须有意义;如:

int k=8; //k的值后面可以改变 float fun1(int); ……

void fun2(float x=fun1(k));

每次默认调用fun2()时,都会调用fun1(),为x取得一个值。

第四章 类与对象

教学目的

建立类与对象的概念,建立函数也可以是数据成员之一的思想,从封装不易有副作用(易读易懂易维护)和软件的可再利用两方面使学生了解建立类与对象的必要性。逐步培养学生从实际问题抽象出类的能力。

建议学时安排

授课7学时,习题1学时

类与对象,构造函数和析构函数。2学时。

引用与复制构造函数,成员对象与构造函数。2学时。 运算符的重载,友元。2学时。

结构、静态数据成员,面向对象程序组织与WINDOWS实现。1学时。

教学方法提示

如果说前面是铺垫,那么现在是全面进入面向对象程序设计的教学,前面学的东西在这里应用和深化。本章主体内容分两部分:定义和概念,应用。

定义和概念这部分看起来没有难点,但内容多,大量概念堆砌,讲透不容易。实际上课程中安排了三个环节来完成教学。第一步是教材中的类和对象的基本概念,构造函数与析构函数,讲解时先给结论,道理作为补充说明。第二步是引用和复制构造函数,以及成员对象与构造函数(直接给出构造函数的固定格式,必须用此格式)。第三步是运算符重载,也就是在应用中加深理解,包括引用、复制构造函数、复制赋值运算符,都在应用中理解。

以上内容都是格式的讲解,只需学生照样去做。

《C++程序设计》教学指导书 34

在本章中复制构造函数与复制赋值运算符是属于按成员语义定义的,可以默认产生,并不困难。特别要求学生掌握的是在什么情况下调用复制构造函数。除了在复制定义(复制方式初始化)对象时使用外,更多使用在函数参数按值传递和函数按值返回时,书上已有说明,这里强调的是:如果按引用传递和返回,则不使用复制构造函数。

聚合(aggregation),也就是使用成员对象的编程方法,在面向对象程序设计中是很重要的,而且使用起来非常方便。比起派生的复杂性,聚合的最大优势是无难点,所以在本教材中是用得多,说得少,可以说是篇幅与重要性并不相符。

在本章中提出了逻辑上和物理上两个概念,希望能帮助学生理解。逻辑上的东西对编程者是直接的,应该讲按这个概念来理解面向对象就可以了。物理上的是进一步的,锦上添花,对希望知之更深一些的同学是不可少的。对象的存储方式讲的是物理上的概念,从本书有关存储的内容安排,应不难理解。在复数运算符重载中,参数为同类的对象时可直接用点号访问私有数据,而不用公有函数,有物理上的存储概念就容易解释。静态函数却是纯逻辑概念,物理上一样,逻辑上不同。对学生而言,逻辑上的概念更重要。

本章中有大量的格式,格式是没商量的,只有照做。千万不要创造。如习题9类型强制转换,有同学认为不合理,不愿模仿,改了一下,结果当然错了。

从本章起凡是规范的面向对象的例题看上去都很庞大,但新内容或关键部分只是其中几个函数,讲解时只重点讲这几个函数。一个庞大的环境留在那里可以让同学始终处于面向对象的氛围中,让他们潜移默化——面向对象就是这样编程,教师千万不可也批评太繁复。

复数运算符重载是本章的重点,这是学生第一次接触C++核心技术,绝不能因学生中学未学复数(如江苏高中教学大纲要求,但高考不考)而降低要求。这里应是全章讲得最细的地方,包括成员函数方式和友元函数方式。

运算符重载是面向对象C++程序设计所特有的(面向过程的C程序设计无此功能),本书用复数类为第一个实用的例子,就是它显示了面向对象的优势:可以用象实数运算一样的算式来进行复数运算。这一点C语言是做不到的。

所谓用友元函数来重载运算符,实际上是用的与特定类无关的函数来重载运算符,采用友元是为了可以直接访问类的私有成员,更方便一些。也就是说重载运算符的两种方法是:成员函数和非成员函数。

采用友元函数还有两个原因。第一,应用方便,运算符具有交换性(这在自定义的复数中已很清楚)。第二,不需要修改原定义该运算符的类,如插入(<<)和提取(>>)运算符重载是不允许修改标准输入输出流类库中的定义的,也就是不可添加成员函数,必须用友元函数。

但是并非所有运算符都可以用友元函数表示,除了教材上所指出的赋值运算符=外,函数调用运算符()、下标运算符[]、成员访问运算符->也必须使用成员函数。

《C++程序设计》教学指导书 35

最后必须指出:只能重载已有的运算符,不能创造新的运算符!否则系统给出语法错。

单目运算符后“++”的成员函数重载方式如下:

Complex Complex::operator++(int){

return Complex(Real ++, Image++); }

采用友元方式则必须使用引用,因为被施加“++”运算的是一个参数。仍以友元函数重载后置“++”为例:

friend Complex operator++(Complex & c , int) { //注意友元方式与前者的区别

return Complex(c.Real++ , c.Image++) ; }

采用引用类型,后“++”是直接施加于实参。否则施加于副本,而实参不变。

必须指出在返回值Complex(Real ++, Image++)中,构造函数是用++前的值来建立无名对象,所以在表达式中参加运算的是原来的值。

到本章学习后,学生对数据,操作(函数),就会有进一步的理解。“会当凌绝顶,一览众山小”,这时学生的信心会提高。

教学的难点就在前面的这4章,学生如果进了门,后面按部就班地学习,一般问题不会太大。就怕前4章卡壳,后面积重难返。

Windows操作系统是面向对象的,C++的事件驱动消息传递是由操作系统实现的。请教师讲清楚。

疑难解答

默认的构造函数

只要定义了一个构造函数,系统不会再生成默认的构造函数,需自己定义。另外类对象数组只能定义时按元素调用默认的构造函数建立。学生都容易疏忽。这些是规定,规定是没商量的,计算机只认规定不讲道理,要提醒同学。

对于指定了全部默认实参的默认的构造函数,必须在类定义中指定默认值,否则认为是普通的构造函数。如例4.2的类定义中有:

Rectangle(int l=0, int t=0, int r=0, int b=0); //构造函数,带默认参数,默认值为全0

即使把该例的三个组成文件合并,也必须在类定义中指定默认值。但把主函数中的

Rectangle rect; //调用默认的构造函数

改为:

Rectangle rect(10); // 调用普通的构造函数

就可以在类外说明构造函数时指定。但系统不认为是默认的构造函数,并且最多用到3个默认值。

聚合的复制构造函数

教材例4.6的复制构造函数如下:

student(student& stu):id(stu.id){

e<《C++程序设计》教学指导书 36

}

注意:参数总表只有一个同类对象的引用作为参数,而冒号后成员对象的参数名表也只有一个实参,是总表参数对象的对应成员对象。 在教材中指出构造函数冒号后的部分是属于函数体的,所以那里是调用成员对象的构造函数,参数是实参。但学生往往疏忽,要多提示几次。到派生类的构造函数,也有同样的表达方式。

C++类的构造函数的作用的讨论

在各种参考书中,C++类的构造函数的作用大致可以分为两种说法: 1.当对象建立好之后,对对象进行初始化。 2.创建一个对象,并进行初始化。

那末究竟哪一种说法正确?当定义一个对象的时候,系统究竟是先为对象分配存储空间,然后再调用构造函数,还是直接调用构造函数,然后在执行构造函数语句初始化对象之前为对象分配内存空间?简单地说,就是为对象分配存储空间究竟是在构造函数的外部实现还是在内部实现?

从物理上讲,对于对象中没有动态分配的资源,第一种说法是正确的。系统按语义给各数据成员分配内存空间,构造函数对这些数据成员进行初始化。

构造函数的引入首先就是为了解决类对象的私有数据初始化问题,采用构造函数进行初始化是C++的标准方法。同时因为C++是由C发展而来的,为了与C语言兼容,当类对象的数据成员全部为公有时也可以不用构造函数做具体的初始化,方法是在对象名后加“=”加“{}”,在花括号中顺序填入全体数据成员的初始值(与C语言结构的初始化相同),但建议不要用这种方法。

按C++的标准方法,建立一个对象总是要调用构造函数,哪怕该构造函数是由系统生成的,任何工作都没有做。从语法上(逻辑上)可以讲,调用构造函数创建一个对象,并进行初始化。这样在教学上解释起来可能比较方便。如在4.5节例4.8中:

return Complex(Real+d , Image) ;

这里调用Complex类的构造函数,物理上是建立一个无名对象并用该构造函数进行初始化。如果教学时从语法上讲调用Complex类的构造函数,建立一个无名对象,可能更简洁。从语法上讲只要调用一次构造函数,就建立一个对象,对象可以没有名字,生命期结束时自动析构,但不代表物理过程。

定义对象初始化时也可以把构造函数显式表示出来,如教材中的例子: CGoods Car1= CGoods(“夏利2000”,30,98000.0);

教学时从语法上讲调用CGoods 类的构造函数建立了Car1,同样很简洁。

更进一步,构造函数不仅完成数据成员的初始化,它还应该进行类对象所需的动态资源的分配等等,在语法上也可以认为构造函数最终建立了一个完整的类对象。

在第10章异常处理中抛出异常对象,也是在异常类后加“()”表示该类的无名对象,语法上讲就是调用构造函数建立一个对象

所以,第二种说法是从语法层面出发的,从语法上讲是正确的,但并不代表计算机运行的物理过程。本教材第二版在涉及物理过程时,采用第一种说法。在与语法相关的教学中采用第二种说法。不过,作为高级语言程序设

《C++程序设计》教学指导书 37

计的教学,过于追究此问题,就失之穿凿了。

类型的转换

将其他类型的变量或对象转换为本类型的对象是由相应的构造函数完成的,前提是对应的构造函数必须存在,如要将实数转换为复数,必须有一个参数为实数的构造函数。特别指出,在参数类型不完全匹配时,赋值兼容是由系统自动调用相应的构造函数实现的。因为转换总是在参数中完成,即使是赋值,例如实数赋值给复数也是作为赋值运算符的参数,所以将其他类型的变量或对象转换为本类型的对象总是由系统自动调用相应的构造函数隐式完成的。而且只有传值调用复制副本才会进行转换。无需转换时调用复制构造函数,需要转换时调用对应的构造函数。当然const引用调用同样会进行转换。

本类型的对象转换为其他类型的变量或对象,必须定义一个类型转换成员函数,这个转换是不会自动完成的,必须显式调用。如第4章习题9将人民币类转换为浮点数。

函数返回值时也会转换。不转换时调用复制构造函数制作副本。如果已经已经显式调用某一构造函数,则使用该构造函数,不会再调用复制构造函数。如是返回引用,则不会转换。

引用的讨论

在面向对象的C++程序设计中要淡化指针的应用,最终指针仅用作纯地址,本章的引用就是替代指针的方法之一。引用的教学中有一些问题解答如下:

不存在引用的引用,请见下式:

int a,&b=a,&c=b;

并非b是a的引用,c是b的引用,而是b和c都是a的引用;以上定义仅仅是表达传递过程,而不是定义引用的引用,不存在引用的引用。

&作为说明符表示引用,仅用于变量和函数的形式参数与返回值类型说明。&符号还用作运算符(取地址&,按位与&和与运算&&)。如下有变量和函数说明:

double a, b;

void swap(double &d1,double &d2);

调用时必须写:swap(a,b); 不可写:swap(&a,&b);

因为后者的实参不是a,b的引用,而是a,b的地址。

正因为不存在引用的引用,我们才能定义多种意义的&来说明引用,否则必须定义一个专用的引用说明符。

函数返回值类型为引用时,被引用的量不能是该函数内定义的局部量。参见下例:

int a;

int & fun(){return ++a;} //A int & fun(){return a++;} //B

表面上看A,B两行都返回表达式,都是错的。但A行正确,因为++a是先++,a的内容先加1,返回的是全局变量a。B行错误,因为a++是在一份临

《C++程序设计》教学指导书 38

时变量中,表达式完成后,再修改a,返回时运算没有结束,返回的是临时变量。

因为数组的引用不存在,所以实参为数组时,不能用引用。但这并没有什么可遗憾的,因为下一章指出,数组名传递的是数组存储的首地址,并没有在函数中建立一个新的数组。要是有数组引用的定义存在,反而重复了。

const引用

引用在内部存放的是被引用对象(或变量)的地址,不可寻址的值(包括字面常量)是不能引用的;当引用作为形参时,实参也不能使用不可寻址的值,更不可能进行类型转换(如:实数转换为整数)。但是const引用不同,它是只读的,为了绝对保证不会发生误改,编译器实现引用时,生成一个临时对象,引用实际上指向该临时对象,但用户不能访问它。所以const引用可以实现不可寻址的值(包括字面常量)的引用,例如:

const int &ri=1024;

是正确的,没有const是错误的。再如下例完成了类型转换:

double dval=1024; const int &ri=dval;

是正确的,编译器将其转换为:

double dval=1024; int temp=dval;

const int &ri=temp;

因有临时对象,引用和类型转换都实现了。

当const引用作为形参时,产生了实参的一个副本,并能进行类型转换(如例4_8后的说明和例4_8_1:调用构造函数将实数转换为复数)。有类型转换时与传值调用一样是调用对应的构造函数完成类型转换的。必须提醒,当类型完全匹配时,尽管这里有副本,与不加const的引用一样,都不调用复制构造函数。换句话说,这里建立副本是用的其他技术。

这里所谓的用其他技术实现的只能是按语义复制,也就是第七章中的浅复制,编译器不可能知道具体的形参类型有何特殊复制要求。但是这一点不必过分担心,因为加const编译器发现修改形参就认为出错,编译通不过,制作副本是双保险,第七章中指出的浅复制的问题不会出现。

const引用作为形参,形参类对象是const对象(见),不能访问非const的成员函数,不能通过那些成员函数修改对象的属性(成员数据)。如果那样做,编译器会指出:不能把this指针(见第五章)从指向const对象转换为指向普通的对象引用。但是如果成员数据是公有的,直接用非成员函数修改成员数据,编译器在编译时是不会发现错误的。或者成员数据是私有的,但用友元函数去修改成员数据,编译器同样不会发现错误。所以良好的封装对面向对象的程序设计是重要的。

函数返回值与返回引用

注意:这里与通常的提法不一样。通常的提法是“函数可以有返回值”和“引用可以作为返回值”,该提法不严格,引用与值是两个不同概念。这里采用与函数调用完全对应的提法。

通常函数可以返回一个值,格式为:return 表达式;

《C++程序设计》教学指导书 39

与函数的传值调用类似,既然是一个值,返回时就必须为它设置一个变量承载该值,该变量建立在调用该函数的表达式域中,是一个无名的临时变量,它对编程者是透明的,所以不需要名字。如果返回的是类对象的值,则必须调用复制构造函数,以该类对象的数据成员为初始值,建立一个无名临时对象。参见下例:

Complex Complex::operator+(Complex c){ Complex Temp(Real+c.Real , Image+c.Image) ; return Temp; }

//显式说明局部对象

隐式调用复制构造函数建立无名临时对象。但并非所有情况都调用复制构造函数,参见下两例:

Complex Complex::operator+(Complex c){ mage) ; //显式说明局部对象 return Complex(Temp.Real,Temp.Image) ; }

调用构造函数建立无名临时对象。

Complex Complex::operator+(Complex c){ return Complex(Real+c.Real , Image+c.Image) ; }

同样调用构造函数建立无名临时对象。只要已经显式给出调用那一个重载的构造函数,那就使用那个最匹配的构造函数。

表达式域是局部域的一种。实际上表达式中每一步运算产生的中间结果都放在无名临时局部变量中以参加下一步运算。可参见节例7.9简单的算术表达式的运算。

函数也可以返回引用。直接由引用变量参加调用该函数的表达式的运算,不再使用那个无名临时变量过渡。该方式在技术处理上,与引用调用类似,仍保留无名临时变量,但其中中放的是temp的地址。不产生副本,效率提高了。

在采用引用返回方式时,返回的变量必须在主调函数域中有效,所以不能用被调函数中的局部变量,如用了,通常编译器会给出一个警告,这个警告是不可忽视的。引用返回最常用变量的是由引用参数传递过来的变量(见例4.5),其次是全局变量,这样返回的变量地址是有效的。

返回引用是常用的,但不是所有情况都可以用。例如复数中+运算符的重载,返回值的生命期必须在调用的表达式中,返回对象本身,产生一个符合要求的临时对象。而返回对象的引用,因为参加运算的对象本身不变,变化的是局部的中间变量,而该中间变量的生命期仅在函数中,不合引用返回的要求。其实所有类中,+、-、*、/等有类似要求的运算符重载都不适用于引用返回。

引用返回是C++高级语言层次上的概念,或者说逻辑上的概念。在底层实现或物理上,它与返回一个指针是等效的。返回指针的值,即返回地址。返回引用也是返回一个地址,从这个意义上说,返回引用是返回值的特例。

传值调用与引用调用讨论

传值调用永远是主流,因为它的隔离作用,可以避免副作用,实参不会

《C++程序设计》教学指导书 40

被误修改。引用调用,可以修改实参的值,除非要修改实参的值,或必须在原变量上而不是在副本上进行操作(如两数据交换、赋值操作符的重载、在二叉树上添加新结点)。即使必须使用引用,也要小心副作用,尤其是那些仅在特殊情况下出现的副作用。

在实验十~十二中分数类的私有成员函数——通分函数是一个很好的例子。那里有三个不同的通分函数,下面做一个比较。

第一个是实验十第2题中的,通分实现的是本分数类对象与由形参分数类对象b传递实参分数类对象的通分,必须返回通分后的形参b。

fraction fraction::makeCommond(fraction b){ int temp; reduction();//约分本对象 b.reduction();//约分形参对象 above*=b.below; b.above*=below; temp=below*b.below; below=b.below=temp; return b;//返回通分后的形参 }

调用方式参见下列加法函数: fraction fraction::add(fraction b){ fraction temp; b=makeCommond(b); //通分 temp.above=above+b.above; temp.below=below; temp.reduction(); //约分 return temp; }

除使用时不太顺眼,还要加一个赋值运算外,是完美的,无副作用。

第二个是实验十一第4题中的,所有成员函数全部采用引用调用。

void fraction::makeCommond(fraction& b){ int temp,t1,t2; reduction(); b.reduction(); t1=above*b.below; t2=b.above*below;

/*这里必须用t1和t2过渡,否则当分数对象a与自己通分时会出问题,如求f1==*/ temp=below*b.below; below=b.below=temp; above=t1; b.above=t2; }

该通分函数应该是最规范的,其调用也最简单,但是编写时必须十分小心,因为潜在的副作用仅在分数对象与自己通分时发生,很难想到要用t1和t2过渡。调用方式如下:

《C++程序设计》教学指导书 41

fraction fraction::add(fraction& b){

//如果参数采用(const fraction& b),通分函数不必用t1、t2过渡 fraction temp; makeCommond(b); //通分 temp.above=above+b.above; temp.below=below; temp.reduction(); //约分 return temp; }

第三个是实验十二第5题中的,

void fraction::makeCommond(fraction& b){ int temp; reduction(); b.reduction(); above*=b.below;//与实验十一第4题相比,未用t1和t2过渡 b.above*=below;//但使用t1和t2过渡更好 temp=below*b.below; below=b.below=temp; }

因为分数类的所有接口函数全部用传值调用,未用t1和t2过渡,但欠规范。调用方式如下:

fraction fraction::operator +(fraction b){ fraction temp; makeCommond(b); //通分 temp.above=above+b.above; temp.below=below; temp.reduction(); //约分 return temp; }

矩形类讨论

在同步实验十二 3.阅读与理解 中有一个定义:

typedef struct tagRECT { LONG left; LONG top; LONG right; LONG bottom; } RECT;

这是C语言定义结构的老格式,这种方式定义的结构类型名tagRECT 和RECT意义是完全一样的,互为别名。Windows开始定义RECT结构时使用的还是C语言。

CRect类的成员函数operator+和operator- 有多个重载函数,如参数是CPoint或CSize类型,则移动矩形;如是CRect类型,则扩展(缩小)矩形。对加法,扩展时是被加数矩形的左上角坐标与加数矩形的左上角坐标相减,

《C++程序设计》教学指导书 42

而被加数矩形的右下角坐标与加数矩形的右下角坐标相加,即在两个方向上扩展。如要取得同步实验十二 2.范例+和+=的结果,必须定义加数矩形的左上角坐标为(0,0)。

要理解,第一,学生必须亲自使用这些函数,即实践出真知,这一条原则在整个C++学习中都是最基本的。第二,要按座标位置画这些矩形的变化。

VC++与标准名字空间

对标准名字空间的支持是不完备的,对于C++标准库中新的头文件等在标准名字空间中,必须使用using namespace std。而由C标准库沿用过来的、那些以c开头的头文件不在标准名字空间中,不可使用using namespace std,如果在某文件仅仅包含了这些以c开头的头文件,又使用了using namespace不可加using namespace std。从头文件的源代码看,VC++6.0也试图兼容新旧标准,但失误了。兼容的很好,同样情况下可以加using namespace std,以与C++标准的要求一致,当然也可以不加。

使用VC++6.0,在包含标准库时(用无.h的头文件),会发生一系列的冲突。表现在:重载的<<、>>不能用,认为有岐义性(ambiguous);友元也不认等等。

标准库采用了更严格的语法要求,以致与原来的C++不兼容。例如例4.8_1,必须在using namespace std后加有关友元函数的声明:

class Complex;

Complex operator+(const Complex &,const Complex &); Complex &operator +=(Complex &,const Complex &); double abs(Complex &);

friend Complex operator*(const Complex &,const Complex &); friend Complex operator/(const Complex &,const Complex &);

采用友元的标准格式。

使用VC++.net则一切正常。(SP5),才能基本正常。

第五章 数组与指针

教学目的

数组及其存储方式是本章的重点。指针是C++的难点。通过学习使大学生对指针(内存地址)和它与数组的关系有全面而深刻的理解,进而对内存管理有初步了解。

建议学时安排

授课3学时,习题1学时

数组、数组元素及其存储方式,数组名作为函数参数,数组存储与

《C++程序设计》教学指导书 43

访问方式,数组作为函数参数。1.5学时。

指针与地址,this指针;数组与指针:数组名,指针与指针运算,指针作为函数参数。1学时。

标准的C++string类。0.5学时。

教学方法提示

新的学时安排,删去部分面向过程的内容,增加面向对象内容,整体难度下降。

在面向对象的教学中,指针要淡化,最后只保留最基本的地址功能。最多加上与数组的关系。

本章主要分两个部分:数组与指针。

对数组有两点要讲清楚:第一数组的顺序存储方式和访问方式;第二C++不检查数组边界,在数组情况下仅是最高维不查边界,但其他较低维是在控制中的。

指针一直是C++的难点,而且是面向过程的东西。由教学实践和C++的新技术(引用)的发展,难度应该降下来。第一版教材难度依旧,原意是兼顾等级考试。当时要求在教学时把难点尽可能削减,第二版大幅度精简了内容。

学生对指针概念最搞不清的是:认为有了指针,就等于有了存储数据的内存单元。甚至学习了动态内存分配后,还犯这样的错。所以必须不断地提醒同学:指针只有依附于(指向)一个变量(或动态分配了内存)后才有意义,才能存储数据和进行各种运算。

教学时要讲清楚:指针是地址,但不完全是地址。如果仅仅是地址,就不必按它指向的变量类型区分。指针还包括所指对象的存储方式和占用内存的大小。有此概念,指针的各种运算都易理解了。内存是由系统分配的,指针只能依附于变量或对象,不能随意赋值。随意赋值是指针出错的最主要原因。进而理解空指针的概念和必要性。

指针常量是固定指向一个对象的指针;而常量指针是其所指对象不可通过该指针进行修改。

与第4章点运算符对应,访问对象的箭头运算符也要求能灵活使用。

指针与数组的对应关系是教学的重点。首先要求学生掌握,代表一维数组的指针,它指向的是数组元素,数组名是指针常量(固定指向一个数组首地址的指针);第二,指针的运算就是数组下标的运算,脱离了对应的数组,指针运算是毫无意义的。第三,不检查数组边界问题实际是为了照顾指针的灵活性,特别是作为函数参数传递时,指针比数组名更灵活,通用性更好。

如果学生未学线性代数,矩阵运算相关内容可不讲

作为阅读内容的多极指针与数组中提到了多级指针,即指向指针的指针。这类指针最常用的有两种表现形式,第一种是本章介绍的指向数组整体的指针,在定义中必须指明数组元素的数量,如果数组是一维的,则该指针可以取代对应的二维数组的数组名进行运算,如果数组是二维的,则该指针对应三维数组名。注意数组只有最高维的边界是不加检查的。第二种是取

《C++程序设计》教学指导书 44

代6.3节中介绍的指针数组的数组名进行运算。两者使用上的差异是:前者多一些,可以取代数组名进行运算,可以用来动态建立数组,可以用来一次性删除该数组,参见7.1.1节中间选读部分;后者使用更加灵活,可以分级动态建立数组,但删除必须一级级逐步进行,参见。

字符型指针,当它指向一个字符串时,可用来输出该字符串。但如下例:

char *p=NULL; cout<运行时出错。如要输出p的地址,可将p强制转换为泛型指针。

C++提供了两种字符串的表示:C风格的字符串和标准C++引入的string类类型。我们建议使用string类。建议C风格字符串教学要求可以降低。string类重载了大量的运算符,使用非常方便。

string是类,它有自己的构造函数和析构函数,如果它作为类或结构的成员,要记住它是成员对象,当整个类对象建立和撤销时,会自动调用作为成员对象的string字符串的构造和析构函数。string在头文件中定义。头文件与头文件是两回事,后者定义了C风格字符串的处理函数。

自定义字符串mystring在习题里给出了完整的定义,借助它可以了解标准string类的内部构造。同时其中有几个函数与采用指针的C风格字符串的库函数作了对比,可以看出类的成员函数可以直接访问数据成员的优势,以及附带的对数组越界进行控制的好处。也可以看出对数组的索引操作与指针操作的对应关系。可以让同学多下一些功夫研究习题11和12的解答。

这两道题还对返回对象和返回对象的引用做了对比,指出了引用返回不调用复制构造函数建立临时对象的优点,也指出当运算是在局部变量上进行时,不可以使用引用返回。

this指针也是必须掌握的,但主要是应用。

疑难解答

this指针的应用

——this指针的应用。类的定义只是定义了一种类型,而不是建立一个占据了内存的实体——对象。在类的成员函数定义中如果需要处理该成员函数所属的具体的类对象本身,规范的方式就是使用this指针。如在运算符重载中,赋值运算符和复合赋值运算符规范的成员函数重载都是用*this返回对象自身。教材第4章及同步实验Exp12_1和12_2都是非规范的,如果采用this指针函数将更加简洁,并可少调构造函数与复制构造函数。再如主教材P.287派生类Student重载复制赋值操作符中就用了this指针调用基类Person的复制赋值操作符。本教学指导书第九章中派生类的插入和提取运算符的重载,利用基类重载的插入和提取运算符时也使用派生类的this指针和赋值兼容原则。类似的情况,都是这样处理的。

this指针是常量指针,不允许通过它来修改成员数据。如要这样使用必须进行类型强制转换。如student类修改姓名name,可如下处理:

《C++程序设计》教学指导书 45

const_cast(*this)->name=Name; *this的类型是:const student *

指针与数组的差异

应用指针最常见的错误有:

1. 混淆指针与数组名,在指针未指向一个数组时就直接将指针当数组名使用。如:将两个数组合并为第三个数组,同学会定义一个指针,然后将两个数组合并放入以该指针为名字的假想的数组中。指针的基本运作的的确确是与数组名一模一样,但只有指针指向一个数组,或如第七章为该指针动态分配一个数组,才能进行这种运算。

2. 函数返回值为指针,但如果它所指的变量或对象已消亡,则返回值无意义。作为返回值的指针所指向的数据的生命期必须不仅仅在函数域中,函数消亡后,数据应该仍然存在。

尽管尽管指针与数组有很多相通的地方,往往可以混用。但是分清两者的差异更重要。系统为数组确定数量的元素分配了内存,而没有为指针作同样的工作。为指针赋一个字符串,是指针依附于该串。

char cstr[ ]= \"C++ programming language\"; char *pstr=\"C++ is a object_oriented language\";

都是正确的。但

char cstr[25 ];

cstr = \"C++ programming language\";

是错的。而

char *pstr;

pstr =\"C++ is a object_oriented language\";

是对的。指针依附于该串的工作随时可以执行,不限于定义初始化时。

数组与多级指针

数组与多级指针是选读内容,但有一些学生感兴趣。代表一维数组的指针,它指向的是数组元素,代表二维数组的二级指针,指向的是嵌套定义的组成二维数组的一维数组。但有同学认为数组名,如3维数组名是指向2维数组的指针而不是3级指针,下例指出两者是同类的,仅在删除时有所差异:

使用指针数组来建立三维数组,这是例7.2的扩展,如果是学生,需学到第7章时再看。

[提示]可用一维二级指针数组,嵌套一维一级指针数组来完成。

#include #include using namespace std; void display(double ***); void de_allocate(double ***); const int m=4; const int n=6; const int l=5; int main(){

《C++程序设计》教学指导书 46

int i,j,k; double *** data;//代表三维数组名 data=new double **[m];//代表组成三维数组的各二维数组名 if((data)==0){ cout<<\"couuld not allocate.bye\"; return -1; } for(j=0;jvoid display(double *** data){//显示各元素 for(int i=0;ivoid de_allocate(double *** data){ //释放动态分配的内存空间 int j,k

for(int i=0;i《C++程序设计》教学指导书 47

}

delete [] data;

注意,本例只是说明,不是认为内容重要。

第六章 模板与数据结构

教学目的

本章介绍参数化程序设计,也就是使算法于数据类型,在面向对象程序设计中称为类属(genericity)机制,C++中采用模板实现。模板提高了程序的通用性,应用很广。C++编译系统已为常用数据结构提供了一个标准模板库(STL),当表、栈、队列和树等数据结构中数据元素类型不同时,采用模板是最佳选择。这类数据结构亦称为包容(container)数据结构,在第11章将学习STL中的各种容器。

建议学时安排

授课5学时,习题1学时

函数模板及应用,类模板与线性表。2学时。

常用的查找与排序方法,索引查找与指针数组。2学时。 模板与类参数。1学时。

教学方法提示

本章要求掌握模板、线性表、查找与排序的主要算法。模板仅仅是一个工具,重点放在线性表、查找与排序,在应用中学会使用模板。

从本章开始教学以算法讲解为主,代码编写主要以学生自学为主,教师指导关键点为辅。

函数指针也是实现通用算法的手段,但这是面向过程的,已被模板与类参数取代。教材第二版删去了函数指针的应用。面向对象编写算法要比面向过程简单,基本没有函数参数传递问题。

模板是新东西,是为了实现高运行效率的通用算法而推出的。模板的使用并不神秘和复杂,它只是套用了一个固定的格式,授课时讲格式和使用步骤,10分钟就可以了。教材中是用它来实现通用算法,特别是数据结构的通用算法。所有的数据结构本书中都是用模板类来实现的,反复使用,学生肯定能掌握得很好。

包含模板的程序在编译时并不一定完成全部编译任务,函数模板根据一组实际类型或(和)值构造出的函数的过程是用到时才进行。如果类模板中的某些成员函数模板没有实际使用,所含的编译错误在编译时是查不出来的,包括漏了所需包含文件之类的错。所以一个类模板定义完成后,一定要配上完整的检验程序进行编译和运行,才能保证正确。

模板中的例子很多,函数模板中两个例子都与前面的内容相呼应,特别是矩阵运算,很方便地解决了通用性,对比后,会使学生立即喜欢上模板。

《C++程序设计》教学指导书 48

模板参数表的使用与函数形式参数表的使用完全一样,都是位置对应。在使用函数模板参数时有两种方式:

函数模板根据一组实际类型或(和)值构造出的函数的过程通常是隐式发生的,称为模板实参推演(template argument deduction)。如例6.1有:

template Groap max(const Groap *r_array,int size){……}

调用时:

int ia[5]={10,7,14,3,25};

double da[6]={10.2,7.1,14.5,3.2,25.6,16.8}; int i=max(ia,5);

double d=max(da,6);

显式的调用格式与类模板的调用格式一样:

i=max(ia,5);

d=max(da,6);

书上例6.1和例6.2,数组是用指针来传递得的,所以类型参数比数组低一维,而且记住数组最高维是不检查边界的。例6.2中matrix2与result都是指向4元素一维数组的指针常量,此例原意包含进一步理解数组与指针概念的目的。

数据结构比过去的教材多了一点,比较全面地介绍了线性表及其应用。但一方面从大多数非计算机专业的大学生不会再去修一门数据结构看,这一点点经典的数据结构知识对培养大学生编程能力是不可少的,同时它们不难理解,趣味性强,同学很欢迎。对于将来要学习数据结构的专业,学生有了基础,可用更少的学时掌握更多更新的数据结构知识。另一方面从类对象的学习看,没有这些内容,类对象的教学基本成了一个语法的空架子,这违背了编此教材的初衷,也违背了我国计算机教学的2002教程的要求——以算法为核心的思想。

在第二版教材中,各种算法更加规范。排序和查找算法中采用重载比较运算符,元素比较即关键字比较,通用性好。这样的模板不仅可以用于元素为自定义的重载了比较运算符的类,也可用于标准库中的string类等,也可用于基本数据类型。这里也体现了基本数据类型重载了运算符,这一面向对象的特性。

同一任务不同算法可对比来讲,如教材中排序的3种算法,任课教师可用图示的方法细讲算法,并对不同算法对比加以讲评,而代码的编写则是指导学生阅读和上机运行演示。最常用的快速排序(STL仅采用此法),放在实验中,作为选读。

查找部分教学也是用对比方式:对半查找的递归算法和迭代算法,要指出递归的隐式循环和迭代的显式循环代码编写的关键点。

讲课时的艺术性也是很重要的。比如插入排序,按图讲解时可以分解为两步,第一步是从前向后查找线性表中正确的插入位置,第二步是将该元素插入线性表指定位置(向后移动采取从后到前的次序)。而介绍代码时则更进一步,改为从后向前一边查找,一边移动空出位置,找到了立即插入。有两个方法的对比和循环,学生印象深刻,也复习了前面学过的内容。

《C++程序设计》教学指导书 49

本章也包含了复习和应用第四章知识的内容,包括类的定义、函数与运算符的重载等等。重载是参数化程序设计的基本技术之一。

习题解答对模板的使用的典型方法作了演示。函数模板有三种应用方式:

(1) 函数模板作为类模板的成员函数,在本类中重载函数和运算符,直接访问私有数据成员,实现通用算法。这是标准的面向对象的方法。

(2) 的非成员函数函数模板处理模板类(或普通类,或普通数据),以类模板(或类对象,或普通数据)为参数,借助模板类中重载的函数或运算符,实现通用算法。但间接访问私有数据成员。这也是常见的。

(3) 的非成员函数函数模板处理普通数据,往往要用函数作为参数,实现通用算法。

第一种是规范的用法,教材绝大多数例题用该方式。第二种也是常用的方法,对库中的类模板要增加一个算法,不宜去改标准库中的类模板,可用此法。第三种不是面向对象的方法,是面向过程的程序设计方法。

疑难解答

下标运算符重载

主教材例6.3,关于下标运算符[]的重载学生不理解,解释如下:

下标运算符[]返回值必须是引用。因为在式 a[i]=k 中,表面上数组元素作为左值,实际上是下标运算符函数为左值,只有返回值为引用,函数才能作为左值。参见例4.5。

对于静态数组,总是“大开小用”,而且要保证不出界;对于顺序表所有元素都是顺序排列的,不可能跳跃式地安排元素;所以用下标运算符访问顺序表元素时,下标i应在0~last范围内,出错条件及程序是:

if(i>last||i<0){

cout<<\"下标出界!\"<return slist[i];

出错时调用exit()函数退出程序运行。该函数说明参见主教材P.119和P.3。

教材中选用的是考虑线性表可以扩充,最大可扩充到元素数为Maxsize,i要小于maxsize;线性表扩充时必须顺序扩充,即每次last能并只能加1,这样下标范围可扩大到0~last+1。最终得到出错条件是:

if(i>last+1||i<0||i>=Maxsize)

当扩充元素时要有附加处理:

if(i>last) last++;

但是该算法,函数为左值是完善的,作为右值稳健性不好,新增元素是随机数。

递归算法的讨论

主教材例6.4,采用递归实现对半查找:

template int Orderedlist::Binarysearch

《C++程序设计》教学指导书 50

(T & x,const int low,const int high){ // x为定值 int mid=-1; //mid赋初值是关键!否则找不到时可能出错停机 if (low<=high){ mid=(low+high)/2; if(slist[mid]mid赋初值为什么是关键?当找到时不是因为low>high退出循环,而是不再进入两个可能的递归调用(中间点小于定值,查找右区间;中间点大于定值,查找左区间),这时mid已经赋值(low+high)/2,返回的是找到的下标。未找到时,是因为low>high退出递归的隐式循环,注意每次递推,mid重新定义,mid如未赋初值,返回的是随机值,并在回归时逐层传递出来。如果算法最后改:

if(slist[mid]!=x) mid=-1; return mid;

希望未找到mid返回-1,原意很好,但必然出错停机,因为mid为随机数,数组出界。

主教材例6.5,采用迭代实现对半查找,尽管因是显式循环,没有不断的调用,前面的问题不存在,但必须考虑数组为空时出错。

模板编译模式

模板编译模式有两种:包含模式(inclusion model)和分离模式(separation model)。教材中采用的是包含模式,模板定义包含在头文件中。该头文件可以被包含在多个程序文本文件中,相同的模板实例化可能在各个程序文本文件中进行,这是一种资源的浪费。采用分离模式可以避免这种浪费,分离模式在头文件中仅有模板的声明,而模板定义放在一个的程序文本文件中,在模板定义的最前面有一个关键字export,它使编译器保证在生成被其他文件使用的模板实例时,该模板定义是可见的。这样的模板称为导出的(exported)模板。这时相同的模板实例化,只做一次,但可用于所有程序文件。

在这两种模式下,同一个程序文本文件中的相同的模板实例化都是只做一次。但是如果相同的模板实例化均是采用模板实参推演,则并不确定在何处做模板实例化编译。如果有一个采用显式实例化方式(即在尖括号中指明模板的类型参数),则就在该处做模板实例化编译,由此也可以推出在同一个文件中相同的模板实例化只能最多有一次是显式的,其他必须采用模板实参推演。

指向类成员的指针

指向类成员的指针所用的运算符.*优先级是第4级,如果在该节采用指向car的指针*pcar,则使用->*运算符(pcar->*pf();)。注意:这两个是

《C++程序设计》教学指导书 51

的运算符,有自己的优先级,右操作数是指向类成员的指针。

第七章 动态内存分配与数据结构

教学目的

通过学习动态数据结构的基本知识和堆内存分配,进一步了解内存管理并掌握一些常用数据结构的基本算法。

建议学时安排授课4学时,习题1学时

自由存储区内存分配与释放,构造函数。1学时。 深复制与浅复制,链表与链表的基本操作。1.5学时。 单链表类模版,栈与队列的基本操作。学时。

教学方法提示

本章是前一章的延续。

本章第一部分动态内存分配与过去的教材相比有较大不同。动态建立变量、对象比较容易,难点在动态建立数组和在对象中动态建立数据成员。

动态建立数组是指针应用的一个要点。困难不在动态分配数组的格式复杂,而在应用。C++指针不是简单的地址,就在于指针的运算不是基于它所指向的变量或对象,而是假定它指向的是一个数组。同学已经习惯于把指针作为数组名进行运算,但总是记不得先要与一个静态建立的有名数组建立关联,或动态建立一个无名数组,并将其首地址赋予该指针。更记不住动态建立的数组,必须适时地显式地撤销它。学生能指出别人犯的这类错,但自己做时,还是犯同样的错。解决的方法只有多上机编写该类程序。

有了在对象中动态建立数据成员,才有了浅复制和深复制,有复杂的析构函数。到此为止,类的封装的概念才完整。动态分配是本章的重点之一。

浅复制和深复制通过打印数据是看不出差异的,教学时可以清空原件,再打印复制,深复制没有变化,浅复制复制是不允许的,在析构时系统会指出错误,以至无法继续运行。

讲解【例7.4】时要提醒同学,如果数据域还有很多其他数据,甚至有好几个是动态建立的C字符串,深复制是不是太复杂了?如果使用C++标准字符串string是否就不需要考虑深复制了?的确是这样的,准确地说,string类的内部包含动态建立字符数组的操作,其复制构造函数是深复制。如果在student类中使用string类而不是C字符串,就不要再考虑深复制问题了。也就是说,动态内存分配和深复制应该放在一个适当的层面上,一个更单纯的类定义中,如string类。在使用中,把它作为一个成员对象,就像使用string类对象那样。

再进一步讨论类的封装。封装的更高境界是在该类对象中一切都是完备的、自给自足的,不仅有数据和对数据的操作,还包括资源的动态安排和释

《C++程序设计》教学指导书 52

放。在需要时可以无条件地安全使用。标准string类模板就是典型的例子。这样的类对象,作为另一个类的成员对象使用时,就不会出任何问题。

在STL中队和栈是采用适配器方式,它将所要用到的线性表(称为容器类)作为一个成员对象使用,实际就是聚合技术。配套的习题解答和习题源代码中7.8、7.9两题的解法2,就模拟了该技术,将含有动态内存分配的动态数组和链表作为一个成员对象使用,而不是重写栈类与队类,可供参考。在第8章中还将对聚合与继承做比对。

堆内存的分配与释放是最基本的知识。对比命名对象和无名对象,无名对象是通过指针访问的,无名对象的生命期也是完全不同的。即动态分配的局部变量(对象)的生命期与静态分配的局部变量(对象)的生命期不同,因为它不是在栈中而是在堆中分配的,从分配开始到整个程序运行结束,都是它的生命期,必须显式地释放资源。

动态建立数组,不能直接对各元素(即使是基本数据类型)初始化(没有给出格式),这与静态数组不同。动态的对象数组建立时只能调用默认的构造函数,没有它不能建立对象数组。最易出错的地方是数组动态建立后的释放,必须在后面的课程中不断提醒同学。要讲清指针的目标所占堆空间释放与删除指针是两回事,要学生理解空悬指针的危险,要避免内存泄漏和重复释放。

空悬指针非常危险,delete删去了指针所指的对象,指针被悬挂,如链表清空后,头结点的指针域必须置为空指针,否则退出时析构会出错。二叉树也是如此(可参看习题7_11,删除树的函数的说明)。系统遇到空指针是不会进行内存释放的。

由指针数组动态建立数组,也是对前一章静态数组定义的复习。在第6章提到用指针数组建立数组的具体方法在此落实。也是一个前后呼应的地方。这是可选内容,如选中,动态建立数组的两种方法,应给学生作比对。

对自由存储区对象要讲清:new只能分配空间不能完成建立对象的全过程,必须调用构造函数;delete也是先调用析构函数来注销对象,再返回空间给堆区。

动态建立的类对象数组,只能重复调用默认的构造函数,也不能对每一个数组元素做具体的初始化工作。

数据结构仍然是本章的另一个议题,仍然是线性表,只不过新引入了链表,单链表是教学的重点。顺序表和链表各有自己的特点和优势,都应该掌握。链表以单链表类模板为主,双链表类模板作为选读内容。在单链表的教学中前面的算法要讲透,后面的单链表类模板只是一个总结,指点一下结构就可以了。

栈是线性表最重要的应用,一定要讲透。顺序栈类和链栈类可以前者精讲,后者对比介绍。

栈的应用介绍了中缀表达式法的实现,列为选读。如选中,可以把算法讲清,程序组织指点一下。也可不讲,由学有余力的学生自己去阅读。

队列部分,循环队列的实现是重点内容,关键是绝大多数函数中的求余算法。链队灵活应用了链表的生成与删除,与链栈都是对前面链表学习的呼

《C++程序设计》教学指导书 53

应与提高,上课只需讲入栈出栈和入队出队的算法借用的是什么基本算法。

在习题7_8和7_9的解2中采用了类似STL的栈和队适配器的处理方式。

二叉树部分为选读,其教学目的是理解二叉排序树,本书中数据结构内容是按C++98标准STL的要求安排的,以够用为原则,都是最基本最适用的内容,现列为选读。学时如富余,电类专业建议讲,其它由教师定。二叉树的遍历是其最基本的操作,掌握递归的描述方法是基本要求。二叉排序树的生成和查找算法是本节的目标。

例7.11问题答案:如不用引用,当找到新结点应在的位置后,将对新建立的临时指针分配建立一个新结点,其双亲结点的孩子指针并没有指向该结点,新结点是孤悬的,不能建立树。请教师用图示的方法给学生讲解。

学生反映书上的例题中的算法一看就懂,自己做习题就是写不出来,关键是有些格式学生不掌握。可以适当地把部分习题答案内容给同学参考,变成填空完成程序。毕竟见多识广,学习编程模仿是重要的,不是仅仅前5章要模仿,后面的很多格式也要模仿。

疑难解答

前向引用声明

例7.5_h中在模板类Node的定义中将模板类List说明为Node的友元类,但这时List尚未定义;如果定义次序反过来,结果一样,都会遇到违背先说明后引用原则的情况。所以该例中首先给出声明: templateclass List; 称为前向引用声明。

Node类和List类的复制构造函数讨论

在链表类模板中没有给出Node类的复制构造函数,并非可以使用默认的复制构造函数,而是在链表类模板中根本避开了所有使用它的情况,函数的参数和返回值仅使用指向Node的指针,类定义中也没有用复制方式初始化。定义复制构造函数与类的实际意义和使用方式有关,并非只是深复制和浅复制的问题。通常对Node类复制的结果应是一个孤立结点:

template Node::Node(Node & node){ info=node.data; link=NULL; }

link域值为NULL。该函数与Node的有参构造函数功能基本相同。考虑到函数的参数和返回值仅使用指向Node的指针,定义复制构造函数已经没有实际意义。

List类的复制是一个复杂的过程,写出它的自定义复制构造函数太复杂,在本章习题4的题解中给出了链表的复制构造函数,可以看出它已不再是传统的复制构造函数,而是一个完整的算法。

templateList::List(List & ls){ //复制构造函数 Node* TempP=ls.head->link,*P1;

《C++程序设计》教学指导书 54

}

head=tail=new Node(); while(TempP!=NULL){ P1=new Node(TempP->info); P1->link=tail->link;//向后生成list1 tail->link=P1; tail=P1; TempP=TempP->link; }

本章中List类甚至从未以任何形式用作函数的参数或返回值。不过复制赋值号=的重载则是有实际意义的。

如果结点的数据域为指针,具体内容动态建立。应在构造函数中动态建立堆变量或堆对象,在析构函数中释放堆空间(delete语句),复制构造函数也不能用按语义的默认的复制构造函数。难点在释放,易出错。

但如上所述这种做法是不妥的,通用性难以实现,会带来程序设计的困难。正确做法是数据域为成员对象,在该成员对象中完成具体数据的动态建立。做到每一层的任务明确,各层次完成自己的任务,这样的封装才是完备的。

顺序队列转化为循环队列的算法讨论

下面分析例7.4中的几个算式:

(rear-front+maxSize)%maxSize;

该式计算,当rear大于front时计算很方便:

rear-front;

但当rear小于front时,上式不适用。怎样解决?

求模运算并非是求余数那么简单。以指针式钟表为例,表面指示12个钟点数,其中12点理解为0点。从0点开始计数,计到11点后应该是12点、然后13点,但钟表是到12点就归0,13点是1点。这种计数方法称以12为模。设实际钟点数为value,显示钟点数为k,则有k=value mod 12。从钟表上看,2点钟可以理解为14点、26点,以0点为原点,也可以理解为-10点(顺时针数为正,逆时针为负)、-22点。从7点到1点间的时间为:1-7=-6在相减为负时要采用:

rear-front+maxSize

使之为正,为了在相减为正时通用,改为:

(rear-front+maxSize)%maxSize;

该例中其他几个算式就容易理解了: rear=(rear+1)%maxSize;

就是实现以maxSize为模的计数方法,rear在0~maxSize-1之间变化。计到maxSize就归0,线性顺序表成为循环顺序表。

举约瑟夫(Josephus)问题为例:一群猴子围成一圈,从第1只猴子起顺时针数到第m个猴子时,该猴子便出围。继续不断数下去,猴子不断出围,最后剩下的一只猴子就是猴大王。问猴大王是第几只猴子?算法的关键是如何用数组表示圆圈,方法就是“加1求模”。可以给顺序表模板类添加一个Josephus()成员函数实现该算法:

《C++程序设计》教学指导书 55

template class seqlist{ T *elements; //存放顺序表的数组 int Maxsize; //最大可容纳项数 int last; //已存表项的最后位置 public: seqlist(int ms=18); ~seqlist(){delete[] elements;} int Josephus(int,int); };

template seqlist::seqlist(int ms){ last=-1; Maxsize=ms; elements=new T[Maxsize]; assert(elements!=NULL); //断言:分配成功 }

template int seqlist::Josephus(int a,int m){ //a猴子总数,m报数终点 T * el; int i,j,k,l; if(a>Maxsize){ el=elements; Maxsize=a; elements=new T [Maxsize]; //扩大数组空间 assert(elements!=0); //分配不成功结束程序 delete [] el; } for(i=0;i《C++程序设计》教学指导书 56

return i; }

int main(){ seqlist seq; int a,m; cout<<\"请输入猴子总数和报数停止数:\"<>a>>m; cout<<\"猴大王下标为:\"<源代码在电子文档C++例题源代码第二版\\Ex7_10_1中。

动态分配的讨论

学生往往对动态分配并未真正理解,对链表必须是动态链接并不理解。以下案例是学生编的: //建立存放学生名的双链表

# include # include

templateclass Dbllist; templateclass DblNode{ T* info; //A 学生这里用的是指针,他的目的是为了用字符串 DblNode* llink,*rlink; public: DblNode(); DblNode(T *data); T* Getinfo(){return info;} friend class Dbllist; };

template DblNode::DblNode(){llink=rlink=NULL;} template DblNode::DblNode(T *data){ info=data; llink=NULL; rlink=NULL; }

template class Dbllist{ DblNode *head,*current; public: Dbllist(); ~Dbllist(); void makeempty(); DblNode* Find(T *data); void insert(DblNode* p); DblNode* remove(DblNode* p); void print();

《C++程序设计》教学指导书 57

};

template Dbllist::Dbllist(){ head=new DblNode(); head->rlink=head->llink=head; current=NULL; }

template Dbllist::~Dbllist(){ DblNode* temp; while(head->rlink!=head){ temp=head->rlink; head->rlink=temp->rlink; temp->rlink->llink=head;

delete temp; // C 既然链表是静态生成,当然不能用delete

}

delete head; cout<<\"析构!!!!\"; }

template void Dbllist::makeempty(){//??? DblNode* temp; while(head->rlink!=head){ temp=head->rlink; head->rlink=temp->rlink; temp->rlink->llink=head; delete temp; // C 既然链表是静态生成,当然不能用delete } current=NULL; cout<<\"清空!!!\"<//其他函数略

void main(){ DblNode s1(\"jenny\"),s2(\"Tom\"),s3(\"jack\"),s4(\"sun\"),s5(\"ken\"), a[5]={s1,s2,s3,s4,s5};//学生名 //D 这里a[0]-a[4]与-s1-s5是两份副本

Dbllist li; for(int i=0;i<5;i++)li.insert(&a[i]); //B 静态方法生成了链表 li.print(); if(li.Find(\"Tom\"))cout<<\"OK!\"<li.remove(&a[4]);

//请问为什么换成\"li.deletenode(&s5);\"语句达不到删除效果? //F a[0]-a[4]与-s1-s5是两份副本 li.print(); li.makeempty();//请问为什么这条语句不能执行成功?

《C++程序设计》教学指导书 58

//E 链表是静态生成,所以不能用makeempty() }

另外A行也是有问题的,用指针必用动态分配,否则那里通用性无法保证。

第八章 类的继承与派生

教学目的

以软件的再利用为出发点,讲解类的继承与派生。使学生对类与对象有一个全面的理解。

建议学时安排

授课5学时,习题1学时

继承与派生的概念,派生类的构造函数与析构函数。2学时。 派生类应用讨论。1学时。

多态性与虚函数,纯虚函数。2学时。

教学方法提示

本章分两部分:继承与派生,运行时的多态性。在本教材中强调了算法,采用的例子都有实用性,避免纯语法讲课。

继承以单继承为主,派生以公有派生为主,学生应能熟练应用。继承首先是为了软件复用,应通过例子使学生掌握这项功能。其次,派生类和多态性可以实现算法的通用性。

要把继承的层次结构,基类和间接基类等概念给学生讲清楚。学生初学时会把继承的层次结构与多重继承概念搞混。

编制派生类的4个步骤要牢记,一定要同学照办,规范化地创建派生类。 派生的一些术语和图示常常把人搞糊涂:指示派生关系的箭头是从派生类指向基类,基类称为超类(派生类称为子类)。这与输入用向外的箭头,输出用向内的箭头一样别扭。

派生类定义的格式是固定的,学生学到这里时,应该理解格式是没商量的,只有照做。构造函数的格式也是固定的,其中各个部分的执行次序也是固定的,可与包含成员对象的构造函数对比。难点是在析构函数,它并不复杂,但非常容易出错。特别要教导学生按规定来,本层次做本层次的事,不要多事。画蛇添足的错误最为常见。

本章例题继续呼应第7章动态的堆内存分配,在构造函数中动态分配堆变量或堆对象,在析构函数中释放堆空间。

要讲清多层次派生结构的概念和多重继承的概念,两者不可混淆。 多重继承可以产生新的东西,这是很重要的,但也会引起岐义,所以派生类成员标识是很重要的。

《C++程序设计》教学指导书 59

在多重继承中虚基类解决了重复的多个同样底层基类的问题,是多重继承不可少的部分。但因为会引起岐义,实际上很少使用多重继承,所以虚基类也列为选读。

在多层派生构造函数中,通常基类名仅指直接基类,写了底层基类,编译器认为出错。多层虚拟继承构造函数中,基类名不仅指直接基类,而且包括底层虚基类,否则定义对象时编译器认为出错。两者相反。

但是必须指出在很多情况下,多个同样底层基类,却是不可少的。如一个通信系统由发送与接受两部分组成,这两个部分都有的存储子系统:

一个是发送缓存,一个是接收缓存,各自,见(a)。

如果是统一的存储子系统,它内部包括动态分配的发送缓存和接收缓存,则为(b)。称为钻石结构(扑克牌中的方块),这时使用虚基类(虚拟继承)。

storabletransmittertelecom(a)storablereceiverstorabletransmittertelecom(b)

receiver8.5节中,派生类与基类关系的讨论,即赋值兼容规则,是多态性学习的一个预备,很重要的。不学好,后面的运行时多态,很难理解。赋值兼容规则实际很好理解:manager是employee,但emplyoee不一定是manager。

在多层次的派生类结构中,复制构造函数是一个难点,单独安排在这一节中讲解,也有一个承前启后的作用,也作为运行时的多态性的教学铺垫。

在该节中对聚合给予很高的评价,无论是在模板还是在派生中,只要需要动态分配都可以体现明显优势,讲解时除了string类以外,电子文档中习题7.8和7.9的解答2,也可以作为实例。在STL中总是将动态内存分配封装在一个成员对象中。特别指出:在使用派生技术时,不要用直接包含动态内存分配的类作为基类,更不要在派生类中直接添加新的动态内存分配的成员,这样会出现极复杂的深复制问题。而一定要将必不可少的动态分配封装在成员对象中,并在该成员对象中完成深复制,这样在派生类族中可以直接使用默认的复制构造函数和复制赋值运算符,大大简化编程。同时动态分配要在构造函数中,动态释放要在析构函数中,以适应异常处理技术。

对于模板和派生技术的对比,要强调各有适用的地方。对于需要实现处理不同数据类型的通用算法的应用,如数据结构,模板有绝对优势,可以用来说明。而处理由简单到复杂的系列问题,更适用派生技术,可以用MFC来说明。

运行时的多态性对学生来讲是最难理解的,很少有教材去解释运行时的多态性的机理,本教材中作了简单的介绍——动态绑定。但只是选读内容。教学要求只是熟练应用虚函数和纯虚函数来实现运行时的多态性。

《C++程序设计》教学指导书 60

虚函数的要点是怎样调用虚函数才能实现运行时的多态性,这是教学的重点。同学们总是很难理解为什么用了虚函数并不一定是运行时的多态性,还可能是编译时的多态性,一定要讲清只有使用基类指针或引用来调用虚函数才能实现运行时的多态性,演示的例子很重要,一定在VC++上运行,让学生看到结果,并解释清楚。

纯虚函数在多态性中最重要的作用是实现算法通用性的,其基类(算法或数据)无法指定,只有派生出的具体类,如只有具体的三角形、矩形等,而平面几何图形只是一个概念。例8.9和例8.10都是通过派生来实现算法通用性的。前者可取代类对象参数或面向过程的函数指针(精讲)。后者取代模板(选读)。

疑难解答

派生常见错误

用一个类作为基类,相当于声明一个该类的(无名)对象,所以要想作为基类,该类必须有定义:

class employee; //只有声明 class manager:public employee{ …… }

是错的,因为employee无定义。

各种派生方式的性质应该在理解的基础上记忆。派生类的访问权限是初学者很容易出错的地方,如:

class employee{

string name; public:

void print(){

cout<<”name is ”<class manager:public employee{

…… public:

void print(){

cout<<”name is ”<派生类的print()应为:

void print(){

employee::print(); …… }

《C++程序设计》教学指导书 61

这些错,编译时会指出,所以要多做实验。

同样在例题8.6中,有了计算业绩分的虚函数Calculate(),基类中似乎不需要设置业绩分函数SetCredit(),但是在派生类中重定义的Calculate()是不能直接访问基类的私有数据业绩分credit的,所以这些看似不必要的简单的访问私有数据的接口函数,往往是不可少的。如要直接访问可说明为保护数据成员。

基类中protected成员的使用

作为基类如果把私有的数据成员改为保护的数据成员,将可以在派生类中直接访问,勿须借助基类的公有函数。如例8.6可以改为:

class Student{

protected: //采用保护成员,在派生类中可以直接访问 string coursename; //课程名 int classhour; //学时 int credit; public: Student(){coursename=\"#\";classhour=0;credit=0;} virtual void Calculate(){credit=classhour/16;} void SetCourse(string str,int hour){ coursename=str; classhour=hour; } void Print(){

cout<class GradeStudent:public Student{ public: GradeStudent(){}; void Calculate(){credit=classhour/20;} };

省了两个公有函数,程序编写也更顺。源代码见电子文档|C++例题源代码第二版|Ex8_6_1。同样例6.7也有对应Ex8_7_1.cpp源代码。

类模板的派生的讨论

由类模板的实例作为基类可以派生类模板,下例是由数组类模板的实例派生栈类模板,注意格式。

#include using namespace std;

template class array{ T a[size]; int last; int maxSize; public:

《C++程序设计》教学指导书 62

array(){last=-1;maxSize=size;} bool isfull(){if(last==maxSize-1) return true; else return false;} bool isempty(){if(last==-1) return true; else return false;} void insertRear(T data){ //将data插在数组最后一个元素位置,可用于创建数组 if(!isfull()) a[++last]=data; else cout<<\"array is full,can not insert!\"<template class stack:private array{ //私有派生屏蔽原有的接口函数 public: void push(T data){ insertRear(data); } T pop(){ return deleteRear(); } void stackprint(){print();} };

int main(){ stack istack; int m,i; cout<<\"请输入9个整数:\"<>m;

istack.push(m); } istack.stackprint(); for(i=0;i<9;i++){ //创建数组 cout<《C++程序设计》教学指导书 63

}

该例的源代码放在电子文档|例题源代码第2版|第8章|Ex8_A中。

习题7.8和7.9同样可以用派生实现,以线性表模板为基类派生出栈和队。但是当线性表包含动态分配的数组时,用作基类可能产生复杂的深复制问题,另外派生技术的运行效率低,所以应该使用聚合,这已经成为C++的标准处理方式。VC++6.0中MFC的群(Collections)类包含了少数几个有关数据结构的模板类,同时包含了一大堆非模板的类,实际这些类完全可以作为那几个模板类的实例,这是因为模板技术当时还是新技术,不为更多的用户接受。

使用多态完成定积分的通用性

例8.9,学生比较难以理解这种形式的多态,调用的是基类的辛普生函数,而辛普生函数调用派生类定义具体的被积函数,但这是切实可行的常用的形式。如果按例中的注释,将类B定义成由A派生,再用指向B的基类指针实现exp的积分,学生可能更易理解。

同学很容易发现本例采用派生技术,远远不如第6章采用模板更方便,更具通用性。所以应提醒同学每一种技术都有其最适用的地方。

模板与派生在实现通用性上的对比

很多教材特别推崇派生,认为是万能的,其实这是片面的。将其发挥到极致——用派生实现通用性,但这不是给派生捧场,而是暴露出硬用派生来作其并不善长事情出现的种种问题,通用性应该使用模板。

如选中例8.10,其重点在虚拟的数据类——抽象类,掌握怎样实现通用单链表派生类的规范化步骤。例8.10呼应链表和堆内存分配和释放,可以算是6、7、8三章的一个小结。首先,这里存在两个层次的动态分配,撤销是借助分层的析构函数的自动调用完成的,千万不要在程序中显式调用,那是一个严重错误。这在全书中是唯一的例子。第二,采用数据域类对象,把对数据的操作封装在里面,可以进行各种必须的函数和运算符的重载,实现通用性,这一方法对模板也是一样的。

从书中的例题可以看到使用虚函数的实际目的是为了在使用现成的应用程序时,可以通过派生来改变被处理的数据(例8.9的被积函数也可看作数据)。实用程序编写时处理的是数据的基类,实际使用时处理的是派生的数据类,按赋值兼容原则是不可能的,只有动态的多态性(既使用虚函数的动态链编)才能实现。但是使用时会受到一些。请看下例:

复制构造函数,应该用深复制:

Node::Node(Node & node){//复制构造函数 info=new Object(*node.info); link=NULL; }

但这是错的,Object是抽象类,不能建立对象。将Object定义为一般的类,可以定义深复制,但结果仍不对,因为我们用Object的派生类来组成Node类,但复制的构造函数中数据域只能写Object,我们要求实际处理其派生类则必须用虚函数,而复制的构造函数又不能是虚函数,动态绑定不能实现。所以所有Node类型的函数参数和返回值都必须是引用和指针,以免调用复

《C++程序设计》教学指导书

制构造函数。其他部分要显式使用复制构造函数的地方一律使用替代技术。这里可以看出模板的优越性。

请注意例8.10种采用的技术。第一,在Node类中用指向Object的指针链接派生类StringObject对象,这样只能访问Object中已有的函数,不能访问新成员,实际上例中是通过虚函数Print()访问串sptr。第二,构造函数是自动调用,不受,所以可以有StringObject(char *),对应也要重新定义析构函数。第三,比较函数Compare()为了能用于StringObject也采用了特殊技术,参数不能变,变了是重载的新成员,不能调用,所以有一个类型强制转换:

StringObject & temp=(StringObject &)obj;

注意在List类中的Find()的参数使用Object & obj。

第7章使用模板实现通用性,而这里使用派生,方法不一样。但由上文可知单纯为了通用性,模板在编程的方便、运算的速度和使用的灵活性上全面优于派生。但动态绑定不一定使用源代码,这一点是其他技术不能取代的,只有该技术可以在不泄露软件秘密的条件下,由用户将通用软件转化为用户专用软件。

构造函数与虚函数

教材中指出构造函数不可以用虚函数,而析构函数包含动态内存释放时最好用虚函数。但有学生提出可否在构造函数和析构函数中调用虚函数,以增加灵活性?C++的缔造者Bjarne Stroustrup建议不要这样做,在构造一个多层次的类的过程中,遇到的往往不过是一个部分构造的对象,无法保证调用的就是你所需要的函数。

虚函数、同名覆盖与重载 在派生类的层次结构中,要实现多态性只能采用虚函数。这里除了要求名字、参数和返回值要完全一样外,还要求加关键字virtual,使用时必须由基类指针来调用。否则不能实现运行时的多态。 仔细推敲,重载与之有多处不同,除编译时就确定了调用哪一个重载函数外,实际上编译器只能在指定的类层次上选择重载函数,不能自动到其他类层次去选择。如果在指定层次上找不到匹配的重载函数,尽管其他层次上有,编译器也给出编译错误。如果派生类中没有同名的重载函数,而基类中有,编译器会进入基类查找匹配的重载函数。也就是说派生类重新定义的同名函数屏蔽了基类的同名函数。

局部变量的对作用域包含了该变量作用域的同名变量的屏蔽作用,并不会受同名变量的类型影响。只要同名,类型不同,同样起屏蔽作用。

在类层次结构中的覆盖,英文是override,与使用虚函数只少了一个virtual,要求名字、参数和返回值完全一样,绝不可能放在同一层次中。调用时必须明确指定是哪一个层次中的同名函数,编译器同样不会自己去不同层次挑选。当然也无法象重载那样挑选,因为参数完全一样。所谓覆盖严格讲并无静态的多态性的概念。

迟缓错误检测(Lazy error detection)

对于初学C++程序编写的同学,往往会遇到一个令人困惑的问题:在过

《C++程序设计》教学指导书 65

去已经运行多次没有问题的程序上添加一些新的应用时,在原来的程序段上意外出现了大量的语法错误。也就是说原来的程序实际上隐藏了语法错误没有被检测出来,这些错误不影响原来的应用,但影响新的应用。

实际在C++中,错误可以在声明点或应用点上标记识出来。

如果一个语法错误是在声明点上被标识,则不能继续编译程序,必须立即改正。然而,如果发生问题的声明是库的一部分,而且库是不可由用户随意修改的,解决这样的冲突真不是小事一桩。实际上,在应用中可能永远不会有机会触发应用程序中的这些潜在的错误,所以这些错误可以留待用到时再标识出来。

如果直到一个使用点上,某错误才被标识出来,则程序中可能充满各种未触发的错误,可能随时被触发。也就是说,在这种策略下,成功编译代码并不能确保它没有语法错误,只能保证程序没有违反语言的语义规则。

在使用点上标识错误是一种迟缓型评估(lazy evaluation)的形式,是提高程序性能的常见设计策略。通常应用在昂贵资源的初始化和分配上,直到真正需要这些资源时才分配和初始化。如果这些资源永远没有被真正用到,则可以省下不必要的运行开销。如需要这些资源,但不是一次需要全部资源,则可以分散程序的初始化开销。

在C++中,处理函数重载、模板和类层次结构时,通常会产生潜在的组合错误,它们往往在使用点而不是声明点上被标识出来。

如派生类的间接基类,如果是虚基类,必须在派生类的构造函数中显式出现,不出现是错的。但只有直接定义该类的对象时才会标识出来,如果只是用它再来派生一个类,则不会被标识。

该策略是正确的,在组合多个组件时,要想同时解决所有潜在的错误,并不符合错误产生的规律。C++采用这个策略意味着程序员必需小心谨慎地测试代码,以便找到并解决潜在的错误。在组合两个或多个大型组件时,少量潜在的错误是可以接受的。但在单个组件中往往不可接受。

第九章 输入/输出流类库

教学目的

使大学生对C++中流类有较全面理解,会建立一个完整的实用程序。

建议学时安排

授课4学时

C++的基本流类体系,提高标准输入/输出的健壮性,重载插入和提取运算符。1.5学时。

文件的打开与关闭,文本文件的读写,二进制文件的读写。1.5学时。 文件与对象。1学时

教学方法提示

《C++程序设计》教学指导书 66

本章使用格式的说明部分基本列为自学(选读)。

C++的基本流类体系的概念要讲清,用图来介绍整个体系各方面的相互联系大约10分钟即可完成。学生稍感疑惑的地方是提取和插入运算符信息传递的方向,他们易理解的是输入和输出,多强调几次对应关系。

列为选读的输入/输出的格式控制讲解的难点是格式控制枚举量合成长整型数x_flags及其操作函数。

标准输入/输出基本由学生自学。教师要指点的是怎样提高标准输入/输出的健壮性:状态字state及其使用技巧。其实状态字同样用于其它流文件。

重载插入和提取运算符也是教学内容之一。例9.5是一道很重要的例题,它给出了提取(>>)和插入(<<)运算符重载方法,在前几章中的print()和show()函数全部可以使用该技术,通用性更好,基本数据类型和自定义的类均可使用,而过去只能顾一头。再举一个例子——学生类:

class student{ public: int key; char name[10]; friend ostream & operator<<(ostream & s,const student & k); friend istream & operator>>(istream & s,student & k); };

ostream & operator<<(ostream & s,const student & k){ s<istream & operator>>(istream & s,student & k){ s>>k.key; s.get(); s.getline(k.name,10); return s; }

输入输出运算符的重载必须采用普通函数,为了方便总是说明为友元函数。因为输入输出运算符是在标准输入输出流类中定义的,并作为成员函数重载了20种左右不同形式。但对用户定义的类,标准输入输出流类不可能预先给予支持,也不允许在标准输入输出流类中作为成员函数重载适用于该类的输入输出运算符,因为用户是不能也不应该修改标准库中的类的定义的。必须使用友元函数。而且是在学习了流类知识以后才能顺利完成重载。所以在本章之前,教材中都是用函数print()或show()来代替输入输出运算符的重载。

重载的输入输出运算符的第一个参数的类型必须是流类的引用。流用作函数的参数时,必须是引用调用,不能是传值调用。在本例中输入输出运算符连续使用时会出错。要求使用同一个流,而不是几个的副本。

文件是本章的重点。

《C++程序设计》教学指导书 67

在第三章中已经简单介绍了输入/输出文本文件的使用,学生也已经会使用,对文本文件是复习与提高。提高包括:文件打开方式,文字型文本文件的读写(关闭跳过空白控制是复制文字型文本文件成败的关键),数据型文本文件的组织(数字串与数字的自动转换)。格式是文件建立与读取的关键,文字型文本文件格式是隐含的;但数据型文本文件和二进制文件格式不知道是根本无法正确读出的。尽管可以用文本阅读器读出数据型文本文件,但这是作为文字显示给人看的。

文件指针,道出了C++文件操作的原理,应该讲清楚。

文件与对象一节内容少但是十分重要,对规范的面向对象的编程方法是不可缺的。【例9.13】是一个非常典型的实例,是讲解的重点。

文件部分的重点之一放在使学生树立在构造函数中由文件建立对象,在析构函数中把对象存入文件的思想。

疑难解答

流类库中的控制枚举常量的定义 这些控制枚举常量(枚举成员)名是在C++标准中指定的,但这些枚举常量代表的数值却并未指定,各种C++平台是不同的。对流类库,定义在ios类中:

enum io_state {

goodbit = 0x00, eofbit = 0x01, failbit = 0x02, badbit = 0x04 };

enum open_mode {

in = 0x01, out = 0x02, ate = 0x04, app = 0x08, trunc = 0x10, nocreate = 0x20, noreplace = 0x40, binary = 0x80 };

enum seek_dir {

beg=0, cur=1, end=2 };

enum {

skipws = 0x0001, left = 0x0002,

《C++程序设计》教学指导书 68

right = 0x0004, internal = 0x0008, dec = 0x0010, oct = 0x0020, hex = 0x0040, showbase = 0x0080, showpoint = 0x0100, uppercase = 0x0200, showpos = 0x0400, scientific = 0x0800, fixed = 0x1000, unitbuf = 0x2000, stdio = 0x4000 };

static const long basefield; // dec | oct | hex static const long adjustfield; // left | right | internal static const long floatfield; // scientific | fixed

而在新标准库中,定义在ios的基类ios_base中,其数值不同:

enum _Iostate {

goodbit = 0x0, eofbit = 0x1, failbit = 0x2,

badbit = 0x4, _Statmask = 0x7 };

enum _Openmode {

in = 0x01, out = 0x02, ate = 0x04, app = 0x08,

trunc = 0x10, binary = 0x20 };

enum seekdir {

beg = 0, cur = 1, end = 2 };

enum _Fmtflags {

skipws = 0x0001, unitbuf = 0x0002, uppercase = 0x0004,

showbase = 0x0008, showpoint = 0x0010,

《C++程序设计》教学指导书 69

showpos = 0x0020, left = 0x0040,

right = 0x0080, internal = 0x0100, dec = 0x0200,

oct = 0x0400, hex = 0x0800,

scientific = 0x1000,

fixed = 0x2000, boolalpha = 0x4000, adjustfield = 0x01c0,

basefield = 0x0e00, floatfield = 0x3000,

_Fmtmask = 0x7fff, _Fmtzero = 0 };

在.net中使用新标准库,定义的数值基本一样,列出不同处:

#define _IOS_Nocreate 0x40 #define _IOS_Noreplace 0x80 #define _IOSbinary 0x20 // TEMPLATE CLASS _Iosb

enum _Openmode{ // constants for file opening options _Openmask = 0xff };

_BITMASK(_Openmode, openmode);

static const _Openmode in = (_Openmode)0x01; static const _Openmode out = (_Openmode)0x02; static const _Openmode ate = (_Openmode)0x04; static const _Openmode app = (_Openmode)0x08; static const _Openmode trunc = (_Openmode)0x10;

static const _Openmode _Nocreate = (_Openmode)_IOS_Nocreate; static const _Openmode _Noreplace = (_Openmode)_IOS_Noreplace; static const _Openmode binary = (_Openmode)_IOSbinary;

enum _Iostate{ // constants for stream states _Statmask = 0x17 };

_BITMASK(_Iostate, iostate);

static const _Iostate goodbit = (_Iostate)0x0; static const _Iostate eofbit = (_Iostate)0x1; static const _Iostate failbit = (_Iostate)0x2;

《C++程序设计》教学指导书 70

static const _Iostate badbit = (_Iostate)0x4; static const _Iostate _Hardfail = (_Iostate)0x10;

这只是VC++中的不同,其他平台均可自行定义,但枚举常量名却是固定的,所以千万不可以用枚举常量的值,只可以用枚举常量名,以保证通用性。更进一步可以发现控制量也可以定义为常变量,与枚举常量使用起来并无两样。

传统运行库和新的标准库中的输入输出流的差异

新标准库不宜与传统运行库混用。。如未安装SP5,使用传统运行库的输入输出流必须放弃标准字符串,回到C风格字符串。因为头文件中包含了标准流类库,如VC++6.0中它包含了,而后者又包含了,如用传统运行库的输入输出流而不放弃标准字符串,必然新旧库冲突,产生歧义。

对于文件,新标准库(无 .h)不再支持iso::nocreate和iso::noreplace;文件打开方式大不一样,特别是文件不存在时是否建立新文件。注意仅在打开输出文件时建立,而打开输入文件或输入输出文件时不建立,给出错信息。参见同步实验二十七题1的处理方式:

dat.open(\"file.dat\//为读写打开二进制文件 if(!dat){ //文件不存在 dat.clear(0); //清状态字 dat.open(\"file.dat\//文件不存在,建立二进制文件 dat.close(); dat.open(\"file.dat\//为读写打开二进制文件 if(!dat){ cout<<\"cannot open file\\n\"; return -1; } }

应该说新标准库更为合理。

使用传统运行库,对3参数get()系列函数和getling()系列函数,第二个参数n是非常有意义的,实际输入n个或更多字符,当读入n-1个字符时,函数停止读入,流正常,下面仍然可以继续读入。用VC++标准库函数,即用头文件iostream时,如果函数未能读到结束字符而停止,流出错(输入输出操作失败),后面不再读入,必须清0流状态字,才能继续读入。新标准库似乎欠合理,是否新标准考虑到第二个参数是为了防止放置输入字符的数组出界,而一行字符分几次读完,原来有可能无法判断是读到结束字符结束还是读到指定字符结束,现在可以判断了:如果流正常,则读到了结束字符。

使用传统运行库,例9.8按行复制文本文件程序代码如下:

while(sfile.getline(buf,100)){//按行拷贝 A行 if(sfile.gcount()<99) dfile<如回车符恰好在第99个的位置上,则丢失,特别在第2个参数较小时概率更高。使用新标准库时,程序代码如下:

《C++程序设计》教学指导书 71

while(sfile.getline(buf,100),sfile.eof()!=1){//按行拷贝 A行 if(sfile.rdstate()==0) dfile<不会丢失回车符。所以还是新的好。

当使用旧的头文件,用键盘输入数字时如以0开头,计算机会将该数看作是八进制数,以0x开头则被看做是十六进制数。当使用不带.h的头文件时,0被忽略,仍作为十进制,0x开头只认0,必须显式地指定数据的进制。非十进制只适用于整型变量,不适用于实型变量。

状态字state及其使用技巧

对于输入流,只要流结束(键盘输入Ctrl_z或已读到文件结束)或流出错(如:对应文件打不开),对应流的状态标志就设置为1(前者置0x01,后者置0x02),此后忽略所有对此流对象的操作,必须用clear()函数清0,然后输入流才能正常运行。如:

sfile.open(filename,ios::in);//打开一个已存在的文件 while(!sfile){ cout<<\"源文件找不到,请重新输入路径名:\"<>filename; sfile.open(filename,ios::in); }

流作为条件判断时,是指流是否正常,如例9.6:if(s) 表示流对象s正常时,则执行if后的语句,即判据是s.good()的返回值。这里while(!sfile)中循环条件!sfile表示流文件sfile不正常。!是重载的运算符,与状态函数ios::fail()等效,返回操作非法和操作失败这两位。。

本指导书第二章对例2.13说明中改用函数istream&istream::get(char &)作为循环的条件时,也是借助函数返回的流引用(*this)的状态字进行判断。当输入Ctrl_z时流结束,状态字state为eofbit=0x01,good()的返回值为0,循环结束。而且其后程序不再对键盘响应,如果程序后面还有键盘输入要求,必须先调用clear()函数清0状态字。

特别注意,当文件读到文件结束处,文件指针将不能移动,必须清状态字,才能恢复。这也是常见的。通常文件结束前应有一个回车,以免读到最后一个数据后,文件指针不能再移动。通常从文件读数据,不要靠EOF来判断,如一定要用,用后要清状态字。

注意,教材中指出状态字为failbit(0x02)和badbit(0x04)时,流可恢复是指清0后仍可对此流对象操作。

标准输入是最易发生错误的,即使使用状态字也必须要求学生严格按标准的格式做。如对VC++:

int n,m=88; char ch='a';

《C++程序设计》教学指导书 72

while(cin>>n) cout<< \"n=\"<>m; //A cout<< \"m=\"<cin.clear(0); //B cin.get(ch);

cout<cout<无论是输入格式错,如2.768,还是输入了Ctrl+z,循环都会停止,因为这样的格式隐含对cin状态的判断。同时A行永远不会执行,m永远是初值88。Ctrl+z必须在一行的开头输入(同一行后面可以有其它数字,但被忽略),并且之后要按两次回车,或者一连输入两次Ctrl+z加回车,否则无效(C++中并无此具体规定)。如有B行,ch第一次取得换行符(10),第二次取得新输入的字符。如无B行,两个cin.get(ch)都被跳过,输出两个97('a')。 又如为:

cout<<\"输入一段文本(无空行):\"<即使正确输入Ctrl+z, cin.get(ch)不能取得EOF(-1),因为其参数为字符型的引用,出错,此后陷入死循环,A行被跳过,B行始终输出10(回车符)。

再如:

int m,n; cin>>m>>n;

cout<如果键盘输入

则输出3 -8593460

后者为随机数(见本指导书第1章),因为第一个数格式错,输入流出错,读入3后停止继续输入。

重载提取和插入运算符

因为标准输入标准输入输出可以与文本文件使用同样的重载的提取和插入运算符。注意:那里重载时流参数类型是ostreamh和istream的引用,用于文件时实参是其派生的流类型ofstream和ifstream的对象,这里是不会有问题的,不需要为基类和派生类分别重载。可以断言这里使用了运行时的多态。,必须单独为二进制文件定义输入输出函数。

《C++程序设计》教学指导书 73

文本文件处理中到处在重载提取和插入运算符,这是一项基本技术,必须注意在重载运算符的函数内部,提取和插入运算符的运算功能还是库函数中原定义的。这种运算符自动完成数字串与数字的转换。例如:

ostream &operator<<(ostream&dest,inventory&iv){

dest.unsetf(ios::right); //要改为左对齐,先清右对齐 dest.setf(ios::left);

dest<dest<} //写入文件是自动把数转为数字串后写入

或:

ostream &operator<<(ostream&dest,inventory&iv){ dest<<return dest;

} //写入文件是自动把数转为数字串后写入 //文件流类作为形式参数必须是引用

类模板的插入和提取运算符的重载

类模板重载插入和提取运算符必须是函数模版,见下例:

templateclass orderlist{ int maxsize; int last; T slist[size]; public: orderlist(){last=-1;maxsize=size;} int binarysearch(T&x,const int low,const int high); bool insert(T&elem,int i); void print(); friend ostream & operator<<(ostream &,const orderlist &); };

template

ostream & operator<<(ostream & s, const orderlist & glist) { //应是函数模版 int i; for(i=0;i<=glist.last;i++) s<派生类的插入和提取运算符的重载

当为派生类重载插入和提取运算符时要利用赋值兼容原则来处理其基类数据成员,请看下例。

《C++程序设计》教学指导书 74

class Person{ string name; //姓名 char sex; //性别 public: Person(string n=\"noname\char s='m'){ //构造函数 name=n; sex=s; } friend ostream& operator<<(ostream& dest,Person& ps);//输出姓名、性别 };

ostream& operator<<(ostream& dest,Person& ps){ dest<class Student:public Person{ int id; //学号 int Eng,Math,Phy; //三门课成绩 double ave; //平均成绩 ofstream ofile; public: Student (string n=\"noname\int i=0,char s='m',int e=0,int m=0,int p=0):Person(n,s){ id=i; Eng=e; Math=m; Phy=p; CalAve();

} ~Student();

void CalAve(){ave=double(Eng+Math+Phy)/3;}; //计算平均成绩 friend ostream& operator<<(ostream& dest,Student& st);

//输出姓名、性别、号、三门课成绩、平均成绩 };

Student::~Student(){ ofile.open(\"myfile.txt\"); ofile<<*this; ofile.close(); }

ostream& operator<<(ostream& dest,Student& st){ Person *p=&st; dest<<*p; //利用赋值兼容原则输出st的基类成员 dest<int main(){ Student st(\"wang\ cout<《C++程序设计》教学指导书 75

本例的源代码放在电子文档|例题源代码|第九章|Ex9_A中。

对Student类重载<<运算符也可以采用复制基类对象的方法:

Person p=st; //或:Person p(st);

//利用赋值兼容原则复制st的基类成员,注意拷贝构造函数参数为引用 dest<或两句合一: dest<可以理解为用基类复制构造函数以派生类对象产生一个无名基类对象;也可以理解为强制类型转换产生一个无名基类对象,同样调用基类复制构造函数。

同样是利用引用和赋值兼容原则,但不如例题中的方法直接与明了。 如果编成:

ostream& operator<<(ostream& dest,Student& st){ dest<< st ; //利用赋值兼容原则输出st的基类成员 dest<则有问题,希望调用ostream& operator<<(ostream& dest,Person& ps), 但编译器实际理解为递归调用,因为按最佳原则ostream& operator<<(ostream& dest,Student& st)更符合重载要求。

文件中的数据格式

将数据放入文件和由文件读出数据是有一定格式的,参见下面以学生类为元素的顺序表的构造函数:

template Orderedlist::Orderedlist(){ last=-1; maxsize=size; student stu; ifstream infile; //构造函数中打开文件,由文件中的信息建立顺序表中各元素 infile.open(\"EXP18_3.txt\ if(!infile){ cout<<\"不能打开文件\"<>stu; last++; putslist(stu,last); } last--; //对应文本文件结束处是回车换行后加文件结束符。 A infile.close(); print(); cout<《C++程序设计》教学指导书 76

A行是很有意思的。本例不知道文件中有多少组数据,用文件结束来判断。这里就有两种情况:一种是文本文件结束处直接是文件结束符,这时读完最后一组数据,就获得文件结束符;第二种是文本文件结束处是回车换行后加文件结束符,这时读完最后一组数据,要多读一次才能获得文件结束符。所以A行要将数组元素数减一。

现在的问题是文本文件生成是用那种方式呢?使用文本编辑器,两者都可能,结束时不打回车是前者,打回车是后者;使用C++生成的是后者,见下面析构函数的代码段:

template Orderedlist::~Orderedlist(){ int i; ofstream outfile; //析构函数中存文件 outfile.open(\"EXP18_3.txt\ if(!outfile){ cout<<\"不能打开文件\"<如果A行缺,则每运行一次程序,增加一组错误数据。

对键盘文件也有同样问题:

while(!cin.eof()) cin>>slist[++last]; last--;//文件结束符判断有延迟

文件的随机访问也是与格式密切相关的,常用于二进制数据文件。

一个流文件一次只能与一个磁盘文件建立联系 下例是学生的练习:

class student{ int id; char name[10]; int tel; public: student(); student(int idty,char[],int phone); friend ostream & operator<<(ostream &,student &); };

int main(){ student a(6000512,\"杨过\虚竹\ ofstream studentfile; studentfile.open(\"stufile2.doc\//为什么word里没东西? studentfile.open(\"stufile.txt\

《C++程序设计》教学指导书 77

}

//在指定目录下没有\"stufile.txt\"文件,删掉上面一句就有了

//一个流文件一次只能与一个磁盘文件建立联系,如与两个联系则流出错 studentfile<第十章 异常处理

教学目的

使大学生认识到当发生运行时错误时,不能简单地结束程序运行,而是退回到任务的起点,指出错误,并可继续工作。

建议学时安排

授课2学时

异常的概念,异常处理的机制,栈展开与异常捕获,异常与继承。2学时。

教学方法提示

本章是以栈展开与异常捕获为核心技术,掌握面向对象编程中异常处理的方法,并能用于简单的软件设计。

异常处理要求严格执行在构造函数中建立资源,在析构函数中释放资源的面向对象程序设计的规范的编程方法。就这点而言,异常处理教学是对规范地编写C++程序的一个极好的总结,也是不可不讲的。

本章要求让学生建立面向对象程序设计异常处理原则的思想:分清责任,谁引发异常由谁来解决,不是就地解决,而是追根寻源。具体做法,有完整的固有格式,必须照办。本章讲的基本是实用的语法。

注意:函数try块(Function try Block),而.net支持。

疑难解答

try块应用

书中提到函数try块对构造函数尤其有用,因为构造函数是自动调用,在建立每一个对象时都会调用,如果构造函数可抛出异常,如何设置try块将是一个难处理的问题,使用函数try块就方便了。,设栈建立动态数组失败时也抛出异常,将main()函数定义为try块是一个好的选择。

异常处理匹配原则

重载总是挑选最佳匹配的函数,而异常处理的原则却是挑选第一个遇到

《C++程序设计》教学指导书 78

的匹配的catch子句,该子句不一定是最佳匹配子句,为什么?古希腊的哲学家柏拉图让他的学生们从小麦田里穿过,不许回头,要求学生途中摘下所遇到最大的麦穗,并只许摘一次,结果大家都两手空空,原因很简单,当学生见到最大的麦穗时认为后面还有更大的。在这种情况下,只能要求学生摘下他认为足够大的。只有当允许回头,或所有麦穗都放在盘子里,才能选出最大的。对重载,所有函数都放在盘子里,是一个静态的过程,当然可以挑出一个最佳的。异常处理是一个运行时的机制,逆调用链返回的过程又是一次性的,要找最佳的最后只能两手空空,只能找最先匹配的catch子句。

第十一章 标准模板库 (选读)

教学目的

掌握STL基础知识和STL的简单应用。

建议学时安排

自学,如讲课4学时

标准模板类库简介,迭代子类,顺序容器类。2学时。 泛型算法与函数对象,关联容器类,容器适配器。2学时

教学方法提示

本章为选读,教师主要讲解概念,指出使用要点,演示例题。要求通过学习,学生能借助STL容器类成员函数与泛型算法的资料使用STL。

STL学习时,入门阶段必须首先掌握容器的基本用法,学生完全可以通过自学进一步深入。泛型算法,也是先掌握几个最常用的算法。这些STL相关函数详细使用必须查手册。

最复杂的是迭代子,书中只介绍了基本内容,先用起来再逐步加深理解。 容器与适配子的关系是一个很有代表性的面向对象编程的范例。在STL中,栈(stack)是一个适配器,它需要一个容器来放置入栈的数据,它不是自己建立一个线性表,而是借用现成的顺序容器,将其作为栈的成员对象。至于使用哪一种顺序容器,由模板类型参数指定。第7章习题7.8和7.9的解法2中模拟了这种技术。

疑难解答

forward与向后移动

迭代子中的一个术语forward,中英文有明显差异,它指的是从容器开头到容器末尾,中文习惯称为向后移动,而英文直译是向前移动。书中选用的译文为正向移动、正向迭代子,而其他地方的说明一律遵从中文习惯,正向为后移,逆向为前移,以免混乱。

《C++程序设计》教学指导书 79

矢量类的边界

注意,矢量类有一个构造函数:

vector(first,last); //元素的初值由区间[first,last)指定的序列中的元素复制而来

区间是半开区间。所以例11.2中有:

int i,search_value,ia[9]={47,29,37,23,11,7,5,31,41};

vector vec(ia,ia+9); //数据填入vector

ia+9已超出数组范围,这是正确的,因为ia+9未包含在数据源中,只是说数据源在ia+9位置之前,即ia[0]~ia[8]。

与标准模板类库

使用VC++6.0标准模板类库,在包含标准流类库时(用无.h的头文件),如用using namespace std会发生一系列的冲突;把流类库头文件改为fstream.h也一样。表现在重载的<<、>>不能用,认为有岐义性(ambiguous),友元也不认等等。

对于岐义性(ambiguous),可用using声明,在每个标识符前用std::,烦琐但保证不出错。方法如下:

#include using std::cout; using std::cin; using std::endl;

使用VC++.net则一切正常。VC++6.0必须加补丁5,才能基本正常。即使如此VC++6.0也仅能支持deque类的部分构造函数。建议使用.net演示例题。

同步实验部分

以Visual C++ 6.0 集成开发环境下的控制台应用程序为主,共26个(实际29个,3个选做)实验。

教师在第二章和第三章课堂教学时应多做控制台应用程序设计全过程演示。

实验一 Visual C++集成开发环境(IDE)入门 实验二 简单的C++程序设计 实验三 分支结构程序设计 实验四 循环结构程序设计

实验五 常用算法:枚举法 递推法 迭代法 实验六 文本文件简单应用 实验七 函数的基本概念 实验八 函数的递归算法

实验九 函数的重载和变量的作用域 实验十 类与对象的基本概念 实验十一 引用与复制构造函数 实验十二 运算符重载 实验十三 数组与数组

《C++程序设计》教学指导书 80

实验十四 指针与数组 实验十五 模板与线性表 实验十六 排序与查找 实验十七 模板与类参数

实验十八 动态内存分配与深复制 实验十九 链表及应用 实验二十 栈与队列的操作

实验二十一 继承与派生基本概念 实验二十二 虚函数与多态 实验二十三 纯虚函数

实验二十四 输入输出与重载 实验二十五 文件 实验二十六 异常处理

课程设计部分

要求采用事件驱动编程方法完成。

课堂教学

Windows程序设计基础 1)传统的Windows编程;

提示:本小节的电子课件内容多于教材,并有大量超链接把知识点联系起来,更便于教学或自学。 2)Windows对象句柄;

提示:因为第一小节中提到句柄,教学中将本节提前为好。 3)MFC层次结构;

提示:在实践教材中MFC层次结构排在第5小节,从利于学生理解的角度出发,应该先讲(或先自学)本小节,这样学生对微软基础类库有一个基本的了解,然后再了解MFC编程更加合理。 4)MFC编程;

5)MFC对象与Windows对象; 6)MFC的消息映射与命令传递;

提示:本小节是MFC编程基础的核心内容。 7)文档/视图结构和序列化。

提示:本小节的内容是MFC的特色,也是最重要的程序结构。以上是第2章内容

8)第3章MFC编程操作,可安排学生提前自学为主,教师进行重点内容

讲解和演示。要求内容简洁而不失完整性:由应用程序向导建立程序框架,由资源编辑器建立用户界面,由类向导建立成员变量、消息映射和处理函数,加上文档序列化和注册;使学生对MFC编程的关键步骤一目了然。

9)第4章中UML面向对象的系统分析和设计

*实践教材中有关UML内容需细化类图,增加活动图内容。

《C++程序设计》教学指导书 81

10)第4章中介绍研究型学习和管理及多媒体软件编制方法。

实验

对话框与控件 4学时 文档-视图结构及图形与文本输出 4学时 序列化和文件操作 4学时 多文档与多重视图 4学时

课程设计

采用研究型学习方法,由学生分组在辅导教师(研究生)指导下自选题目,调研并自定内容,强调参与开发的全过程。

上机 16学时

UML活动图

UML活动图(activity diagram)是一种特殊形式的状态机,用于计算流程和工作流程建模。活动图是状态图的扩展,但它提供了一些特殊的表示法,使得它与状态图看起来非常不同。活动图中的状态表示是计算过程中所处的各种状态,而不是普通对象的状态。

通常使用活动图来表达顺序程序的流程,这点与传统的流程图很相似,仅仅图示方法上有所不同。但是活动图还可以包含并发线程的分叉控制,它可以表示并发线程的同步。也就是说,活动图还能表达并发流程控制。因本教材未触及并发的内容,所以略去活动图中与并发程序相应部分的应用。

建议在授课时用活动图取代流程图。

活动图要素

1)活动(activity):状态机内正在进行的非原子执行,用一个上下为直线两

侧为圆弧的框表示,并在框内写明活动的名称。另有名词动作(action)表示可执行的原子计算(原子,希腊语原意为不可再分的),即它一次性完成而不会被外界请求所中断。 2)转移(transition):采用箭头表示。转移是由上一个活动结束引起的,而

不是由其他事件引起的,所以转移通常不加注解。而在状态图中通常加注解,称为转换。 3)分支(branch):采用菱形符号。表示某个判断或决策,由一个状态引出

不同的信息流。 4)注解(note):采用右上角折叠的矩形表示,说明UML图中符号的意义,

它与被说明的符号间用虚线连接。

5)起点:起始标志,采用黑色实心圆点表示。 6)终点:结束标志,采用实心的小同心圆表示。

7)同步条:是一个粗的平行条,表示活动的同步。仅当所有的引入转移活

《C++程序设计》教学指导书 82

动都完成时,该同步条才能被传递。同样,该同步条所有引出的转移同时被触发,即由那些转移所引导的所有活动被同时启动。总之,同步条提供了一种方法来表示等待所有子任务都完成后再继续,和并发的开始多个子任务。在今天多线程编程已经很普遍,尤其是多核CPU正在逐步成为主流,今后采用对称多处理技术的软件编制也将会越来越多。未来的编程,并发处理将会成为主流。

UML图形绘制工具

Microsoft office 中的绘图工具visio提供了完整的UML各种图形的模板和极方便的使用步骤。进入visio,选择类别中的软件,再点击模板中的UML模型图,就进入UML绘图界面。visio提供了一个简明教程,只需一个小时就可以基本掌握visio的绘图。

用活动图表示程序的三种基本控制结构

分支[f][t][t]动作状态动作状态2[f]动作状态1分支单选择If语句双选择If...else语句

《C++程序设计》教学指导书 83

[t]case 1?动作状态1case 2?[f][f][t]动作状态2动作状态2break合并动作状态1break[t]case n?[f]动作状态n默认处理动作状态nbreak合并顺序带break的switch语句

先求表达式1的值再求表达式2的值初始化动作状态先求表达式3的值再求表达式2的值初始化[t][f][t][f]动作状态[f][t]动作状态while语句do...while语句for语句

活动图中菱形框有两种用法:分支和合并。分支需加以判断,条件写在注释中,不写在菱形框中,这样菱形框全画的同样大小,比流程图美观。参见带break的switch语句活动图。注释与被说明框用虚线连接。

活动图中的活动状态框只是给它命名,详细内容放在注释中,这一点与流程图不同。参见for语句活动图。如果活动状态框的命名已经很清楚了,则不必加注释。

《C++程序设计》教学指导书 84

书中部分例题的活动图

给num1赋值对应C++语句:cin>>num1;对应C++语句:cin>>num2;对应C++语句:sum=num1+num2;给num2赋值求num1和num2的和例题2.1活动图

a>=b?输入3个数a,b,c[t]max赋值a[f]max赋值bc>max?[t][f]max赋值c输出max例题2.5活动图

《C++程序设计》教学指导书 85

输入a,b,c求deltadelta等于0[f][f][t]输出无实根输出两不等实根输出两相等实根delta大于0[t]例题2.8活动图

UML类图

UML类图可以直观地表达类的成员、类与类之间的关系,建议在教学中多利用这些图形表达方式。

基本类图

类图就是用图形来描述类,包括类名、数据成员和函数成员,以及各成员的访问限定特性。在UML中用矩形代表类,通常用两条横线将其分为三个部分,最上面是标题栏填类名,中间属性栏填类的数据成员,最下面是操作栏填函数成员。这三个部分只有类名是必须有的,其他为可选项。

数据成员的表达方式为: 『访问限定特性』名称『[重数]』『:类型』『=默认值』『{约束特征}』 其中,访问限定特性包括public、private和protected三种,分别用“+”、“-”和“#”表示。名称后的方括号中的重数相当于C++中的数组定义。与C++不同,类型放在数据名称的后面,并加冒号。UML的类型并不与C++的类型完全一样,但在实际应用时不必拘泥,可以用C++的类型代替。最后可以在花括号中加约束特征,如“只读”,对应C++的const,对于C++而言,该项不必填写。符号『』中的是可选项。

函数成员的表达方式为:

『访问限定特性』名称『(参数表)』『:返回类型』『{约束特征}』

可以看出UML与C++的返回类型表示方法不同。参数表的表示方法也与

《C++程序设计》教学指导书 86

C++不同,每一个参数表达方式为: 『方向』名称 :类型『=默认值』 方向表示参数是用于输入(in)、输出(out)或同时用于输入输出(inout)。对C++而言,该项不必填写。但使用Visio自动填写。

教材P.117中的商品类CGoods可以由下图表达:

CGoods-Name : string-Amount : int-Price : float-Total_value : float+CGoods()+CGoods(in name : string, in amount : int, in price : float)+CGoods(in name : char, in price : float)+RegisterGoods(in name : string, in amount : int, in price : float)+CountTotal()+GetName(out name : string)+GetAmount() : int+GetPrice() : float+GetTotal_value() : float

可以在成员表述式的最前面加一个说明术语,用书名号(双尖括号)括住。如构造函数可加《constructor》,析构函数可加《destructor》,友元函数可加《friend》,静态成员可加《static》等等

对象图

对象图的标题栏中书写方式为: 对象名:类名

多了对象名,多了冒号,多了下划线。对象图中只有属性,而没有操作,这与操作为所有类对象共有,相一致。Cgoods类的对象Car对象图如下:

Car : CGoodsName : stringAmount : intPrice : floatTotal_value : floatCar : CGoods

类间关系

类与类的关系包括:关联、聚合、组合、泛化及各种形式的依赖关系等

《C++程序设计》教学指导书 87

等。

依赖(dependency)

依赖表示两个元素之间存在一种关系,其中一个元素(提供者)的变化将影响另一个元素(客户),或向它(客户)提供所需信息。但两者反过来是不成立的。这是将数种不同的建模关系组织到一起的简便方法。在UML的基本模型中的依赖关系在C++编程中常见的有:

绑定(bind):为模板参数指定值,以生成一个新的模型元素。等效C++

模板中的实例化。属绑定依赖。 访问(access):允许某个包访问另一个包的内容。以下属许可依赖。 友元(friend):允许某元素访问另一个元素,而不管被访问者是否可见。 调用(call):声明某个类调用其他类的操作方法。以下属使用依赖。 参数(parameter):一个操作和它的参数之间的关系。 实例化(instantiate):这里的实例化不同于C++模板,而是泛指从概念

到实体,即创建实例。如由类创建对象、由用例创建用例实例。创建实例的机制是运行时环境的职责。 发送(send):信号发送者与接受者之间的关系。 另外有抽象依赖,包括跟踪(trace)、精化(refine)、实现(realize)、导出(derive)。

依赖通常用一个从客户指向提供者的虚箭头表示。 下图中类time12是12小时计时,精确到分;而time24是24小时计时,精确到秒。可以将24小时制转换到12小时制,反之不行。类time12是客户,类time24是提供者。

time12+time12(in t24 : time24)time24

*绑定:在计算机编程中,绑定是在某个时间范围和特定的位置内为两个或更多编程对象或值对象创建联系的过程,内涵十分广泛。编译程序时,绑定意味着用一个真实值替换程序中的变量值,或用来保证另一些程序和被编译的程序一起被加载到存储器中。在本C++教材中提到静态绑定和动态绑定是一个具体的实例,也译作静态联编和动态联编;本教材也提到类模板和函数模板分两步进行编译,其中模板实例化也是绑定的一个实例。但是在同一个地方总是要用不同的名称来区分具体的事物,这就造成了C++和UML的术语选用的差异。

绑定这个术语也用于网络通信中,任何两个网络终端、实体、过程或逻辑单元之间建立起一个明确的连接都可以称为绑定。

关联(association)

某些真实世界的实体存在明显的联系,如驾驶员与汽车、书籍与图书馆、引擎与交通工具。如果这样的实体在程序中用类来表示,则它们通过关联关系来联系。在UML图中用实线相连接。

《C++程序设计》教学指导书 88

类的关联关系实际是类对象而不是类本身之间存在一些关系。典型的情况有:如果一个类对象调用另一个类对象的一个操作,则这两个类是相关联的;如果一个类的某属性为另一个类的对象,则两者也可能存在关联关系。上面的例子都属使用关系。

关联具有方向性或称适航性(navigability),采用由客户指向提供者的开箭头表示,如类对象甲调用类对象乙的一个操作,则由甲指向乙的单向开箭头表示。如果甲乙互相调用,用双向箭头表示,称双向(bidirectional)关联关系。适航性箭头是可选的。

多重性(multiplicity)是UML类图中的一个特性,例如一个交通工具可以有0到n个引擎,可以在这两者连线的交通工具类对象一侧标示1,在引擎类对象一侧标示0..n,称为重数。用术语表示是每个引擎对象与1个交通工具对象关联,而每个交通工具与0到n个引擎相关联。 标记 说明 * 任何数量的对象(包括0) 1 一个对象 n n个对象(n为整数,下同) 0..1 0或1个对象(即关联时可选的) n..m 最少为n个对象,最多为m个对象 1,3 离散的结合(这里是1个或3个) 表 重数的标记方法 图书管理系统中有一个Loan类,它将Reader、Librarian和Item三个类相关联,即将整个借阅过程构成整体。

Loan(关联类)-ReadID : long-LibmID : long-CataNum : longReader-读者-Items : Item-Name : string1-ID : long-Counter : int+ShowItem() : void-管理员Librarian1-Name : string-ID : longItem-Title : string-CataNum : long-Cost : double-Type : int-Status : bool-Copies : int+Catalogue() : long+ShowDetail() : void-读物1..*

《C++程序设计》教学指导书

聚合(aggregation)

聚合是一种关联关系,它指明一个聚集和组成部分之间的整体与部分的关系。聚合是一种拥有(has a)关系。图书馆拥有图书,账单拥有明细条目。

聚合关系的表示与关联关系相似,仅仅连线在整体一侧有一个中空的菱形,取代了开箭头。

组合(composition)

组合是一种更简单的聚合,它要求部分只属于一个整体,并且部分的生命期与整体的生命期相同,具有更强的拥有关系。C++中的一个类对象具有成员对象,体现的就是严格的组合关系。其表示方法与聚合基本相同,仅仅菱形是实心的。

在现实世界中聚合和组合都是存在的。如科研活动中的课题组包含许多成员,但每个成员都可以同时是另一个课题组的成员,这对应聚合,不对应组合,部分可以参加多个整体,具有共享性。而汽车与发动机对应组合,是一种特殊的聚合,无共享性。应该说C++在这方面还不能很好反应现实世界。

图书馆类包括6组成员对象,在C++中他们都是对象数组。这里用组合来描述应该更准确一些。在C++中表达聚合还没有一个好办法,一般采用指针。如课题组问题:教师建立一个数组;而课题组中有一个指针数组,数组元素指针指向参加本课题组的教师。

Book-Author : string-Title : string-PubDate : string+ShowDetail() : void1..*111Library-Books : Book-Magzines : Magzine-Recordings : Reader-Readers : Reader-Librarians : Librarian-Loans : Loan(关联类)11..*111..*Librarian-Name : string-ID : longMagzine-Year : int+ShowDetail() : void1..*1..*Recorded-Formats : string+ShowDetail() : void1..*Reader-Items : Item-Name : string-ID : long-Counter : int+ShowItem() : voidLoan(关联类)-ReadID : long-LibmID : long-CataNum : long

泛化(generalization)

在UML中继承称为泛化。从字面上理解出发点相反,C++是从共有的基类(父类)派生出派生类(子类),而UML是由具有某些共性的类,抽象出共有的基类。UML的方式与人类的思维方式一致,UML表征的是由现实世界事物抽象出计算机世界类对象的思维过程。

泛化的图示方法是将关联的开箭头改为三角形箭头,由基类指向派生类。箭头的方向强调了派生类可以访问基类中的函数与数据,而没有基类访问派生类的通道。

《C++程序设计》教学指导书 90

下图是由读物类派生出书、杂志和电子读物类。

Item-Title : string-CataNum : long-Cost : double-Type : int-Status : bool-Copies : int+Catalogue() : long+ShowDetail() : voidBook-Author : string-Title : string-PubDate : string+ShowDetail() : voidMagzine-Year : int+ShowDetail() : voidRecorded-Formats : string+ShowDetail() : void

模板——参数化元素(parameterized element)

在UML中,模板是对一个带有一个或多个未绑定的形式参数的元素的描述符。因此它定义了一系列的潜在元素,其中每个元素都通过把参数绑定到实际值来说明。

T, size:intseqlist-slist[size] : T-Maxsize : int-last : int+seqlist()+Length() : int+Find() : int+IsIn() : bool+Insert() : bool+Remove() : bool+Next() : int+Prior() : int+IsEmpty() : bool+IsFull() : bool+Get() : T+operator[]() : T《bind》(int,)intseqlist

模板类是最常用的参数化元素,与C++的类模板对应得十分完美,参数有类型参数和非类型参数(代表潜在的常量)。模板类的图示方法是在类图

《C++程序设计》教学指导书 91

的右上角加一个虚线框,参数填在框内。指定形式参数后由UML模板类产生相关类,称为绑定(bind),即C++的模板实例化。相关类与模板类存在绑定依赖关系,用相关类指向模板类的虚箭头表示,虚线边加标《bind》和圆括号中的实在参数。绑定可以是显式的,相关类有自己的名字;也可以是隐式的,无名的类。

下图是线性表类模板的UML图,同时给出绑定产生的新类,对应C++的模板实例化。

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- fenyunshixun.cn 版权所有 湘ICP备2023022495号-9

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务