Presentation is loading. Please wait.

Presentation is loading. Please wait.

面向对象程序设计 第三章 C++面向对象程序设计 武汉大学 赵小红.

Similar presentations


Presentation on theme: "面向对象程序设计 第三章 C++面向对象程序设计 武汉大学 赵小红."— Presentation transcript:

1 面向对象程序设计 第三章 C++面向对象程序设计 武汉大学 赵小红

2 本章主要内容 面向对象程序设计概述: C++类: 类继承: 多态性: object oriented programming, OOP
OOP思想、OOP基本特点 C++类: 类的定义与实现、内联成员函数、构造与析构、this指针、静态成员、友元等 类继承: 基类和派生类、多重继承、虚基类 多态性: 虚函数,重载,模板

3 结构化程序设计方法 重点: 特点: 形式: 缺点: 如何实现细节过程,将数据与函数分开。
面向对象程序设计概述 结构化程序设计方法 重点: 如何实现细节过程,将数据与函数分开。 特点: 自顶向下,逐步求精——功能分解。 形式: 算法 + 数据结构 = 程序 主模块+若干个子模块(main()+子函数) 缺点: 程序的可重用性差、可维护性欠佳。 结构化程序设计方法是一种面向过程的程序设计方法,即一个程序是由多个过程(在C++中为函数)模块组成,过程之间通过函数参数和全局变量进行相互联系。

4 软件方法的发展 程序=(算法)+(数据结构) 程序=(算法+数据结构) 对象=(算法+数据结构), 程序=(对象+对象+……)
面向对象程序设计概述 软件方法的发展 程序=(算法)+(数据结构) 程序=(算法+数据结构) 对象=(算法+数据结构), 程序=(对象+对象+……) 主函数 函数1 函数3 函数2 函数1-1 函数1-2 函数2-1 函数3-1 函数3-2 数据1 数据2 数据3 结构化设计方法 系统整体性差 算法和数据结构是一个整体,算法只能适用于特定的数据结构,而一个数据结构也没必要设计多个算法进行同一种操作。 更注重系统整体关系的表示和数据模型技术 数据结构本身可能包含算法和数据结构 面向对象的设计方法

5 面向对象程序设计概述 算法+数据结构 数据结构 算法+数据结构 算法+数据结构 算法+数据结构 对象 算法+数据结构 算法 构成程序的对象

6 面向对象方法的主要优点 与人类习惯的思维方法一致 稳定性好 可重用性好 可维护性好

7 面向对象程序设计方法及特征 目的: 观点: 程序设计方法: 要求: 实现软件设计的产业化。 自然界是由实体(对象)所组成。
面向对象程序设计概述 面向对象程序设计方法及特征 目的: 实现软件设计的产业化。 观点: 自然界是由实体(对象)所组成。 程序设计方法: 使用面向对象的观点来描述模仿并处理现实问题。 要求: 高度概括、分类、和抽象。

8 面向对象程序设计概述 对象 ( object ) 一台计算机 一项计划 实体对象 抽象对象

9 面向对象程序设计概述 对象 ( object ) 什么是对象? 在面向对象程序设计中,对象是用来描述客观事物的一个实体,它反映了系统为之保存信息和与之交互的方法,它是构成系统的一个基本单元。 一个对象由一组属性和方法(行为或操作)组成。

10 对象 ( object ) 对象构成要素: 标识符:是对象的名称,用来区别与其他对象。 属 性:是用来描述对象静态特征的一个数据项。
面向对象程序设计概述 对象 ( object ) 对象构成要素: 标识符:是对象的名称,用来区别与其他对象。 属 性:是用来描述对象静态特征的一个数据项。 方 法:是用来描述对象动态特征和行为的一个操作。

11 对象 ( object ) 例题:对象 “帐户” 的抽象描述 对象名 帐户 帐户号,户主,身份证号,地址,电话,密码,存款。 属性
面向对象程序设计概述 对象 ( object ) 例题:对象 “帐户” 的抽象描述 对象名 帐户 帐户号,户主,身份证号,地址,电话,密码,存款。 属性 修改显示帐户号,户主,身份证号,密码,存款等属性。 方法 注意点: 1. 三要素成为一体 2. 三要素的区别

12 面向对象程序设计概述 类 ( class ) 对象2 对象1 对象4 对象3

13 类 ( class ) 什么是类? 类是对对象的抽象描述。
面向对象程序设计概述 类 ( class ) 什么是类? 类是对对象的抽象描述。 类是具有相同数据结构(属性)和相同操作功能(行为)的对象的集合,它规定了这些对象的公共属性和行为方法。

14 类 ( object ) 类与对象的关系 类  对 象 抽象定义 实 例 轿 车  车号**的轿车 学 生  学生王强
面向对象程序设计概述 类 ( object ) 类与对象的关系 类  对 象 抽象定义 实 例 轿 车  车号**的轿车 学 生  学生王强 类 型  变量

15 抽 象 抽象是指对具体问题(对象)进行概括,抽出一类对象的公共属性和行为并加以描述的过程。
面向对象程序设计概述 抽 象 抽象是指对具体问题(对象)进行概括,抽出一类对象的公共属性和行为并加以描述的过程。 抽象包括数据抽象和过程抽象(或行为抽象,或代码抽象)。 数据抽象:描述某类对象的属性或状态(对象相互区别的物理量)。 过程抽象:描述某类对象的共有的行为特征或具有的功能。 抽象的实现:通过类的声明。 先注意问题的本质及描述,其次是实现过程或细节。

16 抽象实例——钟表 数据抽象: int Hour, int Minute, int Second 代码抽象:
面向对象程序设计概述 抽象实例——钟表 数据抽象: int Hour, int Minute, int Second 代码抽象: SetTime(), ShowTime()

