相关博客目录
C++程序设计基础学习笔记:(1)初识C++语言:从认识变量和常量开始,数据的表示
C++程序设计基础学习笔记:(2)计算:从数据运算开始,数据简单运算
C++程序设计基础学习笔记:(3)分支结构:无处不在的抉择
C++程序设计基础学习笔记:(4)循环结构:周而复始,求同存异
C++程序设计基础学习笔记:(5)数组:实现算法的利器
C++程序设计基础学习笔记:(6)指针:所向披靡的“金箍棒” 魂
C++程序设计基础学习笔记:(7)函数:面向过程的基础
C++程序设计基础学习笔记:(8)文件:让数据流动起来
C++程序设计基础学习笔记:(9)以人为本:用类与对象诠释现实世界
C++程序设计基础学习笔记:(10)从生物遗传说起,取其精华——继承与多态
第10章 从生物遗传说起,取其精华——继承与多态
面向对象程序设计的三大特征
封装:现实世界中的联系
将数据与数据的处理函数结合在一起,隐藏数据,数据的访问只能通过类的公有函数继承:持续发展的,开放的编程态度
新的类在不破坏类的封装特性的情况下得到了原有类的功能,并依据新的需求添加新的数据和成员多态:对程序通用性的追求
同一名称实现不同的功能,达到行为标识统一
10.1 继承
继承与派生
基本概念
继承:保持已有类的特性而构造新类的过程称为继承
派生:在已有类的基础上新增自己的特性而产生新类的过程称为派生
被继承的类称为基类(或父类)
派生出的新类称为派生类(或子类)
继承关系
类的继承关系非常灵活,并且可以传递
依据继承关系的层次和父类的数量可以分为:单继承、多继承、多重继承
通常,单继承和多继承描述的是直接继承关系
继承方式
子类对父类的继承方式有三种
公有继承(public)
私有继承(private)
保护继承(protected)继承方式会改变继承的父类成员的外部访问控制属性
不论派生类以何种方式继承基类,都不能直接访问基类的private成员(下面说的都是针对基类公有属性的一些成员和函数来说的)
继承后,子类会获得父类的全部数据成员全部函数成员(构造函数与析构函数除外)
由上表可知私有继承在继承完后都变成了私有成员,不可能存在多重继承的关系
派生类的定义
语法:
class派生类名 :继承方式1 基类名1, 继承方式2 基类名2, ... { ……… };
“基类名”必须是已经存在的类名
派生类在继承基类时,可以是单继承,也可以多继承,同时继承好几个类
在派生类定义时,对每一个基类都要独立指明“继承方式”
如果不显式的给出继承方式,系统默认为私有继承private
派生类的构成
继承获得父类的成员,派生定义新的成员
10.2 派生(上)
派生类的定义
class派生类名:继承方式1 基类名1, 继承方式2 基类名2,... { .... };
“基类名”必须是已经存在的类名
派生类在继承基类时,可以是单继承,也可以多继承,同时继承好几个类
在派生类定义时,对每一个基类都要独立指明“继承方式”
如果不显式的给出继承方式,系统默认为私有继承private
类的派生步骤
继承基类成员
基类全部的数据成员
除了构造函数析构函数外的全部函数成员。派生类添加新成员
这些新成员体现了派生类与基类的差异,是派生类存在的基础。
如果添加新成员时,使用和被继承基类成员相同的名字
派生类中的新成员会屏蔽基类同名数据成员
在使用派生类对象时,默认访问同名的新成员写派生类的构造函数与析构函数
派生类构造函数的定义
派生类继承了基类的数据成员
派生类构造函数的形参表不但要包含对派生数据成员初始化的参数,还要包含对基类数据成员初始化的参数
派生类名::派生类名(基类所需的形参,本类新成员所需的形参):基类名1(参数表),基类名2(参数表)... { 本类成员初始化赋值语句; };
代码段中展示的
基类名1(参数表),基类名2(参数表)...
为参数传递列表,派生类构造函数还要通过参数传递列表明确指出分配给基类的参数基类数据成员的初始化由基类的构造函数完成
当基类有多个构造函数时,系统会根据参数传递列表自动完成基类构造函数函数的重载,调用最匹配的基类构造函数初始化继承的基类成员;参数列表空,调用基类的无参构造函数
派生类构造函数执行的顺序
调用基类的构造函数初始化继承的基类数据成员
调用顺序和派生类定义时声明的继承顺序一致(即使这个顺序与构造函数声明时候的参数列表顺序不同,也已继承顺序为准)如果新成员中含有对象成员,调用对象成员类的构造函数
调用顺序为它们在派生类中的定义顺序初始化其他数据成员
执行派生类构造函数的函数体中内容
10.2 派生(下)
示例
继承 vs 组合
继承和组合都能够构建更复杂的类,是实现代码复用的方法
从获得其他类的成员的角度来思考,组合和继承在结果上是相似的,但在副本的数量上有区别(即使继承多次也只能从基类中获得一个副本,但是组合可以获得很多)
组合是将已有对象拼合为新对象,是简单的代码重复。新类中可以重复含有已有类的对象,从而多次获得已有类的成员
继承是创建一个新类,将其作为现有类的一个“子类型”,所以只能获得1次原有类的成员
组合比继承更具灵活性和稳定性,在设计的时候优先使用组合
在使用继承和组合时,思考如下约束条件:
类A和类B毫不相关,不要为了使B 的功能更多些而让B 继承A。
类B有必要使用A的功能,则要分两种情况考虑:
若在逻辑上B 是A 的“一种子类型”,则允许B 继承A 的功能。例如“研究生”是“学生”的一种,研究生类可以从学生类派生。
若在逻辑上A 是B 的“一部分”,则不允许B 继承A,而是要用A和其它数据组合出B。例如眼(类Eye)、鼻(类Nose)、口(类Mouth)、耳(类Ear)是头(类Head)的一部分,所以类Head 应该由类Eye、Nose、Mouth、Ear 组合而成,不是派生而成。
类B继承类A后,类A就不再被使用,应该直接定义类B。而非先定义A,然后让B继承A。
10.3 同名覆盖vs类型兼容
新成员的派生 vs 标识符的二义性 vs 同名覆盖
派生为同名的原因:派生类保留基类访问的接口
添加新函数成员时,使用和基类成员相同的名字和参数表
同名覆盖
定义:使用派生类对象时,默认访问同名的新成员
前提条件:正常使用派生类对象
需要访问基类的同名成员,必须在成员名前加上前缀 “类名:: ”
举例:
类型兼容
实现方法:将派生类对象的地址赋值给基类指针,通过基类指针访问派生类对象
派生对象被作为基类对象使用,能访问到的只有继承自基类的成员
同名覆盖vs 类型兼容
类型兼容和同名覆盖都能够解决标识符的“二义性”问题
但它们适用的情境完全不同,是不同的规则
10.4 虚函数与多态
面向对象程序设计的三大特征
封装:现实世界中的联系
将数据与数据的处理函数结合在一起,隐藏数据,数据的访问只能通过类的公有函数继承:持续发展的,开放的编程态度
新的类在不破坏类的封装特性的情况下得到了原有类的功能,并依据新的需求添加新的数据和成员多态:对程序通用性的追求
同一名称实现不同的功能,达到行为标识统一
多态的实现
静态多态:编译时的多态
通过函数重载和运算符重载实现
这种多态是静态的运行时的多态
通过类继承关系和虚函数实现
这种多态是动态的
程序执行前,无法根据函数名和参数来确定该调用哪一个函数
在程序执行过程中,根据执行的具体情况来动态地确定
虚函数的定义
虚函数必须是类的成员函数,使用关键字virtual说明
类内定义格式如下: virtual 返回类型 函数名(参数表) {…};
若虚函数的定义形式为类内声明,类外定义。“virtual”就只用于类内声明,类外定义不使用“virtual”
虚函数具有继承性。基类中声明了虚函数,派生类中无论是否说明,同原型函数都自动为虚函数(如下例子中所示的display)
因此,在派生类中重新定义虚函数时,不必加关键字virtual
在派生类中重新定义虚函数时,不仅要同名,参数表和返回类型也全部必须与基类中的虚函数一样,否则会被认为是重载,而不是虚函数。
虚函数仅适用于有继承关系的类对象,只有类的成员函数才能说明为虚函数。
类中的静态成员函数为同一类对象共有,不受限于某个对象,不能作为虚函数。
因为在调用构造函数时对象还没有完成创建,所以构造函数不能定义虚函数。
析构函数可定义为虚函数,通常把析构函数定义为虚函数,实现撤消对象时的多态性。
虚函数实现多态
运行时的多态通过类继承关系和虚函数来实现
实现方法:基类指针访问派生类的同名虚成员函数
使用基类类型的指针变量或引用指向基类的派生类对象
通过该指针访问派生类对象的虚函数
多态的作用
基类派生出多个子类,这些派生类和基类一起构成了类的“家族”
多态提供了基于类族的通用操作接口
多态示例
场景:餐馆有不同类型的会员卡,不同的卡折扣和享受的服务不同
纯虚函数
纯虚函数是类的成员函数,类内定义格式如下:
virtual 返回类型 函数名(参数表)= 0;
有时,定义基类不是为了衍生基类对象,而是为了建立一个通用模板,派生出一系列子类或者为动态多态提供操作接口。
为了解决以上的问题,可以将基类的成员函数定义为纯虚函数。
抽象类vs 纯虚函数
虽然纯虚函数没有实现代码,但它为动态多态提供了操作接口
含有纯虚拟函数的类称为抽象类
抽象类无法实例化,不能生成对象
继承了抽象类的派生类可以依照本类的特性完成纯虚函数的定义,然后产生对象。
如果继承了抽象类的派生类不完成纯虚函数的定义,派生类也成为抽象类
10.5 函数模板
泛型程序设计 vs 模板
C++支持泛型编程(Generic Programming):算法不限于某个特定数据类型,而能在多种数据类型上实现操作
如何实现泛型编程?:将逻辑结构相同,但具体数据元素类型不同的数据或对象的通用行为抽象为模板(template),把数据类型作为参数传入模板,生成实例代码
模板声明
函数模板
函数模板可以用来创建一个通用功能的函数,以支持多种不同形参,进一步简化重载函数的函数体设计。
函数模板定义
函数模板定义由模板声明和函数定义组成
模板声明中的类型参数必须在函数定义中至少出现一次(否则无法在使用中实现函数的实例化,即使用时候无法知道具体的形参类型,因为形参类型需要根据函数接口得到的参数来确定)
函数形式参数表中的类型可以使用模板声明中的类型参数,也可以使用明确的数据类型
函数模板示例
函数模板的实例化
编译器根据模板自动生成函数的过程称为“模板的实例化”
函数模板的重载规则
函数模板可以重载,形参表必须不同 举例如下(错误例子的原因应该是没有办法实例化出一个函数可能会实例化出两个一模一样的函数,出现二义性)
当有多个同名函数和函数模式时,C++的编译器遵循以下先后顺序
寻找参数完全匹配的普通函数
寻找参数完全匹配的模板函数
寻找通过自动类型转换进行参数匹配的普通函数(定义形参为int输入double的时候强制转换为int)如果按以上步骤均未能找到匹配函数,调用错误
如果调用有多于一个的匹配选择,匹配出现二义性,调用错误
10.6 类模版
类模板定义
类模板用于实现类所需数据的类型参数化
类模板在表示如数组、表、图等数据结构显得特别重要,
这些数据结构的表示和算法不受所包含的元素类型的影响语法
在外部定义的时候即使成员函数只是用到了模板参数表中的部分参数,也需要她类模板所有的参数表都写出来(个人想法,因为这里起到的作用是通过类名去找函数,类名就是之前生命的模板类型所以是需要全部写出来的)