第八章 继承与多态继承( inheritance)机制 是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上,调整部分成员的特性,也可以增加一些新成员。
多态性( polymorphism) 也是面向对象程序设计的标志性特征。多态性是考虑在不同层次的类中,以及在同一类中,同名的成员函数之间的关系问题。函数的重载,
运算符的重载,都属于多态性中的编译时的多态性。运行时的多态性,这是以虚基类为基础的多态性。
第八章 继承与多态
8.1 继承与派生的概念
8.3 虚基类
8.2 多重继承与派生类成员标识
8,5 MFC基础类及其层次结构
8,7 MFC的消息映射与命令传递
8,6 多态性与虚函数
8.4 派生类应用讨论
8.1 继承与派生的概念层次概念是计算机的重要概念。对类( class)分层,提供类型 /子类型的关系,这是通过继承
( inheritance)的机制获得的。 C++通过类派生
( class derivation)的机制来支持继承。被继承的类型称为基类( base class)或超类( superclass),
而新产生的类为 派生类( derived class)或子类
( subclass),反映了事物之间的联系,事物的共性与个性之间的关系。从工作量上看,工作量少,重复的部分可以从基类继承来,不需要单独编程。
8.1 继承与派生的概念
8.1.1 类的派生与继承
8,1.2 公有派生与私有派生
8,1.3 派生类的构造函数与析构函数
8.1.1 类的派生与继承由基类派生出派生类的定义的一般形式为
class 派生类名:访问限定符 基类名 1《,访问限定符 基类名
2,……,访问限定符 基类名 n》
{
《,private:,
成员表 1;,//派生类增加或替代的私有成员
,public:
成员表 2;,//派生类增加或替代的公有成员
,protected:
成员表 3;,//派生类增加或替代的保护成员
}
其中基类 1,基类 2,…… 是已声明的类。
8.1.1 类的派生与继承派生出来的新类同样可以作为基类再继续派生出更新的类,依此类推形成一个层次结构。 直接参与派生出某类称为直接基类,而基类的基类,以及更高层的基类称为间接基类。
在派生类的声明中,还有访问控制,亦称为继承方式,
是对基类成员进一步的限制。访问控制也是三种,公有
( public)方式,保护( protected)方式和私有
( private)方式,亦称公有继承、保护继承和私有继承。
如不显式给出访问控制关键字,则默认为私有继承编制派生类时可分四步 。首先吸收基类的成员;第二步是改造基类成员,称为同名覆盖( override)。第三步发展新成员;第四步是重写构造函数与析构函数。
8.1.2 公有派生与私有派生在派生类的定义中,基类前所加的访问限定符,对由基类继承来的成员的访问作了进一步的限制。有两方面含义:派生类成员成员的访问,及从派生类对象之外对派生类对象中的基类成员的访问。
1.公有派生,基类的公有和保护成员的访问属性在派生类中不变,而基类的私有 成员不可访问。公有派生是最常用的派生方式。
2.私有派生。 基类中的公有成员和保护成员在派生类中成为私有成员
3.保护派生。
8.1.2 公有派生与私有派生派生方式 基类中的访问限定 在派生类中对基类成员的访问限定在派生类对象外访问派生类对象的基类成员公有派生
public public 可直接访问
protected protected 不可直接访问
private 不可直接访问 不可直接访问私有派生
public private 不可直接访问
protected private 不可直接访问
private 不可直接访问 不可直接访问表 8.1 公有派生和私有派生
8.1.3 派生类的构造函数与析构函数派生类的构造函数的定义形式为:
派生类名::派生类名 ( 参数总表 ),基类名 1( 参数表
1),,基类名 2( 参数表 2),……,基类名 n( 参数表
n),,,成员对象名 1( 成员对象参数表 1),……,成员对象名 m( 成员对象参数表 m),{
…… //派生类新增成员的初始化;
}
冒号后的基类名,成员对象名的次序可以随意,这里的次序与调用次序无关。在创建派生类对象时,系统会首先使用各参数表中列出的参数,调用基类和成员对象的构造函数。
8.1.3 派生类的构造函数与析构函数派生类构造函数各部分的执行次序为:
1,调用基类构造函数,按它们在派生类定义的先后顺序,顺序调用 。
2,调用成员对象的构造函数,按它们在类定义中声明的 先后顺序,顺序调用 。
3,派生类的构造函数体中的操作 。
在派生类构造函数中,只要基类不是缺省构造函数都要显式给出基类名和参数表,除非基类使用默认的构造函数。
析构函数的功能是作善后工作,派生类析构函数只要把派生类新增一般成员处理好就可以了。
析构函数各部分执行次序与构造函数相反,首先对派生类新增一般成员析构,然后对新增对象成员析构,最后对基类成员析构。
8.1.3 派生类的构造函数与析构函数
【 例 8.1】 由在册人员类公有派生学生类 。
我们希望派生类总是和基类保持一致,原有的成员和访问方式被保留,这只能采用公有派生来实现 。 在私有派生时要保持接口不变,则要在派生类中重编接口,去调用基类接口成员函数 。 所以绝大多数场合总是用公有派生 。
本例只是为了演示派生类的定义与使用,尽可能顾及最常用的各方面。继承性可以重复使用已经编译好的代码和已设计好的数据结构,避免代码和数据结构的重复设计。面向过程程序设计层次概念使程序易读易懂,在面向对象的程序中,继承也显示了一个层次性,使程序更加易读易懂,便于维护。
程序,Ex8_1.cpp
8.2 多重继承与派生类成员标识由多个基类共同派生出新的派生类,称为多重继承或多继承
( multiple-inheritance) 。
在册人员学生 (单继承 )教职工 (单继承 ) 兼职教师 (单继承 )
教师 (单继承 ) 行政人员 (单继承 )工人 (单继承 ) 研究生 (单继承 )
行政人员兼教师
(多重继承 )
在职研究生
(多重继承 )研究生助教 (多重继承 )
图 8.2 大学在册人员继承关系
8.2 多重继承与派生类成员标识椅子 床沙发 (单继承 ) 躺椅 (多重继承 )
两用沙发 (多重继承 )
图 8.3 椅子,床到两用沙发
8.2 多重继承与派生类成员标识多重继承,必须解决唯一标识问题。通常采用作用域分辨符:
基类名::成员名; //数据成员基类名::成员名(参数表); //函数成员用域分辨符是不能嵌套使用的,不可连续使用多个作用域分辨符
8.2 多重继承与派生类成员标识
【 例 8.2】 由圆和高多重继承派生出圆锥。
程序,Ex8_2.cpp
8.3 虚基类图 8.4(b)给出 class Person有两个拷贝,而实际上这是重复的,
把 class Person这个共同基类设置为虚基类,这样从不同路径继承来的同名数据成员在内存中就只有一个拷贝,同名函数也只有 一种映射 。 虚基类 ( virtual base class) 定义方式如下:
class 派生类名,virtual 访问限定符 基类类名 {...};
class 派生类名,访问限定符 virtual 基类类名 {...};
学生类定义,
class Student::virtual public Person{...};
教职工类定义,
class Employee::virtual public Person{...};
8.3 虚基类这样在职研究生类的储存图参见图 8.5,在 Person的位置上放的是指针,两个指针都指向 Person成员存储的内存 。 这种继承称为 虚拟继承 ( virtual inheritance) 。 在派生类对象的创建中,首先是虚基类的构造函数被调用,并按它们被继承的顺序构造 。 第二批是非虚基类的构造函数按它们被继承的顺序调用 。 第三批是成员对象的构造函数 。 最后是派生类自己的构造函数被调用 。
图 8.5 采用虚基类后在职研究生类储存图
Student
GStudent
EGStudent
Person
Student新成员
GStudent新成员
Person
Employee新成员
Person成员
EGStudent新成员
Person
Person Employe
e
8.3 虚基类
【 例 8.3】 在采用虚基类的多重继承中,构造与析构的次序。
程序,Ex8_3.cpp
【 例 8.4】 虚基类在多层多重继承中的应用 —— 在职研究生类定义。
程序,Ex8_4.cpp
8.4 派生类应用讨论派生类与基类 。在任何需要基类对象的地方都可以用公有派生类的对象来代替,这条规则称 赋值兼容规则。 包括以下情况:
1,派生类的对象可以赋值给基类的对象,这时是把派生类对象中从对应基类中继承来的成员赋值给基类对象 。 反过来不行,因为派生类的新成员无值可赋 。
2,可以将一个派生类的对象的地址赋给其基类的指针变量,但只能通过这个指针访问派生类中由基类继承来的成员,不能访问派生类中的新成员 。 同样也不能反过来做 。
3,派生类对象可以初始化基类的引用。引用是别名,但这个别名只能包含派生类对象中的由基类继承来的成员。
8.4 派生类应用讨论继承与对象成员 。 继承使派生类可以去利用基类的成员,把基类的对象作为一个新类的对象成员,也可以取得类似的效果。派生类可以直接使用基类公有与保护成员,但不能直接使用基类私有成员。使用成员对象中的成员,只能直接访问公有成员。在类的成员函数中不能直接访问和处理成员对象的私有和保护成员,要通过成员对象的接口去间接访问和处理。
派生类与模板。 标准模板库中的类模板也提供代码的复用功能,类模板是相互独立的,独立设计的,没有使用继承的思想。对类模板的扩展是采用 适配子
( adapter) 来完成的。通用性是模板库的设计出发点之一,这是由泛型算法和函数对象等手段达到的。
8.5 MFC基础类及其层次结构图 8.6 根类 CObject分类派生图
8.6 多态性与虚函数在 C++中有两种多态性,编译时的多态性和运行时的多态性。 前者通过函数的重载和运算符的重载来实现的。 运行时的多态性是指在程序执行前,无法根据函数名和参数来确定该调用哪一个函数,必须在程序执行过程中,根据执行的具体情况来动态地确定。 这种多态性是通过类继承关系和 虚函数( virtual fuction) 来实现的。
8.6 多态性与虚函数
8,6,1 虚函数的定义
8,6,3 动态联编
8,6,2 纯虚函数
8.6.1 虚函数的定义虚函数也是一个类的成员函数,定义虚函数的格式如下:
virtual 返回类型 函数名(参数表);
【 例 8.6】 计算学分。可由本科生类派生出研究生类,但它们各自的由课程学时数折算为学分数的算法是不同的,
本科生是 16个学时一学分,而研究生是 20个学时一学分。
程序,Ex8_6.cpp
8.6.1 虚函数的定义注意以下几条:
1,派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型 。 如 基类中返回基类指针,派生类中返回派生类指针是允许的 。
2,只有类的成员函数才能说明为虚函数 。
3,静态成员函数,不能作为虚函数 。
4,实现动态多态性时,必须使用基类指针或引用,使该指针指向不同派生类的对象,并指向虚函数,
5,内联函数不能作为虚函数 。
6.析构函数可定义为虚函数,构造函数不能定义虚函数 在基类中及其派生类中都有动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。
8.6.2 纯虚函数纯虚函数 ( pure virtual function) 是指被标明为不具体实现的虚拟成员函数 。 定义纯虚函数的一般格式为:
virtual 返回类型 函数名 ( 参数表 ) =0;
含有纯虚函数的基类是不能用来定义对象的。含有纯虚函数的类是抽象类。
定义纯虚函数必须注意:
1,定义纯虚函数时,不能定义虚函数的实现部分 。
2.,=0” 本质上是将指向函数体的指针定为 NULL。
3.在派生类中必须有重新定义的纯虚函数的函数体,这样的派生类才能用来定义对象。
8.6.2 纯虚函数
【 例 8.8】 学校对在册人员进行奖励,依据是业绩分,
但是业绩分的计算方法只能对具体人员进行,如学生,
教师,行政人员,工人,算法都不同,所以可以将在册人员类作为一个抽象类,业绩计算方法作为一个纯虚函数。
程序,Ex8_8.cpp
8.6.2 纯虚函数
【 例 8.9】 辛普生法求函数的定积分 。 在梯形法中,我们是用直线来代替曲边梯形的曲边,在辛普生法中是用抛物线来代替,得出的公式为 ( 区间必须为偶数 n个相等区间 ) 。
b
a
nnn yyyyyyyyxdxxf )](2)(4[3
1)(
2421310
程序:E x8_9.cpp
8.6.2 纯虚函数
【 例 8.10】 通用单链表派生类 。
首先改造 【 例 7.4】 的头文件,不采用模板类,而采用虚函数实现多态性,达到通用的目的。节点类数据域被改造为指针,而把数据放在一个抽象类中,由指针与之建立联系。数据抽象类中含有两个纯虚函数:比较函数和输出函数。当抽象类在派生时重新定义两个纯虚函数,可以进行各种类型,包括类和结构对象的比较和输出。抽象类中的析构函数也是虚函数,
这一点非常重要,当抽象类派生的数据类的数据部分是动态产生的,而由结点类删除释放数据类对象时,必须由数据类的析构函数来释放该类对象数据部分占用的动态分配的内存。这时必须重新定义析构函数。
程序,Ex8_10.cpp
8.6.3 动态联编动态联编 ( dynamic binding) 亦称滞后联编 ( late
binding),对应于静态联编 ( static binding) 。
使用对象名和点成员选择运算符,.”引用特定的一个对象来调用虚函数,则被调用的虚函数是在编译时确定的
(称为静态联编),使用基类指针或引用指明派生类对象并使用该指针调用虚函数,则程序动态地(运行时)选择该派生类的虚函数,称为动态联编。
8.7 MFC的消息映射与命令传递
CWinApp类检索和派送消息给相应的窗口函数,但是所有的
MFC窗口函数都使用同一个的窗口函数,而不是有各自的窗口函数。
MFC把设计窗口的任务细化为设计若干消息处理函数,程序员要做的是为每一个要处理的消息提供一个消息处理( Handling Messages)
函数,然后系统通过 MFC提供一套消息映射系统( messages
mapping system)来调用相应的消息处理函数。
消息映射的模型:
首先定义一个 Msgmap的结构:
struct Msgmap{
UINT nMessage; //UINT为无符号整型数 unsigned int
LONG(*pfn)(HWND,UINT,WPARAM,LPARAM);// 指 向 函 数 的
32位长指针
};
8.7 MFC的消息映射与命令传递以这样的结构组成一个数组,以 nMessage(消息)
为关键字进行排序可以很快找到所需要的指向消息处理函数的指针,进而进入消息处理函数进行消息处理。
MFC根据消息处理函数的不同,把消息分为三类:
1.窗口消息:是以 WM_开头,除 WM_COMMAND消息以外的所有消息。
2.控制通知消息:控制通知消息是以
WM_COMMAND形式封装的来自子窗口的通知消息。
3.命令消息:是以 WM_COMMAND形式封装的来自菜单、工具栏,加速键的通知消息。
8.7 MFC的消息映射与命令传递下一步讨论 MFC命令消息的传递过程 。 MFC对于消息循环的规定是:
1,如果是普通的 Windows消息,则一定由派生类流向基类,不会有迂回 。
2,如果是命令消息 ( WM_COMMAND),则路径十分复杂,下面讨论命令 消息的传递 。
MFC对命令消息搜索对应消息处理函数的过程是:由菜单、工具栏等用户界面对象产生的命令消息,首先送给主框架窗口的标准 MFC窗口函数,然后窗口函数把命令传给 MFC主框架窗口对象,进行命令消息的派送,并按表 8.3的次序进行消息匹配。若找不到,则先继续搜索其基类的消息映射入口表,都找不到,再迂回到下一个对象进行处理。
程序类型
SDI( 单文档 ) 当前视图 → 当前文档 → 文档模板 → 主框架窗口 → 应用程序对象
MDI ( 多文档 )
当前视图 → 当前文档 → 创建文档的文档模板 → 活动的子框架窗口 →
主框架窗口 → 应用程序对象对话框 当前对话框 → 对话框的父窗口 → 应用程序对象