17 抽象实例——钟表类 class Clock { public:
面向对象程序设计概述 抽象实例——钟表类 class Clock { public: void SetTime(int NewH, int NewM, int NewS); void ShowTime(); private: int Hour,Minute,Second; };

18 抽象实例——人 数据抽象: 代码抽象: char *name,char *gender,int age,int id
面向对象程序设计概述 抽象实例——人 数据抽象: char *name,char *gender,int age,int id 代码抽象: 生物属性角度: GetCloth(), Eat(),Step(),… 社会属性角度: Work(),Promote() ,…

19 每个开车的人都需要了解汽车的内部结构吗?
面向对象程序设计概述 问题提出: 每个开车的人都需要了解汽车的内部结构吗? 档 位 刹 车 方向盘 油 门 离合器 雨 刷 音 响 车 灯

20 封装(encapsulation) 什么是封装? 把对象的属性和方法结合成一个独立的单元,并尽可能隐蔽对象的内部细节。 一个清楚的边界。
面向对象程序设计概述 封装(encapsulation) 什么是封装? 把对象的属性和方法结合成一个独立的单元,并尽可能隐蔽对象的内部细节。 一个清楚的边界。 所有对象的成分都在这个边界内。 一个接口。 外部通过这个接口访问对象的内部成员。 封装的目的 增强安全性和简化编程,使用者不必了解具体的实现细节,而只需要通过外部接口,以特定的访问权限,来使用类的成员。

21 封装(encapsulation) 实例: class Clock {
public: void SetTime(int NewH,int NewM, int NewS); void ShowTime(); private: int Hour,Minute,Second; }; 外部接口 边界 特定的访问权限

22 封装(encapsulation) 封装的优点: 封装的缺点: ⒈ 数据独立; ⒉ 具有隐藏性和安全性; ⒊ 易于维护; ⒋ 简化编程。
面向对象程序设计概述 封装(encapsulation) 封装的优点: ⒈ 数据独立; ⒉ 具有隐藏性和安全性; ⒊ 易于维护; ⒋ 简化编程。 封装的缺点: 需要更多的输入输出函数。

23 面向对象程序设计概述 教师 学生 职员

24 继承(inheritance) 什么是继承?
面向对象程序设计概述 继承(inheritance) 什么是继承? 继承是指一个新类可以从现有的类派生而来。新类继承了现有类的特性,包括属性和行为,并且可以修改或增加新的属性和行为,使之适合具体的需要。

25 继承(inheritance) 继承的特点: 设 A 为基类, B 为派生类。 1. 共享性: 派生类具有基类所有特征 即:A  B
面向对象程序设计概述 继承(inheritance) 继承的特点: 设 A 为基类, B 为派生类。 1. 共享性: 派生类具有基类所有特征 即:A  B 2. 区别性: 派生类不等于基类 即A≠B 3. 层次性: 表示了类与类之间的关系

26 继承(inheritance) 继承的优点: 1. 减少原代码,即代码可重用。 2. 代码的相互独立性。 3. 易于维护。
面向对象程序设计概述 继承(inheritance) 继承的优点: 1. 减少原代码,即代码可重用。 2. 代码的相互独立性。 3. 易于维护。 4. 体现自然界之间的关系。

27 多态性(polymorphism) 例:几何图形类层次中的多态性。其派生类“圆”、“长方形”、“三角形”等都有服务“求面积”。 几何图形 圆
面向对象程序设计概述 多态性(polymorphism) 例:几何图形类层次中的多态性。其派生类“圆”、“长方形”、“三角形”等都有服务“求面积”。 几何图形 长方形 三角形

28 多态性(polymorphism) 什么是多态性? 目的:达到行为标识统一,减少程序中标识符的个数。 实现:重载和虚函数
面向对象程序设计概述 多态性(polymorphism) 什么是多态性? 同一名称,不同的功能实现方式。 目的:达到行为标识统一,减少程序中标识符的个数。 实现:重载和虚函数

29 C++类 为了支持面向对象程序设计,C++在C语言结构(struct)数据类型的基础上引入了类这种抽象数据类型。

30 类的定义与实现 C++类将对象的属性抽象为数据成员,将对象的行为抽象为成员函数,并对它们进行封装。数据成员又称成员变量,成员函数又称为方法。
C++类在形式上类似于C语言中用户自定义的结构类型,但定义类时规定了成员的访问控制权限。 类的定义分为声明部分和实现部分: 声明部分用来说明该类的成员,主要说明“干什么” 实现部分对成员函数的具体操作进行定义,主要阐述“怎么干”

31 C++类的声明形式 类是一种用户自定义类型,声明形式: class <类名> { private:
<私有数据成员和私有成员函数的声明列表>; public: <公有数据成员和公有成员函数的声明列表>; protected: <保护数据成员和保护成员函数的声明列表>; }; 无顺序关系, 每部分允许 出现多次

32 C++类 公有类型成员 在关键字public后面声明,它们是类与外部的接口,任何外部函数都可以访问公有类型数据和函数, 但必须通过一个对象作为对象的成员使用。

33 私有类型成员 在关键字private后面声明,只允许本类中的函数访问,而类外部的任何函数都不能访问。
C++类 私有类型成员 在关键字private后面声明,只允许本类中的函数访问,而类外部的任何函数都不能访问。 如果紧跟在类名称的后面声明私有成员,则关键字private可以省略。

34 C++类 保护类型成员 在关键字protected后面声明。与private类似,其差别表现在继承与派生时对派生类的影响不同。

35 例 定义类Time(表示时间)。 class Time{ private: // 最好不要省略private
私有数据成员hour、minute 和second只能在类的成员 函数中被访问或赋值; class Time{ private: // 最好不要省略private int hour; // 数据成员,表示小时 int minute; // 数据成员,表示分钟 int second; // 数据成员,表示秒 public: void setTime(int, int, int);// 成员函数,设置时间 void showTime(); // 成员函数,输出时间 }; 公有成员函数setTime、showTime 可在外部被调用,但必须通过一个 对象作为对象的成员使用。

36 类的实现 类的实现实质上是类的成员函数的实现,即定义类的成员函数。
C++类 类的实现 类的实现实质上是类的成员函数的实现,即定义类的成员函数。 成员函数的定义形式与一般函数的定义形式基本相同,但必须在成员函数名前加上类名和作用域限定符(::)。 成员函数的定义也可放在类体内(该函数声明之处),这时成员函数将变成内联函数。

37 例 类Time的实现 void Time::setTime(int h, int m, int s) {
C++类 例 类Time的实现 void Time::setTime(int h, int m, int s) { hour=(h>=0 && h<24) ? h:0; // 设置时间 minute=(m>=0 && m<60) ? m:0; second=(s>=0 && s<60) ? s:0; } void Time::showTime() cout<<hour<<':'<<minute<<':'<<second<<endl; private成员hour、minute和 second不允许外界存取, 所以为类Time增加两个 public成员函数,供外界 设置或显示private成员。

38 C++类 成员数据 与一般的变量声明相同,但需要将它放在类的声明体中。

39 成员函数 在类中说明原形,可以在类外给出函数体实现,并在函数名前使用类名加以限定。也可以直接在类中给出函数体,形成内联成员函数。
C++类 成员函数 在类中说明原形,可以在类外给出函数体实现,并在函数名前使用类名加以限定。也可以直接在类中给出函数体,形成内联成员函数。 允许声明重载函数和带默认形参值的函数。

40 内联成员函数 为了提高运行时的效率,对于较简单的函数可以声明为内联形式。 内联函数体中不要有复杂结构(如循环语句和switch语句)。
在类中声明内联成员函数的方式: 将函数体放在类的声明中。 使用inline关键字。

41 内联成员函数举例(一) class Point { public: void Init(int initX,int initY)
X=initX; Y=initY; } int GetX() {return X;} int GetY() {return Y;} private: int X,Y; };

42 内联成员函数举例(二) class Point { public: void Init(int initX,int initY);
int GetX(); int GetY(); private: int X,Y; };

43 Init(int initX,int initY) { X=initX; Y=initY; }
C++类 inline void Point:: Init(int initX,int initY) { X=initX; Y=initY; } inline int Point::GetX() return X; inline int Point::GetY() return Y;

44 定义类的注意事项 在类体中不允许对所定义的数据进行初始化。 类中的数据成员可以是任意类型但是不能是自身的类对象。
C++类 定义类的注意事项 在类体中不允许对所定义的数据进行初始化。 类中的数据成员可以是任意类型但是不能是自身的类对象。 提倡将类的定义放在头文件(.h)中,类的实现放在源文件(.cpp)中,而main主函数可以放在另一个源文件中。在源文件中用#include编译预处理指令包含头文件。

45 C++类 C++程序结构: 程序=类定义+成员函数定义+主程序

46 对 象 类的对象是该类的某一特定实例,即类类型的变量。 声明形式: 类名 对象名; 例: Time EndTime;
C++类 对 象 类的对象是该类的某一特定实例,即类类型的变量。 声明形式: 类名 对象名; 例: Time EndTime; 一旦定义了类的对象,系统将为其分配空间,并可向对象发送消息,对其进行操作。 第四周开始

47 类中成员的访问方式 类中成员互访 类外访问 直接使用成员名
C++类 类中成员的访问方式 类中成员互访 直接使用成员名 类外访问 使用“对象名成员名”或“对象名->成员名”方式访问 public 属性的成员

48 例:类Time的使用,声明对象并设置对象属性
C++类 例:类Time的使用,声明对象并设置对象属性 main() { Time EndTime; // 声明对象EndTime EndTime.setTime(12, 23, 36); // 设置对象EndTime的时间 cout<<"The time is:"; EndTime.showTime(); // 显示对象EndTime的时间 }

49 C++类 类的应用举例 设计一个学生成绩的简单管理程序,输入10个学生的3门成绩后,计算平均成绩,并在屏幕上输出。利用类来实现。

50 阶段课程小结 理解结构化设计方法与面向对象方法的区别 熟悉面向对象的程序设计方法,学习用面向对象的方法设计程序 理解掌握下列定义:
对象、类、封装、继承、多态性 类的三个组成部分: 类名、属性和方法 理解封装与继承的意义,以及与对现实世界抽象的联系 多态性及其作用。 熟悉类的定义、实现和使用 了解对象和类的关系

51 阶段小结(续) 例:输入任意短语或句子,计算该短语包含多少个字(word)和字符(character)。
注意:输入短语时,必须在字和字之间只空一格

52 面向过程的方法 #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’) { //‘\r’表示换行 chcount++; if(ch==' ') wdcount++; } printf("\n***:The number of word is %d\n",wdcount); printf("\n***:The number of char is %d\n",chcount);

53 面向对象方法 #include <iostream.h> #include <conio.h> class count { public: count(); void process(); void display(); private: int wdcount,chcount; }; count::count() wdcount=1; chcount=0; } void count::display() { printf("\n***:The number of word is %d\n",wdcount); char is %d\n",chcount); }

54 ***Please input any phrase... I am a teacher
void count::process() { char ch; cout<<"***:Please input any phrase…"<<endl; while((ch=getche())!='\r') chcount++; if(ch==' ') wdcount++; } cout<<endl; 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 14

55 面向过程与面向对象程序设计比较 面向过程程序设计 面向对象程序设计
*是建立在认识方法学基础上的一项技术科学,比较自然地模拟了人类认识客观世界的方式; *以相对稳定的“对象”和“数据结构”为中心来构造系统 *可重用性较好 *是一种数学思维或计算机思维方法,与人们认识世界的方法不同 *以不稳定的、多变的“过程”和“操作” 为中心来构造系统 *可重用性较差

56 引 例 class Time{ private: int hour; int minute; int second; public:
引 例 class Time{ private: int hour; int minute; int second; public: void setTime(int, int, int) { hour=(h>=0 && h<24) ? h:0; // 设置时间 minute=(m>=0 && m<60) ? m:0; second=(s>=0 && s<60) ? s:0; } void showTime() cout<<hour<<':'<<minute<<':'<<second<<endl; };

57 Time EndTime(12, 23, 36);//初始化 void main() {
C++类 void main() { Time EndTime; // 声明对象EndTime EndTime.setTime(12, 23, 36); // 设置对象EndTime的时间 cout<<"The time is:"; EndTime.showTime(); // 显示对象EndTime的时间 } Time EndTime(12, 23, 36);//初始化

58 构造函数 ? 在定义类时不能对成员变量进行初始化,只能在声明对象之后进行初始化。
C++类 构造函数 在定义类时不能对成员变量进行初始化,只能在声明对象之后进行初始化。 成员变量一般都定义为私有属性,不能在声明对象后利用赋值运算对成员变量进行初始化。 成员变量的初始化一般是利用一个名为构造函数的成员函数来完成。 如何进行成员 变量的初始化?

59 什么是构造函数 构造函数是一种与类名相同的特殊成员函数,在创建类对象时(声明或new动态创建)系统将自动调用的成员函数。 Time t1;
C++类 什么是构造函数 构造函数是一种与类名相同的特殊成员函数,在创建类对象时(声明或new动态创建)系统将自动调用的成员函数。 Time t1; Time *m_t=new Time[10]; Time *m_t2=new Time; Time *m_t3; m_t3=new Time; 每个对象声明语句都隐式调用了构造函数

60 关于构造函数的规定 构造函数的名称与类名相同;
C++类 关于构造函数的规定 构造函数的名称与类名相同; 构造函数不能指定任何返回值类型,包括void返回类型。函数体内也不允许返回值,但允许用无值返回语句“return”(它的返回类型隐含为指向类对象本身的指针this) 构造函数可以有参数也可以无参数。 构造函数可以放在类的外部定义,但也要象其他成员函数一样在函数名前加上“类名::” 。 如果一个类对象是另一个类的数据成员,则在那个类的对象创建时除了调用自己的构造函数外,还会自动调用成员(对象)的构造函数。

61 构造函数举例 运行结果: 构造A 构造B class A { Public: A(){cout<<“构造A”;} };
class B{ A a; B(){ cout<<“构造B”;} void main(){ B b; } 运行结果: 构造A 构造B

62 C++类 默认构造函数 在类定义时若没有定义任何构造函数,编译器自动生成一个不带参数的默认构造函数。当用户定义任何一个构造函数后,默认构造函数自动消失。 在定义对象时,若没有指定参数,将调用默认构造函数或不需要参数的构造函数。

63 默认构造函数举例 运行结果: #include <iostream.h> class A { private:
int m_x; int m_y; public: A() { cout<<"不带参数构造A"<<endl; } A(int x,int y){ m_x=x; m_y=y; cout<<"带参数构造A"<<endl; } }; void main() { A a1; A a2(2,5); } 运行结果: 不带参数构造A 带参数构造A

64 引 例 ? class A { int *m_ip; A(); } A::A() m_ip=new int; void main() {
引 例 class A { int *m_ip; A(); } A::A() m_ip=new int; void main() { A a; } 这样做 有没有问题?

65 什么是析构函数 一个类可能在构造函数里分配资源,这些资源需要在对象不复存在以前被释放。例如:
C++类 什么是析构函数 一个类可能在构造函数里分配资源,这些资源需要在对象不复存在以前被释放。例如: 如果构造函数打开了一个文件,文件就需要被关闭; 构造函数从堆里分配了内存,这块内存也必须在对象消失前被释放。 析构函数允许类自动完成这些清理工作,它是一种特殊的成员函数,它是在对象生存期结束时系统自动调用的成员函数。

66 析构函数举例 class XYZ { public: XYZ(){name=new char[20];} ~XYZ()
delete []name; } protected: char * name;

67 关于析构函数的规定 析构函数的名称必须在类名前加上“~”符号。 析构函数也不能指定任何返回值类型,包括void返回类型。
C++类 关于析构函数的规定 析构函数的名称必须在类名前加上“~”符号。 析构函数也不能指定任何返回值类型,包括void返回类型。 析构函数没有参数,不能随意调用,也没有重载。它只能在类对象生命周期结束时,由系统自动调用。 析构函数以调用构造函数相反的顺序被调用。

68 例 为类Time添加构造函数和析构函数。 #include <iostream.h> class Time{ private:
int hour; int minute; int second; public: Time(int, int, int); // 构造函数 ~Time(); // 析构函数 };

69 构造函数和析构函数的实现 Time::Time(int h, int m, int s) { hour=h; // 对私有成员变量初始化
C++类 构造函数和析构函数的实现 Time::Time(int h, int m, int s) { hour=h; // 对私有成员变量初始化 minute=m; second=s; cout<<"The constructor be called: "<<hour<<':’ <<minute<<':'<<second<<endl; } Time::~Time() cout<<"The destructor be called: "<<hour<<':’

70 构造函数和析构函数的自动调用 ? void main(void) { Time t1(10, 35, 55) ; // 自动调用构造函数
C++类 构造函数和析构函数的自动调用 void main(void) { Time t1(10, 35, 55) ; // 自动调用构造函数 Time t2(16, 53, 9) ; // 自动调用构造函数 } // 退出main()主函数时自动调用析构函数 为什么是 这个结果? 程序运行结果为: The constructor be called:10:35:55 The constructor be called:16:53:9 The destructor be called:16:53:9 The destructor be called:10:35:55

71 栈:后进先出表 创建对象的过程: 撤消对象的过程: 系统根据类定义的成员变量为对象分配内存空间
C++类 创建对象的过程: 撤消对象的过程: 系统根据类定义的成员变量为对象分配内存空间 系统先自动调用对象的析构函数,释放对象成员所占资源 自动调用对象的构造函数,申请对象成员需要的资源 释放对象所占内存空间 栈:后进先出表

72 C++类 强调几点 与一般数据类型的变量相比,对象在它的生存期会有大量的操作,有时这些操作的结果必须在对象的生存期结束时加以清理。因此可以在析构函数中进行动态分配的内存清理工作。 如果定义类时没有提供构造函数和析构函数,编译系统将会自动为类分别添加一个缺省的构造函数和析构函数。如果用户加上自定义的构造函数和析构函数,编译系统将不会再添加缺省的构造函数和析构函数。 构造函数可以有参数也可以没有参数。若构造函数无参数,则声明对象时也不能给出参数。

73 C++类 类的应用举例 一圆型游泳池如图所示,现在需在其周围建一圆型过道,并在其四周围上栅栏。栅栏价格为35元/米,过道造价为20元/平方米。过道宽度为3米,游泳池半径由键盘输入。要求编程计算并输出过道和栅栏的造价。 过道 游泳池 游泳池

74 #include <iostream.h> const float PI = 3.14159;
const float FencePrice = 35; const float ConcretePrice = 20; //声明类Circle 及其数据和方法 class Circle { private: float radius; public: Circle(float r); //构造函数 float Circumference() const; //圆周长 float Area() const; //圆面积 };

75 Circle::Circle(float r) { radius=r;} // 计算圆的周长
// 类的实现 // 构造函数初始化数据成员radius Circle::Circle(float r) { radius=r;} // 计算圆的周长 float Circle::Circumference() const { return 2 * PI * radius; } // 计算圆的面积 float Circle::Area() const return PI * radius * radius;

76 float FenceCost, ConcreteCost; // 提示用户输入半径
void main () { float radius; float FenceCost, ConcreteCost; // 提示用户输入半径 cout<<"Enter the radius of the pool: "; cin>>radius; // 声明 Circle 对象 Circle Pool(radius); Circle PoolRim(radius + 3);

77 FenceCost = PoolRim.Circumference() * FencePrice;
// 计算栅栏造价并输出 FenceCost = PoolRim.Circumference() * FencePrice; cout << "Fencing Cost is ¥" << FenceCost << endl; // 计算过道造价并输出 ConcreteCost = (PoolRim.Area() - Pool.Area())*ConcretePrice; cout << "Concrete Cost is ¥" << ConcreteCost << endl; } 运行结果 Enter the radius of the pool: 10 Fencing Cost is ¥ Concrete Cost is ¥

78 void setTime (int hour,int minute,int second); void showTime(); };
class CTimeClass { private: int hour; int minute; int second; public: void setTime (int hour,int minute,int second); void showTime(); }; 类成员变量与函数的形参重名,该如何用形参给成员变量赋值? 第五周开始

79 void CTimeClass::setTime(int hour,int minute,int second) {
this->hour=hour; this-> minute=minute; this-> second=second; } void CTimeClass::showTime() cout<<hour<<":"<<minute<<":"<<second<<endl;

80 this指针 this指针是一个特殊的隐藏在对象中的指针,每一个处于生存期的对象都有一个this指针,用于指向对象本身。
C++类 this指针 this指针是一个特殊的隐藏在对象中的指针,每一个处于生存期的对象都有一个this指针,用于指向对象本身。 当类的某个非静态成员函数被调用时,系统通过this指针确定是哪一个对象的该成员函数被调用。实际上,this指针总是作为一个隐含参数传递给类的每一个成员函数。

81 下面定义的成员函数并没有声明this参数
C++类 下面定义的成员函数并没有声明this参数 void Time::showTime() { cout<<hour<<':'<<minute<<':'<<second<<endl; } 编译器会把this指针作为成员函数的参数: void Time::showTime(Time* this); { cout<<this->hour<<':'<<this->minute<<':’ <<this->second<<endl; }

82 调用时: 当程序中调用某个成员函数时,编译器会把该对象的地址赋值给this指针,并将该地址值加入到参数表中,如下所示:
C++类 调用时: 当程序中调用某个成员函数时,编译器会把该对象的地址赋值给this指针,并将该地址值加入到参数表中,如下所示: 执行语句: EndTime.showTime(); 编译器调用:EndTime.showTime(&EndTime);

83 C++类 This指针的作用: 在一个成员函数中经常需要调用其它函数(非本类的成员函数),而有时需要把对象本身(即对象的地址)作为参数传递给被调用函数,这时必须使用this指针。

84 例 this指针的使用。 #include <iostream.h> #include <string.h>
class Person{ public: // 可在外部直接访问public属性的数据成员 char m_strName[20]; char m_ID[18]; public: Person(char* strName, char* ID) // 内联构造函数 { strcpy(m_strName, strName); strcpy(m_ID, ID); } void Show(); };

85 如果没有this指针如何解决? void Display(Person* pObj) // 非成员函数 {
C++类 void Display(Person* pObj) // 非成员函数 { cout<<"Name:"<<pObj->m_strName<<endl <<"ID:"<<pObj->m_ID<<endl; } void Person::Show() Display(this); // 以this指针作为参数调用其它函数 void main(void) Person *pPerson = // new运算也调用构造函数 new Person("LiMing"," "); pPerson->Show(); // 通过调用Show调用Display 如果没有this指针如何解决?

86 例子回顾 #include <iostream.h> void fun() { // 静态变量a是局部变量,但具有全局的生存期
static int a=5; a++; cout<<"a="<< a << endl; } void main () for ( int i =0 ; i < 2; i ++ ) fun(); 程序输出: a=6 a=7

87 静态成员 静态成员分为静态数据成员和静态成员函数。
C++类 静态成员 静态成员分为静态数据成员和静态成员函数。 静态数据成员类似于一般的static静态变量,它具有全局性。静态数据成员属于整个类,为类的所有对象共享。 无论类的对象有多少,类的静态数据成员只有一份,存储在同一个内存空间。即使没有创建类的一个对象,类的静态数据成员也是存在的。 使用静态数据成员保证了该数据成员值的唯一性。

88 类所创建的所有对象共享静态成员的一个拷贝。静态成员属于类,不属于类的对象
C++类 static 一般情况下,同一个类不同对象的数据成员所占用的内存空间是不同的(体现了不同对象具有不同的属性值)。在有些情况下,类的数据成员的值对每个对象都是相同的,如当前已创建对象的数量,这时可以将该数据成员声明为静态数据成员(占有相同的存储单元)。 类所创建的所有对象共享静态成员的一个拷贝。静态成员属于类,不属于类的对象

89 静态数据成员 静态数据成员的的赋值必须放在类外的全局范围内进行,不能在构造函数中对其进行赋初值。
C++类 静态数据成员 静态数据成员的的赋值必须放在类外的全局范围内进行,不能在构造函数中对其进行赋初值。 在类的外部赋初值时,必须同时指明数据类型和所属的类。

90 静态数据成员 静态成员的声明:放在类定义的内部 静态成员的初始化:放在类定义的外部 static int m_nCount; public:
int Person :: m_nCount=0;

91 静态数据成员的访问 公有静态成员:三种方式 (1)通过对象访问,如: person1.m_nCount=100;
(2)利用类名和作用域限定符(::)访问,如: int Person::m_nCount=100; // 初始化 (3)在成员函数中访问,如: m_nCount++; 私有和保护静态成员:只能在成员函数中访问

92 静态成员函数 成员函数也可以是静态的,其声明方式与静态成员变量类似。如: public:
static int GetCount(); // 获取静态数据成员 静态成员函数也与一个类相关联,而不只与一个特定的对象相关联。 区别于非静态成员函数,静态成员函数没有this指针,因为类的静态成员函数只有一个运行实例。 静态成员函数一般是公有属性,可以通过对象、类名和作用域限定符、在成员函数中三种方式调用静态成员函数。---与公有属性的静态数据成员类似。

93 例 静态成员变量和静态成员函数的使用。 #include <iostream.h>
#include <string.h> class Person{ public: char m_strName[20]; long m_ID; static int m_nCount; // 静态成员变量,表示已创建对象的数量 Person(char*, long); // 构造函数 static int GetCount(); // 静态成员函数 static long GetID(Person); // 对象作为静态成员函数的参数 };

94 Person::Person(char* strName, long ID) { strcpy(m_strName, strName);
m_ID=ID; m_nCount++; // 对象数目加1 } int Person::GetCount() return m_nCount; // 访问静态成员变量 long Person::GetID(Person x) return x.m_ID; // 通过对象访问非静态成员m_ID

95 int Person::m_nCount=0; // 初始化静态成员变量 void main() {
Person e1("LiuJun", ); cout<<Person::m_nCount<<" , "<<e1.m_nCount <<'\n'; // 通过类或对象访问静态成员变量 cout<<Person::GetCount()<<" , ” <<Person::GetID(e1)<<'\n'; // 通过类调用静态成员函数 cout<<e1.GetCount()<<" , "<<e1.GetID(e1)<<'\n'; // 通过对象调用静态成员函数

96 Person e2("WangXiaogang",1101058);
C++类 Person e2("WangXiaogang", ); cout<<Person::GetCount()<<" , ” <<Person::GetID(e2)<<'\n'; cout<<e2.GetCount()<<" , "<<e2.GetID(e2)<<'\n'; cout<<e1.GetCount()<<" , "<<e1.GetID(e1)<<'\n'; // e1和e2共享静态成员变量m_nCount } 程序运行结果为: 1,1 1, 2, 2,

97 将对象作为静态成员函数的参数,然后在静态成员函数中通过对象访问它的非静态成员。
C++类 注 意 静态成员函数只能访问类的静态成员(成员变量和成员函数),而不能访问类的非静态成员。因为当通过类名和运算符“::”调用一个静态成员函数时,不能确定函数中所访问的非静态成员属于哪一个对象。 静态成员函数访问对象的方法: 将对象作为静态成员函数的参数,然后在静态成员函数中通过对象访问它的非静态成员。

98 C++类 友元的种类 友元函数 友元成员 友元类

99 C++类 友元函数 C++提供了一种函数,它虽然不是一个类的成员函数,但可以象成员函数一样访问该类的所有成员,包括私有成员和保护成员。这种函数称为友元(friend)函数。 友元函数的声明 一个函数要成为一个类的友员函数,需要在类的定义中声明该函数,并在函数声明的前面加上关键字friend。 友元函数本身的定义没有什么特殊要求,可以是一般函数,也可以是另一个类的成员函数(这时也称为友元成员)。 为了能够在友元函数中访问并设置类的私有数据成员,一个类的友元函数一般将该类的引用作为函数参数。 类具有封装性,类的私有成员一般只能通过该类的成员函数访问,这种封装性隐藏了对象的数据成员,保证了对象的安全,但有时带来了编程的不方便。

100 class 1 class 2 friend friend 函数 friend friend class 3 class 4
类具有封装性,类的私有成员一般只能通过该类的成员函数访问,这种封装性隐藏了对象的数据成员,保证了对象的安全,但有时带来了编程的不方便。 friend class 3 class 4

101 举 例 class A { friend void display(A); // 友元函数是一个一般函数
举 例 class A { friend void display(A); // 友元函数是一个一般函数 friend void B::BMemberFun(A&); // 友元函数是另一个类B的成员函数(友元成员) public: }

102 C++类 友元类 友元的另一种类型是友元类,一个类可以声明另一个类为其友元类,这个友元类(另一个类)的所有成员函数都可以访问声明其为友元的类(第一个类)的所有成员。 由于访问权限控制符不影响友元声明,友元声明可放在类体中任何地方,建议把友元声明放在类体的开始位置。 例:友元(一般友元函数、友元成员函数和友元类)的声明和使用。 P78-79,例3-7。

103 关于友元的几点说明 友元关系是单方向的,不具有交换性和传递性。 友元本身不是类的成员。 友元可以访问类的私有成员和保护成员。
C++类 关于友元的几点说明 友元关系是单方向的,不具有交换性和传递性。 友元本身不是类的成员。 友元可以访问类的私有成员和保护成员。 使用友元虽然简化了编程,并可避免调用成员函数的开销,但破坏了类的封装性,建议谨慎使用。

104 类的继承 继承与派生问题举例

105 类的继承 继承与派生问题举例

106 继承与派生的目的 继承的目的:实现代码重用。 派生的目的:当新的问题出现,原有程序无法解决(或不能完全解决)时,需要对原有程序进行改造。
类的继承 继承与派生的目的 继承的目的:实现代码重用。 派生的目的:当新的问题出现,原有程序无法解决(或不能完全解决)时,需要对原有程序进行改造。

107 基类和派生类的概念 在继承关系中,新定义的类称为被继承类的派生类或子类,而被继承的类称为新定义类的基类或父类。派生类继承了基类的所有成员。
类的继承 基类和派生类的概念 在继承关系中,新定义的类称为被继承类的派生类或子类,而被继承的类称为新定义类的基类或父类。派生类继承了基类的所有成员。 一个派生类也可以作为另一个派生类的基类。

108 派生类的定义 class <派生类名> : [<派生方式>] <基类名> {
类的继承 派生类的定义 class <派生类名> : [<派生方式>] <基类名> { // 派生类新增加的成员声明列表 }; 说明: 派生方式决定了基类的成员在派生类中的访问权限。派生方式共有三种:public、private和protected(缺省值为private)。 虽然派生类继承了基类的所有成员,但为了不破坏基类的封装性,无论采用哪种派生方式,基类的私有成员在派生类中都是不可见的,即不允许在派生类的成员函数中访问基类的私有成员。

109 class CStudent : public CPerson
类的继承 class CPerson { private: char m_name[12]; char m_ID[18]; int m_age; public: //…… }; class CStudent : public CPerson char m_student[22];//学校 char m_number[18];//学号 // ……

110 类的继承 类的访问权限控制回顾 public属性:表示数据成员和成员函数是类的公有成员,它们允许被本类的成员函数访问或调用,也允许通过对象访问或调用,是类的外部接口。 protected属性:表示数据成员和成员函数是类的保护成员,它们允许被本类的成员函数和派生类的成员函数访问或调用。 private属性:表示数据成员和成员函数是类的私有成员,它们只允许被本类的成员函数访问或调用。

111 类的继承 三种派生方式的区别 采用public派生,基类成员的访问权限在派生类中保持不变,即基类所有的公有或保护成员在派生类中仍为公有或保护成员。public派生最常用。 可以在派生类的成员函数中访问基类的非私有成员; 可通过派生类的对象直接访问基类的公有成员。 采用protected保护派生,基类所有的公有和保护成员在派生类中都成为保护成员,只允许在派生类的成员函数和该派生类的派生类的成员函数中访问基类的非私有成员。 采用private私有派生,基类所有的公有和保护成员在派生类中都成为私有成员,只允许在派生类的成员函数中访问基类的非私有成员。private派生很少使用。

112 例 定义类Point,然后定义类Point的派生类Circle。
类的继承 例 定义类Point,然后定义类Point的派生类Circle。 #include <iostream.h> class Point // 定义基类,表示点 { private: int x; int y; public: void setPoint(int a, int b) { x=a; y=b; }; // 设置坐标 int getX() { return x; }; // 取得X坐标 int getY() { return y; }; // 取得Y坐标 };

113 class Circle : public Point // 定义派生类,表示圆 { private: int radius;
类的继承 class Circle : public Point // 定义派生类,表示圆 { private: int radius; public: void setRadius(int r) { radius=r; }; // 设置半径 int getRadius() { return radius; }; // 取得半径 int getUpperLeftX() { return getX()-radius; }; // 取得外接正方形左上角的X坐标 int getUpperLeftY() { return getY() + radius; }; // 取得外接正方形左上角的Y坐标 };

114 程序运行结果: X=200,Y=250,Radius=100 UpperLeft X=100,UpperLeft Y=350
类的继承 公有派生类的对象可以直接访问基类Point的公有成员 main() { Circle c; c.setPoint(200, 250); c.setRadius(100); cout<<"X="<<c.getX()<<", Y="<<c.getY() <<", Radius="<<c.getRadius()<<endl; cout<<"UpperLeft X="<<c.getUpperLeftX() <<", UpperLeft Y="<<c.getUpperLeftY()<<endl; } 程序运行结果: X=200,Y=250,Radius=100 UpperLeft X=100,UpperLeft Y=350

115 类的继承 几点说明 派生类Circle通过public派生方式继承了基类Point的所有成员(除私有成员外所有成员的访问权限不变),同时还定义了自己的成员变量和成员函数。 若将类Circle的派生方式改为private或protected,则下述语句是非法的:c.setPoint(200, 250); 无论哪种派生方式,派生类都继承了基类的所有成员,包括私有成员。我们虽然不能在派生类Circle中直接访问私有数据成员x和y,但可以通过继承的公有成员函数getX()、getY()和setPoint()访问或设置它们。

116 类的继承 类成员的重新定义问题 利用类继承定义类可能带来一个问题:派生类会继承它不需要的基类中的数据成员和成员函数,这时,基类中不适合于派生类的成员可以在派生类中重新加以定义。

117 例 派生类成员函数对基类成员函数的覆盖。 #include <iostream.h> class A { public:
类的继承 例 派生类成员函数对基类成员函数的覆盖。 #include <iostream.h> class A { public: void Show( ) { cout<<"A::Show\n"; }; }; class B : public A void Show( ) { cout<<"B::Show\n"; }; // 在派生类中重新定义成员函数 void Display() { Show( ); }; // 调用派生类B的成员函数Show() 如果想调用基类A的成员函数Show(),可以使用作用域限定符“::”:{A:: Show();};

118 ? void main() { A a; B b; a.Show(); // 调用基类A的成员函数 Show()
类的继承 void main() { A a; B b; a.Show(); // 调用基类A的成员函数 Show() b.Show(); // 调用派生类B的成员函数Show() b.Display(); } 如果类B中没有对 Show() 进行重定义, 程序运行结果如何 从本例可以看出,虽然派生类继承了基类的所有成员函数,但如果派生类某个成员函数的名称和参数与基类成员函数一致(即在派生类中对该成员函数重新进行了定义),则在派生类中调用的成员函数是派生类的成员函数。 程序运行结果: A::Show B::Show

119 类的继承 继承的重要性 为什么我们经常在现有类的基础上采用继承的方法来定制新类,而不通过直接修改现有类来设计自己的类?除了代码重用的优越性,其主要原因是可能得不到基类的实现源码。------基类并不是自己写的,手头只有头文件和含有成员函数目标代码的OBJ文件。 在利用微软基础类MFC派生自己的类时,我们只需要MFC类声明的头文件(利用#include指令将头文件包含)和含有成员函数目标代码的OBJ文件,并不需要整个MFC类库的实现源码。

120 基类和派生类的构造函数 问题的提出 一个派生类对象也属于其基类,因此当程序创建一个派生类对象时,系统首先自动创建一个基类对象。
类的继承 基类和派生类的构造函数 问题的提出 一个派生类对象也属于其基类,因此当程序创建一个派生类对象时,系统首先自动创建一个基类对象。 在调用派生类的构造函数构建派生类对象时,系统首先调用基类的构造函数构建基类对象。当派生类对象的生存期结束时,首先调用派生类的析构函数,然后调用基类的析构函数。 编译器在对程序编译时,首先生成基类构造函数的调用代码,然后生成派生类构造函数的调用代码。

121 基类构造函数的调用方式 ! 隐式调用和显式调用两种方式:
类的继承 基类构造函数的调用方式 隐式调用和显式调用两种方式: 隐式方式是指在派生类的构造函数中不指定对应的基类的构造函数,调用的是基类的默认构造函数(即含有缺省参数值或不带参数的构造函数)。 显式方式是指在派生类的构造函数中指定要调用的基类构造函数,并将派生类构造函数的部分参数值传递给基类构造函数。 注意:除非基类有默认的构造函数,否则必须采用显式调用方式。

122 显式方式构造函数的定义 设类B是类A的派生类,则派生类B显式方式构造函数的定义形式如下:
类的继承 显式方式构造函数的定义 设类B是类A的派生类,则派生类B显式方式构造函数的定义形式如下: B::B( <形参声明> ) : A( <参数表> ) { // 类B构造函数的实现代码 } 派生类构造函数既初始化派生类的数据成员,又通过基类构造函数初始化其基类的数据成员。 参数表中参数的个数和类型要与基类某个构造函数的形参声明一致。 形参声明中的部分参数,传递给基类构造函数 派生类构造函数形参的名称和类型

123 类的继承 注 意 当基类有多个构造函数时,编译器根据派生类构造函数为基类构造函数提供的参数表来确定调用基类的哪一个构造函数。

124 例 首先定义类Point,然后定义类Point的派生类Circle,再定义类Circle的派生类Cylinder。
类的继承 例 首先定义类Point,然后定义类Point的派生类Circle,再定义类Circle的派生类Cylinder。 (x, y) r h (x, y) r (x, y) Point Circle Cylinder

125 #include <iostream.h> class Point // 定义基类Point { protected:
类的继承 #include <iostream.h> class Point // 定义基类Point { protected: int x, y; public: Point(int a=0, int b=0) { // 含有缺省参数值的构造函数也是默认的构造函数 x=a; y=b; cout<<"Point constructor:"<<'['<<x<<','<<y<<'] endl; }; ~Point() { cout<<"Point destructor:"<<'['<<x<<','<<y<<']'<<endl;

126 class Circle : public Point // 定义类Point的派生类 { protected: int radius;
类的继承 class Circle : public Point // 定义类Point的派生类 { protected: int radius; public: // 显式调用基类的构造函数 Circle(int a=0, int b=0, int r=0) : Point(a, b) radius=r; cout<<"Circle constructor:"<<'['<<radius<<']'<<'[' <<x<<','<<y<<']'<<endl; }; ~Circle() { cout<<"Circle destructor:"<<'['<<radius<<']'<<'[‘

127 class Cylinder : public Circle // 定义类Circle的派生类 { protected:
类的继承 class Cylinder : public Circle // 定义类Circle的派生类 { protected: int height; public: // 显式调用基类的构造函数 Cylinder(int a=0, int b=0, int r=0, int h=0) : Circle(a, b, r) height=h; cout<<"Cylinder constructor:"<<'['<<height<<']'<<'[‘ <<radius<<']‘<<'['<<x<<','<<y<<']'<<endl; }; ~Cylinder() { cout<<"Cylinder destructor:"<<'['<<height<<']'<<'[‘

128 // 调用了类Point、Circle和Cylinder的构造函数 }
类的继承 main() { Cylinder cylinder(200, 300, 100, 400); // 调用了类Point、Circle和Cylinder的构造函数 } 程序运行结果: Point constructor:[200,300] Circle constructor:[100] [200,300] Cylinder constructor:[400] [100] [200,300] Cylinder destructor:[400] [100] [200,300] Circle destructor:[100] [200,300] Point destructor:[200,300]

129 构造函数的执行顺序: 析构函数的执行顺序: Point( ) ~ Cylinder( ) Circle( ) ~ Circle( )
类的继承 构造函数的执行顺序: 析构函数的执行顺序: Point( ) ~ Cylinder( ) Circle( ) ~ Circle( ) Cylinder( ) ~ Point( ) 当声明Cylinder对象时 当程序结束时

130 指出下面程序中的错误并改正 Line1: class MyClass Line2: { Line3: public:
面向对象程序设计 指出下面程序中的错误并改正 Line1: class MyClass Line2: { Line3: public: Line4: MyClass(int i) {member=i;} Line5: void SetMember(int m) {member=m;} Line6: int GetMember() const {return member;} Line7: private: Line8: int member; Line9: }; Line10: void main() Line11: { Line12: MyClass obj1; Line13: MyClass obj2(3); Line14: obj1.member=5; Line15: obj2.SetMember(10); Line16: }

131 指出下面程序中的错误并说明改正后的运行结果
面向对象程序设计 指出下面程序中的错误并说明改正后的运行结果 #include <string.h> #include <IOSTREAM.H> class CPerson{ private: char m_name[12]; char m_ID[18]; int m_age; public: CPerson(char name[],char ID[],int age) { strcpy(m_name,name); strcpy(m_ID,ID); m_age=age; cout<<m_name<<" CPerson'CON "<<endl; } CPerson() { strcpy(m_name,"test"); ~CPerson() { cout<<m_name<<" CPerson'DES "<<endl; };

132 class CStudent : public CPerson{ private: char m_school[22];//学校
面向对象程序设计 class CStudent : public CPerson{ private: char m_school[22];//学校 char m_number[18];//学号 public: CStudent(char name[],char ID[],int age,char school[], char number[] ) :CPerson(name,ID,age) { strcpy(m_school,school); strcpy(m_number,number); } CStudent() { cout<<m_name<<" CStudent'CON "<<endl; ~CStudent() { cout<<m_name<<" CStudent'DES "<<endl; }; void main(){ CStudent st1; CStudent st2("Mike","12345",18,"武汉大学","23456"); 第五周结束

133 派生类举例 编写一个学生和教师数据输入和显示程序。学生数据有编号、姓名、班号和成绩,教师数据有编号、姓名、职称和部门。 类的继承
将编号、姓名输入和显示设计成CPerson类,作为基类(要求使用类的继承实现)

134 单继承和多重继承的概念 class A class A class B class B class C class C
类的继承 单继承和多重继承的概念 class A class A class B class B class C 第六周 class C 一个派生类同时从多个基类派生而来,即有多个直接基类 —— 多重继承 每个派生类只有一个直接基类 —— 单继承

135 多重继承派生类的定义 class <派生类名> : [<派生方式1>] <基类1>,
类的继承 多重继承派生类的定义 class <派生类名> : [<派生方式1>] <基类1>, [<派生方式2>] <基类2>, … , [<派生方式n>] <基类n> { <派生类新定义的成员> }; 多重继承的派生方式也有private、public和protected三种,各基类的派生方式可以不同

136 多重继承举例 class A class B { { public: public: A(int i){ B(int i){ a=i;
类的继承 多重继承举例 class A { public: A(int i){ a=i; cout<<"A Constructor"; cout<<endl; } ~A(){ cout<<"A Destructor"; void display(){ cout<<"a="<<a<<endl; private: int a; }; class B { public: B(int i){ b=i; cout<<"B Constructor"; cout<<endl; } ~B(){ cout<<"B Destructor"; void display(){ cout<<"b="<<b<<endl; private: int b; };

137 B::display(); class C:public B,public A { public:
类的继承 class C:public B,public A { public: C(int k):A(k+2),B(k-2) c=k; cout<<"C Constructor"; cout<<endl; } ~C( ){ cout<<"C Destructor"; void display( ){ A::display(); B::display(); cout<<"c="<<c<<endl; private: int c; }; void main() { C obj(10); obj.display(); } 运行结果: B Constructor A Constructor C Constructor a=12 b=8 c=10 C Destructor B Destructor A Destructor

138 多重继承中的二义性问题 ? class A { public: int a; }; class B : public A int b;
类的继承 多重继承中的二义性问题 class A { public: int a; }; class B : public A int b; class C : public A int c; class D : public B, public C // 类D派生于类B和类C { public: int d; }; main() D d1; d1.a=100; } 二义性错误: 编译器无法确定数据成员a是哪一个副本

139 派生类D的对象中存在间接基类A的两份副本
类的继承 class A D B A C class B class C class D 派生类D的对象中存在间接基类A的两份副本

140 解决方法 缺点: 浪费了存储空间,大部分情况下不需要保存基类多个相同的副本。 在访问基类的成员时,要求指明访问路径。
类的继承 解决方法 利用作用域限定符(::)把基类的成员与下一层基类关联起来: d1.B::a=100; 或: d1.C::a=100 从路径D→B→A继承而来 从路径D→C→A继承而来 缺点: 浪费了存储空间,大部分情况下不需要保存基类多个相同的副本。 在访问基类的成员时,要求指明访问路径。

141 类的继承 使用虚基类 class B class C class D class A D A B C

142 虚基类 虚基类并不是一种新的类型的类,而是一种派生方式。
类的继承 虚基类 虚基类并不是一种新的类型的类,而是一种派生方式。 采用虚基类方式定义派生类,在创建派生类的对象时,类层次结构中虚基类的成员只出现一次,即基类的一个副本被所有派生类对象所共享。

143 虚基类派生方式的定义 采用虚基类方式定义派生类的方法是在基类的前面加上关键字virtual,而定义基类时与一般基类完全一样。
类的继承 虚基类派生方式的定义 采用虚基类方式定义派生类的方法是在基类的前面加上关键字virtual,而定义基类时与一般基类完全一样。 class <派生类名> :virtual <派生方式> <共同基类名>

144 √ 例 采用virtual虚基类方式定义派生类。 class A { public: int a; };
类的继承 例 采用virtual虚基类方式定义派生类。 class A { public: int a; }; class B : virtual public A int b; class C : virtual public A int c; class D : public B, public C // 类D派生于类B和类C { public: int d; }; main() D d1; d1.a=100; }

145 虚基类的初始化 虚基类的初始化与一般多继承的初始化在语法上相同,但构造函数的调用顺序有所不同,规则如下:
类的继承 虚基类的初始化 虚基类的初始化与一般多继承的初始化在语法上相同,但构造函数的调用顺序有所不同,规则如下: 先调用虚基类的构造函数,再调用非虚基类的构造函数。 若同一层次包含多个虚基类,其调用顺序为定义时的顺序。 若虚基类由非虚基类派生而来,则仍按先调用基类构造函数,再调用派生类构造函数的顺序。

146 引入虚基类后构造函数的调用顺序举例 运行结果: class Base1 class Base2 class Level2
类的继承 引入虚基类后构造函数的调用顺序举例 #include <iostream.h> class Base1 { public: Base1() { cout<<"class Base1"; cout<<endl; } }; class Base2 Base2() { cout<<"class Base2"; cout<<endl; } class Level1:public Base2,virtual public Base1 Level1(){ cout<<"class Level1"; cout<<endl; } class Level2:public Base2,virtual public Base1 { public: Level2(){ cout<<"class Level2"; cout<<endl; } }; class TopLevel:public Level1,virtual public Level2 TopLevel(){ cout<<"class TopLevel"; void main() TopLevel obj; 运行结果: class Base1 class Base2 class Level2 class Level1 class TopLevel

147 类的继承 使用虚基类派生方式的好处 节约内存空间; 避免在多重派生类中类成员的不明确性。

148 类的继承小结 继承是基类创建一个派生类的过程; 继承是实现代码重用的重要手段;
派生类的生成经历了三个过程:吸收基类的成员、改造基类的成员和添加派生类新成员; 三种继承方式:公有、私有和保护继承; 基类构造函数的调用:显示和隐式; 继承分单继承和多继承。

149 多态性 多态性是指类中同一函数名对应多个具有相似功能的不同函数,可以使用相同的调用方式来调用这些具有不同功能的同名函数的特性。
多态性和虚函数 多态性 多态性是指类中同一函数名对应多个具有相似功能的不同函数,可以使用相同的调用方式来调用这些具有不同功能的同名函数的特性。 在C++程序中,多态性表现为同一种调用方式完成不同的处理。 从实现角度来划分,多态可分为编译时多态和运行时多态。 编译时多态是指在编译阶段由编译系统根据操作数据确定调用哪个同名函数。 运行时多态是指在运行阶段才根据产生的信息确定需要调用哪个同名的函数。

150 联 编 C++通过采用联编技术来支持多态性。 多态性实现过程中,确定调用哪个同名函数的过程就是联编,又称绑定。
多态性和虚函数 联 编 C++通过采用联编技术来支持多态性。 多态性实现过程中,确定调用哪个同名函数的过程就是联编,又称绑定。 联编是指计算机程序自身彼此关联的过程,也就是将函数调用语句与函数代码相关联。 两种联编方式:静态联编和动态联编。 静态联编是指编译器在编译阶段就确定了要调用的函数,即早期绑定。重载采用静态联编方式。 动态联编是指在程序执行过程中根据具体情况再确定要调用的函数,即后期绑定。当通过基类指针调用虚函数时,C++采用动态联编方式。虚函数体现出一种动态多态性或运行时多态性。

151 静态联编 在编译阶段完成的联编称为静态联编。在编译过程中,编译系统可根据参数不同来确定哪一个同名函数。
多态性和虚函数 静态联编 在编译阶段完成的联编称为静态联编。在编译过程中,编译系统可根据参数不同来确定哪一个同名函数。 函数重载和运算符重载就是通过静态联编方式实现编译时多态性的体现。 优点:函数调用速度快,效率高。 缺点:编程不 够灵活。

152 静态联编举例 #include <iostream.h> class Student { public:
多态性和虚函数 静态联编举例 #include <iostream.h> class Student { public: void print() cout<<"A Student"<<endl; } }; class GStudent:public Student cout<<"A graduate Student<<endl;

153 静态联编举例(续) 希望调用对象s2的输出函数 ,但是实际调用的却是对象s1的输出函数。 void main() {
多态性和虚函数 静态联编举例(续) void main() { Student s1,*ps; GStudent s2; s1.print(); s2.print(); s2.Student::print( ); ps=&s1; ps->print( ); ps=&s2; } 运行结果: A Student A graduate Student 希望调用对象s2的输出函数 ,但是实际调用的却是对象s1的输出函数。

154 多态性和虚函数 静态联编举例(续) 说明: 基类指针ps指向派生类对象s2时并没有调用派生类的print(),而仍然调用基类的print(),这是因为静态联编的结果。 在程序编译阶段,基类指针ps对print()的操作只能绑定到基类的print()上,导致程序输出了不期望的结果。而期望的是执行派生类的print函数。

155 用基类指针指向派生类对象 声明一个派生类的对象的同时也自动声明了一个基类的对象。
多态性和虚函数 用基类指针指向派生类对象 声明一个派生类的对象的同时也自动声明了一个基类的对象。 派生类的对象可以认为是其基类的对象。C++允许一个基类对象的指针指向其派生类的对象 —— 这是实现虚函数的关键 不允许派生类对象的指针指向其基类的对象。 即使将一个基类对象的指针指向其派生类的对象,通过该指针也只能访问派生类中从基类继承的公有成员,不能访问派生类自定义的成员,除非通过强制类型转换将基类指针转换为派生类指针。

156 例 基类指针与派生类指针之间的相互转换。 class B : public A class A { { private: private:
多态性和虚函数 例 基类指针与派生类指针之间的相互转换。 class B : public A { private: int b; public: void setB(int i) { b=i; } void showB() { cout<<"b="<<b<<'\n'; } } ; class A { private: int a; public: void setA(int i) { a=i; } void showA() { cout<<"a="<<a<<'\n'; } };

157 pa=&b; // 基类指针pa指向派生类对象b // 通过基类指针pa访问B中从基类A继承的公有成员 pa->setA(100);
多态性和虚函数 void main() { A a, *pa; // pa为基类对象的指针 B b, *pb; // pb为派生类对象的指针 pa=&b; // 基类指针pa指向派生类对象b // 通过基类指针pa访问B中从基类A继承的公有成员 pa->setA(100); pa->showA(); pb=(B*)pa; // 将基类指针强制转化为派生类指针 // 不能通过基类指针pa访问派生类自己定义的成员 pb->setB(200); pb->showB(); } pb=&a 或者 pa->setB() pa->showB() 程序运行结果为: a=100 b=200

158 动态联编 有些联编工作无法在编译阶段准确完成 ,只有在运行程序时才能确定将要调用的函数。这种在运行阶段进行的联编称为动态联编。
多态性和虚函数 动态联编 有些联编工作无法在编译阶段准确完成 ,只有在运行程序时才能确定将要调用的函数。这种在运行阶段进行的联编称为动态联编。 C++规定,动态联编通过继承和虚函数来实现。 优点:提供了更好的编程灵活性、问题抽象性和程序易维护性。 缺点:与静态联编相比,函数调用速度慢。

159 为什么要引入虚函数 ? class A { public: void Show( )
多态性和虚函数 为什么要引入虚函数 class A { public: void Show( ) { cout<<"A::Show\n"; }; }; class B : public A { public: void Show( ) { cout<<"B::Show\n"; }; }; void main() { A *pa; B b; pa=&b; pa->Show(); } 调用哪一个Show() 如果想通过基类指针调用派生类中覆盖的成员函数,只有使用虚函数。

160 虚函数 虚函数是动态联编的基础,是非静态的成员函数,经过派生之后,虚函数在类族中可以实现运行时多态。
多态性和虚函数 虚函数 虚函数是动态联编的基础,是非静态的成员函数,经过派生之后,虚函数在类族中可以实现运行时多态。 虚函数是在某一个基类中声明为virtual,并在一个或多个派生类中被重新定义的成员函数。 声明虚函数的格式: virtual <返回值类型> <函数名> (<参数表>); 一个函数一旦被声明为虚函数,则无论声明它的类被继承了多少层,在每一层派生类中该函数都保持虚函数特性。因此在派生类中重新定义该函数时,可省略关键字virtual。但是为了提高程序的可读性,往往不省略。

161 { cout<<"A::show\n"; }; }; class B : public A
多态性和虚函数 class A { public: virtual void Show() { cout<<"A::show\n"; }; }; class B : public A void Show() { cout<<"B::show\n"; }; void main() { A a, *pa; B b; pa=&a; pa->Show(); // 调用函数A::Show() pa=&b; pa->Show(); // 调用函数B::Show() } 程序运行结果: A::Show B::Show

162 使用虚函数的注意事项 在派生类中重新定义虚函数时,必须保证函数的返回值类型和参数与基类中的声明完全一致。否则不具备多态性。
多态性和虚函数 使用虚函数的注意事项 在派生类中重新定义虚函数时,必须保证函数的返回值类型和参数与基类中的声明完全一致。否则不具备多态性。 如果在派生类中没重新定义虚函数,则派生类的对象将使用基类的虚函数代码。 只有通过对象指针或对象引用来调用虚函数,才能实现动态联编。如果采用对象来调用虚函数,则采用的是静态联编方式。

163 动态联编举例 程序运行结果: A Student A graduate Student
多态性和虚函数 动态联编举例 #include <iostream.h> class Student { public: virtual void print(){ cout<<"A Student"; cout<<endl; } }; class GStudent:public Student cout<<"A graduate Student"; void fun(Student &s) { s.print(); } void main() { Student s1,*ps; GStudent s2; //对象引用调用虚函数 fun(s1); fun(s2); //对象指针调用虚函数 ps=&s2; ps->print(); 程序运行结果: A Student A graduate Student

164 构造函数、析构函数与虚函数 基于构造函数的特点,不能将 构造函数定义为虚函数。
多态性和虚函数 构造函数、析构函数与虚函数 声明派生类对象时自动调用基类的构造函数 基于构造函数的特点,不能将 构造函数定义为虚函数。 当撤消派生类的对象时,先调用派生类析构函数,然后自动调用基类析构函数,如此看来析构函数没必要定义为虚函数。但是,假如使用基类指针指向其派生类的对象,而这个派生类对象是用new运算创建的。当程序使用delete运算撤消派生类对象时,这时只调用了基类的析构函数,而没有调用派生类的析构函数。 如果使用虚析构函数,无论指针所指的对象是基类对象还是派生类对象,程序执行时都会调用对应的析构函数。

165 例 虚析构函数的使用 ? 程序运行结果: B::destructor A::destructor class A { public:
多态性和虚函数 例 虚析构函数的使用 class A { public: // 构造函数不能是虚函数 A() { }; // 析构函数是虚函数 virtual ~A() {cout<<"A::destructor\n";}; }; class B : public A B() { }; // 虚析构函数 ~B() { cout<<"B::destructor\n"; }; void main() { A *pA=new B; // delete pA; /*先调用派生类B的析构函数,再调用基类A的析构函数*/ } 如果析构函数不是虚函数,则得不到下面的运行结果。请思考会是什么结果 总结:由于使用了虚析构函数,当撤消pA所指派生类B的对象时,首先调用派生类B的析构函数,然后再调用基类A的析构函数。 程序运行结果: B::destructor A::destructor

166 虚函数的适用性 一般来说,可将类族中的具有共性的成员函数声明为虚函数,而具有个性的函数则没有必要声明为虚函数。但是以下情况例外:
多态性和虚函数 虚函数的适用性 一般来说,可将类族中的具有共性的成员函数声明为虚函数,而具有个性的函数则没有必要声明为虚函数。但是以下情况例外: 静态成员函数不能声明为虚函数。因为静态函数不属于某一个对象,没有多态性的特征。 内联函数不能声明为虚函数。因为内联函数的执行代码明确,没有内联函数的特征。 构造函数不能声明为虚函数。 析构函数可以是虚函数。

167 多态性和虚函数 关于虚函数的说明 利用虚函数可以在基类和派生类中使用相同的函数名和参数类型,但定义不同的操作。这样,就为同一个类体系中所有派生类的同一类行为(其实现方法可以不同)提供了一个统一的接口。 例如,在一个图形类继承结构中,设类CShape是所有具体图形类(如矩形、三角形或圆等)的基类,则函数调用语句“pShape->Draw()”可能是绘制矩形,也可能是绘制三角形或圆。具体绘制什么图形,取决于pShape所指的对象。 有时在声明一个基类时无法为虚函数定义其具体实现,这时可以将其声明为纯虚函数。包含纯虚函数的类成为抽象类。

168 抽象类 抽象类是类的一些行为(成员函数)没有给出具体定义的类,即纯粹的一种抽象。
多态性和虚函数 抽象类 抽象类是类的一些行为(成员函数)没有给出具体定义的类,即纯粹的一种抽象。 抽象类专门作为基类派生新类,只能用于类的继承,自身无法实例化,不能用来创建对象,主要是为一个类族提供统一的操作接口,抽象类又称为抽象基类。 抽象基类只提供了一个框架,仅仅起着一个统一接口的作用,而很多具体的功能由派生出来的类去实现。 通过抽象类为一个类族建立一个公共的接口,这个公共的接口就是纯虚函数。 在一般的类库中都使用了抽象基类,如类CObject就是微软基础类库MFC的抽象基类。

169 抽象类的定义 一个类如果满足以下两个条件之一就是抽象类: 抽象类不能用作参数类型、函数返回值类型或显式转换的类型。
多态性和虚函数 抽象类的定义 一个类如果满足以下两个条件之一就是抽象类: 至少有一个成员函数不定义具体的实现; 定义了一个protected属性的构造函数或析构函数。 抽象类不能用作参数类型、函数返回值类型或显式转换的类型。 可以说明指向抽象类的指针或引用,该指针或引用可以指向抽象类的派生类,进而实现多态性。 纯虚函数

170 纯虚函数 不定义具体实现的成员函数称为纯虚函数。纯虚函数不能被调用,仅起提供一个统一接口的作用。 纯虚函数的声明:
多态性和虚函数 纯虚函数 不定义具体实现的成员函数称为纯虚函数。纯虚函数不能被调用,仅起提供一个统一接口的作用。 纯虚函数的声明: virtual <数据类型> <成员函数名>(<形参表>)= 0 ; 当基类是抽象类时,只有在派生类中重新定义基类中的所有纯虚函数,该派生类才不会再成为抽象类。

171 例 纯虚函数和抽象类的使用 #include <iostream.h> const double PI=3.14159;
多态性和虚函数 例 纯虚函数和抽象类的使用 #include <iostream.h> const double PI= ; class CShapes{ public: void SetValue(int d,int w=0) {x=d; y=w;} virtual void Area()=0; void Disp() { cout<<"面积:"<<s<<endl; } protected: int x,y; double s; }; class CSquare: public CShapes{ virtual void Area() {s=x*y;}

172 class CCircle : public CShapes{ public:
多态性和虚函数 class CCircle : public CShapes{ public: virtual void Area() { s=PI*x*x; }; }; void main() { CShapes *ptr[2]; CSquare s1; CCircle c1; ptr[0]=&s1;//抽象类指针指向派生类对象 ptr[0]->SetValue(10,5); ptr[0]->Area();//抽象类指针指向派生类成员函数 ptr[0]->Disp(); ptr[1]=&c1; //抽象类指针指向派生类对象 ptr[1]->SetValue(10); ptr[1]->Area(); ();//抽象类指针指向派生类成员函数 ptr[1]->Disp(); }

173 多态性小结 多态性也是面向对象程序设计方法的一个重要特征,它主要表现在函数调用时实现“一种接口、多种方法”。
多态性和虚函数 多态性小结 多态性也是面向对象程序设计方法的一个重要特征,它主要表现在函数调用时实现“一种接口、多种方法”。 两种多态性:编译时多态性和运行时多态性。 基类指针指向派生类对象问题是个难点问题。 虚函数:引入虚函数的原因,虚函数的声明和使用,以及什么时候把成员函数设为虚函数等等。 要清楚联编的两种方式。 抽象类和纯虚函数。

174 #include <iostream.h> class Point { public:
多态性和虚函数 #include <iostream.h> class Point { public: Point(int x1,int y1){ x=x1; y=y1;} int area(){ return 0; } private: int x,y; }; class Rect :public Point{ Rect(int x1,int y1,int u1,int w1):Point(x1,y1){u=u1;w=w1;} int area() {return u*w;} int w,u; void fun(Point &p){ cout<<p.area()<<endl;} void main(){ Rect rct(2,4,10,6); fun(rct); } 课堂练习:写出下面程序的运行结果。

175 重 载 C++重载分为函数重载和运算符重载,这两种重载的实质是一样的。
重 载 C++重载分为函数重载和运算符重载,这两种重载的实质是一样的。 通过使用重载机制,可以对一个函数名(或运算符)定义多个函数(或运算功能),只不过要求这些函数的参数(或参加运算的操作数)的类型有所不同。 重载使C++程序具有更好的可扩充性。 第七周

176 函数重载 函数重载:指一组功能类似但函数参数类型(个数)不同的函数可以共用一个函数名。
当C++编译器遇重载函数的调用语句时,它能够根据不同的参数类型或不同的参数个数选择一个合适的函数。 重载函数的种类: 构造函数 成员函数 一般函数

177 例 通过函数参数类型的不同实现函数重载。 int abs(int val) { return val<0 ? –val : val;
} float abs(float val) { return (val<0) ? –val : val; } main() { int i=100; cout<<abs(i)<<endl; // int型 float f= F; cout<<abs(f)<<endl; // float型 } 在程序中,求绝对值函数的名称相同,但参数类型不同,这时C++编译器自动按参数表的不同来分别联编不同的求绝对值函数。

178 函数重载的注意事项 不能利用函数返回类型的不同进行函数重载。因为在没有确定调用的是哪个函数之前,不知道函数的返回类型。
long abc(int); float abc(int); 不能利用引用进行函数重载: void fun(int&); void fun(int); 因为对于下面的调用语句,编译器无法决定调用哪一个函数: fun(i); // i是一个整型变量

179 例 构造函数的重载 class Box { private: int height, width, depth; public:
Box() { height=0; width=0; depth=0; } // 避免给成员变量赋不安全的值 Box(int ht, int wd, int dp) // 重载构造函数 { height=ht; width=wd; depth=dp; } int Volume() { return height*width*depth; } };

180 cout<<"Volume1="<<box1.Volume()
重载 类Box有两个构造函数。第一个构造函数不带参数,把默认值0赋给对象;第二个构造函数使用参数值初始化创建的对象。 void main() { Box box1; Box box2(10, 15, 20); cout<<"Volume1="<<box1.Volume() <<", Volume2="<<box2.Volume()<<endl; } 程序运行结果: Volume1=0,Volume2=3000

181 运算符重载 运算符重载:指对于不同数据类型的操作数,同一个运算符所代表的运算功能可以不同。
一个运算符定义了一种操作,一个函数也定义了一种操作,其本质是相同的,当程序遇到运算符时会自动调用相应的运算符函数。 虽然重载运算符完成的功能都能够用一个真正的成员函数来实现,但使用运算符重载使程序更易于理解。 与函数重载类似,编译器是根据参加运算的操作数的类型来识别不同的运算。

182 例: 对于表达式:10+20 编译器把它看成如下函数调用: int operator+(10, 20); 对于表达式:10.0+20.0
重载 例: 对于表达式:10+20 编译器把它看成如下函数调用: int operator+(10, 20); 对于表达式: float operator+(10.0, 20.0); 参加运算的数是整数 参加运算的数是单精度实型数 我们可以将字符串operator+看成一个运算符函数名,这些同名的运算符函数根据不同类型的操作数完成不同的加法运算。

183 重载运算符的形式 重载一个运算符,就是编写一个运算符函数,重载运算符(函数)的原型为:
<数据类型> operator<运算符>(<形参表>); 运算结果的类型 参加运算的操作数 要重载的运算符

184 例 定义复数类型,重载运算符“+” class Complex { public: // 公有成员,以便运算符函数(非成员函数)访问
例 定义复数类型,重载运算符“+” class Complex { public: // 公有成员,以便运算符函数(非成员函数)访问 float r; // 实部 float i; // 虚部 Complex(float x=0, float y=0) { r=x; i=y; } };

185 利用普通函数重载运算符 Complex operator+(Complex c1 , Complex c2) { Complex temp;
temp.r=c1.r+c2.r; temp.i=c1.i+c2.i; return temp; } void main() Complex complex1(3.34f, 4.8f), complex2(12.8f, 5.2f); Complex complex; complex=complex1+complex2; // 进行两个复数的相加运算 cout<<complex.r<<'+'<<complex.i<<'i'<<endl; 利用普通函数重载运算符 本例采用普通函数的形式重载运算符。 可以采用成员函数的形式重载运算符。并且如果运算符函数要求直接访问类的非公有成员时,运算符函数不能定义为非成员函数,除非将它声明为该类的友元函数。

186 例 利用成员函数进行运算符重载 class Complex { private: // 私有成员能够在成员函数(运算符函数)中访问
例 利用成员函数进行运算符重载 class Complex { private: // 私有成员能够在成员函数(运算符函数)中访问 float r; // 实部 float i; // 虚部 public: Complex(float x=0, float y=0) { r=x; i=y; }; Complex operator+(Complex); void Display() { cout<<r<<'+'<<i<<'i'<<endl; }; // 输出实部和虚部 };

187 Complex Complex::operator+(Complex other) { Complex temp;
重载 Complex Complex::operator+(Complex other) { Complex temp; temp.r=this->r+other.r; temp.i=this->i+other.i; // 可以省略this指针 return temp; } 利用成员函数重载运算符 void main() { Complex complex1(3.34f, 4.8f), complex2(12.8f, 5.2f); Complex complex; complex=complex1+complex2; complex.Display(); }

188 说 明 当利用非成员函数重载双目运算符时,运算符函数的第一个参数代表运算符左边的操作数,运算符函数第二个参数代表运算符右边的操作数。
说 明 当利用非成员函数重载双目运算符时,运算符函数的第一个参数代表运算符左边的操作数,运算符函数第二个参数代表运算符右边的操作数。 当利用成员函数重载双目运算符时,运算符左边的操作数就是对象本身,不能再将它作为运算符函数的参数,运算符函数只需要一个函数参数。

189 运算符重载与函数重载的区别 同一个重载运算符的参数个数是相同的。 不能定义新的运算符,只能重载现有的运算符。
运算符重载后仍然保持原来的优先级和结合性。

190 运算符重载规则和限制 可以重载C++中除下列运算符外的所有运算符: . -> .* :: ?: sizeof
不改变原运算符的优先级和结合性。 不能改变操作数个数。 经重载的运算符,其操作数中至少应该有一个是自定义类型。

191 重载小结 重载包括函数重载和运算符重载两种形式,重载体现的是编译期的多态性。 函数重载与函数重定义是完全不同的。
对于用户自定义的数据类型来说,用运算符重载实现该类型数据之间或该类型数据与其他类型数据之间的运算比用函数实现更容易理解,也更便于使用。

192 引 例 void swap(int &a,int &b) int abs(int val) { { int temp=a;
C++模板 引 例 void swap(int &a,int &b) { int temp=a; a=b; b=temp; } void swap(float &a,float &b) float temp a; int abs(int val) { return val<0 ? –val : val; } float abs(float val) return (val<0) ? –val : val;

193 C++模板 模板是对具有相同特性的函数或类的再抽象。是一个将数据类型参数化的工具,它把“一般性的算法”和其“对数据类型的实现”区分开来。
模板分为函数模板和类模板两种,通过参数实例化可以再构造出具体的函数或类,称为模板函数和模板类。 采用模板方式定义函数或类时不确定某些函数参数或数据成员的类型,而将它们的数据类型作为模板的参数。在使用模板时根据实参的数据类型确定模板参数(数据类型)的数据类型。 模板提高了软件的重用性。当函数参数或数据成员可以是多种类型而函数或类所实现的功能又相同时,使用C++模板在很大程度上简化了编程。

194 C++模板 模板与实例之间的关系

195 函数模板 把逻辑功能相同而函数参数和返回值的类型不同的多个重载函数用一个函数来描述,这种参数化的函数称为函数模板。
C++模板 函数模板 把逻辑功能相同而函数参数和返回值的类型不同的多个重载函数用一个函数来描述,这种参数化的函数称为函数模板。 它是一种不指定某些参数的数据类型的函数,在函数模板被调用时根据实际参数的类型决定这些函数模板参数的类型。 函数重载与函数模板 函数模板扩展了函数重载:利用函数重载可以让多个函数共享一个函数名,只是所重载的函数的参数类型必须有所不同。但是,由于参数的类型不一样,虽然这些函数所完成的功能完全一样,也必须为每一个重载函数编写代码。 一个函数模板可用来生成多个功能相同但参数和返回值的类型不同的函数。

196 函数模板的定义格式 template < 模板参数表 >
C++模板 函数模板的定义格式 template < 模板参数表 > <返回值类型><函数名>(<参数表>) { <函数体> } 说明:函数模板的定义只是一种说明,不是一个具体的函数,编译系统不为其产生任何执行代码,只有当编译系统在程序的编译过程中,发现了一个具体的函数调用时,才根据具体的参数类型,生成相应的代码,此时的代码被称为模板函数。

197 函数模板的定义举例 以下定义了一个可对任何类型变量进行操作(求绝对值)的函数模板: template < class T >
T abs( T val ) { return val<0 ? -val : val; } 模板定义以关键字template开头; 关键字class后面的标识符T由用户自定义,称为类型参数,是函数模板abs()中没有确定数据类型的参数val的类型。 模板定义的下面是模板函数abs()的定义。 类型参数T的作用: 定义函数的参数和返回值; 在函数体中用来声明变量。

198 含有多个类型参数的函数模板 定义函数模板时可以使用多个类型参数,每个类型参数前面只需加上关键字class,用逗号分隔:
template <class T1,class T2,class T3> 例如: template <class T1,class T2> T1 Max( T1 x, T2 y) { return x>=y ? x : (T1)y; }

199 C++模板 函数模板的实例化 函数模板将数据类型参数化,这使得在程序中能够用不同类型的参数调用同一个函数(模板函数)。在调用模板函数时即创建函数模板的一个实例,这个过程称为函数模板的实例化。 函数模板的实例化由编译器完成:编译时函数模板本身并不产生可执行代码,只有在函数模板被实例化时,编译器才按照实参的数据类型进行类型参数的替代,生成新的函数。 编译器 函数模板 函 数

200 例 函数模板的定义和使用 #include <iostream.h>
例 函数模板的定义和使用 #include <iostream.h> template <class T> // 定义模板 T abs(T val) // 定义模板函数 { return val<0 ? -val : val; } void main() int i=100; cout<<abs(i)<<endl; long l=-12345L; cout<<abs(l)<<endl; float f= F; cout<<abs(f)<<endl; 类型参数T 替换为int 类型参数T 替换为long 类型参数T 替换为float

201 C++模板 函数模板与模板函数的关系

202 C++模板 类模板 类模板称为带参数(或参数化)的类,也称为类工厂,它可用来生成多个功能相同而某些数据成员的类型不同或成员函数的参数及返回值的类型不同的类。 类模板与函数模板 函数模板只能用于定义非成员函数,它是模板的一个特例。类模板实际上是函数模板的推广,它是一种不确定类的某些数据成员的类型或成员函数的参数及返回值的类型的类。 类模板与类 类是对问题的抽象,而类模板是对类的抽象,即更高层次上的抽象。

203 包含一个或多 个用逗号分开的类型,参数项可以包含基本数据类型,也可以包含类类型;如果是类类型,则须加前缀class
类模板的定义 为了起到模板的作用,与函数模板一样,定义一个类模板时必须将某些数据类型作为类模板的类型参数。 模板类的实现代码与普通类没有本质上的区别,只是在定义其成员时要用到类模板的类型参数。 类模板定义的一般格式: template <类型参数表> class <类模板名> { <类成员的声明> }; 包含一个或多 个用逗号分开的类型,参数项可以包含基本数据类型,也可以包含类类型;如果是类类型,则须加前缀class

204 含有一个类型参数的类模板举例 template < class T > class MyTemClass { private:
T x; // 类型参数T用于声明数据成员 public: void SetX( T a ) { x=a; }; // 类型参数T用于声明成员函数的参数 T GetX( ) { return x; }; // 类型参数T用于声明成员函数的返回值 };

205 注 意 如果在模板类的外部定义模板类的成员函数,必须采用如下形式:
C++模板 注 意 如果在模板类的外部定义模板类的成员函数,必须采用如下形式: template < class T > // 不能省略模板声明 void MyTemClass < T > :: SetX( T a ) { x=a; }

206 类模板的实例化 利用类模板的定义只是对类的描述,它本身不是一个实实在在的类。 类模板的实例就是模板类,其格式如下:
C++模板 类模板的实例化 利用类模板的定义只是对类的描述,它本身不是一个实实在在的类。 类模板的实例就是模板类,其格式如下: <类模板名><实际的类型>; 定义模板类的对象的格式如下: <类模板名><实际的类型><对象名>[(<实参表>)]; 例如,以下使用类模板声明了一个类型参数为int的模板类的对象: MyTemClass < int > intObject; 编译器首先用int替代模板类定义中的类型参数T,生成一个所有数据类型已确定的类class;然后再利用这个类创建对象intObject 。

207 类模板的实例化 编译系统不为类模板(包括成员函数定义)创建程序代码,但是通过对类模板的实例化可以生成一个具体的类以及该具体类的对象。
C++模板 类模板的实例化 编译系统不为类模板(包括成员函数定义)创建程序代码,但是通过对类模板的实例化可以生成一个具体的类以及该具体类的对象。 类模板与函数模板的区别: 函数模板的实例化是由编译系统在处理函数调用时自动完成的; 类模板的实例化必须由程序员在程序中显式地指定。

208 模板类举例 class A { double x; public: A(double xx){ x=xx; }
#include<iostream.h> template <class T> class A { T x; public: A(T xx){ x=xx; } T fn(){ return x*x*x; } }; void main() A<int> a1(5); A<double> a2(0.5); cout<<a1.fn()<<" "<<a2.fn()<<endl; } class A { double x; public: A(double xx){ x=xx; } double fn(){ return x*x*x;} }; class A { int x; public: A(int xx){ x=xx; } int fn(){ return x*x*x; } }; 程序运行结果: 如果换成:A<float> a1(5); 那么对T参数实例化为什么类型?程序运行结果是什么?

209 含有多个参数类模板的定义 template < class T1,int i,class T2 >
class MyTemClass { . . . } 例如,声明模板类的对象应采用如下形式: MyTemClass < int, 100, float > MyObject ;

210 例 使用多个类型参数的类模板 template <class T1, class T2>// 使用2个类型参数
例 使用多个类型参数的类模板 template <class T1, class T2>// 使用2个类型参数 class MyTemClass // 定义模板类 { private: T1 x; T2 y; public: MyTemClass(T1 a, T2 b) { x=a; y=b; }; void ShowMax() { cout<<"MaxMember="<<(x>=y?x:y)<<endl; }; };

211 MyTemClass< int, float > mt(a, b); mt.ShowMax(); }
void main() { int a=100; float b=123.45F; // 声明模板类的对象 MyTemClass< int, float > mt(a, b); mt.ShowMax(); } 类模板 的实例化

212 本章小结 面向对象程序设计方法和4个特征 C++类: 类的定义、实现 构造函数和析构函数 this指针、静态成员 类的继承: 派生方式
基类构造函数的显式调用和隐式调用 多重继承及二义性问题、虚基类 多态性 两种多态性:编译时多态性和运行时多态性 虚函数、抽象类和纯虚函数 静态联编和动态联编 函数重载与运算符重载 C++模板


Download ppt "面向对象程序设计 第三章 C++面向对象程序设计 武汉大学 赵小红."

Similar presentations


Ads by Google