面向对象程序设计
面向对象程序设计 前言 第1章 C++语言概述 第2章 数据类型和表达式 第3章 预处理和语句 第4章 函数和作用域 第5章 类和对象(一) 第2章 数据类型和表达式 第3章 预处理和语句 第4章 函数和作用域 第5章 类和对象(一) 第6章 类和对象(二) 第7章 继承性和派生类 第8章 多态性和虚函数 第9章 C++的I/O流库
内容简介 面向对象的基本思想 C++语言的基本概念、基本语法和编程方法 C++语言的面向对象特征 类与对象 继承与派生 多态性与虚函数
学习方法 多练习,掌握基本概念 多读程序,学习编程方法与技巧 多上机实践,加强动手能力 多剖析范例,积累编程经验
教材及参考书目 C++语言基础教程.吕凤翥著.清华大学出版社.1999
第1章 C++语言概述 1.1 C++的起源及特点 1.2 面向对象的方法 1.3 面向对象的程序设计语言C++ 1.2 面向对象的方法 1.3 面向对象的程序设计语言C++ 1.4 C++程序的编辑、编译和运行 1.5 C++的词法及词法规则 1.6 C++程序的结构
1.1.1 面向对象的由来和发展 机器语言(二进制码) 汇编语言 20世纪50年代中期,FORTRAN语言 1.1.1 面向对象的由来和发展 机器语言(二进制码) 汇编语言 20世纪50年代中期,FORTRAN语言 在计算机语言发展史上具有划时代的意义,引入了许多程序设计概念。如变量、数组、循环、分支等。 20世纪50年代中期,Algol语言 提出块(Begin…End)思想,对数据进行保护,是一种初级封装。
1.1.1 面向对象的由来和发展(续) 20世纪60年代中期,Simula 67语言 1.1.1 面向对象的由来和发展(续) 20世纪60年代中期,Simula 67语言 面向对象语言的鼻祖,提出了对象、类的概念,并支持类的继承。 20世纪70年代中期,Ada语言 支持数据抽象类型的最重要的语言之一,但不完全支持继承。 20世纪70年代中期,Smalltalk语言 最有影响的面向对象语言之一,丰富了面向对象的概念。 20世纪80年代中期后出现C++等多种面向对象语言
1.1.2 C++的起源和特点 一、C++的起源 在C语言基础上为支持面向对象的程序设计研制的一个通用目的的程序设计语言; 由AT&T贝尔实验室Bjarne Stroustrup博士开发; 二、C++的特点 与C兼容,既保持了C的简洁、高效和接近汇编的特点,又比C更安全,结构化程度更高; 既支持面向过程的程序设计,又支持面向对象的程序设计; 注意其两面性
1.2 面向对象的方法 必须先研究事物,而后才能研究过程。必须先知道一个事物是什么,而后才能觉察这个事物中所发生的变化。 1.2 面向对象的方法 必须先研究事物,而后才能研究过程。必须先知道一个事物是什么,而后才能觉察这个事物中所发生的变化。 《路德维希·费尔巴哈和德国古典文学的终结》 恩格斯
1.2.1 面向过程与面向对象程序设计 例1.1:输入任意短语或句子,计算该短语包含多少个字(word)和字符(character)。 1.2.1 面向过程与面向对象程序设计 例1.1:输入任意短语或句子,计算该短语包含多少个字(word)和字符(character)。 注意:输入短语时,必须在字和字之间只空一格。
1.2.1 面向过程与面向对象程序设计(续) 面向过程的方法 #include <stdio.h> 1.2.1 面向过程与面向对象程序设计(续) 面向过程的方法 #include <stdio.h> #include <conio.h> void main() { char ch; int wdcount,chcount; wdcount=1; chcount=0; printf("***:Please input any phrase…\n"); while((ch=getche())!='\r') chcount++;
1.2.1 面向过程与面向对象程序设计(续) if(ch==' ') wdcount++; } 1.2.1 面向过程与面向对象程序设计(续) if(ch==' ') wdcount++; } printf("\n***:The number of word is %d\n",wdcount); printf("\n***:The number of char is %d\n",chcount); 面向对象的方法 #include <iostream.h> #include <conio.h> class count { public: 定义类 公有成员
1.2.1 面向过程与面向对象程序设计(续) count(); void process(); void display(); 1.2.1 面向过程与面向对象程序设计(续) 构造函数 count(); void process(); void display(); private: int wdcount,chcount; }; count::count() { wdcount=1; chcount=0; } void count::process() 成员函数 私有成员 数据成员
1.2.1 面向过程与面向对象程序设计(续) char ch; 1.2.1 面向过程与面向对象程序设计(续) char ch; cout<<"***:Please input any phrase…"<<endl; while((ch=getche())!='\r') { chcount++; if(ch==' ') wdcount++; } cout<<endl; void count::display() printf("\n***:The number of word is %d\n",wdcount);
1.2.1 面向过程与面向对象程序设计(续) printf("\n***:The number of char is %d\n",chcount); } void main() { count A; A.process(); A.display(); 定义对象 调用公有成员函数 运行结果 ***Please input any phrase... I am a teacher ***:The number of word is 4 ***:The number of char is 15
1.2.1 面向过程与面向对象程序设计(续) 面向过程程序设计 面向对象程序设计 是一种数学思维或计算机思维方法,与人们认识世界的方法不同 1.2.1 面向过程与面向对象程序设计(续) 面向过程程序设计 面向对象程序设计 是一种数学思维或计算机思维方法,与人们认识世界的方法不同 以不稳定的、多变的“过程”和“操作” 为中心来构造系统 可重用性较差 是建立在认识方法学基础上的一项技术科学,比较自然地模拟了人类认识客观世界的方式; 以相对稳定的“对象”和“数据结构”为中心来构造系统 可重用性较好
1.2.1 面向过程与面向对象程序设计(续) Changing? 面向过程程序设计 面向对象程序设计 P5 O5 P6 P4 O4 P3 1.2.1 面向过程与面向对象程序设计(续) 面向过程程序设计 面向对象程序设计 P5 O5 P6 P4 O4 P3 O3 Changing? P2 O2 P1 O1
1.2.1 面向过程与面向对象程序设计(续) Changing? 面向过程程序设计 面向对象程序设计 P3 O5 P5 O4 O4 O3 1.2.1 面向过程与面向对象程序设计(续) 面向过程程序设计 面向对象程序设计 P3 O5 P5 O4 O4 O3 Changing? O2 P1 O1 P2 P6
1.2.2 抽象在面向对象中的作用 一、抽象的概念 抽象代表着一个对象的本质特征,这个特征将这个对象与所有其他种类的对象区别开来; 1.2.2 抽象在面向对象中的作用 一、抽象的概念 抽象代表着一个对象的本质特征,这个特征将这个对象与所有其他种类的对象区别开来; 抽象是通过从特定的实例中抽取共同的性质以形成一般化的概念的过程; 抽象具有层次; 交通工具 汽车 轮船 飞机
1.2.2 抽象在面向对象中的作用(续) 二、面向对象抽象的原理(面向对象计算的本质) 数据抽象、行为共享、进化、确定性 1、数据抽象 1.2.2 抽象在面向对象中的作用(续) 二、面向对象抽象的原理(面向对象计算的本质) 数据抽象、行为共享、进化、确定性 1、数据抽象 为程序员提供了一种对数据和为操作这些数据所需要的算法的抽象;是面向对象方法的核心,包括: 公有成员 类 模块化:构成了面向对象计算的本质; 信息隐藏:将一个模块的细节部分对用户隐藏起来,用户只能通过一个受保护的接口来访问某个模块,而不能直接访问一个模块内部的细节;
1.2.2 抽象在面向对象中的作用(续) 2、行为共享 行为是由实体的外部接口定义的 行为共享指许多实体具有相同的接口,可增加系统的灵活性; 1.2.2 抽象在面向对象中的作用(续) 2、行为共享 对象 公有成员函数名 行为是由实体的外部接口定义的 行为共享指许多实体具有相同的接口,可增加系统的灵活性; 支持行为共享的方式 分类与层次分类 多态与继承
1.2.2 抽象在面向对象中的作用(续) 3、进化 需求进化(虚函数) 进化式的问题求解(继承的构造函数) 4、确定性 1.2.2 抽象在面向对象中的作用(续) 3、进化 需求进化(虚函数) 进化式的问题求解(继承的构造函数) 4、确定性 确保每个行为项都有一个正确的解释,系统不会因不能响应某一行为而失败; 确定性与类型的正确性有关;
1.2.3 面向对象计算的基本特征 面向对象系统的三要素:对象、类和继承; 一、对象 1、概念上 对象是代表着正在创建的系统中的一个实体; 1.2.3 面向对象计算的基本特征 面向对象系统的三要素:对象、类和继承; 一、对象 1、概念上 对象是代表着正在创建的系统中的一个实体; 2、实现形式上 对象是一个状态和操作(或方法)的封装体; 3、对象的定义 一个对象具有状态、行为和标识。 状态:对象的状态由这个对象的属性和这些属性的当前值决定。属性是静态的,当前值是动态的;
1.2.3 面向对象计算的基本特征(续) 行为:一个对象如何以状态变化和消息传递的形式进行作用和对外界进行反应。 1.2.3 面向对象计算的基本特征(续) 行为:一个对象如何以状态变化和消息传递的形式进行作用和对外界进行反应。 一个对象的行为代表了这个对象的外部可见的和可测试的活动; 一个对象的状态代表着它的行为的累积结果; 标识:标识是一个对象固有的一种特性,该特性将这个对象与其他对象区别开来; 对象1 值11 值21 ... 值n1 值12 值22 值n2 对象2 行为 值12 值22 ... 值n2 值11 值21 ... 值n1 属性1 属性2 ... 属性n 行为
1.2.3 面向对象计算的基本特征(续) 4、对象的分类 5、对象的确认 发现对象:主要是实体对象或界面对象; 发明对象:主要是控制对象; 1.2.3 面向对象计算的基本特征(续) 4、对象的分类 实体对象 存储信息的对象 按作用分类 界面对象 控制对象 支持系统的主要功能 5、对象的确认 发现对象:主要是实体对象或界面对象; 发明对象:主要是控制对象; 二、类 1、什么是类
1.2.3 面向对象计算的基本特征(续) 类是创建对象的样板,它包含着所创建对象的状态描述和方法的定义。类的完整描述包含了外部接口和内部算法以及数据结构的形式; 2、类是对象的抽象及描述 类中包含生成对象的具体方法,由一个类所创建的对象称为该类的实例; 3、类是抽象数据类型的实现 类是所有对象的共同的行为和不同的状态的集合体; 三、继承 继承提供了创建新类的一种方法,它的本质特征是行为共享;
1.2.3 面向对象计算的基本特征(续) 属性1 属性2 …… 属性n 行为1 行为2 行为m 值11 值21 值n1 值12 值22 1.2.3 面向对象计算的基本特征(续) 属性1 属性2 …… 属性n 行为1 行为2 行为m 值11 值21 值n1 值12 值22 值n2 对象1 对象2 对象k 对象p 类 类与对象的关系
1.3.1 C++语言中的抽象支持 控制抽象:用于排列任意动作的顺序的一种方法; 三种语句控制结构:顺序、循环、分支 过程抽象(面向过程):对一组输入数据的一个计算动作和产生的输出结果; 数据抽象(面向对象):类是实现抽象数据类型的工具;
1.3.2 C++对面向对象程序设计方法的支持 C++支持数据封装(数据抽象) C++中,类是支持数据封装的工具,对象则是数据封装的实现; 每个可能的消息对应一个相应的方法,方法通过函数来定义; C++中允许友元破坏封装性 C++中允许函数名和运算符重载 C++支持继承性 C++支持动态联编
1.3.3 C++对C语言的改进 增加了新的运算符:::,new,delete等; 改进了类型系统,增加了安全性; 引进了引用概念; 允许函数重载,允许设置缺省参数,提高了编程的灵活性; 引进了内联函数,提高了程序的效率; 可以根据需要随时对变量进行说明;
1.4 C++程序的编辑、编译和运行 一、编辑:源文件的扩展名为.cpp 二、编译 1、预处理过程 词法分析:单词 语法分析:构造程序的格式 2、编译 过程 符号表:程序中的各种符号及其属性 错误处理程序: 生成目标代码:目标文件扩展名.obj 3、连接过程:可执行文件扩展名.exe 三、运行
1.4 C++程序的编辑、编译和运行(续) 源程序.cpp 磁盘中的#include文件 编译器 目标文件.obj C++库文件.LIB 可执行文件.exe 编译器 连接器 磁盘中的#include文件 C++库文件.LIB
1.5.1 C++的字符集 大小写的英文字母:a~z,A~Z 数字字符:0~9 特殊字符
1.5.2 词法记号 1、关键字(保留字) 表1-1 C++的关键字
1.5.2 词法记号(续) 2、标识符 组成规则:以字母或下划线开始,其后跟零个或多个字母、数字或下划线; 1.5.2 词法记号(续) 2、标识符 组成规则:以字母或下划线开始,其后跟零个或多个字母、数字或下划线; 正确标识符:Result, DoubleList, _first, first_ 错误标识符:1first 不能以数字开始 标识符的长度任意(受编译器限制); 区分字母的大小写; 例如:ADD, Add, add 不能使用系统的保留字;
1.5.2 词法记号(续) 3、运算符 5、分隔符 4、各种文字 单目 () 运算符 双目 {} 三目 分隔符 , : 数字 ; 字符文字 1.5.2 词法记号(续) 3、运算符 5、分隔符 单目 () 运算符 双目 {} 三目 分隔符 , 4、各种文字 : 数字 ; 字符文字 文字 串文字 布尔文字
1.5.3 空白 一、空白 包括:空格、制表符、换行符、注释 功能:指示词法记号的开始和结束位置; 二、注释 /*……*/ //
1.6 C++程序的结构 一、C++示范程序 #include <iostream.h> void main() { 有的输入输出操作 #include <iostream.h> void main() { cout<<"Hello world! "<<endl; } cout:流类对象 <<:插入符 提供屏幕输出; 提供键盘输入: cin:流类对象 >>:提取符 例如: cin>>"Please input two integers:"; cin>>a>>b; endl:换行; 二、C++程序的组成 预处理命令 语句 输入输出 变量 函数 其他
第2章 数据类型和表达式 2.1 基本数据类型 2.2 常量和变量 2.3 数组类型 2.4 枚举类型 2.5 指针和引用 2.6 运算符 第2章 数据类型和表达式 2.1 基本数据类型 2.2 常量和变量 2.3 数组类型 2.4 枚举类型 2.5 指针和引用 2.6 运算符 2.7 表达式 2.8 类型定义
2.1 基本数据类型 一、基本数据类型 整型int 单精度浮点数float 浮点型(实型) 双精度浮点数double 基本数据类型 2.1 基本数据类型 一、基本数据类型 整型int 单精度浮点数float 浮点型(实型) 双精度浮点数double 基本数据类型 字符型char 逻辑型bool 空值型void 用于函数和指针
2.1 基本数据类型(续) 二、数据类型修饰符 signed:有符号 unsigned:无符号 short:短型 long:长型 说明: 2.1 基本数据类型(续) 二、数据类型修饰符 signed:有符号 unsigned:无符号 short:短型 long:长型 说明: 1) 类型修饰符可以修饰除void、bool类型以外的其他类型; 2) 上述修饰符均可用于整型和字符型; 3) long修饰符还适用于双精度浮点数;
2.1 基本数据类型(续) 三、基本数据类型列表 表2-1 C++的基本数据类型
2.1 基本数据类型(续) 说明: 1) 表中的[int]可以省略,即在int之前有修饰符出现时,可以省略关键字int; 2.1 基本数据类型(续) 说明: 1) 表中的[int]可以省略,即在int之前有修饰符出现时,可以省略关键字int; 2) 单精度类型float、双精度类型double、长精度类型long double统称浮点类型; 3) char类型和各种int类型统称整型类型;char类型变量在内存中以它的ASCII码值的形式存储; 4) 字宽(字节)取决于操作系统和编译器的实现,可用sizeof验证;
2.2.1 常量 一、整型常量(无小数部分) 1、表示方法 3、八进制表示 十进制 由0 ~ 7的数字组成 八进制 以0为前缀 十六进制 2.2.1 常量 一、整型常量(无小数部分) 1、表示方法 3、八进制表示 十进制 由0 ~ 7的数字组成 八进制 以0为前缀 十六进制 例:010, -0536 2、十进制表示 4、十六进制表示 由0~9的数字组成 由0~9的数字及A ~ F的字母(大小写均可)组成 不能以0开始 无前缀 以0x或0X为前缀 例:132, -345 例:0x7A, -0X3de
2.2.1 常量(续) 5、说明: 1) 长整型用L(或l)做后缀表示。例如: 32765L,793l; 2.2.1 常量(续) 5、说明: 1) 长整型用L(或l)做后缀表示。例如: 32765L,793l; 2) 无符号型用U(或u)做后缀表示。例如: 4352U,3100u; 3) unsigned long型用后缀U(或u)和L(或l)一起表示,L与U的先后顺序无关。例如: 49321ul,37825LU,41152Lu; 4) 无后缀时,整型常量类型按如下顺序确定: int, (unsigned), long, unsigned long 十进制时无
2.2.1 常量(续) 二、浮点型常量 由整数部分和小数部分构成; 只有十进制表示; 一般表示形式(小数表示形式): 2.2.1 常量(续) 二、浮点型常量 由整数部分和小数部分构成; 只有十进制表示; 一般表示形式(小数表示形式): 整数部分与小数部分可以省去一部分,但不能全部省去; 例如:5.,.25,4.07
2.2.1 常量(续) 科学表示形式: 在小数表示法后面加E(或e)表示指数; 指数部分可正可负,但必须为整数; 2.2.1 常量(续) 科学表示形式: 在小数表示法后面加E(或e)表示指数; 指数部分可正可负,但必须为整数; 例如:23.5E6,.032E-5,.3e10 浮点常量的缺省数据类型为double型; 后缀F(或f)表示float类型; 后缀l(或l)表示long double类型;
2.2.1 常量(续) 三、字符常量 由一对单引号括起的一个字符表示; 其值为所括起字符在ASCII表中的编码; 所括起字符的表示方法: 2.2.1 常量(续) 三、字符常量 由一对单引号括起的一个字符表示; 其值为所括起字符在ASCII表中的编码; 所括起字符的表示方法: 图形表示法 该方法适用于有图形符号的可打印字符; 例如:'A','a','*' 转义序列表示法 该方法适用于所有字符,尤其是无图形符号的不可打印字符;
2.2.1 常量(续) 转义序列表示方法:以反斜线(\)开头,后跟字符的ASCII码值; 八进制表示:\ddd; 例如:\101 2.2.1 常量(续) 转义序列表示方法:以反斜线(\)开头,后跟字符的ASCII码值; 表2-2 C++中常用转义序列符 八进制表示:\ddd; 例如:\101 十六进制表示:\xhh; 例如:\x41
2.2.1 常量(续) 四、布尔常量 有两个值:true和false; 五、字符串常量(串常量,字符串) 2.2.1 常量(续) 四、布尔常量 有两个值:true和false; 五、字符串常量(串常量,字符串) 一对双引号括起的字符序列,字符序列可以包含空格、转义序列或任何其他字符,这些字符不一定是C++字符集中的字符,只要C++编译器支持即可; 例如:"This is a string; " 串常量与字符常量的区别:
2.2.1 常量(续) 字符常量 串常量 由一个字符型变量存放 由一维数组存放 用单引号括起 用双引号括起 2.2.1 常量(续) 字符常量 串常量 由一个字符型变量存放 由一维数组存放 用单引号括起 用双引号括起 字符串有一个结束符,该结束符用'\0'表示 字符常量'a'在内存中占用一个字节 字符串常量"a"在内存中占用两个字节 可进行加、减法运算 可进行连接、拷贝运算
2.2.1 常量(续) 与#define的区别? 六、符号常量 用来表示C++中的常量,即用一个与常量相关的标识符来替代常量; 2.2.1 常量(续) 六、符号常量 与#define的区别? 用来表示C++中的常量,即用一个与常量相关的标识符来替代常量; 优点:增加可读性,增强可维护性; 例如:PI表示3.1415926 定义方法:使用类型说明符const; 例如:const int size=80; 定义的符号常量必须初始化; 一个符号常量可看作是一个只读变量,由const定义的常量的值不可以改变;
2.2.1 常量(续) Line1: #include <iostream.h> 2.2.1 常量(续) Line1: #include <iostream.h> Line2: const double pi=3.1415; Line3: const double r; Line4: void main() Line5: { Line6: double perimeter,area; Line7: perimeter=2*pi*r; Line8: pi=3.14; Line9: area=pi*r*r; Line10: cout<<perimeter<<", "<<area<<endl; Line11: } 错误 const double r=3.2; 错误,不能修改pi的值
2.2.2 变量 一、变量的三个基本要素 int c; c=5; 二、变量的定义 可以在程序中随时定义变量,只要在该变量被使用前定义即可; 2.2.2 变量 一、变量的三个基本要素 变量名 内存 5 ... c 地址值 1000H 变量 类型 名字 int c; c=5; 三要素 类型 数据值 值 地址值 数据值 地址值 二、变量的定义 可以在程序中随时定义变量,只要在该变量被使用前定义即可; 定义格式:<类型> <变量名表>; 例如:int a,b,c; double x,y,z;
数据类型 标识符1(初始值1), …,标识符n(初始值n); 2.2.2 变量(续) 同一程序块内不可以定义同名变量; 初始值 变量定义时可赋初始值; 声明格式: 数据类型 标识符1(初始值1), …,标识符n(初始值n); 数据类型 标识符1=初始值1, …,标识符n=初始值n; 例如:double price=15.5; int size=100; 未被初始化的变量的值或者是默认值,或者是无效值,由变量类型决定; 变量可被赋值,由变量名标识;
2.3 数组类型 数目固定、类型相同的若干个变量的有序集合;
<类型> <数组名>[<大小1>][<大小2>]…; 2.3.1 数组的定义 1、格式 <类型> <数组名>[<大小1>][<大小2>]…; 说明: 方括号([ ])表示数组的维; 某维的大小必须是大于1的常量表达式; 2、示例 int a[3]; char b[3][5]; const int size=80; int m[size];
<数组名>[<下标表达式1>][<下标表达式2>]…; 2.3.2 数组的赋值 1、数组元素的表示 下标表示: <数组名>[<下标表达式1>][<下标表达式2>]…; 说明: <下标表达式>为常量表达式; 下标从0开始; 各个元素在内存中按其下标的升序顺序连续存放; 指针表示:
2.3.2 数组的赋值(续) 2、数组元素赋初值 利用初始值表(由一对花括号括起来的若干数据项组成)实现; 2.3.2 数组的赋值(续) 2、数组元素赋初值 利用初始值表(由一对花括号括起来的若干数据项组成)实现; 数组元素的个数要大于等于初始值表中数据项的个数; 例如:int a[5]={1,2,3,4,5}; int a[4]={5,4}; int b[2][3]={{1,2,3},{4,5,6}}; int b[2][3]={1,2,3,4,5,6}; int c[2][3][2]={{{5,4},{3}},{{2},{1,0}}}; 3、数组元素的赋值 例如:int m[3]; m[0]=5; m[1]=3; m[2]=1;
2.3.3 字符数组 说明: 字符数组是指数组元素是char类型的数组; 注意字符常量、字符数组与字符串常量的区别; 例如: 2.3.3 字符数组 说明: 字符数组是指数组元素是char类型的数组; 注意字符常量、字符数组与字符串常量的区别; 例如: char s1[4]={'a', 'b', 'c', 'd'}; 字符数组 char s2[5]={'a', 'b', 'c', 'd', '\0'}; 字符数组 (字符串常量) 等价于 char s2[5]="abcd"; char s3[5]="abcde"; ╳ char s3[]="abcde";
2.4 枚举类型 枚举类型是若干个有名字的整型常量的集合;
2.4.1 枚举声明和枚举变量 一、枚举声明 enum <枚举名>{<枚举表>}; 2.4.1 枚举声明和枚举变量 一、枚举声明 enum <枚举名>{<枚举表>}; <枚举表>由若干个枚举符组成,多个枚举符之间用逗号分隔; 枚举符是用标识符表示的整型常量,又称枚举常量; 枚举常量的值默认为最前边的一个为0,其后的值依次加1; 枚举常量的值也可显式定义,未显式定义的则在前一个值的基础上加1;
2.4.1 枚举声明和枚举变量(续) 例如:enum day {Sun,Mon,Tue,Wed,Thu,Fri,Sat}; 2.4.1 枚举声明和枚举变量(续) 例如:enum day {Sun,Mon,Tue,Wed,Thu,Fri,Sat}; enum day {Sun=7,Mon=1, Tue,Wed,Thu,Fri,Sat}; 枚举变量的值不一定互不相同; 二、枚举变量 enum <枚举名> <枚举变量名表>; 多个枚举变量之间用逗号分隔; 例如: enum day d1,d2,d3; enum day {Sun,Mon,Tue,Wed,Thu,Fri,Sat} d1,d2,d3;
2.4.2 枚举变量的值 其值是该枚举变量所属的枚举声明的枚举表的某一个枚举符; 2.4.2 枚举变量的值 其值是该枚举变量所属的枚举声明的枚举表的某一个枚举符; 利用枚举符所表示的整型值给枚举变量赋值时,需要进行类型强制; 例如: d1=Sun; d2=Sat; d3=(enum day)4;
2.5.1 指针 1、什么是指针 指针是用来存放某个变量的地址值的一种变量; 指针的类型是它所指向变量的类型; 2.5.1 指针 1、什么是指针 指针是用来存放某个变量的地址值的一种变量; 指针的类型是它所指向变量的类型; 内存 1000H 5 ... a 地址值 3000H p 指针本身数据值的类型是unsigned long int型; 例如: int a(5); int *p=&a;
<类型> *<指针名1>, *<指针名2>,…; 2.5.1 指针(续) 2、如何定义指针 <类型> *<指针名1>, *<指针名2>,…; 例如: int *pi; char (*pa)[3]; float *pl; int (*pf)(); char *pc; int *pp; 3、指针的赋值(内存地址值) 指针必须被赋值后才可使用; 一般变量、数组元素、结构成员的地址值为变量名前加运算符&; 数组的地址值用该数组名表示;
2.5.1 指针(续) 函数的地址值用该函数的名字表示; 例如: int a,b[10]; double sin(double x); 2.5.1 指针(续) 函数的地址值用该函数的名字表示; 例如: int a,b[10]; double sin(double x); int *p=&a,*p=&b[3]; double (*pf)(); int a[10],*p=a; pf=sin; 3、指针的运算 赋值运算。例如:int a,*p=&a,*q; q=p; 一个指针可以加上或减去一个整数值; 在一定条件下(指向同一数组的不同元素),两个指针可以相减; 在一定条件下(指向同一数组元素),两个指针可以比较;
2.5.2 指针和数组 1、C++中指针与数组的关系 C++通过指针访问数组中的每个元素; 2.5.2 指针和数组 1、C++中指针与数组的关系 C++通过指针访问数组中的每个元素; 数组不能参与表达式求值。在运算表达式中,一个标识数组对象的操作数在运算表达式中被自动转换为一个指向数组对象的第一个元素对象的指针值。 2、一维数组的指针表示法 C++中规定:任何一个数组的名字是一个常量指针,其值是该数组的首元素的地址值; 例如: int a[5]; 数组表示法:a[i],i=0,1,2,3,4 指针表示法:*(a+i) a与&a[0] 相同
2.5.2 指针和数组(续) 3、二维数组的指针表示法 int b[2][5]; 2.5.2 指针和数组(续) 3、二维数组的指针表示法 int b[2][5]; 数组表示法:b[i][j] i=0,1; j=0,1,2,3,4 指针表示法: *(*(b+i)+j) *(b[i]+j) (*(b+i))[j] (&b[0][0]+5*i+j) 4、三维数组的指针表示法(同二维) 5、示例
2.5.2 指针和数组(续) 例2.1:分析下列程序的输出结果。 #include <iostream.h> 2.5.2 指针和数组(续) 例2.1:分析下列程序的输出结果。 #include <iostream.h> void main() { static int a[5]={5,4,3,2,1}; int i,j; i=a[0]+a[4]; j=*(a+2)+*(a+4); cout<<i<<endl<<j<<endl; } 4 a[1] 5 a[0] 3 a[2] 2 a[3] 1 a[4] a a+2 a+4 输出 6 4
2.5.2 指针和数组(续) 例2.2:分析下列程序的输出结果。 #include <iostream.h> 2.5.2 指针和数组(续) 例2.2:分析下列程序的输出结果。 #include <iostream.h> void main() { static char s1[]="abcde",s2[5]={'m', 'n', 'p', 'q', '\0'}; char *ps=s1; cout<<s1<<" or "<<s2<<endl; cout<<s2[1]<<s2[2]<<s1[3]<<s1[4]<<endl; cout<<*ps<<*(ps+2)<<*(ps+4)<<*ps+2<<endl; } 输出 abcde or mnpq npde ace99 a的ASCII码值 为0x61,即97
2.5.2 指针和数组(续) 例2.3:分析下列程序的输出结果。 #include <iostream.h> 2.5.2 指针和数组(续) 例2.3:分析下列程序的输出结果。 #include <iostream.h> void main() { static char t[][3][5]={"abcd", "efgh", "ijkl", "mnop", "qrst", "uvwx"}; cout<<t[1][2][3]<<*(t[1][2]+3)<<*(*(*(t+1)+2)+3) <<*(*(t[1]+2)+3)<<endl; cout<<**t<<"\t"<<*(*(t+1)+1)<<"\t"<<t[0][2] <<"\t"<<**t+1<<endl; } 输出 xxxx abcd qrst ijkl bcd
<类型> &<引用名>(<变量名>); 2.5.3 引用 标识对象的一种机制; 对对象存储地址的抽象,但引用不是值; 引用有类型; 变量的别名; 1、定义格式 <类型> &<引用名>(<变量名>); 或 <类型> &<引用名>=<变量名>; 2、初始化与赋值 定义引用时必须初始化; 可以将一个引用赋给某个变量; 引用可被赋值;
2.5.3 引用(续) 示例: int a=3; int &m=a; int n=m; int *p=&m; m=m+5; 3、引用的功能 2.5.3 引用(续) 示例: int a=3; int &m=a; int n=m; int *p=&m; m=m+5; 3 a p 8 m 定义引用并初始化 3 n 将引用赋值给变量 a=8,对引用的操作就是对被引用者的操作 3、引用的功能 用做函数的参数或函数的返回值; 函数不能返回对局部对象的引用;
2.5.3 引用(续) 为什么? 示例: int& f(int index,int a[]) { int r=a[index]; 2.5.3 引用(续) 示例: int& f(int index,int a[]) { int r=a[index]; return r; } 为什么? 正确:int &r=a[index]; 错,r是局部对象 4、指针与引用的区别 指针通过地址间接访问某个变量,引用通过别名直接访问某个变量; 引用必须初始化,一旦被初始化后不得再作为其他变量的别名;
2.5.3 引用(续) 例2.4:分析下列程序的输出结果。 #include <iostream.h> void main() 2.5.3 引用(续) 例2.4:分析下列程序的输出结果。 #include <iostream.h> void main() { int val(5); int &refv=val; refv=refv+5;; cout<<val<<endl; int *p=&refv,val1(refv); cout<<*p<<"\t"<<val1<<endl; } 输出 10 10 10
2.5.3 引用(续) 例2.5:分析下列程序的输出结果。 #include <iostream.h> 输出: 2.5.3 引用(续) 例2.5:分析下列程序的输出结果。 #include <iostream.h> int& f(int index,int a[]) { int &r=a[index]; return r; } void main() int a[]={1,3,5,7,9}; f(2,a)=55; for(i=0;i<5;i++) cout<<a[i]<<"\t"; 输出: 1 3 55 7 9 a[0] 9 1 3 5 7 … a[2] 55 r r 5 传址调用(C++中数组自动转换为指针);引用可作左值;
2.6.1 算术运算符 1、普通算术运算符 单目算术运算符:-(取负)、+(取正); 双目算术运算符:+、-、*、/、%(只用于int型); 2.6.1 算术运算符 1、普通算术运算符 单目算术运算符:-(取负)、+(取正); 双目算术运算符:+、-、*、/、%(只用于int型); 单目运算符优先级高于双目运算符; 2、增1和减1运算符 单目运算符:++、--; ++运算符的功能: 由该运算符组成的表达式具有一定的值; 由该运算符组成的表达式计算后,其变量值要发生改变;
2.6.1 算术运算符(续) ++运算符的两种方式:前缀方式与后缀方式; 前缀方式与后缀方式的区别: 前缀运算表达式的值为原来变量值加1; 2.6.1 算术运算符(续) ++运算符的两种方式:前缀方式与后缀方式; 前缀方式与后缀方式的区别: 前缀运算表达式的值为原来变量值加1; 后缀运算表达式的值为原来变量值; 两种方式的变量的值都加1; 示例: int a(1); ++a; int b(1); b++; 表达式++a的值为2,变量a的值为2; 表达式b++的值为1,变量b的值为2;
2.6.2 关系运算符 双目运算符:>、<、>=、<=、==、!= 前四种优先级高于后两种;
2.6.3 逻辑运算符 单目运算符:! 双目运算符:&&、|| 优先级:&&、||、!
2.6.4 位操作运算符 1、逻辑位运算符 单目逻辑位运算符:~ 双目逻辑位运算符:&、|、^ 双目逻辑位运算符的优先级:&、|、^; 2.6.4 位操作运算符 1、逻辑位运算符 单目逻辑位运算符:~ 双目逻辑位运算符:&、|、^ 双目逻辑位运算符的优先级:&、|、^; 2、移位运算符 双目运算符:<<、>>; <<时移掉的位被丢弃,右边移出的空位补0; >>时移掉的位被丢弃,左边移出的空位补0或符号位;
2.6.5 赋值运算符 简单赋值运算符:= 复合赋值运算符:+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=
2.6.6 其他运算符 1、三目运算符 格式:d1?d2:d3 功能:先计算d1,若其非零,表达式的值为d2的值,否则为d3的值; 2.6.6 其他运算符 1、三目运算符 格式:d1?d2:d3 功能:先计算d1,若其非零,表达式的值为d2的值,否则为d3的值; 表达式类型:d2和d3中类型较高者的类型; 2、逗号运算符 格式:d1,d2,…,dn 表达式的值和类型:由最后一个表达式确定; 优先级在所有运算符中最低; 3、sizeof运算符
2.6.6 其他运算符(续) 格式: sizeof(<类型说明符>); 或 sizeof(<表达式>); 2.6.6 其他运算符(续) 格式: sizeof(<类型说明符>); 或 sizeof(<表达式>); 功能:返回其后的类型说明符或表达式所表示的数在内存中所占的字节; 4、单目运算符&和* &:取地址; *:用在指针名前,表示取指针的内容; 5、强制类型运算符 格式: <类型说明符> (<表达式>); 或 (<类型说明符>) <表达式>;
2.6.6 其他运算符(续) 强制类型转换可将高类型转换为低类型,是一种不安全的转换; 示例: double f(3.85); int h; 2.6.6 其他运算符(续) 强制类型转换可将高类型转换为低类型,是一种不安全的转换; 示例: double f(3.85); int h; h=int(f); 该转换是暂时的,一次性的; 示例: int a(3),m; double b; b=3.56+double(a); m=a+5;
2.6.7 运算符和优先级和结合性 P46,表2.3
2.7.1 表达式的种类 由运算符和操作数组成的式子; 常见的表达式 算术表达式; 逻辑表达式; 关系表达式; 赋值表达式; 条件表达式; 2.7.1 表达式的种类 由运算符和操作数组成的式子; 常见的表达式 算术表达式; 逻辑表达式; 关系表达式; 赋值表达式; 条件表达式; 逗号表达式;
2.7.1 表达式的种类(续) 注意事项: 连续的两个运算符之间用空格分隔; 可用括号来改变运算符优先级; 2.7.1 表达式的种类(续) 注意事项: 连续的两个运算符之间用空格分隔; 可用括号来改变运算符优先级; 双目运算符的左右可以用空格符和操作数分开; 过长的表达式可分成几个表达式;
2.7.2 表达式的值和类型 一、确定表达式的值 先确定运算符的功能; 确定计算顺序:先考虑优先级,再考虑结合性; 2.7.2 表达式的值和类型 一、确定表达式的值 先确定运算符的功能; 确定计算顺序:先考虑优先级,再考虑结合性; 二、表达式求值方法与确定类型的方法
2.7.2 表达式的值和类型(续) 例2.6:分析下列程序的输出结果(算术表达式)。 #include <iostream.h> 2.7.2 表达式的值和类型(续) 例2.6:分析下列程序的输出结果(算术表达式)。 #include <iostream.h> void main() { unsigned a(0xab),b(20); a&=b; a^=a; cout<<a<<"\t"<<b<<endl; int x(-3),y(5); x>>y; x<<=y; x|=y^~y; y&=~x+1; cout<<x<<"\t"<<y<<endl; } 输出: 0 20 -1 1
2.7.2 表达式的值和类型(续) 例2.7:分析下列程序的输出结果(关系表达式)。 #include <iostream.h> 2.7.2 表达式的值和类型(续) 例2.7:分析下列程序的输出结果(关系表达式)。 #include <iostream.h> void main() { char x('m'),y('n'); int n; n=x<y; cout<<n<<endl; n=x==y-1; n=('y'!= 'Y')+(5>3)+(y-x==1); } 输出: 1 1 3
2.7.2 表达式的值和类型(续) 例2.8:分析下列程序的输出结果(逻辑表达式)。 #include <iostream.h> 2.7.2 表达式的值和类型(续) 例2.8:分析下列程序的输出结果(逻辑表达式)。 #include <iostream.h> void main() { int x,y,z; x=y=z=1; --x&&++y&++z; cout<<x<<"\t"<<y<<"\t"<<z<<endl; ++x&&++y&&++z; ++x&&y--||++z; } 输出: 0 1 1 1 2 2 2 1 2 注意逻辑 表达式的 求值方法
2.7.2 表达式的值和类型(续) 例2.9:分析下列程序的输出结果(条件表达式)。 #include <iostream.h> 2.7.2 表达式的值和类型(续) 例2.9:分析下列程序的输出结果(条件表达式)。 #include <iostream.h> void main() { int a(3),b(4),c; c=a>b?++a:++b; cout<<a<<","<<b<<","<<c<<endl; c=a-b?a+b:a-3?b:a; } 注意三目 运算符的 判断条件 输出 3,5,5 3,5,8
2.7.2 表达式的值和类型(续) 例2.10:分析下列程序的输出结果(赋值表达式)。 2.7.2 表达式的值和类型(续) 例2.10:分析下列程序的输出结果(赋值表达式)。 #include <iostream.h> void main() { int x(1),y(3),z(5); x+=y*=z-=2; cout<<x<<", "<<y<<", "<<z<<endl; x*=y/=z-=x; x=y=z=2; z=(x+=2)+(y+=4)+2; cout<<x<<", "<<y<<“, "<<z<<endl; } 输出: 10,9,3 -10,-1,-7 4,6,12
2.7.2 表达式的值和类型(续) 例2.11:分析下列程序的输出结果(逗号表达式)。 2.7.2 表达式的值和类型(续) 例2.11:分析下列程序的输出结果(逗号表达式)。 #include <iostream.h> void main() { int a,b,c; a=1,b=2,c=a+b+3; cout<<a<<", "<<b<<", "<<c<<endl; c=(a++,a+=b,a+b); } 输出 1,2,6 4,2,6
int unsigned long unsigned long double 2.7.3 表达式中的类型转换 一、隐含转换 双目运算中操作数的类型转换; 是一种保值转换,转换类型由低到高; 转换规则: int unsigned long unsigned long double short,char float 二、强制转换 显式强制转换(通过强制转换运算符实现);
2.7.3 表达式中的类型转换(续) 隐式强制转换: 在赋值表达式中,当左值(赋值运算符左边的值)和右值(赋值运算符右边的值) 的类型不同时,一律将右值类型强制转换为左值的类型; 在函数有返回值的调用中,将return后面的表达式的类型强制转换为该函数的类型; 显式 隐式 赋值表达式 函数返回值 隐含转换 强制转换 类型转换
typedef <已有类型名> <新类型名表>; 2.8 类型定义 1、格式 typedef <已有类型名> <新类型名表>; 例如: typedef double wages,bouns; wages weekly; bouns monthly; 2、自定义类型的作用 改善程序的可读性,增加所定义变量的信息 书写简练 提高程序的可移植性
第3章 预处理和语句 3.1 预处理功能 3.2 语句 3.3 选择语句 3.4 循环语句 3.5 转向语句
3.1 预处理功能 一、预处理命令及预处理功能 1、预处理命令 C++源程序中包含的各种编译命令在程序被正常编译之前执行; 3.1 预处理功能 一、预处理命令及预处理功能 1、预处理命令 C++源程序中包含的各种编译命令在程序被正常编译之前执行; 预处理命令不是C++语言的一部分; 2、预处理功能 由预处理命令实现的功能; 二、常用的预处理命令 文件包含命令 宏定义命令
3.1 预处理功能(续) 条件编译命令 停止编译命令 三、预处理命令使用说明 以" # "为引导; 3.1 预处理功能(续) 条件编译命令 停止编译命令 三、预处理命令使用说明 以" # "为引导; 每条预处理命令单独占用一行,同一行不能有其他预处理命令和语句; 预处理命令可以续行,续行符为" \ "; 预处理命令不是语句,不能以分号(;)结束; 预处理命令的位置可以放在开头、中间和结尾;
3.1.1 文件包含命令 一、功能 指示C++编译器将一个文件(头文件)的内容 嵌入到该指令所在的文件中该指令所在位置处; 3.1.1 文件包含命令 一、功能 指示C++编译器将一个文件(头文件)的内容 嵌入到该指令所在的文件中该指令所在位置处; 头文件指存放与标准函数有关的信息,或者存放符号常量、类型定义、类和其他复杂类型的定义以及与程序环境相关信息的.h文件; 二、格式 #include <文件名> 由系统提供并放在指定子目录中的头文件; #include "文件名" 由用户定义,放在当前目录或其他目录下的头文件或其他源文件;
3.1.1 文件包含命令(续) 三、说明 文件包含命令一般放在程序头; 一条文件包含命令只能包含一个文件; 文件包含命令可以嵌套; 3.1.1 文件包含命令(续) 三、说明 文件包含命令一般放在程序头; 一条文件包含命令只能包含一个文件; 文件包含命令可以嵌套; 文件名:myfile.h #include "myfile2.h" #include "myfile3.h" 包含文件不易过多;
#define <宏名> <字符串> 3.1.2 宏定义命令 一、功能 用来将一个标识符定义为一个字符串,该标识符称为宏名,被定义的字符串称为替换文本; 二、宏定义命令格式 1、两种定义格式 简单宏定义 带参数的宏定义 2、简单宏定义 #define <宏名> <字符串> 定义符号常量,例如:#define PI 3.1415
#define与const定义符号常量的区别 3.1.2 宏定义命令(续) #define与const定义符号常量的区别 const double PI=3.1415; #define PI 3.1415 const产生一个具有类型的符号 #define仅产生文本替换,而不管内容是否正确 使用const可以定义一个局部常量,可局部在一个函数体内 用#define定义的常量的作用域是从定义时开始,直到使用#undef取消定义时为止,如果不取消定义,直到整个文件结束 使用const定义常量是一个说明语句,用分号(;)结束 使用#define定义常量是一个预处理命令,不能用分号(;)结束
3.1.2 宏定义命令(续) 说明 书写#define命令时,<宏名>与<字符串>之间用空格分隔,不能使用等号连接; 3.1.2 宏定义命令(续) 说明 书写#define命令时,<宏名>与<字符串>之间用空格分隔,不能使用等号连接; 使用#define定义的标识符不是变量,它只用作宏替换,不占用内存; #define是一条预处理命令,不用分号结束,它所定义的标识符等价于其后的字符串; 标识符被宏定义后,在取消这次宏定义之前,不允许重新对它进行宏定义; 宏定义可以嵌套,已定义的标识符可以用来定义新的字符串;
#define <宏名>(<参数表>) <宏体> 3.1.2 宏定义命令(续) 3、带参数的宏定义 #define <宏名>(<参数表>) <宏体> 说明 <宏体>应写在一行上,如果需要写在多行时,需使用续行符(\)结束,并在其后按下回车键; <宏名>与左括号之间不能出现空格,否则空格右边都将作为宏体; 定义带参数的宏体时,宏体中与参数名相同的字符序列适当地加上括号,可以避免宏替换后出现的优先级问题; C++中,带参数的宏定义常由内联函数取代;
3.1.2 宏定义命令(续) 例3.1:分析下列程序的输出结果。 #include <iostream.h> 3.1.2 宏定义命令(续) 例3.1:分析下列程序的输出结果。 #include <iostream.h> void main() { int b(5); #define b 2 #define f(x) b*(x) int y(3); cout<<f(y+1)<<endl; #undef b #define b(3) } 输出: 8 20 12 简单宏定义 带参数的宏定义 b=2 b=5 b=3
3.1.2 宏定义命令(续) 4、替换正文中的操作符##与# ##:将它两边的操作数连接成一个符号; 3.1.2 宏定义命令(续) 4、替换正文中的操作符##与# ##:将它两边的操作数连接成一个符号; #:将它右边的操作数变成一个字符串文字; 例如: #define CON(a,b) a##b double i=CON(5,E-10) double i=5E-10; #define STR(a) #a cout<<STR(Programming is fun!)<<endl; cout<< " Programming is fun! " <<endl; 5、取消宏定义命令 #undef <标识符>
3.1.3 条件编译命令 一、功能 用来定义某些编译内容要在满足一定条件下才参与编译,否则不参与编译;可使同一源程序在不同的编译条件下产生不同的目标代码。 二、格式 格式一: #ifdef <标识符> <程序段1> #else <程序段2> #end if 或 #ifdef <标识符>
3.1.3 条件编译命令(续) 格式二 格式三 #if <常量表达式1> #ifndef <标识符> 3.1.3 条件编译命令(续) 格式二 #ifndef <标识符> <程序段1> #else <程序段2> #end if 或 格式三 #if <常量表达式1> <程序段1> #elif <常量表达式2> <程序段2> #elif <常量表达式3> <程序段3> ... #else <程序段n+1> #end if
3.1.3 条件编译命令(续) 例3.2:分析下列程序的输出结果。 #include <iostream.h> 3.1.3 条件编译命令(续) 例3.2:分析下列程序的输出结果。 #include <iostream.h> #define A 10 void main() { #if A>0 cout<<"a>0"<<endl; #elif A<0 cout<<"a<0"<<endl; #else cout<<"a==0"<<endl; #end if } 输出: a>0
3.1.3 条件编译命令(续) 例3.3:避免重复引用某个头文件。 //main.cpp #include "myfile1.h" 3.1.3 条件编译命令(续) 例3.3:避免重复引用某个头文件。 //main.cpp #include "myfile1.h" #include "myfile2.h" //myfile1.h #include "myhead.h" //myfile2.h 改进: #ifndef MYHEAD_H #define MYHEAD_H #include "myhead.h" #endif //myfile2.h
3.1.3 条件编译命令(续) 例3.4:用于调试。 调试时: #define DEBUG 1 ... #if DEBUG=1 3.1.3 条件编译命令(续) 例3.4:用于调试。 调试时: #define DEBUG 1 ... #if DEBUG=1 cout<<"OK"<<endl; #end if 调试后: #define DEBUG 0 ...
3.1.4 停止编译命令 一、格式 #error <字符序列> 二、功能 3.1.4 停止编译命令 一、格式 #error <字符序列> 二、功能 当编译器遇到该指令时,显示“字符序列”,即错误信息,然后停止对该程序的编译,从而可以在编译阶段发现程序中的错误; 三、示例 假设country值为:US、ENGLAND、CHINA 程序中有如下语句:
3.1.4 停止编译命令(续) //… #else #error You define country incorrectly #endif 3.1.4 停止编译命令(续) //… #else #error You define country incorrectly #endif 当country的值不是上述三者之一时,编译器将显示: You define country incorrectly 然后停止编译。
3.2 语句 1、表达式语句和空语句 表达式语句:任何一个表达式加上分号(;); 空语句:只有一个分号(;)的语句; 2、复合语句和分程序 3.2 语句 1、表达式语句和空语句 表达式语句:任何一个表达式加上分号(;); 空语句:只有一个分号(;)的语句; 2、复合语句和分程序 复合语句:由两条或两条以上的程序构成,并由一对花括号括起; 分程序:又称块结构,含有一条或多条说明语句的复合语句;
3.3.1 条件语句 if (<条件1>) <语句1> 3.3.1 条件语句 if (<条件1>) <语句1> else if (<条件2>) <语句2> else if (<条件3>) <语句3> ... else if (<条件n>) <语句n> else <语句n+1> 说明: if语句可以嵌套,在此情况下,else与最近的一个没有与else配对的if配对。
3.3.2 开关语句 switch (<整型表达式>) { case <整常型表达式1>:<语句序列1> 3.3.2 开关语句 switch (<整型表达式>) { case <整常型表达式1>:<语句序列1> case <整常型表达式2>:<语句序列2> ... case <整常型表达式n>:<语句序列n> default <语句序列n+1> } 注意: 在执行语句序列中如果遇到break语句,则退出switch语句,执行后面的语句;如果其后的语句序列中没有break语句,则一直执行至switch语句结束。
3.3.2 开关语句(续) 例3.5:分析下列程序的输出结果。 #include <iostream.h> 3.3.2 开关语句(续) 例3.5:分析下列程序的输出结果。 #include <iostream.h> void main() { int i(1),j(0),m(1),n(2); switch(i++) case 1:m++; n++; case 2:switch(++j) case 2:n++; } case 3:m++; break; case 4:m++;n++; cout<<m<<', '<<n<<endl; 输出: 4,5
3.4 循环语句 1、while循环语句 while (<条件>) <语句>; 2、do-while循环语句 3.4 循环语句 1、while循环语句 while (<条件>) <语句>; 2、do-while循环语句 do <语句> while (<条件>); do-while循环与while循环的区别: do-while循环至少执行一次循环体,while循环可能一次也不执行循环体; 3、for循环语句 for (d1;d2;d3) <语句>; 4、多重循环
3.5 转向语句 1、goto语句 格式: goto <语句编号>; goto语句只能在一个函数内进行转向; 2、break语句 3.5 转向语句 1、goto语句 格式: goto <语句编号>; goto语句只能在一个函数内进行转向; 2、break语句 格式: break; 适用情况: 用于开关语句的语句序列中,其功能是退出开关语句,执行其后的语句; 用于循环体中,其功能是用来退出该重循环; 3、continue语句 格式: continue; 功能:在循环体中用来结束本次循环;
第4章 函数和作用域 4.1 函数的基本概念 4.2 函数的定义和说明 4.3 函数的调用 4.4 函数的参数 4.5 内联函数 第4章 函数和作用域 4.1 函数的基本概念 4.2 函数的定义和说明 4.3 函数的调用 4.4 函数的参数 4.5 内联函数 4.6 函数重载 4.7 异常处理基础 4.8 作用域
4.1 函数的基本概念 函数结构:由花括号括起来的一个语句序列; 函数抽象:使用标识符对语句序列进行的抽象; 4.1 函数的基本概念 函数结构:由花括号括起来的一个语句序列; 函数抽象:使用标识符对语句序列进行的抽象; 函数调用:函数级上的控制抽象,一种控制转移; 参数化:在函数抽象中对其所操作的值进行抽象的过程; 形参与实参 返回类型为 void类型的 函数抽象 为过程抽象 函数抽象与过程抽象 函数抽象的目的是进行求值 过程抽象的目的是更新对象 C++中只有函数抽象
4.2.1 函数的定义格式 <类型> <函数名>(<参数表>) { <若干条语句> } 4.2.1 函数的定义格式 <类型> <函数名>(<参数表>) { <若干条语句> } 函数体 说明: <类型>为函数返回值类型,若为void,则为过程调用; <参数表>中的参数为形参,在函数被调用时进行初始化,从而从被调用处获得数据;
<类型> <函数名>(<参数表>); 4.2.2 函数的说明方法(函数的声明) 一、函数的说明原则 如果一个函数定义在先,调用在后,调用前可以不必说明; 如果一个函数定义在后,调用在先,调用前必须说明; 二、函数的说明方法(原型说明) <类型> <函数名>(<参数表>); 三、示例 参数表中的参数 名称可以省略
4.2.2 函数的说明方法(续) 例4.1:分析下列程序的输出结果。 #include <iostream.h> 4.2.2 函数的说明方法(续) 例4.1:分析下列程序的输出结果。 #include <iostream.h> void fun1(),fun2(),fun3(); void main() { cout<<"It is in main. "<<endl; fun2(); cout<<"It is back in main. "<<endl; } void fun1() cout<<"It is in fun1. "<<endl; fun3(); cout<<"It is back in fun1. "<<endl; 函数原型声明
4.2.2 函数的说明方法(续) void fun2() { 4.2.2 函数的说明方法(续) void fun2() { cout<<"It is in fun2. "<<endl; fun1(); cout<<"It is back in fun2. "<<endl; } void fun3() cout<<"It is in fun3. "<<endl; It is in main. It is back in fun1. It is in fun2. It is back in fun2. It is in fun1. It is back in main. It is in fun3. 输 出
<函数名>(<实参表>); 4.3.1 函数的值和类型 一、函数调用格式 <函数名>(<实参表>); 说明: 实参表的个数由形参决定,用来初始化实参,多个实参用逗号分隔; 实参的个数与类型必须与形参的个数与类型完全一致; 实参对形参的初始化按其位置进行;
4.3.1 函数的值和类型(续) 二、返回语句格式 格式一: return <表达式>; 格式二: return; 4.3.1 函数的值和类型(续) 二、返回语句格式 格式一: return <表达式>; 格式二: return; 关于return语句的说明: 有返回值的return语句可以返回一个表达式的值,从而实现函数之间的信息传递; 无返回值的函数必须用void说明其返回类型;
4.3.2 函数的传值调用 一、传值调用的分类 传值调用:传递变量本身的值(数据值); 传址调用:传递变量的地址值; 4.3.2 函数的传值调用 一、传值调用的分类 传值调用:传递变量本身的值(数据值); 传址调用:传递变量的地址值; 二、传值调用的实现机制和特点 用法:调用函数的实参用常量、变量(数据)值或表达式值,被调用函数的形参用变量; 实现机制:系统将实参拷贝一个副本给形参(数据值) ; 特点:形参值的改变不影响实参值;
4.3.2 函数的传值调用(续) 例4.2:分析下列程序的输出结果(传值调用)。 #include <iostream.h> 4.3.2 函数的传值调用(续) 例4.2:分析下列程序的输出结果(传值调用)。 #include <iostream.h> void swap1(int x,int y) { int temp; temp=x; x=y; y=temp; cout<<"x="<<x<<", "<<"y="<<y<<endl; } void main() int a(5),b(9); swap1(a,b); cout<<"a="<<a<<", "<<"b="<<b<<endl; 输出: x=9,y=5 a=5,b=9
4.3.3 函数的传址调用 传址调用的实现机制和特点 用法:调用函数的实参用地址值,被调用函数的形参用指针; 4.3.3 函数的传址调用 传址调用的实现机制和特点 用法:调用函数的实参用地址值,被调用函数的形参用指针; 实现机制:让形参的指针直接指向实参; 特点:可以通过改变形参所指向的变量值来影响实参值;
4.3.3 函数的传址调用(续) 例4.3:分析下列程序的输出结果(传址调用)。 #include <iostream.h> 4.3.3 函数的传址调用(续) 例4.3:分析下列程序的输出结果(传址调用)。 #include <iostream.h> void swap2(int *x,int *y) { int temp; temp=*x; *x=*y; *y=temp; cout<<"x="<<*x<<", "<<"y="<<*y<<endl; } void main() int a(5),b(9); swap2(&a,&b); cout<<"a="<<a<<", "<<"b="<<b<<endl; 输出: x=9,y=5 a=9,b=5
4.3.4 函数的引用调用(C++特有) 引用调用的实现机制和特点 用法:调用函数的实参用变量名,被调用函数的形参用引用; 实现机制:直接通过引用来改变实参的数据值; 特点:起到传址调用的作用,但比传址调用更方便、更直接;
4.3.4 函数的引用调用(续) 例4.4:分析下列程序的输出结果(引用调用)。 #include <iostream.h> 4.3.4 函数的引用调用(续) 例4.4:分析下列程序的输出结果(引用调用)。 #include <iostream.h> void swap3(int &x,int &y) { int temp; temp=x; x=y; y=temp; cout<<"x="<<x<<", "<<"y="<<y<<endl; } void main() int a(5),b(9); swap3(a,b); cout<<"a="<<a<<", "<<"b="<<b<<endl; 输出: x=9,y=5 a=9,b=5
4.3.4 函数的引用调用(续) 三种调用方式比较
4.4.1 函数参数的求值顺序 当一个函数带有多个参数时,C++语言没有规定函数调用时实参的求值顺序; 4.4.1 函数参数的求值顺序 当一个函数带有多个参数时,C++语言没有规定函数调用时实参的求值顺序; 编译器根据对代码进行优化的需要自行规定对实参的求值顺序; 在实参中注意不要使用带有副作用的运算符,此时可能产生二义性; 例4.5:由于使用对参数求值顺序不同的编译器造成的二义性。
4.4.1 函数参数的求值顺序(续) #include <iostream.h> int add(int x,int y) { 4.4.1 函数参数的求值顺序(续) #include <iostream.h> int add(int x,int y) { return x+y; } void main() int x(4),y(6); int z=add(++x,x+y); cout<<z<<endl; 产生二义性 可能的结果(按照编译器对实参求值顺序不同): 自左至右,两个实参的值分别为5和11; 自右至左,两个实参的值分别为5和10;
4.4.2 设置函数参数的默认值 C++中,在函数声明或定义时可以为一个或多个参数指定缺省参数值; 4.4.2 设置函数参数的默认值 C++中,在函数声明或定义时可以为一个或多个参数指定缺省参数值; int add(int x,int y=10); 进行函数调用时,若未指定足够的实参,则编译器将按从左向右的顺序用函数声明或定义中的缺省值来补足所缺少的实参; add(15); add(15,10); 注意: 函数参数求值顺序与参数默认值补足顺序不同 参数求值顺序:由编译器决定求值方向; 参数默认值补足顺序:自左向右
void f(int x,int y=1,int z); 4.4.2 设置函数参数的默认值(续) 在一个指定了缺省值的参数的右边,不能出现没有指定缺省值的参数; 错误 void f(int x,int y=1,int z); 例如:f(2,4); 理想:f(2,1,4),实际:z参数未被赋值 在给某个参数指定缺省值时,不仅可以是一个数值,而且可以是任意复杂的表达式; int f(); …… void delay(int k,int time=f());
4.4.2 设置函数参数的默认值(续) 例4.6:分析下列程序的输出结果。 #include <iostream.h> 4.4.2 设置函数参数的默认值(续) 例4.6:分析下列程序的输出结果。 #include <iostream.h> void fun(int a=1,int b=3,int c=5) { cout<<"a="<<a<<", "<<"b="<<b<<", " <<"c="<<c<<endl; } void main() fun(); fun(7); fun(7,9); fun(7,9,11); cout<<"OK! "<<endl; 输出: a=1,b=3,c=5 a=7,b=3,c=5 a=7,b=9,c=5 a=7,b=9,c=11 OK!
4.5.1 内联函数引入的原因 目的 解决程序中一些函数体代码不是很大,但又频繁地被调用的函数的函数调用的效率问题。 解决方法 4.5.1 内联函数引入的原因 目的 解决程序中一些函数体代码不是很大,但又频繁地被调用的函数的函数调用的效率问题。 解决方法 以目标代码的增加为代价来换取时间上的节省;即在编译时将函数体中的代码替换到程序中,增加目标程序代码量,进而增加空间开销,从而在时间开销上不像函数调用时那么大。 普通函数 内联函数
4.5.2 内联函数的定义方法 在函数定义的前面加上关键字inline inline int add(int x,int y,int z) 4.5.2 内联函数的定义方法 在函数定义的前面加上关键字inline inline int add(int x,int y,int z) { return x+y+z; }
4.5.3 使用内联函数的注意事项 内联函数的定义必须出现在该内联函数第一次被调用之前; 类结构中所有在类说明内部定义的函数都是内联函数; 4.5.3 使用内联函数的注意事项 内联函数的定义必须出现在该内联函数第一次被调用之前; 类结构中所有在类说明内部定义的函数都是内联函数; 在内联函数内不允许用循环语句或开关语句; 对内联函数不能进行异常接口说明; 内联函数无法递归调用; 内联函数具有与带参数的宏定义#define相同的作用和相似的机理,但内联函数具有宏定义的所有优点而没有其缺点,它消除了宏定义的不安全性。
4.5.3 使用内联函数的注意事项 include <iostream.h> #define f(x) x*x 4.5.3 使用内联函数的注意事项 include <iostream.h> #define f(x) x*x void main() { int x(2); cout<<f(x)<<endl; cout<<f(x+1)<<endl; } 程序运行结果: 4 5 原因: f(x) 替换为2*2 f(x+1) 替换为2+1*2+1 inline int f(int x) return x*x; 9 f(x) 替换为 2*2 f(x+1) 替换为 3*3
4.6 函数重载 函数重载是指同一个函数名可以对应着多个函数的实现; 要求:编译器能够唯一确定调用一个函数时应执行哪个函数代码; 条件: 4.6 函数重载 函数重载是指同一个函数名可以对应着多个函数的实现; 要求:编译器能够唯一确定调用一个函数时应执行哪个函数代码; 条件: 参数个数不同; 参数类型不同; 注意: 返回值类型不能作为重载条件;
4.6.1 参数类型不同的重载函数 例4.7:分析下列程序的输出结果。 #include <iostream.h> 4.6.1 参数类型不同的重载函数 例4.7:分析下列程序的输出结果。 #include <iostream.h> int add(int,int); double add(double,double); void main() { cout<<add(5,10)<<endl; cout<<add(5.0,10.5)<<endl; } int add(int x,int y) cout<<"int"<<endl; return x+y; 两个add函数的参数类型不同
4.6.1 参数类型不同的重载函数(续) double add(double x,double y) { 4.6.1 参数类型不同的重载函数(续) double add(double x,double y) { cout<<"double"<<endl; return x+y; } 输 出 int 15 double 15.5
4.6.2 参数个数不同的重载函数 例4.8:分析下列程序的输出结果。 #include <iostream.h> 4.6.2 参数个数不同的重载函数 例4.8:分析下列程序的输出结果。 #include <iostream.h> int min(int a,int b); int min(int a,int b,int c); int min(int a,int b,int c,int d); void main() { cout<<min(13,5,4,9)<<endl; cout<<min(-2,8,0)<<endl; } int min(int a,int b) return a<b?a:b; 三个min函数的参数个数不同
4.6.2 参数个数不同的重载函数(续) int min(int a,int b,int c) { int t=min(a,b); 4.6.2 参数个数不同的重载函数(续) int min(int a,int b,int c) { int t=min(a,b); return min(t,c); } int min(int a,int b,int c,int d) int t1=min(a,b); int t2=min(c,d); return min(t1,t2); 输 出 4 -2
4.6.3 带有缺省参数时的函数重载 使用缺省参数时,注意满足函数重载条件; void fun(int x,int y=0); 4.6.3 带有缺省参数时的函数重载 使用缺省参数时,注意满足函数重载条件; void fun(int x,int y=0); void fun(int x); fun(3);重载哪个函数? 此时函数fun不能进行重载,因为编译器不能唯一确定调用哪个函数(fun(3)或fun(3,0)均可)。
4.7 异常处理基础 一、异常 当一个函数在执行过程中出现了一些不平常的情况,或运行结果无法定义的情况,使操作不得不被中断时,就出现了异常。 4.7 异常处理基础 一、异常 当一个函数在执行过程中出现了一些不平常的情况,或运行结果无法定义的情况,使操作不得不被中断时,就出现了异常。 二、异常处理的三个关键步骤 引发异常、捕获异常、异常处理 表达式类型 称为异常类型 三、引发异常(throw) throw <表达式>; 当一个异常被一个函数引发后,执行流程返回到该函数的调用者中;
4.7 异常处理基础(续) double Div(double a,double b) { if (b==0.0) 4.7 异常处理基础(续) double Div(double a,double b) { if (b==0.0) throw 1.0E-38; return a/b; } 引发double类型的异常 四、捕获异常(try) try { <语句序列> } try块的语句在执行过程中可能会引发多种类型的异常,由catch块进行处理; 捕获异常后,执行流程立即转到catch块,进行异常处理;
4.7 异常处理基础(续) 五、异常处理(catch) catch (<参数声明1>) { <语句序列1> } …… 4.7 异常处理基础(续) 五、异常处理(catch) catch (<参数声明1>) { <语句序列1> } …… catch (<参数声明n>) <语句序列n> 每个catch块声明了所要捕获的一种类型; catch块中的语句表示在该异常被捕获时应执行的动作;
double except 或 double& except 4.7 异常处理基础(续) 参数声明形式: double except 或 double& except 通过标识符except,catch可以获得更多的异常信息; 一般情况下,捕获异常时只关心异常的类型;
4.7 异常处理基础(续) 例4.9:分析下列程序的输出结果。 #include <iostream.h> 4.7 异常处理基础(续) 例4.9:分析下列程序的输出结果。 #include <iostream.h> double Div(double a,double b) { if (b==0.0) throw 1.0E-38; return a/b; } void main() double x,y,result(1234); try 引发异常 try块捕获异常
4.7 异常处理基础(续) cin>>x>>y; result=Div(x,y); 4.7 异常处理基础(续) cin>>x>>y; result=Div(x,y); cout<<"Continue. "<<endl; } catch (double&) { cout<<"Divided by zero. "<<endl; cout<<result<<endl; 处理double型异常 运 行 当输入y=0时,输出结果为: Divided by zero 1234
4.7 异常处理基础(续) double Div(double x,double y) { ... throw 1E-38; … } 4.7 异常处理基础(续) double Div(double x,double y) { ... throw 1E-38; … } void main() try { ... result=Div(x,0); ... } catch(double &) { … } catch(…) { … } ... } 引发异常 捕获异常 异常处理
4.8 作用域 1、什么是作用域 一个声明在程序正文中有效的那一部分区域; 2、作用域的分类 函数原型作用域 块作用域 类作用域 文件作用域
double Area(double Radius); 4.8.1 函数原型作用域 作用域范围:开始于函数原型声明的左括号处,结束于函数原型声明的右括号处; double Area(double Radius); 函数原型声明中的变量名可以省略;
4.8.2 块作用域 作用域范围:块作用域中声明的标识符的作用域从其声明点开始,直到结束块的花括号处; 4.8.2 块作用域 作用域范围:块作用域中声明的标识符的作用域从其声明点开始,直到结束块的花括号处; 块作用域种类:分程序、if语句、switch语句以及循环语句;
4.8.3 类作用域 类X的一个成员M在下列情况下局部于X,或者说具有类作用域: 4.8.3 类作用域 类X的一个成员M在下列情况下局部于X,或者说具有类作用域: M出现在X的成员函数内,该成员函数中没有声明同名的局部作用域的标识符; 在x.M这样的表达式中,其中x为X类型的对象; 在ptr->M这样的表达式中,其中ptr为指向X类型的一个对象的指针; 在X::M这样的表达式中;
4.8.4 文件作用域 作用域范围:文件作用域中声明的标识符的作用域开始于声明点,结束于文件尾; 4.8.4 文件作用域 作用域范围:文件作用域中声明的标识符的作用域开始于声明点,结束于文件尾; 头文件中声明的标识符的作用域可扩展到包含它的文件作用域中; 面向对象程序设计中,文件作用域中只进行类声明,其他声明在类模块内部进行;
4.8.5 重新定义标识符的作用域规定 在某个作用范围内定义的标识符在该范围内的子范围内可以重新定义该标识符。这时,原定义的标识符在子范围内是不可见的,但是它还是存在的,只是在子范围内由于出现了同名的标识符而暂时被地被隐藏起来。过了子范围后,它又是可见的。
4.8.5 重新定义标识符的作用域规定(续) 例4.10:分析下列程序的输出结果。 #include <iostream.h> 4.8.5 重新定义标识符的作用域规定(续) 例4.10:分析下列程序的输出结果。 #include <iostream.h> void main() { int a(5),b(7),c(10); cout<<a<<", "<<b<<", "<<c<<endl; int b(8); float c(8.8); a=b; int c; a=5,b=7,c=10 重新定义b和c;a=5,b=8,c=8.8 a=8,b=8,c=8.8 重新定义c;
4.8.5 重新定义标识符的作用域规定(续) c=b; 4.8.5 重新定义标识符的作用域规定(续) a=8,b=8,c=8 c=b; cout<<a<<", "<<b<<", "<<c<<endl; } c的重定义取消a=8,b=8,c=8.8 b和c的重定义取消, a=8,b=7,c=10 5,7,10 5,8,8.8 8,8,8 8,8,8.8 8,7,10 输 出
第5章 类和对象(一) 5.1 类的定义 5.2 对象的定义 5.3 对象的初始化 5.4 成员函数的特性 5.5 静态成员 5.6 友元 第5章 类和对象(一) 5.1 类的定义 5.2 对象的定义 5.3 对象的初始化 5.4 成员函数的特性 5.5 静态成员 5.6 友元 5.7 对象的生存期
5.1 类的定义 类是面向对象程序设计的核心; 类是C++实现抽象数据类型的工具; 类是通过抽象数据类型的方法来实现的一种数据类型; 5.1 类的定义 类是面向对象程序设计的核心; 类是C++实现抽象数据类型的工具; 类是通过抽象数据类型的方法来实现的一种数据类型; 类是对某一类对象的抽象,对象是某一种类的实例;
5.1.1 什么是类 类是一种复杂数据类型,它是将不同类型的数据和与这些数据相关的操作封装在一起的集合体; 5.1.1 什么是类 类是一种复杂数据类型,它是将不同类型的数据和与这些数据相关的操作封装在一起的集合体; 类具有更高的抽象性,类中的数据具有隐藏性;
5.1.2 类的定义格式 1、类定义格式的构成 说明部分:说明该类中的成员,包含数据成员的说明和成员函数的说明; 5.1.2 类的定义格式 1、类定义格式的构成 说明部分:说明该类中的成员,包含数据成员的说明和成员函数的说明; 实现部分:对成员函数的定义; 2、类的一般定义格式 class <类名> { public: <成员函数或数据成员的说明> private: <数据成员或成员函数的说明> }; <各个成员函数的实现> 说明部分 访问权限 类定义 关键字 语句 结束符 实现部分
5.1.2 类的定义格式(续) 3、类定义的说明 访问权限修饰符:公有的(public)、私有的(private)和保护的(protected); 访问权限修饰符出现的先后次序无关,并且允许多次出现; 缺省访问权限为私有的; 公有部分:一些操作(即成员函数),是提供给用户的接口功能; 私有部分:一些数据成员,通常用来描述该类中的对象的属性;
5.1.2 类的定义格式(续) 4、示例(tdate.h) class TDate { public: 5.1.2 类的定义格式(续) 4、示例(tdate.h) class TDate { public: void SetDate(int y,int m,int d); int IsLeapYear(); void Print(); private: int year,month,day; }; void TDate::SetDate(int y,int m,int d) year=y; month=m; 成员函数定义 作用域运算符
5.1.2 类的定义格式(续) day=d; } int TDate::IsLeapYear() { 5.1.2 类的定义格式(续) day=d; } int TDate::IsLeapYear() { return (year%4==0 && year%100!=0)|| (year%400==0) void TDate::Print() cout<<year<<"."<<month<<"."<<day<<endl; 成员函数定义 成员函数定义 作用域运算符:: 作用: 标识某个成员属于哪个类; 格式: <类名>::<函数名>(<参数表>)
5.1.3 定义类时的注意事项 在类体中不允许对所定义的数据成员进行初始化; class TDate { public: …... 5.1.3 定义类时的注意事项 在类体中不允许对所定义的数据成员进行初始化; class TDate { public: …... private: int year(1998),month(4),day(9); }; 错误 类中的数据成员的类型可以是任意的; 包含整型、浮点型、字符型、数组、指针和引用等; 另一个类的对象,可以作该类的成员; 自身类的对象不可以作该类的成员;
5.1.3 定义类时的注意事项(续) class N; class M { public: …... private: N *n; }; 5.1.3 定义类时的注意事项(续) 自身类的指针或引用,可以作该类的成员; class N; class M { public: …... private: N *n; }; class N void f(M m); …… 提前说明类N 当另一个类的对象作为该类的成员时,如果另一个类的定义在后,需要提前说明; n是N类的对象 一般在类体内先说明用户感兴趣的公有成员,再说明私有成员; 习惯将类定义的说明部分或者整个定义部分(包含实现部分)放到一个头文件中; m是M类的对象
<类名> <对象名表>; 5.2.1 对象的定义格式 对象的定义格式。 <类名> <对象名表>; 例如: TDate date1,date2,*Pdate,date[31];
5.2.2 对象成员的表示方法 1、一般对象 数据成员: <对象名>.<成员名> 5.2.2 对象成员的表示方法 1、一般对象 .运算符:表示对象的成员 数据成员: <对象名>.<成员名> 成员函数: <对象名>.<成员名>(<参数表>) 例如: date1.year, date1.month, date1.day; date1.SetDate(1998,4,9); 2、指针对象 ->运算符:表示对象的成员 数据成员: <对象名>-><成员名> 成员函数: <对象名>-><成员名>(<参数表>) 例如: Pdate->year, Pdate->SetDate(1998,4,9);
5.2.2 对象成员的表示方法(续) ->运算符与.运算符的区别 ->表示指向对象的指针的成员; .表示一般对象的成员; 5.2.2 对象成员的表示方法(续) ->运算符与.运算符的区别 ->表示指向对象的指针的成员; .表示一般对象的成员; 两种等价表示 <对象指针名>-><成员名> (*<对象指针名>).<成员名> 3、引用对象 与一般对象相同;
5.2.2 对象成员的表示方法(续) 例5.1:分析下列程序的输出结果。 #include <iostream.h> 5.2.2 对象成员的表示方法(续) 例5.1:分析下列程序的输出结果。 #include <iostream.h> #include "tdate.h" void main() { TDate date1,date2; date1.SetDate(1996,5,4); date2.SetDate(1998,4,9); int leap=date1.IsLeapYear(); cout<<leap<<endl; date1.Print(); date2.Print(); } 输出: 1 1996.5.4 1998.4.9
5.3.1 构造函数和析构函数 1、构造函数与析构函数的功能 特殊的成员函数; 构造函数:在创建对象时,使用特定的值来将对象初始化; 5.3.1 构造函数和析构函数 1、构造函数与析构函数的功能 特殊的成员函数; 构造函数:在创建对象时,使用特定的值来将对象初始化; 析构函数:用来释放对象,在对象删除前做一些清理工作;
5.3.1 构造函数和析构函数(续) 示例(tdate1.h) class TDate1 { public: 5.3.1 构造函数和析构函数(续) 示例(tdate1.h) class TDate1 { public: TDate1(int y,int m,int d); ~TDate1(); void Print(); private: int year,month,day; }; TDate1::TDate1(int y,int m,int d) year=y; month=m; 构造函数 析构函数
5.3.1 构造函数和析构函数(续) day=d; cout<<"Constructor called. "<<endl; } TDate1::~TDate1() { cout<<"Destructor called. "<<endl; void TDate::Print() cout<<year<<"."<<month<<"."<<day<<endl;
5.3.1 构造函数和析构函数(续) 2、构造函数的特点 3、析构函数的特点 构造函数是成员函数,函数体可写在类体内,也可写在类体外; 5.3.1 构造函数和析构函数(续) 2、构造函数的特点 3、析构函数的特点 构造函数是成员函数,函数体可写在类体内,也可写在类体外; 析构函数是成员函数,函数体可写在类体内,也可写在类体外; 构造函数的名字与类名相同; 析构函数的名字在类名前加~ 字符; 构造函数不指定返回类型,它有隐含的返回值,该值由系统内部使用; 析构函数不指定返回类型;
5.3.1 构造函数和析构函数(续) 2、构造函数的特点 3、析构函数的特点 构造函数可以有一个或多个参数; 析构函数没有参数; 5.3.1 构造函数和析构函数(续) 2、构造函数的特点 3、析构函数的特点 构造函数可以有一个或多个参数; 析构函数没有参数; 构造函数可以重载; 一个类中只能定义一个析构函数; 程序中不能直接调用构造函数,在创建对象时系统自动调用构造函数; 析构函数在对象存在的函数体结束时或使用delete运算符释放new运算符创建的对象时被自动调用;
5.3.1 构造函数和析构函数(续) 例5.2:分析下列程序的输出结果。 #include <iostream.h> 5.3.1 构造函数和析构函数(续) 例5.2:分析下列程序的输出结果。 #include <iostream.h> #include "tdate1.h" void main() { TDate1 today(1998,4,9), tomorrow(1998,4,10); cout<<"Today is "; today.Print(); cout<<"Tomorrow is "; tomorrow.Print(); } 输出: Constructor called. Today is 1998.4.9 Tomorrow is 1998.4.10 Destructor called.
5.3.2 缺省构造函数和缺省析构函数 1、缺省构造函数 类定义中没有任何构造函数时,由编译器自动生成一个不带参数的缺省构造函数; 5.3.2 缺省构造函数和缺省析构函数 1、缺省构造函数 类定义中没有任何构造函数时,由编译器自动生成一个不带参数的缺省构造函数; <类名>::<缺省构造函数名>() { } 类名 缺省构造函数即参数表为空的构造函数; 2、缺省析构函数(定义时机同缺省构造函数) <类名>::~<缺省析构函数名>() { }
<类名>::<类名>(const <类名>& <引用名>) 5.3.3 拷贝初始化构造函数 1、功能 用一个已知的对象来初始化一个被创建的同类对象; 2、特点 函数名同类名,无返回类型; 只有一个参数,是对某个对象的引用; <类名>::<类名>(const <类名>& <引用名>) 每个类都必须有一个拷贝初始化构造函数; 3、缺省拷贝初始化构造函数 如果类中没有说明拷贝初始化构造函数,则编译系统自动生成一个具有上述形式的缺省拷贝初始化构造函数,作为该类的公有成员;
5.3.3 拷贝初始化构造函数(续) 例5.3:分析下列程序的输出结果。 //tpoint.h class TPoint { public: 5.3.3 拷贝初始化构造函数(续) 例5.3:分析下列程序的输出结果。 //tpoint.h class TPoint { public: TPoint(int x,int y) {X=x;Y=y;} TPoint(TPoint &p); ~TPoint() {cout<<"Destructor called."<<endl;} int Xcoord() {return X;} int Ycoord() {return Y;} private: int X,Y; }; 构造函数 拷贝初始化构造函数
5.3.3 拷贝初始化构造函数(续) TPoint::TPoint(TPoint &p) { X=p.X; Y=p.Y; 5.3.3 拷贝初始化构造函数(续) TPoint::TPoint(TPoint &p) { X=p.X; Y=p.Y; cout<<"Copy_initialization Constructor called.\n"; } #include <iostream.h> #include "tpoint1.h" TPoint f(TPoint Q); void main() { TPoint M(20,35),P(0,0); TPoint N(M); M为已知对象,N是正在创建的对象
5.3.3 拷贝初始化构造函数(续) P=f(N); cout<<"P="<<P.Xcoord()<<", " <<P.Ycoord()<<endl; } TPoint f(TPoint Q) { cout<<"OK! "<<endl; int x,y; x=Q.Xcoord()+10; y=Q.Ycoord()+20; TPoint R(x,y); return R; 传值调用 将R的值作为返回值
5.3.3 拷贝初始化构造函数(续) 输出: Copy_initialization Constructor called. OK! 5.3.3 拷贝初始化构造函数(续) 输出: Copy_initialization Constructor called. OK! Destructor called. P=30,55 匿名对象
5.3.3 拷贝初始化构造函数(续) 4、使用拷贝初始化构造函数初始化的三种情况 明确表示由一个对象初始化另一个对象时; 5.3.3 拷贝初始化构造函数(续) 4、使用拷贝初始化构造函数初始化的三种情况 明确表示由一个对象初始化另一个对象时; 例如:TPoint N(M); 当对象作为函数实参传递给函数形参时(传值调用); 例如:P=f(N); 当对象作为函数返回值时(数据值); 例如:return R;
5.3.4 赋值 1、功能 用于更新类类型的对象; 2、特点 该函数的函数名是一个操作符,必须与关键字operator合用; 5.3.4 赋值 1、功能 用于更新类类型的对象; 2、特点 该函数的函数名是一个操作符,必须与关键字operator合用; 该函数只有一个参数,是对该类某个对象的常引用; 一般赋值操作返回对被赋值对象的引用;
5.3.4 赋值(续) <类名>& <类名>::operator = 5.3.4 赋值(续) <类名>& <类名>::operator = (const <类名>& <引用名>) 每个类都有一个赋值操作; 类中未声明赋值操作时,编译器自动生成一个公有的赋值操作; 赋值操作的等价表示形式: B=A; 等价于 B.operator =(A);
5.3.4 赋值(续) 例5.4:分析下列程序的输出结果。 #include <iostream.h> 5.3.4 赋值(续) 例5.4:分析下列程序的输出结果。 #include <iostream.h> class Location { public: Location(int xx=0,int yy=0) {X=xx;Y=yy;} Location(Location &p) {X=p.X;Y=p.y;} Location& operator =(Location& p); int GetX() {return X;} int GetY() {return Y;} private: int X,Y; }; 赋值函数
5.3.4 赋值(续) Location& Location::operator =(Location& p) { X=p.X; 5.3.4 赋值(续) Location& Location::operator =(Location& p) { X=p.X; Y=p.Y; cout<<"Assignment operator called."<<endl; return *this; } void main() Location A(1,2),B; B=A; cout<<"B="<<B.GetX()<<","<<B.GetY()<<endl; }; 输 出 Assignment operator called. B=1,2
5.3.4 赋值(续) 3、何时需要在类中定义拷贝初始化和赋值操作 5.3.4 赋值(续) 3、何时需要在类中定义拷贝初始化和赋值操作 当类中声明有指针数据成员时,必须定义拷贝初始化和赋值操作,否则编译器生成的拷贝初始化操作和赋值操作的执行将导致程序在运行时产生问题; class A A::A(int i) { { public: p=new int(i); A(int i); } ~A(); A::~A() private: { int *p; delete p; }; }
5.3.4 赋值(续) void main() 拷贝初始化:a和b对象的指针 { 指向同一个对象,执行析构 A a(5); 5.3.4 赋值(续) void main() { A a(5); A b(a); …... A c(5),d(10); d=c; }; 拷贝初始化:a和b对象的指针 指向同一个对象,执行析构 函数将删除同一个对象两次; 赋值:d对象的p指针被更新 为c对象的p指针的值,则: (1)d对象的p指针原先所指向的 值将无法访问;(2)执行析构函 数将删除同一个对象两次; c.p d.p 5 a.p b.p 5 d.p 10
5.3.4 赋值(续) //修改 A::A(A& r) { p=new int(*r.p); } 5.3.4 赋值(续) //修改 A::A(A& r) { p=new int(*r.p); } A& A::operator =(A& r) if(this==&r) return *this; p=r.p; return *this; }; c.p 5 a.p 5 d.p 5 10 b.p 5
5.4.1 内联函数和外联函数 内联函数:定义在类体内的成员函数,或定义在类体外,但使用inline关键字进行说明的成员函数; 5.4.1 内联函数和外联函数 内联函数:定义在类体内的成员函数,或定义在类体外,但使用inline关键字进行说明的成员函数; 外联函数:说明在类体内,定义在类体外的成员函数;
5.4.1 内联函数和外联函数(续) 例5.5:分析下列程序的输出结果。 #include <iostream.h> 5.4.1 内联函数和外联函数(续) 例5.5:分析下列程序的输出结果。 #include <iostream.h> class A { public: A(int x,int y) {X=x;Y=y;} int a() {return X;} int b() {return Y;} int c(); int d(); private: int X,Y; }; 内联函数
5.4.1 内联函数和外联函数(续) inline A::c() { return a()+b(); } inline int A::d() 5.4.1 内联函数和外联函数(续) 内联函数 inline A::c() { return a()+b(); } inline int A::d() return c(); void main() A m(3,5); int i=m.d(): cout<<"d() return:"<<i<<endl; 内联函数 输出: d() return:8
5.4.2 重载性 构造函数可以重载; 析构函数不能重载; 一般成员函数可以重载;
5.4.2 重载性(续) 例5.6:分析下列程序的输出结果。 #include <iostream.h> class M { 5.4.2 重载性(续) 例5.6:分析下列程序的输出结果。 #include <iostream.h> class M { public: M(int x,int y) {X=x;Y=y;} M(int x) {X=x;Y=x*x;} int Add(int x,int y); int Add(int x); int Add(); int Xout() {return X;} int Yout() {return Y;} private: int X,Y; }; 构造函数重载 一般成员函数重载
5.4.2 重载性(续) int M::Add(int x,int y) { X=x; Y=y; return X+Y; } 5.4.2 重载性(续) int M::Add(int x,int y) { X=x; Y=y; return X+Y; } int M::Add(int x) X=Y=x; int M::Add()
5.4.2 重载性(续) void main() { M a(10,20),b(4); 5.4.2 重载性(续) void main() { M a(10,20),b(4); cout<<"a="<<a.Xout()<<", "<<a.Yout()<<endl; cout<<"b="<<b.Xout()<<", "<<b.Yout()<<endl; int i=a.Add(); int j=a.Add(3,9); int k=a.Add(5); cout<<i<<endl<<j<<endl<<k<<endl; } a=10,20 b=4,16 30 12 10 输 出
5.4.3 设置参数的缺省值 一般成员函数和构造函数都可以被设置缺省参数值。
5.4.3 设置参数的缺省值(续) 例5.7:分析下列程序的输出结果。 #include <iostream.h> 5.4.3 设置参数的缺省值(续) 例5.7:分析下列程序的输出结果。 #include <iostream.h> class N { public: N(int a=3,int b=5,int c=7); int Aout() {return A;} int Bout() {return B;} int Cout() {return C;} private: int A,B,C; }; int N::N(int a,int b,int c) 构造函数设置缺省参数值
5.4.3 设置参数的缺省值(续) { A=a; B=b; 输出: C=c; X=3,5,7 } Y=9,11,7 void main() 5.4.3 设置参数的缺省值(续) { A=a; B=b; C=c; } void main() N X,Y(9,11),Z(13,15,17); cout<<"X="<<X.Aout()<<", "<<X.Bout()<<", " <<X.Cout()<<endl; cout<<"Y="<<Y.Aout()<<", "<<Y.Bout()<<", " <<Y.Cout()<<endl; cout<<"Z="<<Z.Aout()<<", "<<Z.Bout()<<", " <<Z.Cout()<<endl; 输出: X=3,5,7 Y=9,11,7 Z=13,15,17
5.5 静态成员 目的: 解决数据共享问题,即不通过全局对象,而实现多个对象之间的数据共享。
5.5.1 静态数据成员 1、静态数据成员 是类的所有对象共享的成员,而不是某个对象的成员; 5.5.1 静态数据成员 1、静态数据成员 是类的所有对象共享的成员,而不是某个对象的成员; 对多个对象来说,静态数据成员只存储在一个地方,供所有对象使用; 静态数据成员的值对每个对象都是一样的,但其值可以被任何一个对象更新; 2、使用方法与注意事项 静态数据成员在定义或说明时前面加上关键字static; private: static int s; s是私有的静态数据成员;
5.5.1 静态数据成员(续) 静态数据成员是静态存储的,它是静态生存期,必须对它进行初始化; 5.5.1 静态数据成员(续) 静态数据成员是静态存储的,它是静态生存期,必须对它进行初始化; 静态数据成员的初始化与一般数据成员初始化不同,格式如下: <数据类型> <类名>::<静态数据成员名>=<值>; 说明: 初始化在类体外进行,前面不加static,以免与一般静态变量或对象混淆; 初始化时不加该成员的访问权限控制符(静态数据成员初始化位置与访问权限无关); 初始化时使用作用域运算符表明它所属的类; <类名>::<静态成员名> 引用格式:
5.5.1 静态数据成员(续) 例5.8:分析下列程序的输出结果。 #include <iostream.h> 5.5.1 静态数据成员(续) 例5.8:分析下列程序的输出结果。 #include <iostream.h> class Myclass { public: Myclass(int a,int b,int c); void GetNumber(); void GetSum(); private: int A,B,C; static int Sum; }; int Myclass::Sum=0; 私有静态数据成员Sum 静态数据成员Sum初始化
5.5.1 静态数据成员(续) Myclass::Myclass(int a,int b,int c) { A=a; B=b; C=c; 5.5.1 静态数据成员(续) Myclass::Myclass(int a,int b,int c) { A=a; B=b; C=c; Sum+=A+B+C; } void Myclass::GetNumber() cout<<"Number="<<A<<", "<<B<<", "<<C<<endl; void Myclass::GetSum() cout<<"Sum="<<Sum<<endl;
5.5.1 静态数据成员(续) void main() { Myclass M(3,7,10),N(14,9,11); 5.5.1 静态数据成员(续) void main() { Myclass M(3,7,10),N(14,9,11); M.GetNumber(); N.GetMumber(); M.GetSum(); N.GetSum(); } Myclass类 Sum 对象a 对象e 对象i 对象z Number=3,7,10 Number=14,9,11 Sum=54 输 出
<类名>::<静态成员函数名>(<参数表>) 5.5.2 静态成员函数 1、作用 操作静态数据成员; 2、使用格式 <类名>::<静态成员函数名>(<参数表>) 3、注意事项 静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员; 静态成员函数中要引用非静态成员时,可以通过对象来引用;
5.5.2 静态成员函数(续) 例5.9:分析下列程序的输出结果。 #include <iostream.h> class M 5.5.2 静态成员函数(续) 例5.9:分析下列程序的输出结果。 #include <iostream.h> class M { public: M(int a) {A=a;B+=a;} static void f1(M m); private: int A; static int B; }; void M::f1(M m) 公有静态成员函数 私有静态数据成员B 静态成员函数体
<类名>::<静态成员函数名>(<参数表>) 5.5.2 静态成员函数(续) 通过对象引用非静态成员 cout<<"A="<<m.A<<endl; cout<<"B="<<B<<endl; } int M::B=0; void main() { M P(5),Q(10); M::f1(P); M::f1(Q); 直接引用静态成员 私有静态数据成员初始化 调用静态成员函数 <类名>::<静态成员函数名>(<参数表>) A=5 B=15 A=10 输 出
5.5.2 静态成员函数(续) 例5.10: 某商店经销一种货物,货物成箱购进,成箱卖出,购进和卖出时以重量为单位,各箱的重量不一样,因此,商店需要记录下目前库存的货物的总重量,现在要求用C++语言来模拟商店货物购进和卖出的情况。
5.5.2 静态成员函数(续) #include <iostream.h> class Goods { public: 5.5.2 静态成员函数(续) #include <iostream.h> class Goods { public: Goods(int w); ~Goods(); int Weight(); static int TotalWeight(); private: int weight; static int totalWeight; }; Goods::Goods(int w) 货物购进 货物卖出 现有库存
5.5.2 静态成员函数(续) weight=w; totalWeight+=w; } Goods::~Goods() { 5.5.2 静态成员函数(续) weight=w; totalWeight+=w; } Goods::~Goods() { totalWeight-=weight; int Goods::Weight() return weight; int Goods::TotalWeight() return totalWeight;
5.5.2 静态成员函数(续) int Goods::totalWeight=0; void main() { int w; 5.5.2 静态成员函数(续) int Goods::totalWeight=0; void main() { int w; cin>>w; Goods g1(w); Goods g2(w); cout<<Goods::TotalWeight()<<endl; }
5.6 友元 1、为什么引入友元 在对某些成员函数多次调用时,由于参数传递、类型检查和安全性检查等都需要时间开销,从而影响了程序的运行效率,引入友元后可以提高程序的运行效率; 2、使用格式 是一种定义在类外部的类或普通函数,但需要在类体内进行说明(前面加friend关键字); 不是成员函数,但可以访问类中的私有成员; 3、友元的分类 友元函数; 友元类;
5.6.1 友元函数 例5.11:分析下列程序的输出结果。 #include <iostream.h> class Time { 5.6.1 友元函数 例5.11:分析下列程序的输出结果。 #include <iostream.h> class Time { public: Time(int new_hours,int new_minutes) {hours=new_hours;minutes=new_minutes;} friend void Time12(Time time); friend void Time24(Time time); private: int hours,minutes; }; void Time12(Time time) 友元函数的说明 友元函数的定义
5.6.1 友元函数(续) if(time.hours>12) { time.hours-=12; 5.6.1 友元函数(续) if(time.hours>12) { time.hours-=12; cout<<time.hours<<":"<<time.minutes <<" PM"<<endl; } else <<" AM"<<endl; void Time24(Time time) cout<<time.hours<<":"<<time.minutes<<endl; 友元函数中使用私有数据成员 友元函数的定义
5.6.1 友元函数(续) void main() { Time time1(20,30),time2(10,45); 5.6.1 友元函数(续) void main() { Time time1(20,30),time2(10,45); Time12(time1); Time24(time1); Time12(time2); Time24(time2); } 友元函数的调用 输 出 8:30 PM 20:30 10:45 AM 10:45 问题:若友元函数Time12和Time24的形参为引用,输出结果是什么?
5.6.2 友员类 例5.12:分析下列程序的输出结果。 #include <iostream.h> class X { 5.6.2 友员类 例5.12:分析下列程序的输出结果。 #include <iostream.h> class X { friend class Y; public: void Set(int i) {x=i;} void Display() {cout<<"x="<<x<<", "<<"y="<<y<<endl;} private: int x; static int y; }; 友元类
5.6.2 友员类(续) class Y { public: Y(int i,int j); void Display(); 5.6.2 友员类(续) class Y { public: Y(int i,int j); void Display(); private: X a; }; int X::y=1; Y::Y(int i,int j) a.x=i; X::y=j; } 子对象,类X的对象a作为类Y的数据成员 类Y中访问类X的私有数据成员x和y;
如果类X中定义了参数非空的构造函数,结果如何? 5.6.2 友员类(续) void Y::Display() { cout<<"x="<<a.x<<", "; cout<<"y="<<X::y<<endl; } void main() X b; b.Set(5); b.Display(); Y c(6,9); c.Display(); 类Y中访问类X的私有数据成员x和y; 输出: x=5,y=1 x=6,y=9 x=5,y=9 如果类X中定义了参数非空的构造函数,结果如何?
5.7 对象的生存期 1、对象的生存期 指对象从被创建开始到被释放为止的时间; 2、按生存期对对象的分类 5.7 对象的生存期 1、对象的生存期 指对象从被创建开始到被释放为止的时间; 2、按生存期对对象的分类 局部对象:被定义在一个函数体或程序块内,作用域小,生存期短; 静态对象:被定义在一个文件中,它的作用域从定义时起到文件结束时止;它的作用域较大,生存期也较长; 全局对象:被定义在某个文件中,它的作用域在包含该文件的整个程序中;它的作用域最大,生存期最长;
5.7 对象的生存期(续) 例5.13:分析下列程序的输出结果。 #include <iostream.h> 5.7 对象的生存期(续) 例5.13:分析下列程序的输出结果。 #include <iostream.h> #include <string.h> class A { public: A(char *st); ~A(); private: char string[50]; }; A::A(char *st) strcpy(string,st); cout<<"Constructor called for "<<string<<endl;
5.7 对象的生存期(续) } A::~A() { cout<<"Destructor called for "<<string<<endl; void fun() A FunObject("FunObject"); static A StaticObject("StaticObject"); cout<<"In fun() "<<endl; A GlobalObject("GlobalObject"); void main() A MainObject("MainObject");
5.7 对象的生存期(续) cout<<"In main(), before called fun() "<<endl; fun(); cout<<"In main(), after called fun() "<<endl; } Constructor called for GlobalObject Constructor called for MainObject In main(), before called fun() Constructor called for FunObject Constructor called for StaticObject In fun() Destructor called for FunObject In main(), after called fun() Destructor called for MainObject Destructor called for StaticObject Destructor called for GlobalObject 输 出
第6章 类和对象(二) 6.1 对象指针和对象引用 6.2 数组 6.3 常类型 6.4 子对象和堆对象
6.1.1 对象指针和对象引用作函数参数 1、对象指针作函数参数 优点 6.1.1 对象指针和对象引用作函数参数 1、对象指针作函数参数 优点 实现传址调用。可在被调用函数中改变调用函数的参数对象的值,实现函数之间的信息传递; 使用对象指针实参仅将对象的地址值传递给形参,而不进行副本的拷贝,这样可以提高运行效率,减少时间开销;
6.1.1 对象指针和对象引用作函数参数(续) 例6.1:分析下列程序的输出结果。 #include <iostream.h> 6.1.1 对象指针和对象引用作函数参数(续) 例6.1:分析下列程序的输出结果。 #include <iostream.h> class M { public: M() {x=y=0;} M(int i,int j) {x=i;y=j;} void copy(M *m); void setxy(int i,int j) {x=i;y=j;} void print() {cout<<x<<","<<y<<endl;} private: int x,y; };
6.1.1 对象指针和对象引用作函数参数(续) void M::copy(M *m) { x=m->x; y=m->y; } 6.1.1 对象指针和对象引用作函数参数(续) void M::copy(M *m) { x=m->x; y=m->y; } void fun(M m1,M *m2); void main() M p(5,7),q; q.copy(&p); fun(p,&q); p.print(); q.print();
6.1.1 对象指针和对象引用作函数参数(续) void fun(M m1,M *m2) { m1.setxy(12,15); 6.1.1 对象指针和对象引用作函数参数(续) void fun(M m1,M *m2) { m1.setxy(12,15); m2->setxy(22,25); } 输 出 5,7 22,25
6.1.1 对象指针和对象引用作函数参数(续) 2、对象引用作函数参数 该方法除了具有对象指针作函数参数的优点外,还更简单更直接,应用更广;
6.1.1 对象指针和对象引用作函数参数(续) 例6.2:分析下列程序的输出结果。 #include <iostream.h> 6.1.1 对象指针和对象引用作函数参数(续) 例6.2:分析下列程序的输出结果。 #include <iostream.h> class M { public: M() {x=y=0;} M(int i,int j) {x=i;y=j;} void copy(M &m); void setxy(int i,int j) {x=i;y=j;} void print() {cout<<x<<", "<<y<<endl;} private: int x,y; };
6.1.1 对象指针和对象引用作函数参数(续) void M::copy(M &m) { x=m.x; y=m.y; } 6.1.1 对象指针和对象引用作函数参数(续) void M::copy(M &m) { x=m.x; y=m.y; } void fun(M m1,M &m2); void main() M p(5,7),q; q.copy(p); fun(p,q); p.print(); q.print();
6.1.1 对象指针和对象引用作函数参数(续) void fun(M m1,M &m2) { m1.setxy(12,15); 6.1.1 对象指针和对象引用作函数参数(续) void fun(M m1,M &m2) { m1.setxy(12,15); m2.setxy(22,25); } 输 出 5,7 22,25
6.1.2 this指针 该指针是隐含于每一个类的成员函数中的特殊指针; 该指针指向正在被某个成员函数操作的对象;
6.1.2 this指针(续) 例6.3:分析下列程序的输出结果。 #include <iostream.h> class A { public: A() {a=b=0;} A(int i,int j) {a=i;b=j;} void copy(A &aa); void print() {cout<<a<<", "<<b<<endl;} private: int a,b; };
6.1.2 this指针(续) void A::copy(A &aa) { if(this==&aa) return; *this=aa; } void main() A a1,a2(3,4); a1.copy(a2); a1.print(); 输出: 3,4
<类名> <数组名>[<大小>]... 6.2.1 对象数组 1、对象数组的定义 <类名> <数组名>[<大小>]... 例如: DATE dates[7]; DATE date2[3][5]; 2、对象数组赋初值与赋值 DATE dates[3]={DATE(7,22,1998), DATE(7,23,1998), DATE(7,24,1998)}; dates[0]=DATE(7,22,1998); dates[1]=DATE(7,23,1998); dates[2]=DATE(7,24,1998); 注意数组元素的 赋初值方式; 数组元素通过匿名 对象赋值,即: DATE d1(7,22,1998); dates[0]=d1; 释放d1;
6.2.1 对象数组(续) 例6.4:分析下列程序的输出结果。 #include <iostream.h> class DATE 6.2.1 对象数组(续) 例6.4:分析下列程序的输出结果。 #include <iostream.h> class DATE { public: DATE() month=day=year=0; cout<<"Default constructor called."<<endl; } DATE(int m,int d,int y) month=m; day=d; year=y; cout<<"Constructor called."<<day<<endl; 缺省构造函数 构造函数
6.2.1 对象数组(续) } ~DATE() { cout<<"Destructor called."<<day<<endl; void Print() cout<<"Month="<<month<<",Day="<<day <<",Year="<<year<<endl; private: int month,day,year; }; void main() DATE dates[5]={DATE(7,22,1998), 析构函数 数组元素赋初值
6.2.1 对象数组(续) DATE(7,23,1998), DATE(7,24,1998)}; 6.2.1 对象数组(续) DATE(7,23,1998), DATE(7,24,1998)}; dates[3]=DATE(7,25,1998); dates[4]=DATE(7,26,1998); for(int i=0;i<5;i++) dates[i].Print(); } 数组元素赋值 Constructor called.22 Constructor called.23 Constructor called.24 Default constructor called. Constructor called.25 Destructor called.25 数组元素 赋初值 输 出 缺省构造函 数赋初值 匿名对象 赋值
6.2.1 对象数组(续) Constructor called.26 Destructor called.26 6.2.1 对象数组(续) Constructor called.26 Destructor called.26 Month=7,Day=22,Year=1998 Month=7,Day=23,Year=1998 Month=7,Day=24,Year=1998 Month=7,Day=25,Year=1998 Month=7,Day=26,Year=1998 Destructor called.25 Destructor called.24 Destructor called.23 Destructor called.22 匿名对象 赋值 析构函数的执行顺序 与构造函数相反
<类型说明符> (*<指针名>)[<大小>]... 6.2.2 指向数组的指针和指针数组 1、指向数组的指针 <类型说明符> (*<指针名>)[<大小>]... 例如: int (*p)[3]; DATE (*pl)[4];
6.2.2 指向数组的指针和指针数组(续) 例6.5:分析下列程序的输出结果。 #include <iostream.h> 6.2.2 指向数组的指针和指针数组(续) 例6.5:分析下列程序的输出结果。 #include <iostream.h> class M { public: M() {a=b=0;} M(int i,int j) {a=i;b=j;} void Print() {cout<<a<<", "<<b<<endl;} private: int a,b; }; void main() M m[2][4]; int x=10,y=10; for(int i=0;i<2;i++) 对象数组
6.2.2 指向数组的指针和指针数组(续) for(int j=0;j<4;j++) m[i][j]=M(x+=2,y+=10); 6.2.2 指向数组的指针和指针数组(续) for(int j=0;j<4;j++) m[i][j]=M(x+=2,y+=10); M (*pm)[4](m); for(i=0;i<2;i++) { cout<<endl; for(j=0;j<4;j++) (*(*(pm+i)+j )).Print(); } 指向对象数组的指针 输 出 12,20 14,30 16,40 18,50 20,60 22,70 24,80 26,90
<类型名> * <数组名>)[<大小>]... 6.2.2 指向数组的指针和指针数组(续) 2、指针数组 <类型名> * <数组名>)[<大小>]... 例如: int * pa[3]; DATE * pb[4];
6.2.2 指向数组的指针和指针数组(续) 例6.6:分析下列程序的输出结果。 #include <iostream.h> 6.2.2 指向数组的指针和指针数组(续) 例6.6:分析下列程序的输出结果。 #include <iostream.h> class A { public: A(int i=0,int j=0) {a=i;b=j;} void Print() {cout<<a<<", "<<b<<endl;} private: int a,b; }; void main() A a1(7,8),a2,a3(5,7); A *b[3]={&a3,&a2,&a1}; for(int i=0;i<3;i++) b[i]->Print(); } 输出: 5,7 0,0 7,8 指针数组的定义及赋值
6.3 常类型 常类型:使用类型修饰符const说明的类型; 常类型的变量或对象的值是不能被更新的; 定义或说明常类型时必须初始化; 6.3 常类型 常类型:使用类型修饰符const说明的类型; 常类型的变量或对象的值是不能被更新的; 定义或说明常类型时必须初始化; const修饰其左边的类型;
6.3.1 一般常量和对象常量 <类型说明符> const <常量名> 6.3.1 一般常量和对象常量 1、一般常量(简单类型的常量) <类型说明符> const <常量名> 或 const <类型说明符> <常量名> int const x=2; 或 const int x=2; int const a[3]={1,2,3}; 或 const int a[3]={1,2,3}; 数组元素的值是常量,不能更新; 2、常对象 <类名> const <对象名> class A int x,y; { }; public: const A a1(3,4); A(int i,int j) {x=i;y=j;} private: 常对象A A const a1(3,4);
6.3.2 常指针和常引用 const的位置 char * const ptr1=strptr1; ptr1是一个常量指针; 6.3.2 常指针和常引用 1、常指针 const的位置 char * const ptr1=strptr1; ptr1是一个常量指针; ptr1=strptr2; *ptr1="m"; ptr1不可以更新 ptr1所指向的变量可以更新; const char * ptr2=strptr1; ptr2是一个指向字符串常量的指针; ptr2=strptr2; *ptr2="m"; ptr2可以更新 ptr2所指向的字符串不可以更新; 错误 正确 正确 错误
6.3.2 常指针和常引用(续) 常引用所引用的对象不能被更新; const <类型说明符> & <引用名> 6.3.2 常指针和常引用(续) 2、常引用 常引用所引用的对象不能被更新; const <类型说明符> & <引用名> double x=1.2; const double & v=x; 则:v=12.3错误。 3、常指针与常引用的作用 使用常参数表明该函数不会更新某个参数所指向或所引用的对象,并使该函数具有更大的适应性;
6.3.2 常指针和常引用(续) 例6.7:分析下列程序的输出结果。 #include <iostream.h> 6.3.2 常指针和常引用(续) 例6.7:分析下列程序的输出结果。 #include <iostream.h> const int N=6; void print(const int *p,int n); void main() { int array[N]; for(int i=0;i<N;i++) cin>>array[i]; print(array,N); } void print(const int *p,int n) cout<<"{"<<*p; cout<<","<<*(p+1); cout<<"}"<<endl; 整型常量 常指针作形参 形参为常指针, 实参为一般数组 输入:1 2 3 4 5 6 输出:{1,2,3,4,5,6}
6.3.2 常指针和常引用(续) 类型适应 一种类型的变量或对象能够用于另一种类型的变量或对象可以使用的环境; 例如: 6.3.2 常指针和常引用(续) 类型适应 一种类型的变量或对象能够用于另一种类型的变量或对象可以使用的环境; 例如: 非常对象使用在常对象中,就是类型适应,反之则不是;
6.3.2 常指针和常引用(续) 例6.8:分析下列程序的输出结果。 #include <iostream.h> class K 6.3.2 常指针和常引用(续) 例6.8:分析下列程序的输出结果。 #include <iostream.h> class K { public: K(int i) {k=i;} int setk() const {return k;} private: int k; }; int add(const K& g1,const K& g2); void main() K k1(8),k2(17); 常成员函数 常引用作形参
6.3.2 常指针和常引用(续) int s=add(k1,k2); cout<<s<<endl; } 6.3.2 常指针和常引用(续) int s=add(k1,k2); cout<<s<<endl; } int add(const K& g1,const K& g2) { int sum=g1.setk()+g2.setk(); return k; 形参为常引用,实参为非常对象 类型适应 输出 25
<类型说明符> <函数名>(<参数表>) const; 6.3.3 常成员函数 1、常成员函数 使用const关键字进行说明的成员函数; <类型说明符> <函数名>(<参数表>) const; 说明: const是函数类型的一个组成部分,在函数实现部分必须带有const关键字; 只有常成员函数才能操作常对象; 表 成员函数与对象之间的操作关系
6.3.3 常成员函数(续) 例6.9:分析下列程序是否正确。 #include <iostream.h> class M { 6.3.3 常成员函数(续) 例6.9:分析下列程序是否正确。 #include <iostream.h> class M { public: M(int x,int y) {X=x;Y=y;} void Move(int x,int y) {X=x;Y=y;} void Print() const {cout<<X<<", "<<Y<<endl;} private: int X,Y; }; 一般成员函数 常成员函数
6.3.3 常成员函数(续) void main() { const M m1(1,2); m1.Move(3,3); 6.3.3 常成员函数(续) void main() { const M m1(1,2); m1.Move(3,3); m1.Print(); M m2(3,4); m2.Move(3,3); m2.Print(); } 常对象m1 错误,一般成员函数 不能操作常对象 一般对象m2
6.3.3 常成员函数(续) 例6.10:分析下列程序的输出结果。 #include <iostream.h> class R 6.3.3 常成员函数(续) 例6.10:分析下列程序的输出结果。 #include <iostream.h> class R { public: R(int r1,int r2) {R1=r1;R2=r2;} void Print() {cout<<R1<<":"<<R2<<endl;} void Print() const {cout<<R1<<";"<<R2<<endl;} private: int R1,R2; }; void main() R a(5,4);
6.3.3 常成员函数(续) a.Print(); const R b(20,52); b.Print(); } 输 5:4 出 20;52 6.3.3 常成员函数(续) a.Print(); const R b(20,52); b.Print(); } 常对象调用常成员函数, 一般对象调用一般成员函数 输 出 5:4 20;52 对重载条件的补充: void Print(); void Print() const; 可重载;
6.3.4 常数据成员 const类型对象必须被初始化,并且不能被更新; 常数据成员只能通过成员初始化列表的方法进行初始化;
6.3.4 常数据成员(续) 例6.11:分析下列程序的输出结果。 #include <iostream.h> class A 6.3.4 常数据成员(续) 例6.11:分析下列程序的输出结果。 #include <iostream.h> class A { public: A(int i); void Print(); private: const int a; static const int b; const int& r; }; const int A::b=10; A::A(int i):a(i),r(a) } 私有成员,常量a 私有常静态数据成员b 私有成员,常引用r 私有常静态数据成员b初始化 成员初始化列表,常成员 (除常静态成员外)在此初始化
6.3.4 常数据成员(续) void A::Print() { 6.3.4 常数据成员(续) void A::Print() { cout<<a<<":"<<b<<":"<<r<<endl; } void main() A a1(100),a2(0); a1.Print(); a2.Print(); 输 出 100:10:100 0:10:0
6.4.1 子对象 子对象:当一个类的成员是另一个类的对象时,该对象就为子对象; 子对象即对象成员; 6.4.1 子对象 子对象:当一个类的成员是另一个类的对象时,该对象就为子对象; 子对象即对象成员; 当类中出现了子对象(对象成员)时,该类的构造函数要包含对子对象的初始化,通常采用成员初始化列表的方法来初始化子对象;
6.4.1 子对象(续) 例6.12:分析下列程序的输出结果。 #include <iostream.h> class A { 6.4.1 子对象(续) 例6.12:分析下列程序的输出结果。 #include <iostream.h> class A { public: A(int i,int j) {A1=i;A2=j;} void Print() {cout<<A1<<", "<<A2<<endl;} private: int A1,A2; }; class B B(int i,int j,int k):a(i,j),b(k) {} 成员初始化列表
6.4.1 子对象(续) void Print(); private: A a; int b; }; void B::Print() { 6.4.1 子对象(续) void Print(); private: A a; int b; }; void B::Print() { a.Print(); cout<<b<<endl; } void main() B b(6,7,8); b.Print(); 子对象a 对子对象成员函数的调用 输出: 6,7 8
6.4.1 子对象(续) 例6.13:分析下列程序的输出结果。 //part.h #if !defined(_PART_H) 6.4.1 子对象(续) 例6.13:分析下列程序的输出结果。 //part.h #if !defined(_PART_H) #define _PART_H class Part { public: Part(); Part(int i); ~Part(); void Print(); private: int val; }; #endif
6.4.1 子对象(续) //part.cpp #include <iostream.h> #include "part.h" 6.4.1 子对象(续) //part.cpp #include <iostream.h> #include "part.h" Part::Part() { val=0; cout<<"Default Constructor of Part. "<<endl; } Part::Part(int i) val=i; cout<<"Constructor of Part "<<val<<endl; Part::~Part()
6.4.1 子对象(续) cout<<"Destructor of Part "<<val<<endl; 6.4.1 子对象(续) cout<<"Destructor of Part "<<val<<endl; } void Part::Print() { cout<<val<<endl; //whole.h #if !defined(_WHOLE_H) #define _WHOLE_H #include "part.h" class Whole public: Whole();
6.4.1 子对象(续) Whole(int i,int j,int k); ~Whole(); void Print(); 6.4.1 子对象(续) Whole(int i,int j,int k); ~Whole(); void Print(); private: Part one; Part two; int date; }; #endif //whole.cpp #include <iostream.h> #include "whole.h" Whole::Whole() { 子对象one、two
6.4.1 子对象(续) date=0; cout<<"Default constructor of Whole. "<<endl; } Whole::Whole(int i,int j,int k):two(i),one(j),date(k) { cout<<"Constructor of Whole. "<<endl; Whole::~Whole() cout<<"Destructor of Whole. "<<endl; void Whole::Print() one.Print(); two.Print(); 成员初 始化列表 构造函数体
6.4.1 子对象(续) cout<<date<<endl; } 输出: Constructor of Part 6 6.4.1 子对象(续) cout<<date<<endl; } //ex613.cpp #include "whole.h" void main() { Whole anObject(5,6,10); anObject.Print(); 输出: Constructor of Part 6 Constructor of Part 5 Constructor of Whole. 6 5 10 Destructor of Whole. Destructor of Part 5 Destructor of Part 6
6.4.1 子对象(续) 输出: #include "whole.h" Default constructor of Part. 6.4.1 子对象(续) #include "whole.h" void main() { Whole anObject; anObject.Print(); } 输出: Default constructor of Part. Default constructor of Whole. Destructor of Whole. Destructor of Part 0
6.4.1 子对象(续) 说明: 子对象必须在成员初始化列表中初始化; 建立一个对象时,它的所有子对象一起建立; 6.4.1 子对象(续) 说明: 子对象必须在成员初始化列表中初始化; 建立一个对象时,它的所有子对象一起建立; 先执行子对象构造函数,再执行对象的构造函数体; 析构函数的执行顺序与构造函数的执行顺序严格相反; 构造函数的调用顺序仅与子对象在类中声明的顺序有关,而与成员初始化列表中给出的对构造函数的调用顺序无关; 构造函数的成员初始化列表中未给出对子对象的调用,则表示使用子对象的缺省构造函数;
new <类型说明符> (<初始值列表>) 6.4.2 堆对象 1、堆对象 在程序运行过程中根据需要可以随时建立或删除的对象; 2、堆对象运算符 new、delete 3、new运算符 动态创建堆对象; new <类型说明符> (<初始值列表>) new运算符返回一个与new所分配对象类型相匹配的指针; 如果new运算符不能分配到所需要的内存,将返回0,这时为空指针;
new <类型名> [<算术表达式>] 6.4.2 堆对象(续) 使用new运算符创建对象时,它可以根据其参数来选择适当的构造函数; new创建数组: new <类型名> [<算术表达式>] 例如: A *ptr; ptr=new A[5]; 使用new创建对象数组或一般数组时,不能为该数组指定初始值,其初始值为缺省值; 使用new[]创建对象数组时,类中必须说明缺省构造函数; 4、delete运算符
6.4.2 堆对象(续) 功能:删除用new创建的对象或一般类型的指针; delete <指针名> 例如: A *ptr; 6.4.2 堆对象(续) 功能:删除用new创建的对象或一般类型的指针; delete <指针名> 例如: A *ptr; ptr=new A(5,6); … delete ptr; 删除对象数组 delete[] <指针名> 例如: A *ptr; ptr=new A[5]; … delete[] ptr;
6.4.2 堆对象(续) 注意事项: 必须用于由运算符new返回的指针; 该运算符也适用于空指针(即其值为0的指针); 6.4.2 堆对象(续) 注意事项: 必须用于由运算符new返回的指针; 该运算符也适用于空指针(即其值为0的指针); 对一个指针只能使用一次delete操作; 指针名前只用一对方括号符([]),并且不管所删除数组的维数,忽略方括号内的任何数字;
6.4.2 堆对象(续) 例6.14:分析下列程序的输出结果。 //aa.h #include <iostream.h> 6.4.2 堆对象(续) 例6.14:分析下列程序的输出结果。 //aa.h #include <iostream.h> class AA { public: AA(); AA(int i,int j); ~AA(); void Set(int i,int j); void Print(); private: int A,B; }; AA:AA()
6.4.2 堆对象(续) { A=B=0; cout<<"Default constructor called. "<<endl; } AA::AA(int i,int j) A=i; B=j; cout<<"Constructor called. "<<endl; AA::~AA() cout<<"Destructor called. "<<endl; void AA::Set(int i,int j)
6.4.2 堆对象(续) A=i; B=j; } AA::Print() { 6.4.2 堆对象(续) A=i; B=j; } AA::Print() { cout<<A<<", "<<B<<endl; //ex614.cpp #include "aa.h" void main() AA *a1,*a2; a1=new AA(1,2); a2=new AA(5,6); a1.Print();
6.4.2 堆对象(续) a2.Print(); delete a1; delete a2; } Constructor called. 输 6.4.2 堆对象(续) a2.Print(); delete a1; delete a2; } Constructor called. 1,2 5,6 Destructor called. 输 出
6.4.2 堆对象(续) #include "aa.h" void main() { AA *ptr; ptr=new AA[2]; 6.4.2 堆对象(续) #include "aa.h" void main() { AA *ptr; ptr=new AA[2]; ptr[0].Set(1,2); ptr[1].Set(5,6); ptr[0].Print(); ptr[1].Print(); delete[] ptr; } 输出: Default constructor called. 1,2 5,6 Destructor called.
第7章 继承性和派生性 7.1 基类和派生类 7.2 单继承 7.3 多继承 7.4 虚基类
7.1 基类和派生类 1、基类与派生类 基类(父类):已存在的用来派生新类的类; 派生类(子类):由已存在的类派生出的新类; 7.1 基类和派生类 1、基类与派生类 基类(父类):已存在的用来派生新类的类; 派生类(子类):由已存在的类派生出的新类; 2、单继承与多继承 单继承:从一个基类派生的继承; 多继承:从多个基类派生的继承; 基类 派生类 A B 单继承 A C B 多继承
7.1.1 派生类的定义格式 1、单继承 class <派生类名>:<继承方式> <基类名> { 7.1.1 派生类的定义格式 1、单继承 class <派生类名>:<继承方式> <基类名> { <派生类新定义成员> }; 2、多继承 class <派生类名>:<继承方式1> <基类名1>, <继承方式2> <基类名2>... { <派生类新定义成员> };
7.1.1 派生类的定义格式(续) 3、继承方式 public:公有继承; private:私有继承; protected:保护继承; 7.1.1 派生类的定义格式(续) 3、继承方式 public:公有继承; private:私有继承; protected:保护继承; 作用:控制基类中声明的成员在多大的范围内能被派生类的用户访问;
7.1.1 派生类的定义格式(续) 私有成员 公有成员 保护成员 基类部分 新定义部分 派生类 派生类成员 派生类的构成
7.1.2 派生类的三种继承方式 基类实例 基类 继承方式: (J) public private protected 派生类实例 派生类 7.1.2 派生类的三种继承方式 基类实例 基类 继承方式: public private protected (J) 派生类 派生类实例 直接继承 (P) 水平访问 (H) 派生类 垂直访问 (V)
7.1.2 派生类的三种继承方式(续) 表:继承对基类成员的访问能力 (私) (保)
7.1.2 派生类的三种继承方式(续) 说明: 私有成员不参与继承的访问控制; 基类实例(J):与继承方式无关,遵循访问控制权限的定义; 7.1.2 派生类的三种继承方式(续) 说明: 私有成员不参与继承的访问控制; 基类实例(J):与继承方式无关,遵循访问控制权限的定义; 直接继承(P):可以访问基类中的公有成员和保护成员,但成员的权限随继承方式而改变; 水平访问(H)=P+J; 垂直访问(V)=P+P; 保护成员:在垂直访问(V)时相当于公有成员,在水平访问(H)时相当于私有成员; 保护继承:在垂直访问(V)时相当于公有继承,在水平访问(H)时相当于私有继承;
7.1.2 派生类的三种继承方式(续) 例7.1:分析下列程序中的访问权限。 class Location { public: 7.1.2 派生类的三种继承方式(续) 例7.1:分析下列程序中的访问权限。 class Location { public: void InitL(int xx,int yy); void Move(int xOff,int yOff); int GetX() {return X;} int GetY() {return Y;} private: int X,Y; }; void Location::InitL(int xx,int yy) X=xx; Y=yy;
7.1.2 派生类的三种继承方式(续) } void Location::Move(int xOff,int yOff) { 7.1.2 派生类的三种继承方式(续) } void Location::Move(int xOff,int yOff) { X+=xOff; Y+=yOff; class Rectangle:public Location public: void InitR(int x,int y,int w,int h); int GetH() {return H;} int GetW() {return W;} private: int H,W; }; void Rectangle::InitR(int x,int y,int w,int h) 公有继承
7.1.2 派生类的三种继承方式(续) { InitL(x,y); W=w; H=h; } 7.1.2 派生类的三种继承方式(续) { InitL(x,y); W=w; H=h; } #include <iostream.h> void main() Rectangle rect; rect.InitR(2,3,20,10); rect.Move(3,2); cout<<rect.GetX()<<“, "<<rect.GetY()<<", " <<rect.GetH()<<", "<<rect.GetW()<<endl; 输出: 5,5,10,20 水平访问
若继承方式为private,Move(3,2)仍然正确。 7.1.2 派生类的三种继承方式(续) //派生类 class V:public Rectangle { public: void Function(); }; void V::Function() Move(3,2); } 公有继承 若继承方式为private, Move(3,2)是否正确?为什么? 垂直访问,正确 若继承方式为private,Move(3,2)仍然正确。 原因:由于类Rectangle对类Location是公有继承,而类V对类Rectangle是直接继承,直接继承时不考虑继承方式,因此在类V内可以访问基类Location的公有成员;
7.1.2 派生类的三种继承方式(续) class Rectangle:private Location { public: 7.1.2 派生类的三种继承方式(续) 私有继承 class Rectangle:private Location { public: void InitR(int x,int y,int w,int h); int GetH() {return H;} int GetW() {return W;} private: int W,H; }; void Rectangle::InitR(int x,int y,int w,int h) InitL(x,y); W=w; H=h; } 直接继承,正确
7.1.2 派生类的三种继承方式(续) #include <iostream.h> void main() { 7.1.2 派生类的三种继承方式(续) #include <iostream.h> void main() { Rectangle rect; rect.InitR(2,3,20,10); rect.Move(3,2); cout<<rect.GetX()<<", "<<rect.GetY()<<", " <<rect.GetH()<<", "<<rect.GetW()<<endl; } 错误 水平访问 //修改 class Rectangle:private Location { public: void InitR(int x,int y,int w,int h); void Move(int xOff,int yOff)
7.1.2 派生类的三种继承方式(续) { Location::Move(xOff,yOff); } 7.1.2 派生类的三种继承方式(续) { Location::Move(xOff,yOff); } int GetX() {return Location::GetX();} int GetY() {return Location::GetY();} int GetH() {return H;} int GetW() {return W;} private: int W,H; }; void Rectangle::InitR(int x,int y,int w,int h) { InitL(x,y); W=w; H=h; } 通过成员名限定符(::) 指明调用基类中的成员
若继承方式为private,Move(3,2)仍然错误。 7.1.2 派生类的三种继承方式(续) 公有继承 class V:public Rectangle { public: void Function(); }; void V::Function() Move(3,2); } 若继承方式为private, Move(3,2)是否正确?为什么? 垂直访问,错误 若继承方式为private,Move(3,2)仍然错误。 原因:由于类Rectangle对类Location是私有继承,而类V对类Rectangle是直接继承,直接继承时不考虑继承方式,因此在类V内不可以访问基类Location的公有成员;
7.1.2 派生类的三种继承方式(续) class A { protected: int X; }; void main() A a; 7.1.2 派生类的三种继承方式(续) class A { protected: int X; }; void main() A a; a.X=5; } class B:public A { public: void Function(); }; void B::Function() X=5; } 错误,水平访问时 保护成员相当于 私有成员 正确,垂直访问时 保护成员相当于 公有成员
7.1.3 基类与派生类的关系 1、派生类是基类的具体化 7.1.3 基类与派生类的关系 1、派生类是基类的具体化 基类是对若干个派生类的抽象,而派生类是基类的具体化;基类抽取了它的派生类的公共特征,而派生类通过增加行为将抽象类变为某种有用的类型。 2、派生类是基类定义的延续 3、派生类是基类的组合 派生类将其自身与基类区别开来的方法是添加数据成员和成员函数;
7.2.1 成员访问权限的控制 例7.2:分析下列程序中的访问权限,并回答问题。 #include <iostream.h> 7.2.1 成员访问权限的控制 例7.2:分析下列程序中的访问权限,并回答问题。 #include <iostream.h> class A { public: void f1(); protected: int j1; private: int i1; }; class B:public A void f2();
7.2.1 成员访问权限的控制(续) protected: int j2; private: int i2; }; 7.2.1 成员访问权限的控制(续) protected: int j2; private: int i2; }; class C:public B { public: void f3(); 回答下列问题,并说明原因。 1、派生类B中成员函数f2()能否访问基类A中的成员:f1()、j1和i1?
7.2.1 成员访问权限的控制(续) 可以访问f1()和j1,不可以访问i1; 7.2.1 成员访问权限的控制(续) 可以访问f1()和j1,不可以访问i1; 原因:类B对类A是直接继承,可以访问A中的公有成员和保护成员,而不可以访问私有成员; 2、派生类B的对象b1能否访问基类A中的成员:f1()、j1和i1? 可以访问f1(),不可以访问j1和i1; 原因:类B的对象b1对类A中的成员是水平访问,可以访问A中的公有成员,而不可以访问保护成员和私有成员; 3、派生类C中的成员函数f3()能否访问直接基类B中的成员:f2()、j2和i2?能否访问间接基类A中的成员: f1()、j1和i1?
7.2.1 成员访问权限的控制(续) 可以访问直接基类中的f2()和j2以及间接基类中的f1()和j1,不可以访问i2和i1; 7.2.1 成员访问权限的控制(续) 可以访问直接基类中的f2()和j2以及间接基类中的f1()和j1,不可以访问i2和i1; 原因:类C对类B是直接继承,原因同1;类C对类A是垂直访问,可以访问A中的公有成员和保护成员,而不可以访问私有成员; 4、派生类C的对象c1能否访问直接基类B中的成员:f2()、j2和i2?能否访问间接基类A中的成员: f1()、j1和i1? 可以访问直接基类中的f2()以及间接基类中的f1(),其他都不可以访问; 原因:类C的对象c1对B中的成员是水平访问,原因同2;c1对A的访问,相当于先直接继承后水平访问; 5、如将上述两处继承方式由public改为private,试回答上述问题。
7.2.1 成员访问权限的控制(续) 例7.3:分析下列程序,并回答问题。 #include <iostream.h> 7.2.1 成员访问权限的控制(续) 例7.3:分析下列程序,并回答问题。 #include <iostream.h> class A { public: void f(int i) {cout<<i<<endl;} void g() {cout<<"g"<<endl;} }; class B:A void h() {cout<<"h"<<endl; A::f; 缺省继承方式 为private 将基类中的成员 说明为派生类中 的成员
7.2.1 成员访问权限的控制(续) void main() { B d1; d1.f(6); d1.g(); d1.h(); } 7.2.1 成员访问权限的控制(续) void main() { B d1; d1.f(6); d1.g(); d1.h(); } 编译错。 B以私有继承 方式继承A, 因此B的对象 b1不能访问A 的成员函数 输出(2): 6 h 输出(5): 6 g h 回答下列问题,并说明原因。 1、执行该程序时,哪个语句会出现编译错? 2、去掉出错语句后,执行结果是什么? 3、类B从类A继承时的缺省继承方式是什么? 4、派生类B中,A::f的含义是什么? 5、将B的继承方式改为public,输出结果是什么?
7.2.2 构造函数和析构函数 1、派生类构造函数 基类子对象:派生类的对象中由基类中说明的数据成员和操作所构成的封装体; 7.2.2 构造函数和析构函数 1、派生类构造函数 注意与基类的子对象的区别 基类子对象:派生类的对象中由基类中说明的数据成员和操作所构成的封装体; 基类子对象由基类中的构造函数进行初始化; 构造函数不能被继承; 必须在成员 初始化列表 中进行 派生类构造函数的工作: 对自己的数据成员进行初始化; 负责调用基类构造函数使基类的数据成员得以初始化; 调用子对象的构造函数,对派生类中的子对象进行初始化;
7.2.2 构造函数和析构函数(续) 派生类构造函数格式: <派生类名>(<派生类构造函数总参数表>) 7.2.2 构造函数和析构函数(续) 派生类构造函数格式: <派生类名>(<派生类构造函数总参数表>) :<基类构造函数>(<参数表1>), <子对象名>(<参数表2>) { <派生类中数据成员初始化> } 说明: 若某项的参数表为空,则该项可从成员初始化列表中省略,表示使用缺省构造函数初始化该基类子对象;
7.2.2 构造函数和析构函数(续) 派生类构造函数调用顺序: 基类的构造函数; 子对象的构造函数; 派生类构造函数体;
7.2.2 构造函数和析构函数(续) 例7.4:分析下列程序的输出结果。 #include <iostream.h> 7.2.2 构造函数和析构函数(续) 例7.4:分析下列程序的输出结果。 #include <iostream.h> class B { public: B(); B(int i); ~B(); void Print() const; private: int b; }; B:B() b=0; cout<<"B's default constructor called. "<<endl;
7.2.2 构造函数和析构函数(续) } B::B(int i) { b=i; 7.2.2 构造函数和析构函数(续) } B::B(int i) { b=i; cout<<"B's constructor called. "<<endl; B::~B() cout<<"B's destructor called. "<<endl; void B:Print() const cout<<b<<endl; class C:public B
7.2.2 构造函数和析构函数(续) public: C(); C(int i,int j); ~C(); 7.2.2 构造函数和析构函数(续) public: C(); C(int i,int j); ~C(); void Print() const; private: int c; }; C::C() { c=0; cout<<"C's default constructor called. "<<endl; } C::C(int i,int j):B(i) c=j;
7.2.2 构造函数和析构函数(续) cout<"C's constructor called. "<<endl; } 7.2.2 构造函数和析构函数(续) cout<"C's constructor called. "<<endl; } C::~C() { cout<<"C's destructor called. "<<endl; void C::Print() const B::Print(); cout<<c<<endl; void main() C obj(5,6); obj.Print(); 输出: B's constructor called. C's constructor called. 5 6 C's destructor called. B's destructor called.
7.2.2 构造函数和析构函数(续) 2、派生类析构函数 析构函数不能被继承; 执行派生类的析构函数时,基类的析构函数也将被调用; 7.2.2 构造函数和析构函数(续) 2、派生类析构函数 析构函数不能被继承; 执行派生类的析构函数时,基类的析构函数也将被调用; 析构函数的执行顺序与构造函数严格相反; 派生类的析构函数; 基类的析构函数;
7.2.2 构造函数和析构函数(续) 例7.5:分析下列程序的输出结果。 #include <iostream.h> 7.2.2 构造函数和析构函数(续) 例7.5:分析下列程序的输出结果。 #include <iostream.h> class M { public: M(); M(int i,int j); ~M(); void Print(); private: int m1,m2; }; M:M() m1=m2=0; } Print()函数是否可以 定义为常成员函数?
7.2.2 构造函数和析构函数(续) M::M(int i,int j) { m1=i; m2=j; 7.2.2 构造函数和析构函数(续) M::M(int i,int j) { m1=i; m2=j; cout<<"M's constructor called. "<<m1<<", " <<m2<<endl; } M::~M() cout<<"M's destructor called. "<<m1<<", " void M::Print() cout<<m1<<", "<<m2<<", ";
7.2.2 构造函数和析构函数(续) class N:public M { public: N() {n=0;} 7.2.2 构造函数和析构函数(续) class N:public M { public: N() {n=0;} N(int i,int j,int k); ~N(); void Print(); private: int n; }; M::M(int i,int j,int k):M(i,j),n(k) cout<<"N's costructor called. "<<n<<endl; } N::~N()
7.2.2 构造函数和析构函数(续) cout<<"N's destructor called. "<<n<<endl; } void N::Print() { M::Print(); cout<<n<<endl; void main() N n1(5,6,7), n2(-2,-3,-4); n1.Print(); n2.Print(); 输出: M's constructor called.5,6 N's constructor called.7 M's constructor called.-2,-3 N's constructor called.-4 5,6,7 -2,-3,-4 N's destructor called.-4 M's destructor called.-2,-3 N's destructor called.7 M's destructor called.5,6
7.2.2 构造函数和析构函数(续) 3、派生类构造函数使用中应注意的问题 7.2.2 构造函数和析构函数(续) 3、派生类构造函数使用中应注意的问题 派生类构造函数的定义中可以省略对基类构造函数的调用,其条件是在基类中必须有缺省的构造函数或者根本没有定义任何构造函数; 编译器自动生成缺省构造函数 当基类的构造函数使用一个或多个参数时,派生类必须定义构造函数,提供将参数传递给基类构造函数的途径; 设基类数据成员为m个,派生类数据成员为n个, 派生类的参数个数为x,则:0=<x<=m+n;
7.2.2 构造函数和析构函数(续) 例7.6:分析下列程序的输出结果。 #include <iostream.h> 7.2.2 构造函数和析构函数(续) 例7.6:分析下列程序的输出结果。 #include <iostream.h> class A { public: A() {a=0;} A(int i) {a=i;} void Print() {cout<<a<<", ";} private: int a; }; class B:public A B() {b1=b2=0;} B(int i) {b1=i;b2=0;}
7.2.2 构造函数和析构函数(续) B(int i,int j,int k):A(i),b1(j),b2(k) {} 7.2.2 构造函数和析构函数(续) B(int i,int j,int k):A(i),b1(j),b2(k) {} void Print() { A::Print(); cout<<b1<<", "<<b2<<endl; } private: int b1,b2; }; void main() B d1,d2(5),d3(4,5,6); d1.Print(); d2.Print(); d3.Print(); 输出: 0,0,0 0,5,0 4,5,6
7.3.1 多继承的概念 class <派生类名>:<继承方式1> <基类名1>, 7.3.1 多继承的概念 class <派生类名>:<继承方式1> <基类名1>, <继承方式2> <基类名2> …… { <派生类类体> };
7.3.2 多继承的构造函数 多继承构造函数格式: <派生类名>(<总参数表>) 7.3.2 多继承的构造函数 多继承构造函数格式: <派生类名>(<总参数表>) :<基类名1>(<参数表1>), <基类名2>(<参数表2>) …… <子对象名>(<参数表n+1>), …... { <派生类构造函数体> }
7.3.2 多继承的构造函数(续) 派生类构造函数负责所有基类构造函数的调用; 派生类构造函数执行顺序: 执行所有基类的构造函数; 7.3.2 多继承的构造函数(续) 派生类构造函数负责所有基类构造函数的调用; 派生类构造函数执行顺序: 执行所有基类的构造函数; 执行所有子对象的构造函数; 执行派生类构造函数体; 处于同一层次的各基类构造函数的执行顺序取决于定义派生类时所指定的各基类顺序,与派生类构造函数中所定义的成员初始化列表中的各项顺序无关;
7.3.2 多继承的构造函数(续) 例7.7:分析下列程序的输出结果。 #include <iostream.h> 7.3.2 多继承的构造函数(续) 例7.7:分析下列程序的输出结果。 #include <iostream.h> class B1 { public: B1(int i) { b1=i; cout<<"Constructor B1. "<<endl; } void Print() {cout<<b1<<endl;} private: int b1; }; class B2 B2(int i) { b2=i; cout<<"Constructor B2. "<<endl; } void Print() {cout<<b2<<endl;}
7.3.2 多继承的构造函数(续) private: int b2; }; class B3 { public: 7.3.2 多继承的构造函数(续) private: int b2; }; class B3 { public: B3(int i) { b3=i; cout<<"Constructor B3. "<<endl; } int Getb3() {return b3;} int b3; class A:public B2,public B1 A(int i,int j,int k,int l); 多继承
7.3.2 多继承的构造函数(续) void Print(); private: int a; B3 bb; }; 7.3.2 多继承的构造函数(续) void Print(); private: int a; B3 bb; }; A::A(int i,int j,int k,int l):B1(i),B2(j),bb(k) { a=l; cout<<"Constructor A. "<<endl; } void A::Print() B1::Print(); B2::Print(); cout<<a<<bb.Getb3()<<endl; 基类构造函数调用顺序 与定义时的顺序不同 子对象
7.3.2 多继承的构造函数(续) } void main() { A aa(1,2,3,4); aa.Print(); 7.3.2 多继承的构造函数(续) } void main() { A aa(1,2,3,4); aa.Print(); Constructor B2.2 Constructor B1.1 Constructor B3.3 Constructor A.4 1 2 4,3 输 出
7.3.3 二义性问题 1、产生二义性的原因 在多继承情况下,造成的对基类中某个成员的访问出现的不唯一的情况; 7.3.3 二义性问题 1、产生二义性的原因 在多继承情况下,造成的对基类中某个成员的访问出现的不唯一的情况; class A class C:public A,public B { { public: public: void f(); void g(); }; void h(); class B }; { public: void f(); void g(); }; void f(); A.f() B.f() C c1.f()
问题:若定义C c1;,则c1.f()是否正确? 7.3.3 二义性问题(续) 问题:若定义C c1;,则c1.f()是否正确? 答:c1.f()将产生二义性; 原因:不能识别是调用类A或类B的f函数; 解决方法: a.区别出是类A或类B的f函数; c1.A::f(); 或 c1.B::f(); b.在类中定义同名函数f; 当一个派生类从多个基类派生,而这些基类又有一个共同的基类,则对该基类中说明的成员进行访问时,可能会出现二义性;
7.3.3 二义性问题(续) class A class B2:public A { { public: private: 7.3.3 二义性问题(续) class A class B2:public A { { public: private: int a; int b2; }; }; class B1:public A class C:public B1,public B2 { { private: public: int b1; int f(); }; private: int c; }; A.a B1.b1 C B2.b2 c1.a
问题:若定义C c1;,则c1.a与c1.A::a是否正确? 7.3.3 二义性问题(续) 问题:若定义C c1;,则c1.a与c1.A::a是否正确? 答:c1.a与c1.A::a将产生二义性; 原因:不能识别是通过类B1或类B2调用类A的a; 解决方法: a.区别出是通过类B1或类B2调用类A的a; c1.B1::a; 或 c1.B2::a; b.虚基类; 2、解决方法 利用成员名限定法消除二义性; 在类中定义一个同名成员; 虚基类;
7.3.3 二义性问题(续) 3、支配规则(定义同名成员方法) 支配规则:类X中的名字N支配类Y中同名的名字N是指类X以类Y为它的一个基类; 7.3.3 二义性问题(续) 3、支配规则(定义同名成员方法) 支配规则:类X中的名字N支配类Y中同名的名字N是指类X以类Y为它的一个基类; 如果一个名字支配另外一个名字,则二者之间不存在二义性。当选择该名字时,使用支配者的名字; 4、说明 一个类不能从同一个类中直接继承一次以上; 二义性检查在访问控制和类型检查之前进行,访问控制和类型检查不能解决二义性问题;
7.3.3 二义性问题(续) class A { public: void fun(); }; class B private: 7.3.3 二义性问题(续) class A { public: void fun(); }; class B private: class C:public A,public B void main() C obj; obj.fun(); } obj.fun()产生二义性
7.3.3 二义性问题(续) 例7.8:分析下列程序的输出结果。 #include <iostream.h> class A { 7.3.3 二义性问题(续) 例7.8:分析下列程序的输出结果。 #include <iostream.h> class A { public: A(int i) { a=i; cout<<"Constructor A. "<<i<<endl; } ~A() {cout<<"Destructor A. "<<endl;} void Print() {cout<<a<<endl;} private: int a; }; class B1:public A B1(int i,int j):A(i) { b1=j;
7.3.3 二义性问题(续) cout<<"Constructor B1. "<<endl; } 7.3.3 二义性问题(续) cout<<"Constructor B1. "<<endl; } ~B1() {cout<<"Destructor B1. "<<endl;} void Print() { A::Print(); cout<<b1<<endl; } private: int b1; }; class B2:public A { public: B2(int i,int j):A(i) { b2=j; cout<<"Constructor B2. "<<endl; } ~B2() {cout<<"Destructor B2. "<<endl;} void Print() { A::Print();cout<<b2<<endl; } int b2;
7.3.3 二义性问题(续) class C:public B1,public B2 { 7.3.3 二义性问题(续) class C:public B1,public B2 { C(int i,int j,int k,int l,int m):B1(i,j),B2(k,l),c(m) { cout<<"Constructor C. "<<endl; } ~C() {cout<<"Destructor C. "<<endl;} void Print() {B1::Print(); B2::Print(); cout<<c<<endl; } private: int c; }; void main() C c1(1,2,3,4,5); c1.Print(); } a1.a b1.b1 c1.c b2.b2 a2.a
7.3.3 二义性问题(续) Constructor A. Constructor B1. 执 Constructor B2. 7.3.3 二义性问题(续) Constructor A. Constructor B1. Constructor B2. Constructor C. 1 2 3 4 5 Destructor C. Destructor B2. Destructor A. Destructor B1. 执 行 结 果 注意基类A的实例的数目 如果a是类A的公有成员, obj.a是否正确?
virtual <继承方式> <基类名> 7.4.1 虚基类的引入和说明 引入目的:解决二义性问题; 格式: virtual <继承方式> <基类名> 说明: 关键字virtual与关键字public或private的相对位置无关,但必须位于虚基类名之前,且virtual只对紧随其后的基类名起作用; 例如: class D:virtual public A,private B,virutal public C 其中:类A和类C是虚基类,而类B是非虚基类;
7.4.1 虚基类的引入和说明(续) class A { public: void f(); protected: int a; }; 7.4.1 虚基类的引入和说明(续) class A { public: void f(); protected: int a; }; class B:virtual public A int b; class C:virtual public B int c; 虚基类 虚基类
7.4.1 虚基类的引入和说明(续) }; class D:public B,public C { public: int g(); 7.4.1 虚基类的引入和说明(续) }; class D:public B,public C { public: int g(); private: int d; B D.g() C A.f() n.f() 下列各语句是否正确? D n; n.f(); void D::g() { f(); } 正确 能够唯一确定调用类A的f();
7.4.1 虚基类的引入和说明(续) 虚基类与非虚基类的存储结构 虚基类 非虚基类 B C D A B C A D
7.4.2 虚基类的构造函数 派生类中只有一个虚基类子对象; 虚基类构造函数必须只被调用一次,目的是要保证虚基类子对象只被初始化一次; 7.4.2 虚基类的构造函数 派生类中只有一个虚基类子对象; 虚基类构造函数必须只被调用一次,目的是要保证虚基类子对象只被初始化一次; 最派生类:继承结构中建立对象时所指定的类; 虚基类子对象由最派生类的构造函数通过调用虚基类的构造函数进行初始化; 在一个成员初始化列表中出现对虚基类和对非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数的执行; 最派生类的构造函数的成员初始化列表中必须给出对虚基类的构造函数的调用;如果未列出,则相应的虚基类必须有缺省构造函数;
7.4.2 虚基类的构造函数(续) 最派生类 若定义E e;则E是最派生类; 若定义D d;则D是最派生类; 若定义B b;则B是最派生类; 7.4.2 虚基类的构造函数(续) 最派生类 若定义E e;则E是最派生类; 若定义D d;则D是最派生类; 若定义B b;则B是最派生类; 若定义C c;则C是最派生类; B D C A E 构造函数 B(…):A(…) ... C(…):A(…) ... D(…):B(...),C(…),A(…) ... E(…):D(...),A(…) ...
7.4.2 虚基类的构造函数(续) 例7.9:分析下列程序的输出结果。 #include <iostream.h> 7.4.2 虚基类的构造函数(续) 例7.9:分析下列程序的输出结果。 #include <iostream.h> class A { public: A(const char *s) { cout<<s<<endl; } ~A() {} }; class B:virtual public A B(const char *s1,const char *s2):A(s1) { cout<<s2<<endl; } class C: virtual public A
7.4.2 虚基类的构造函数(续) { public: C(const char *s1,const char *s2):A(s1) 7.4.2 虚基类的构造函数(续) { public: C(const char *s1,const char *s2):A(s1) { cout<<s2<<endl; } }; class D:public B,public C D(const char *s1,const char *s2, const char *s3,const char *s4) :B(s4,s2),C(s2,s3),A(s1) { cout<<s4<<endl; } void main()
7.4.2 虚基类的构造函数(续) D *ptr=new D("class A", "class B", 7.4.2 虚基类的构造函数(续) D *ptr=new D("class A", "class B", "class C", "class D"); delete ptr; } class A class B class C class D 输 出 说明: 虚基类子对象由最派生类构造函数进行初始化; 虚基类子对象只被初始化一次; 虚基类构造函数先于非虚基类构造函数执行;
7.4.2 虚基类的构造函数(续) 基类 子对象 数据成员 初始化 虚基类 非虚基类 常数据成员 静态数据成员 一般数据成员 7.4.2 虚基类的构造函数(续) 基类 子对象 数据成员 初始化 虚基类 非虚基类 常数据成员 静态数据成员 一般数据成员 存在多个基类时,执行顺序取决于派生类定义时的顺序 存在多个子对象时,执行顺序取决于类中声明的顺序 成员初始化列表 类外初始化
第8章 多态性和虚函数 8.1 重载 8.2 静态束定与动态束定 8.3 虚函数 8.4 纯虚函数与抽象类 8.5 虚析构函数
第8章 多态性和虚函数 多态性:发出同样的消息被不同类型的对象接受导致完全不同的行为; 多态可分为:静态多态性与动态多态性; 第8章 多态性和虚函数 多态性:发出同样的消息被不同类型的对象接受导致完全不同的行为; 多态可分为:静态多态性与动态多态性; 动态多态性必须存在于继承的环境之中;
8.1.1 函数重载 在类中,构造函数可以重载,普通成员函数也可以重载;构造函数重载给初始化带来了多种方式,为用户提供了更大的灵活性。
8.1.1 函数重载(续) 例8.1:分析下列程序的输出结果。 #include <iostream.h> 8.1.1 函数重载(续) 例8.1:分析下列程序的输出结果。 #include <iostream.h> #include <string.h> class string { public: string(char *s); string(string &s1); string(int size=80); ~string() {delete sptr;} int GetLen() {return length;} void Print() {cout<<sptr<<endl;} private: char *sptr; int length; 构造函数重载
8.1.1 函数重载(续) }; string::string(char *s) { length=strlen(s); 8.1.1 函数重载(续) }; string::string(char *s) { length=strlen(s); sptr=new char[length+1]; strcpy(sptr,s); } string::string(string &s1) length=s1.length; strcpy(sptr,s1.sptr); string::string(int size) 带有指针成员的拷贝初始化构造函数
8.1.1 函数重载(续) length=size; sptr=new char[length+1]; 输出: *sptr='\0'; 8.1.1 函数重载(续) length=size; sptr=new char[length+1]; *sptr='\0'; } void main() { string str1("This is a string. "); str1.Print(); cout<<str1.GetLen()<<endl; char *s1="That is a program. "; string str2(s1); string str3(str2); str3.Print(); cout<<str3.GetLen()<<endl; 输出: This is a string. 17 That is a program. 18
8.1.2 运算符重载的几个问题 1、哪些运算符可以重载? 算术运算符:+、-、*、/、%、++、--; 8.1.2 运算符重载的几个问题 1、哪些运算符可以重载? 算术运算符:+、-、*、/、%、++、--; 位操作运算符:&、|、~、^、<<、>>; 逻辑运算符:!、&&、||; 比较运算符:>、<、>=、<=、==、!=; 赋值运算符:=、+=、-=、*=、/=、%=、 &=、|=、~=、<<=、>>=; 其他运算符:[]、()、->、' 、new、delete、 new[]、delete[]、->*; 不允许重载的运算符:.、.*、::、?:、sizeof;
8.1.2 运算符重载的几个问题(续) 2、编译程序如何选用哪一个运算符函数? 运算符实质上是函数,遵循函数重载原则; 8.1.2 运算符重载的几个问题(续) 2、编译程序如何选用哪一个运算符函数? 运算符实质上是函数,遵循函数重载原则; 3、运算符重载时必须遵循哪些原则? 重载运算符含义必须清楚; 重载运算符不能有二义性; 4、重载运算符有哪些限制? 不可臆造新的运算符; 重载运算符坚持4个“不能改变”:
8.1.2 运算符重载的几个问题(续) 不能改变运算符操作数的个数; 不能改变运算符原有的优先级; 不能改变运算符原有的结合性; 8.1.2 运算符重载的几个问题(续) 不能改变运算符操作数的个数; 不能改变运算符原有的优先级; 不能改变运算符原有的结合性; 不能改变运算符原有的语法结构;
8.1.3 运算符重载函数的两种形式 1、重载为类的成员函数 重载一元运算符,不再显式说明参数; 8.1.3 运算符重载函数的两种形式 1、重载为类的成员函数 重载一元运算符,不再显式说明参数; 重载二元运算符,只显式说明一个参数;该参数为操作数的右操作数,左操作数由this指针(指向调用该成员函数的对象)提供; 重载为成员函数时,隐含了一个参数(this指针);
8.1.3 运算符重载函数的两种形式(续) 例8.2:分析下列程序的输出结果。 #include <iostream.h> 8.1.3 运算符重载函数的两种形式(续) 例8.2:分析下列程序的输出结果。 #include <iostream.h> class complex { public: complex(double r=0,double i=0); complex operator +(const complex& c); complex operator -(const complex& c); complex operator -(); void print() const; private: double real,imag; }; complex::complex(double r,double i) +运算符 -运算符 求负运算符
8.1.3 运算符重载函数的两种形式(续) real=r; imag=i; } 8.1.3 运算符重载函数的两种形式(续) real=r; imag=i; } complex complex::operator +(const complex& c) { double r=real+c.real; double i=imag+c.imag; return complex(r,i); complex complex::operator -(const complex& c) double r=real-c.real; double i=imag-c.imag;
8.1.3 运算符重载函数的两种形式(续) complex complex::operator -() { 8.1.3 运算符重载函数的两种形式(续) complex complex::operator -() { return complex(-real,-imag); } void complex::print() const cout<<' ('<<real<<', '<<imag<<') '<<endl; void main() complex c1(2.5,3.7),c2(4.2,6.5); complex c; c=c1-c2; c.print(); c=c1+c2; c=c1.operator -(c2); c=c1.operator +(c2);
8.1.3 运算符重载函数的两种形式(续) c.print(); c=-c1; } (-1.7,-2.8) 输 (6.7,10.2) 出 8.1.3 运算符重载函数的两种形式(续) c.print(); c=-c1; } c=c1.operator -(); (-1.7,-2.8) (6.7,10.2) (-2.5,-3.7) 输 出
8.1.3 运算符重载函数的两种形式(续) 2、重载为友元函数 重载为友元函数时,没有隐含的参数this指针,即不改变原有运算符的语法结构; 8.1.3 运算符重载函数的两种形式(续) 2、重载为友元函数 重载为友元函数时,没有隐含的参数this指针,即不改变原有运算符的语法结构; 下列运算符不能重载为友元函数: =、()、[]、-> 重载为友元函数的运算符重载函数的格式: friend <类型说明符> operator <运算符>(<参数表>) { …… }
8.1.3 运算符重载函数的两种形式(续) 例8.3:分析下列程序的输出结果。 #include <iostream.h> 8.1.3 运算符重载函数的两种形式(续) 例8.3:分析下列程序的输出结果。 #include <iostream.h> class complex { public: complex(double r=0,double i=0); friend complex operator + (const complex& c1,const complex& c2); friend complex operator - friend complex operator -(const complex& c); void print() const; private: double real,imag; }; +运算符 -运算符 求负运算符
8.1.3 运算符重载函数的两种形式(续) complex::complext(double r,double i) { real=r; 8.1.3 运算符重载函数的两种形式(续) complex::complext(double r,double i) { real=r; imag=i; } complex operator +(const complex& c1, const complex& c2) double r=c1.real+c2.real; double i=c1.imag+c2.imag; return complex(r,i); complex operator -(const complex& c1,
8.1.3 运算符重载函数的两种形式(续) double r=c1.real-c2.real; 8.1.3 运算符重载函数的两种形式(续) double r=c1.real-c2.real; double i=c1.imag-c2.imag; return complex(r,i); } complex operator -(const complex& c) { return complex(-c.real,-c.imag); void complex::print() const cout<<' ('<<real<<', '<<imag<<') '<<endl; void main() complex c1(2.5,3.7),c2(4.2,6.5);
8.1.3 运算符重载函数的两种形式(续) complex c; c=c1-c2; c.print(); c=c1+c2; c=-c1; } 8.1.3 运算符重载函数的两种形式(续) complex c; c=c1-c2; c.print(); c=c1+c2; c=-c1; } c=operator -(c1,c2); c=operator +(c1,c2); c=operator -(c1); (-1.7,-2.8) (6.7,10.2) (-2.5,-3.7) 输 出
8.1.3 运算符重载函数的两种形式(续) 3、两种重载形式的比较 8.1.3 运算符重载函数的两种形式(续) 3、两种重载形式的比较 一般情况下,单目运算符最好重载为成员函数;双目运算符则最好重载为友元函数; 有些双目运算符重载为成员函数较好: 如:赋值运算符=;
8.1.3 运算符重载函数的两种形式(续) 例8.4:指出程序中的错误,并改正。 class integer { public: 8.1.3 运算符重载函数的两种形式(续) 例8.4:指出程序中的错误,并改正。 class integer { public: integer(int i=0) {value=i;} integer operator +(integer i) { return integer(value+=i.value); } private: int value; }; void main() integer i1=10; integer i2=i1+5; integer i3=3+i2; } 原因:3+i2等价于 3.operator +(i2) 错误
8.1.3 运算符重载函数的两种形式(续) 改正: class integer { public: 8.1.3 运算符重载函数的两种形式(续) 改正: class integer { public: integer(int i=0) {value=i;} friend integer operator +(integer i1,integer i2); private: int value; }; integer operator +(integer i1,integer i2) integer temp=i1.value+i2.value; return temp; }
8.1.4 运算符重载示例 例8.5:++运算符。 #include <iostream.h> class counter { 8.1.4 运算符重载示例 例8.5:++运算符。 #include <iostream.h> class counter { public: counter() {value=0;} counter operator ++(); counter operator ++(int); void display() {cout<<value<<endl;} private: int value; }; counter counter::operator ++() value++; 前缀运算符 后缀运算符
8.1.4 运算符重载示例(续) return *this; } counter counter::operator ++(int i) { 8.1.4 运算符重载示例(续) return *this; } counter counter::operator ++(int i) { counter t; t.value=value++; return t; void main() counter c; c.display(); for(int i=0;i<10;i++) c++; 前缀运算符变量值与表达式值均加1 后缀运算符变量值加1,表达式值不变 c.operator ++(0);
8.1.4 运算符重载示例(续) for(i=0;i<10;i++) ++c; c.display(); } 0 输 10 出 20 8.1.4 运算符重载示例(续) for(i=0;i<10;i++) ++c; c.display(); } c.operator ++(); 0 10 20 输 出
8.1.4 运算符重载示例(续) 例8.6:下标运算符。 #include <iostream.h> class vector 8.1.4 运算符重载示例(续) 例8.6:下标运算符。 #include <iostream.h> class vector { public: vector(int size) {v=new int[size];} ~vector() {delete[] v;} int& operator [](int i); private: int *v; }; int& vector::operator [](int i) return v[i]; }
8.1.4 运算符重载示例(续) void main() { vector a[5]; for(int i=0;i<5;i++) 8.1.4 运算符重载示例(续) void main() { vector a[5]; for(int i=0;i<5;i++) a[i]=i; a[2]=12; cout<<a[i]<<endl; } 0 1 12 3 4 输 出
8.1.4 运算符重载示例(续) 例8.7:用函数调用运算符实现函数f(x,y)=x*y+5。 8.1.4 运算符重载示例(续) 例8.7:用函数调用运算符实现函数f(x,y)=x*y+5。 #include <iostream.h> class F { public: double operator () (double x,double y) const; }; double F::operator () (double x,double y) const return x*y+5; } void main() F f; cout<<f(5.2,2.5)<<endl; 输出: 18
8.2.1 子类型化和类型适应 1、子类型化 子类型:有一个特定的类型S,当且仅当它至少提供了类型T的行为时,称类型S是类型T的子类型; 8.2.1 子类型化和类型适应 1、子类型化 子类型:有一个特定的类型S,当且仅当它至少提供了类型T的行为时,称类型S是类型T的子类型; 派生类S是基 类T的子类型 公有继承可以实现子类型; 子类型关系是不可逆的; 子类型关系可以传递;
8.2.1 子类型化和类型适应(续) 例8.8:分析下列程序的输出结果。 #include <iostream.h> 8.2.1 子类型化和类型适应(续) 例8.8:分析下列程序的输出结果。 #include <iostream.h> class A { public: void Print() const {cout<<"A::Print() called. "<<endl;} }; class B:public A void f() {} void f1(const A& r) 公有继承,B是A的子类型
8.2.1 子类型化和类型适应(续) r.Print(); } void main() { B b; f1(b); 问题: 8.2.1 子类型化和类型适应(续) r.Print(); } void main() { B b; f1(b); 问题: 1、形参与实参类型不匹配? 2、若类型匹配是正确的,输出结果是什么? 类型适应 A::Print() called. 输 出
8.2.1 子类型化和类型适应(续) 2、类型适应 类型S的对象能被用于类型T的对象所能使用的场合,称为类型S适应类型T。 8.2.1 子类型化和类型适应(续) 2、类型适应 类型S的对象能被用于类型T的对象所能使用的场合,称为类型S适应类型T。 类型适应的三种情况: S*或S&适应T*或T&; S*或S&适应const T*或const T&; const S*或const S&适应const T*或const T&; 说明 此处的对象一般是指针或引用;
8.2.1 子类型化和类型适应(续) 3、子类型的作用 一个函数可被重用于处理T类型的对象和T的各个子类型的对象,而不必为处理这些子类型的对象去重载该函数,可以减轻程序人员编写程序代码的负担;
8.2.1 子类型化和类型适应(续) 例8.9:分析下列程序的输出结果。 #include <iostream.h> 8.2.1 子类型化和类型适应(续) 例8.9:分析下列程序的输出结果。 #include <iostream.h> class A { public: A() {a=0;} A(int i) {a=i;} void Print() {cout<<a<<endl;} int Geta() {return a;} private: int a; }; class B:public A 公有继承,B是A的子类型
8.2.1 子类型化和类型适应(续) B() {b=0;} B(int i,int j):A(i),b(j) {} 8.2.1 子类型化和类型适应(续) B() {b=0;} B(int i,int j):A(i),b(j) {} void Print() {A::Print();cout<<b<<endl;} private: int b; }; void fun(A& d) { cout<<d.Geta()*10<<endl; } void main() B bb(9,5); A aa(5); aa=bb;
8.2.1 子类型化和类型适应(续) aa.Print(); A *pa=new A(8); B *pb=new B(1,2); 8.2.1 子类型化和类型适应(续) aa.Print(); A *pa=new A(8); B *pb=new B(1,2); pa=pb; pa->Print(); fun(bb); } 类型适应,B&适应A& 9 1 90 输 出 调用A.Geta()*10;
8.2.2 静态束定 例8.10:分析下列程序的输出结果。 #include <iostream.h> class Point 8.2.2 静态束定 例8.10:分析下列程序的输出结果。 #include <iostream.h> class Point { public: Point(double i,double j) {x=i;y=j;} double Area() const {return 0;} private: double x,y; }; class Rectangle:public Point Rectangle(int i,int j,int k,int l); double Area() const {return w*h;} 公有继承,Rectangle是Point的子类型
8.2.2 静态束定(续) double w,h; }; Rectangle::Rectangle(int i,int j,int k,int l):Point(i,j) { w=k; h=l; } void fun(Point& s) cout<<s.Area()<<endl; void main() Rectangle rect(3.0,5.2,15.0,25.0); fun(rect); 输出: 0 调用Point::Area(); 类型适应
8.2.2 静态束定(续) 静态束定: 在编译时进行的束定,即编译时就确定了程序中的操作调用与执行该操作代码之间的关系。
8.2.3 动态束定 动态束定:在程序执行时进行的束定; 实现:C++动态束定在虚函数的支持下实现; 动态束定的原因: 8.2.3 动态束定 动态束定:在程序执行时进行的束定; 实现:C++动态束定在虚函数的支持下实现; 动态束定的原因: 当实现一个子类型时变动了其基类中相应行为的实现时,必须将这种变动告诉编译器,即进行动态束定;
virtual <类型说明符> <函数名>(<参数表>) 8.3 虚函数 1、虚函数 虚函数是动态束定的基础; 虚函数是非static成员函数; 说明方法: virtual <类型说明符> <函数名>(<参数表>) 含义: 若类中一成员函数被说明为虚函数,则该成员函数在派生类中可能有不同的实现。当使用该成员函数操作指针或引用所标识的对象时,对该成员函数调用可采用动态束定方式。
8.3 虚函数(续) 操作方法: 动态束定只能通过指针或引用标识对象来操作虚函数。如果采用一般类型的标识对象来操作虚函数,则将采用静态束定方式调用虚函数。
8.3 虚函数(续) 例8.11:分析下列程序的输出结果。 #include <iostream.h> class Point 8.3 虚函数(续) 例8.11:分析下列程序的输出结果。 #include <iostream.h> class Point { public: Point(double i,double j) {x=i;y=j;} virtual double Area() const {return 0;} private: double x,y; }; class Rectangle:public Point Rectangle(int i,int j,int k,int l); virtual double Area() const {return w*h;} 虚函数 虚函数
8.3 虚函数(续) double w,h; }; Rectangle::Rectangle(int i,int j,int k,int l):Point(i,j) { w=k; h=l; } void fun(Point& s) cout<<s.Area()<<endl; void main() Rectangle rect(3.0,5.2,15.0,25.0); fun(rect); 输出: 375
8.3 虚函数(续) 例8.12:分析下列程序的输出结果,并回答问题。 #include <iostream.h> 8.3 虚函数(续) 例8.12:分析下列程序的输出结果,并回答问题。 #include <iostream.h> class A { public: virtual void act1() { cout<<"A::act1() called. "<<endl; } void act2() { act1(); } }; class B:public A void act1() {cout<<"B::act1() called. "<<endl;} 虚函数 公有继承,B是A的子类型
8.3 虚函数(续) void main() { B b; b.act2(); } 回答下列问题: 8.3 虚函数(续) void main() { B b; b.act2(); } 回答下列问题: (1)、该程序执行后的输出结果是什么?为什么? 输出结果为:B::act1() called. 原因:a. B从A公有继承,B是A的子类型; b. B中的act1()为虚函数; c. b.act2()调用A中的act2(),进一步调用 act1(),产生动态束定,运行时选择B::act1(); 为什么?
8.3 虚函数(续) (2)、如果将A::act2()的实现改为: void A::act2() { this->act1(); } 8.3 虚函数(续) (2)、如果将A::act2()的实现改为: void A::act2() { this->act1(); } 输出结果是什么?为什么? 输出结果与(1)相同,即:B::act1() called. 原因:this指向操作该成员函数的对象,基于与(1)相同的原因,此处调用B::act1()。
8.3 虚函数(续) (3)、如果将A::act2()的实现改为: void A::act2() { A::act1(); } 8.3 虚函数(续) (3)、如果将A::act2()的实现改为: void A::act2() { A::act1(); } 输出结果是什么?为什么? 输出结果:A::act1() called. 原因:此处增加了成员名限定,因此要进行静态束定,即调用的是A::act1()。
8.3 虚函数(续) 动态束定实现的三个条件: 要有说明的虚函数; 建立子类型关系; 8.3 虚函数(续) 动态束定实现的三个条件: 要有说明的虚函数; 建立子类型关系; 调用虚函数操作的是指向对象的指针或者对象引用;或者是由成员函数调用虚函数;
满足上述条件的派生类虚函数,可不加virtual说明。 8.3 虚函数(续) 派生类中对基类的虚函数进行替换时,要求派生类中说明的虚函数与基类中的被替换的虚函数之间满足下列条件: 参数个数:与基类的虚函数有相同的参数个数; 参数类型:与基类的虚函数的对应参数类型相同; 返回值类型: 与基类的虚函数的返回值类型相同; 都返回指针或引用,并且派生类虚函数所返回的指针或引用的基类型是基类中被替换的虚函数所返回的指针或引用的基类型的子类型; 满足上述条件的派生类虚函数,可不加virtual说明。
8.3 虚函数(续) 例8.13:分析下列程序的输出结果。 #include <iostream.h> class A { 8.3 虚函数(续) 例8.13:分析下列程序的输出结果。 #include <iostream.h> class A { public: A() {} virtual void f() { cout<<"A::f() called. "<<endl; } }; class B:public A B() { f(); } void g() { f(); } 构造函数调用虚函数 一般成员函数调用虚函数
8.3 虚函数(续) class C:public B { public: C() {} 8.3 虚函数(续) class C:public B { public: C() {} virtual void f() { cout<<"C::f() called. "<<endl; } }; void main() C c; c.g(); } 输 出 A::f() called. C::f() called.
8.3 虚函数(续) 构造函数与析构函数调用虚函数: 8.3 虚函数(续) 构造函数与析构函数调用虚函数: 构造函数中调用虚函数时,采用静态束定,即构造函数调用的虚函数是自己类中实现的虚函数,如果自己类中没有实现这个虚函数,则调用基类中的虚函数,而不是任何派生类中实现的虚函数; 析构函数中调用虚函数同构造函数,即析构函数所调用的虚函数是自身类中的或者基类中实现的虚函数; B A C 构造(析构)函数 一般成员函数 调用 虚函数
8.4.1 纯虚函数 1、引入 在基类中不能为虚函数给出一个有意义的实现时,可将其声明为纯虚函数,其实现留待派生类完成; 2、作用 8.4.1 纯虚函数 1、引入 在基类中不能为虚函数给出一个有意义的实现时,可将其声明为纯虚函数,其实现留待派生类完成; 2、作用 为派生类提供一个一致的接口; 3、声明格式 class <类名> { virtual <类型> <函数名>(<参数表>)=0; …… }
8.4.1 纯虚函数(续) 例8.14:分析下列程序的输出结果。 #include <iostream.h> 8.4.1 纯虚函数(续) 例8.14:分析下列程序的输出结果。 #include <iostream.h> class Number { public: Number(int i) {val=i;} virtual void show()=0; protected: int val; }; class Hextype:public Number Hextype(int i):Number(i) {} 纯虚函数
8.4.1 纯虚函数(续) virtual void show() 8.4.1 纯虚函数(续) virtual void show() { cout<<hex<<val<<dec<<endl;} }; class Dectype:public Number { public: Dectype(int i):Number(i) {} {cout<<dec<<val<<endl;} void fun(Number& n) n.Show(); }
如果要增加一种表示方式:八进制表示方式,应如何修改? 8.4.1 纯虚函数(续) void main() { Dectype d(50); fun(d); Hextype h(16); fun(h); } 输出: 50 10 如果要增加一种表示方式:八进制表示方式,应如何修改?
8.4.1 纯虚函数(续) //定义八进制表示方式类Octtype class Octtype:public Number { 8.4.1 纯虚函数(续) //定义八进制表示方式类Octtype class Octtype:public Number { public: Octtype(int i):Number(i) {} virtual void show() { cout<<oct<<val<<dec<<endl; } }; void main() ... Octtype o(9); fun(o); } 输出: 50 10 11
8.4.1 纯虚函数(续) 例8.15:分析下列程序的输出结果。 #include <iostream.h> 8.4.1 纯虚函数(续) 例8.15:分析下列程序的输出结果。 #include <iostream.h> class Point { public: Point(int i=0,int j=0) {x0=i;y0=j;} virtual void Set()=0; virtual void Draw()=0; protected: int x0,y0; }; class Line:public Point
8.4.1 纯虚函数(续) Line(int i=0,int j=0,int m=0,int n=0):Point(i,j) 8.4.1 纯虚函数(续) Line(int i=0,int j=0,int m=0,int n=0):Point(i,j) {x1=m;y1=n;} void Set() {cout<<"Line::Set() called. "<<endl;} void Draw() {cout<<"Line::Draw() called. "<<endl;} protected: int x1,y1; }; class Ellipse:public Point { public: Ellipse(int i=0,int j=0,int p=0,int q=0):Point(i,j) {x2=p;y2=q;} void Set() {cout<<"Ellipse::Set() called. "<<endl;}
8.4.1 纯虚函数(续) void Draw() {cout<<"Ellipse::Draw() called. "<<endl;} protected: int x2,y2; }; void DrawObj(Point* p) { p->Draw(); } void SetObj(Point* p) p->Set(); void main()
8.4.1 纯虚函数(续) { Line *lineobj=new Line; 8.4.1 纯虚函数(续) { Line *lineobj=new Line; Ellipse *ellipseobj=new Ellipse; DrawObj(lineobj); DrawObj(ellipseobj); cout<<endl; SetObj(lineobj); SetObj(ellipseobj); cout<<endl<<"Redraw the object…"<<endl; }
8.4.1 纯虚函数(续) Line::Draw() called. 执 Ellipse::Draw() called. 行 8.4.1 纯虚函数(续) 执 行 结 果 Line::Draw() called. Ellipse::Draw() called. Line::Set() called. Ellipse::Set() called. Redraw the object...
8.4.2 抽象类 带有纯虚函数的类称为抽象类; 抽象类只能作为基类使用,其纯虚函数的实现由派生类给出;但派生类仍可不给出纯虚函数的定义,继续作为抽象类存在; 抽象类不能定义对象,一般将该类的构造函数说明为保护的访问控制权限; 抽象类的作用: 用作基类:在一个继承层次结构中,提供一个公共的根,并基于抽象类的操作设计出对抽象类所描述的一类对象进行操作的公共接口,其完整的实现由派生类完成;
8.4.2 抽象类(续) 用作指针或引用的基类型:保证进入继承层次的每个类都具有(提供)纯虚函数所要求的行为; 8.4.2 抽象类(续) 用作指针或引用的基类型:保证进入继承层次的每个类都具有(提供)纯虚函数所要求的行为; 在成员函数内可以调用纯虚函数,但在构造函数或析构函数内不能调用纯虚函数(纯虚函数没有实现代码); class A { public: virtual void f()=0; void g() { f(); } A() { f(); } } 正确 错误
8.4.2 抽象类(续) 例8.16:计算各类图形的总面积。 //shape.h #if !defined(_SHAPE_H) 8.4.2 抽象类(续) 例8.16:计算各类图形的总面积。 //shape.h #if !defined(_SHAPE_H) #define _SHAPE_H class Shape { public: virtual double Area() const=0; }; #endif //_SHAPE_H //program.h #if !defined(_PROGRAM_H) #define _PROGRAM_H
8.4.2 抽象类(续) #include "shape.h" class Application { public: 8.4.2 抽象类(续) #include "shape.h" class Application { public: double Compute(Shape *s[],int n) const; }; #endif //_PROGRAM_H //program.cpp #include "program.h" double Application::Compute(Shape *s[],int n) const double sum=0; for(int i=0;i<n;i++)
8.4.2 抽象类(续) sum+=s[i]->Area(); return sum; } //polygon.h 8.4.2 抽象类(续) sum+=s[i]->Area(); return sum; } //polygon.h #if !defined(_POLYGON_H) #define _POLYGON_H #include <iostream.h> #include "shape.h" class Polygon:public Shape { public: double Area() const=0; };
8.4.2 抽象类(续) class Triangle:public Polygon { public: 8.4.2 抽象类(续) class Triangle:public Polygon { public: Triangle(double h,double w) {H=h;W=w;} double Area() const {return H*W*0.5;} private: double H,W; }; class Rectangle:public Polygon Rectangle(double h,double w) {H=h;W=w;} double Area() const {return H*W;}
8.4.2 抽象类(续) double H,W; }; class Circle:public Polygon { public: 8.4.2 抽象类(续) double H,W; }; class Circle:public Polygon { public: Circle(double r) {radius=r;} double Area() const {return radius*radius*3.14;} private: double radius; #endif //_POLYGON_H //ex816.cpp #include <iostream.h>
8.4.2 抽象类(续) #include "polygon.h" #include "program.h" 8.4.2 抽象类(续) #include "polygon.h" #include "program.h" class MyProgram:public Application { public: MyProgram(); ~MyProgram(); int Run(); private: Shape **s; }; MyProgram::MyProgram() s=new Shape* [4];
8.4.2 抽象类(续) s[0]=new Triangle(3.0,4.0); s[1]=new Rectangle(2.0,5.0); 8.4.2 抽象类(续) s[0]=new Triangle(3.0,4.0); s[1]=new Rectangle(2.0,5.0); s[2]=new Circle(5.0); s[3]=new Circle(8.0); } MyProgram::~MyProgram() { for(int i=0;i<4;i++) delete s[i]; delete[] s; int MyProgram::Run() double sum=Compute(s,4);
8.4.2 抽象类(续) cout<<sum<<endl; return 0; } int main() { 8.4.2 抽象类(续) cout<<sum<<endl; return 0; } int main() { return MyProgram().Run(); 293.46 输 出
8.4.2 抽象类(续) Shape Polygon Application MyProgram T r i a n g l e R c t 8.4.2 抽象类(续) Shape Polygon Application MyProgram T r i a n g l e R c t C MyProgram() ~MyProgram() Run() 抽象类
8.5 虚析构函数 虚析构函数 在析构函数前加关键字virtual进行说明,则该析构函数称为虚析构函数; 格式: class B { 8.5 虚析构函数 虚析构函数 在析构函数前加关键字virtual进行说明,则该析构函数称为虚析构函数; 格式: class B { public: virtual ~B(); …... }
8.5 虚析构函数(续) 如果一个类的析构函数被说明为虚析构函数,则它的派生类中的析构函数也是虚析构函数,不管它是否使用了关键字virtual进行说明; 目的: 使用delete运算符删除一个对象时,能保证析构函数被正确地执行;
8.5 虚析构函数(续) 例8.17:分析下列程序的输出结果。 #include <iostream.h> class A { 8.5 虚析构函数(续) 例8.17:分析下列程序的输出结果。 #include <iostream.h> class A { public: virtual ~A() {cout<<"A::~A() called. "<<endl;} }; class B:public A B(int i) {buf=new char[i];} virtual ~B() delete buf;
8.5 虚析构函数(续) cout<<"B::~B() called. "<<endl; } private: 8.5 虚析构函数(续) cout<<"B::~B() called. "<<endl; } private: char *buf; }; void fun(A *a) { delete a; void main() A *a=new B(15); fun(a); 输出: B::~B() called. A::~A() called. 进行动态束定,调用B的析 构函数,而B是A的派生类, 进一步调用A的析构函数;
8.5 虚析构函数(续) 问:若类A的析构函数不是虚析构函数,则输出结果是什么?为什么?会导致什么后果? 8.5 虚析构函数(续) 问:若类A的析构函数不是虚析构函数,则输出结果是什么?为什么?会导致什么后果? 输出结果为:A::~A() called. 原因:此时进行静态束定,直接调用基类A的析构函数; 后果:此时将不能释放类B中的资源; 说明: 子类型化要求析构函数被声明为虚函数,尤其是在析构函数要完成一些有意义的工作时; 构造函数不能被声明为虚函数;
第9章 C++的I/O流库 9.1 流抽象的继承结构 9.2 预定义的插入符与提取符 9.3 插入符和提取符的重载 9.1 流抽象的继承结构 9.2 预定义的插入符与提取符 9.3 插入符和提取符的重载 9.4 磁盘文件的输入和输出 9.5 字符串流
9.1 流抽象的继承结构 1、流的基本概念 流的引入 流:数据从一个对象流动到另一个对象,这种流动抽象为流; 9.1 流抽象的继承结构 1、流的基本概念 流的引入 流:数据从一个对象流动到另一个对象,这种流动抽象为流; 流的操作:建立流、删除流、提取、插入 2、C++流的继承结构 ios streambuf istream ostream iostream
9.1 流抽象的继承结构(续) ios:对流状态进行设置,虚基类; streambuf:提供对数据的缓冲支持; 9.1 流抽象的继承结构(续) ios:对流状态进行设置,虚基类; streambuf:提供对数据的缓冲支持; istream、ostream、iostream:提取与插入; 3、文件的继承结构 fstreambase filebuf ifstream ofstream iofstream fstreambase:公共基类;
9.1 流抽象的继承结构(续) filebuf:提供对上述类的缓冲支持; ifstream、ofstream、iofstream:文件操作; 9.1 流抽象的继承结构(续) filebuf:提供对上述类的缓冲支持; ifstream、ofstream、iofstream:文件操作; 4、字符串类 提供处理内部初始化字符序列的操作; istrstream:从序列中取字符; ostrstream:将字符放入序列; 5、预定义的流 cin:istream类对象,处理标准输入,即键盘输入;
9.1 流抽象的继承结构(续) cout:ostream类对象,处理标准输出,即屏幕输出; 9.1 流抽象的继承结构(续) cout:ostream类对象,处理标准输出,即屏幕输出; cerr:ostream类对象,处理标准出错信息,提供不带缓冲区的输出; clog:ostream类对象,处理标准出错信息,提供带缓冲区的输出;
ostream& ostream::operator <<(const type& obj); 9.2.1 预定义的插入符 1、预定义插入符的格式 ostream& ostream::operator <<(const type& obj); 其中:type为char、int、short、long类型和它们的unsigned和signed类型,以及float、double、long double、char *和void *; 2、说明 一般情况下将插入符作用于cout对象; 输出语句中可以串联多个插入运算符,输出多个数据项;
9.2.1 预定义的插入符(续) 插入运算符后可以是任意复杂的表达式,系统可自动计算其值并传给插入符; 9.2.1 预定义的插入符(续) 插入运算符后可以是任意复杂的表达式,系统可自动计算其值并传给插入符; 指针类型的地址值,默认为十六进制显示,用类型long强制后才可以十进制显示; 字符指针的地址值的输出格式为:(void *)s或void *(s),此时仍为十六进制格式;
9.2.1 预定义的插入符(续) 例9.1:分析下列程序的输出结果。 #include <iostream.h> 9.2.1 预定义的插入符(续) 例9.1:分析下列程序的输出结果。 #include <iostream.h> #include <string.h> void main() { char *str="Hello"; int a=100; int *pa=&a; cout<<"*pa="<<*pa<<endl; cout<<"&pa="<<&pa<<" or "<<long(&pa)<<endl; cout<<"The string is "<<str<<endl; cout<<"The address is "<<(void *)str <<" or "<<long((void *)str)<<endl; } 地址值的 十进制 表示方法 字符指针 地址值的 十六进制 表示方法 字符指 针地址
9.2.1 预定义的插入符(续) 输出 *pa=100 &pa=0x0065FDEC or 6684144 9.2.1 预定义的插入符(续) 输出 *pa=100 &pa=0x0065FDEC or 6684144 The string is Hello The address is 0x00426064 or 4350052
9.2.1 预定义的插入符(续) 3、使用put()输出一个字符 ostream& ostream::put(char c); 9.2.1 预定义的插入符(续) 3、使用put()输出一个字符 ostream& ostream::put(char c); 4、使用write()输出一个字符 ostream& ostream::write(char *buf,int n); 说明: 这些成员函数既可用于文本流,也可用于二进制流,尤其适用于二进制流;
9.2.1 预定义的插入符(续) 例9.2:分析下列程序的输出结果。 #include <iostream.h> 9.2.1 预定义的插入符(续) 例9.2:分析下列程序的输出结果。 #include <iostream.h> void main() { cout<<'a'<<', '<<'b'<<'\n'; cout.put('a').put(', ').put('b').put('\n'); char c1='A',c2='B'; cout.put(c1).put(c2).put('\n'); } a,b AB 输 出
9.2.1 预定义的插入符(续) 例9.3:分析下列程序的输出结果。 #include <iostream.h> 9.2.1 预定义的插入符(续) 例9.3:分析下列程序的输出结果。 #include <iostream.h> #include <string.h> void PrintString(char *s) { cout.write(s,strlen(s)).put('\n'); cout.write(s,6)<<"\n"; } void main() char str[]="I love C++"; cout<<"The string is: "<<str<<endl; PrintString(str); PrintString("this is a string");
9.2.1 预定义的插入符(续) 输出 The string is: I love C++ I love C++ I love 9.2.1 预定义的插入符(续) 输出 The string is: I love C++ I love C++ I love this is a string this i
istream& istream::operator >>(type& obj); 9.2.2 预定义的提取符 1、预定义提取符的格式 istream& istream::operator >>(type& obj); 其中:type为char、int、short、long类型和它们的unsigned和signed类型,以及float、double、long double、char *; 2、说明 一般情况下将提取符作用于cin对象; 输入语句中可以串联多个提取运算符,每个提取符后为一表达式,该表达式是获得输入值的变量或对象;
9.2.2 预定义的提取符(续) 提取操作时,空白符(空格、tab键、换行符)只用于字符的分隔符,而本身不作为从输入流中提取的字符; 9.2.2 预定义的提取符(续) 提取操作时,空白符(空格、tab键、换行符)只用于字符的分隔符,而本身不作为从输入流中提取的字符; 提取符可从输入流中读取一个字符串,该字符串是以空白符结束的一个字符序列,由系统自动加上'\0'字符;
9.2.2 预定义的提取符(续) 例9.4:分析下列程序的输出结果。 #include <iostream.h> 9.2.2 预定义的提取符(续) 例9.4:分析下列程序的输出结果。 #include <iostream.h> #include <string.h> void main() { const int SIZE=20; char buf[SIZE]; char *largest; int curLen,maxLen=-1,cnt=0; cout<<"Input words:"<<endl; while(cin>>buf) curLen=strlen(buf); cnt++; if(curLen>maxLen)
9.2.2 预定义的提取符(续) maxLen=curLen; largest=buf; } cout<<endl; 9.2.2 预定义的提取符(续) maxLen=curLen; largest=buf; } cout<<endl; cout<<cnt<<endl; cout<<maxLen<<endll; cout<<largest<<endl; 输 入 Input words: if else return do while continue<ctrl+z> 6 8 continue 输 出 输入ctrl+z键后,cin>>buf 的值为0,退出while循环;
9.2.2 预定义的提取符(续) 3、使用get()获取一个字符 istream& istream::get(char& c); 9.2.2 预定义的提取符(续) 3、使用get()获取一个字符 istream& istream::get(char& c); int istream::get(); 4、使用getline()获取多个字符 istream& istream::getline (char *buf,int Limit,Deline='\n'); 说明: getline()最多可读取Limit-1个字符; getline()函数结束操作的条件:
9.2.2 预定义的提取符(续) 从输入流中读取Limit-1个字符后; 从输入流中读取换行符或其他终止符后; 9.2.2 预定义的提取符(续) 从输入流中读取Limit-1个字符后; 从输入流中读取换行符或其他终止符后; 从输出流中读取到文件或输入流结束符后; getline()通常用来读取一行字符: 5、使用read()读取一串字符 istream& istream::read(char *,int);
9.2.2 预定义的提取符(续) 例9.5:分析下列程序的输出结果。 #include <iostream.h> 9.2.2 预定义的提取符(续) 例9.5:分析下列程序的输出结果。 #include <iostream.h> void main() { char ch; cout<<"Input: "; while((ch=cin.get())!=EOF) cout.put(ch); cout<<"OK! "<<endl; } abc xyz 123<Enter> <ctrl+z> 输入 输出 abc xyz 123<Enter>
9.2.2 预定义的提取符(续) 例9.6:分析下列程序的输出结果。 #include <iostream.h> 9.2.2 预定义的提取符(续) 例9.6:分析下列程序的输出结果。 #include <iostream.h> void main() { const int S=80; char buf[S]=""; cout<<"Input…"<<endl; cin>>read(buf,s); cout<<endl; cout<<buf<<endl; } 输入: Input... abcd<Enter> efgh<Enter> ijkl<Enter> Input… abcd efgh ijkl 输 出
9.3 插入符和提取符的重载 1、重载为友元函数 ostream& operator<<(ostream&s,const type& p); istream& operator>>(istream&s,type& p); 2、函数调用形式 ostrm<<obj; 等价于 operator <<(ostrm,obj); ostrm<<obj1<<obj2; 等价于 operator <<(operator<<(ostrm,obj1),obj2); istrm>>obj; 等价于 operator >>(istrm,obj); istrm>>obj1>>obj2; 等价于 operator >>(operator>>(istrm,obj1),obj2);
9.3 插入符和提取符的重载(续) 例9.7:分析下列程序的输出结果。 #include <iostream.h> 9.3 插入符和提取符的重载(续) 例9.7:分析下列程序的输出结果。 #include <iostream.h> class Date { public: Date(int y,int m,int d) Year=y; Month=m; Day=d; } friend ostream& operator << (ostream& stream,Date& date); friend istream& operator >> (istream& stream,Date& date);
9.3 插入符和提取符的重载(续) private: int Year,Month,Day; }; 9.3 插入符和提取符的重载(续) private: int Year,Month,Day; }; ostream& operator <<(ostream& stream,Date& date) { stream<<date.Year<<"/"<<date.Month<<"/" <<date.Day<<endl; return stream; } istream& operator >>(istream& stream,Date& date) stream>>date.Year>>date.Month>>date.Day; void main()
9.3 插入符和提取符的重载(续) { Date CDate(1999,10,22); 9.3 插入符和提取符的重载(续) { Date CDate(1999,10,22); cout<<"Current date: "<<CDate<<endl; cout<<"Enter new date: "; cin>>CDate; cout<<"New date: "<<CDate<<endl; } Current date: 1999/10/22 Enter new date: 2001 10 22 New date: 2001/10/22 输 出
9.4.1 磁盘文件的打开和关闭 1、打开文件 void fstream::open(const char *fname, 9.4.1 磁盘文件的打开和关闭 1、打开文件 void fstream::open(const char *fname, int mode,int= filebuf::openprot); fstream::fstream(const char *fname, int mode,int= filebuf::openprot); void ofstream::open(const char *fname, int mode=ios::out,int= filebuf::openprot); ofstream::ofstream(const char *fname, int mode=ios::out,int= filebuf::openprot); void ifstream::open(const char *fname, int mode=ios::in,int= filebuf::openprot); ifstream::ifstream(const char *fname, int mode=ios::in,int= filebuf::openprot);
9.4.1 磁盘文件的打开和关闭(续) 2、关闭文件 void fstream::close(); 9.4.1 磁盘文件的打开和关闭(续) 2、关闭文件 void fstream::close(); void ofstream::close(); void ifstream::close();
9.4.1 磁盘文件的打开和关闭(续) 例9.8:分析下列程序的输出结果。 #include <iostream.h> 9.4.1 磁盘文件的打开和关闭(续) 例9.8:分析下列程序的输出结果。 #include <iostream.h> void main() { ofstream ostrm; ostrm.open("f1.dat"); ostrm<<120<<endl; ostrm<<310.85<<endl; ostrm.close(); ifstream istrm("f1.dat"); int n; double d; istrm>>n>>d; cout<<n<<", "<<d<<endl; istrm.close(); } 输出: 120,310.85
9.4.2 文本文件的读写操作 例9.9:将文本写入指定的文件。 #include <iostream.h> 9.4.2 文本文件的读写操作 例9.9:将文本写入指定的文件。 #include <iostream.h> #include <fstream.h> #include <stdlib.h> void main() { fstream outfile; outfile.open("f2.dat",ios::out); if(!outfile) cout<<"f2.dat can't open. "<<endl; abort(); } outfile<<"this is a program. "<<endl; outfile.close(); 退出程序
9.4.2 文本文件的读写操作(续) 例9.10:从文本文件中读出信息。 #include <iostream.h> 9.4.2 文本文件的读写操作(续) 例9.10:从文本文件中读出信息。 #include <iostream.h> #include <fstream.h> #include <stdlib.h> void main() { fstream infile; infile.open("f2.dat",ios::in); if(!infile) cout<<"f2.dat can't open. "<<endl; abort(); } char s[80]; while(!infile.eof())
9.4.2 文本文件的读写操作(续) { infile.getline(s,sizeof(s)); 9.4.2 文本文件的读写操作(续) { infile.getline(s,sizeof(s)); cout<<s<<endl; } infile.close(); this is a program. 输 出
9.4.2 文本文件的读写操作(续) 例9.11:使用get()和put()函数读写文件。 9.4.2 文本文件的读写操作(续) 例9.11:使用get()和put()函数读写文件。 #include <iostream.h> #include <fstream.h> #include <stdlib.h> #include <string.h> void main() { fstream outfile,infile; outfile.open("f3.dat",ios::out); if(!outfile) cout<<"f3.dat can't open. "<<endl; abort(); } char str[]="this is a c++ program. ";
9.4.2 文本文件的读写操作(续) for(int i=0;i<strlen(str);i++) 9.4.2 文本文件的读写操作(续) for(int i=0;i<strlen(str);i++) outfile.put(str[i]); outfile.close(); infile.open("f3.dat",ios::in); if(!infile) { cout<<"f3.dat can't open. "<<endl; abort(); } char ch; while(infile.get(ch)) cout<<ch; cout<<endl; infile.close(); 输出: this is a c++ program.
9.4.2 文本文件的读写操作(续) 例9.12:将一文件内容拷贝到另一文件。 #include <iostream.h> 9.4.2 文本文件的读写操作(续) 例9.12:将一文件内容拷贝到另一文件。 #include <iostream.h> #include <fstream.h> #include <stdlib.h> void main() { fstream outfile,infile; infile.open("f2.dat",ios::in); if(!infile) cout<<"f2.dat can't open. "<<endl; abort(); } outfile.open("f4.dat",ios::out); if(!outfile)
9.4.2 文本文件的读写操作(续) { cout<<"f4.dat can't open. "<<endl; 9.4.2 文本文件的读写操作(续) { cout<<"f4.dat can't open. "<<endl; abort(); } char ch; while(infile.get(ch)) outfile.put(ch); infile.close(); outfile.close();
9.4.3 二进制文件的读写操作 例9.13:对一二进制文件进行读写操作。 #include <iostream.h> 9.4.3 二进制文件的读写操作 例9.13:对一二进制文件进行读写操作。 #include <iostream.h> #include <fstream.h> #include <stdlib.h> struct person { char name[20]; double height; int age; } struct person people[4]={"Wang",1.65,25, "Zhang", 1.72,24, "Li",1.89,21, "Huang",1.70,22}; void main() fstream outfile,infile;
9.4.3 二进制文件的读写操作(续) outfile.open("f5.dat",ios::out|ios::binary); 9.4.3 二进制文件的读写操作(续) outfile.open("f5.dat",ios::out|ios::binary); if(!outfile) { cout<<"f5.dat can't open. "<<endl; abort(); } for(int i=0;i<4;i++) outfile.write((char *)&people[i], sizeof(people[i])); outfile.close(); infile.open("f5.dat",ios::in|ios::binary); if(!infile)
9.4.3 二进制文件的读写操作(续) } for(int i=0;i<4;i++) { 9.4.3 二进制文件的读写操作(续) } for(int i=0;i<4;i++) { outfile.read((char *)&people[i], sizeof(people[i])); cout<<people[i].name<<"\t" <<people[i].height<<"\t" <<people[i].age<<endl; infile.close(); 输 出 Wang 1.65 25 Zhang 1.72 24 Li 1.69 21 Huang 1.7 22
9.4.4 随机访问数据文件 1、读文件指针 istream& istream::seekg(streampos); 9.4.4 随机访问数据文件 1、读文件指针 istream& istream::seekg(streampos); istream& istream::seekg(streamoff,ios::seek_dir); streampos istream::tellg(); streampos为long型; seek_dir的值: cur=1,相对于当前读指针指定的位置; beg=0,相对于流的开始位置; end=2,相对于流的结尾位置;
9.4.4 随机访问数据文件(续) 2、写文件指针 ostream& ostream::seekp(streampos); 9.4.4 随机访问数据文件(续) 2、写文件指针 ostream& ostream::seekp(streampos); ostream& ostream::seekp(streamoff,ios::seek_dir); streampos ostream::tellp();
9.4.4 随机访问数据文件(续) 例9.14:分析下列程序的输出结果。 #include <iostream.h> 9.4.4 随机访问数据文件(续) 例9.14:分析下列程序的输出结果。 #include <iostream.h> #include <fstream.h> #include <stdlib.h> void main() { struct student char name[20]; long number; double totalscore; }struct stu[5]={"Ma",97001,85.72, "Li",97002,92.62, "Hu",97003,89.25, "Yan",97004,90.84, "Lu",97005,80.92};
9.4.4 随机访问数据文件(续) fstream file1; student one; 9.4.4 随机访问数据文件(续) fstream file1; student one; file1.open("f6.dat",ios::out|ios::in|ios::binary); if(!file1) { cout<<"f6.dat can't open. "<<endl; abort(); } for(int i=0;i<5;i++) file1.write((char *)&stu[i],sizeof(student)); file1.seekg(sizeof(student)*4); file1.read((char *)&one,sizeof(student)); cout<<one.name<<"\t"<<one.number<<"\t" <<one.totalscore<<endl; file1.seekg(sizeof(student)*1);
9.4.4 随机访问数据文件(续) file1.read((char *)&one,sizeof(student)); 9.4.4 随机访问数据文件(续) file1.read((char *)&one,sizeof(student)); cout<<one.name<<"\t"<<one.number<<"\t" <<one.totalscore<<endl; file1.close(); } Lu 97005 80.92 Li 97002 92.62 输 出
9.5.1 ostrstream类 1、功能 将不同类型的信息格式化为字符串,并存放到一个字符数组中;但ostrstream并不在输出流的末尾自动添加空字符,必须在程序中显式添加该空字符; 2、格式 ostrstream::ostrstream(); ostrstream::ostrstream (char *s,int n,int mode=ios::out);
9.5.1 ostrstream类(续) 例9.15:分析下列程序的输出结果。 #include <iostream.h> #include <strstrea.h> const int N=80; void main() { char buf[N]; ostrstream out1(buf,sizeof(buf)); int a=50; for(int i=0;i<6;i++,a+=10) out1<<"a="<<a<<"; " out1<<'\0'; cout<<"Buf: "<<buf<<endl; double pi=3.14159265;
9.5.1 ostrstream类(续) out1.setf(ios::fixed|ios::showpoint); out1.seekp(0); out1<<"The value of pi is "<<pi<<'\0'; out1<<buf<<endl; char *pstr=out1.str; cout<<pstr<<endl; } Buf: a=50;a=60;a=70;a=80;a=90;a=100; The value of pi is 3.14159265 输 出
9.5.2 istrstream类 1、功能 将文本项转换为变量所需要的内部格式; 2、格式 将文本项转换为变量所需要的内部格式; 2、格式 istrstream::istrstream(char *s); istrstream::istrstream(char *s,int n)
9.5.2 istrstream类(续) 例9.16:分析下列程序的输出结果。 #include <iostream.h> #include <strstrea.h> void main() { char buf[]="123 45.67"; int a; double b; istrstream ss(buf); ss>>a>>b; cout<<a+b<<endl; } 输出 168.67
9.5.2 istrstream类(续) 例9.17:分析下列程序的输出结果。 #include <iostream.h> #include <strstrea.h> void main() { char buf[]="12345"; int i,j; istrstream s1(buf); s1>>i; istrstream s2(buf,3); s2>>j; cout<<i+j<<endl; } 输出: 12468