类的继承——派生
概述继承Inheritance单继承protected访问控制友元问题访问控制派生类成员标识符的作用域继承方式和子类型继承方式public继承与子类型派生类对象的初始化和消亡处理初始化派生类对象的赋值派生类拷贝构造函数派生类对象的赋值操作虚函数与消息的动态绑定消息的多态性虚函数虚函数动态绑定的实现抽象类纯虚函数抽象类用抽象类实现类的真正抽象作用多继承多继承的需要性多继承的类定义多继承带来的问题虚基类聚合和组合继承不是类代码复用的唯一方式类之间的整体与部分关系聚合组合
概述
软件复用: 把现有软件及其部分拿来复用
一般途径:
- 函数(库)
- 函数颗粒度太小
- 函数的功能不完全符合要求
- 类(库)
- 类实现较大的功能
- 对某个具体领域来讲, 类具有通用性
代码复用: 开发新软件时, 把现有软件的代码拿过来用. 可以提高开发效率, 保证软件质量.修改代码使之更能适应程序的需要。Ctrl+C, Ctrl+V过于傻逼, 使我骂娘. 且源代码不好得到, 不好读懂.
继承Inheritance
- 类的分类: 基类(父类) 派生类(子类)
- 继承的分类 单继承: 只有一个派生类 多继承: 有多个派生类
单继承
- 定义格式:
其中:
<成员说明表>: 在派生类中新定义和重定义的成员
e.g.定义派生类时, 基类一定要定义。
- 除了拥有新定义的成员外,派生类拥有基类的所有成员(基类的构造函数、析构函数和赋值操作除外)。
- 实际上,派生类的对象包含了基类的一个子对象。
- 重名与Hidden
- 如果基类中有个重名的成员m,则在派生类作用域中基类成员名不可见,访问的时候需要加基类名加以限制
- 可以在派生类中使用using声明把基类中某个的函数名对派生类开放
protected访问控制
友元问题
若未直接说明, 基类的友元不是派生类的友元
访问控制
派生类不能直接访问private成员, 可以直接访问public成员
为了解决这个冲突,我们定义了
protected 类,使得类的对象本身不能访问这个数据,但派生类可以访问一般情况下,把今后不太会变动但又需要被派生类使用的不应对用户公开的成员声明为
protexted 派生类成员标识符的作用域
- 派生类对基类成员的访问除了受到基类的访问控制的限制以外,还要受到标识符作用域的限制。
- 对基类而言,派生类成员标识符的作用域是嵌套在基类作用域中的。
继承方式和子类型
继承方式
继承方式/派生类/基类 | public | private | protected |
public | public | denied | protected |
private | denied | denied | denied |
protected | protected | denied | protected |
当然在派生类中还可以调整继承方式
public继承与子类型
- 子类型
- 对于用T类型表达的所有程序P,当用类型S去替换程序P中的所有类型T的时候,程序P的功能不变,则称类型S是类型T的子类型
- 即,S能够替代T的所有功能
- 显然,在C++中,继承方式为public的派生类可以看作是一个子类型
在需要基类对象的地方也可以用派生类去替代
看下面的例子
下面的合法
下面的不合法
派生类对象的初始化和消亡处理
初始化
- 派生类对象的初始化由基类和派生类共同完成
- 从基类继承的数据成员由基类的构造函数构造
- 新的派生类数据成员由自己的构造函数构造
- 当创建派生类的对象时
- 是先调用本身类的构造函数,但在进入函数体之前,会去调用基类的构造函数,然后再执行本身类构造函数的函数体!
- 默认情况下,调用基类的默认构造函数,如果要调用基类的非默认构造函数,则必须在派生类构造函数的成员初始化表中显式指出。
- 也就是说,构造函数的成员初始化表(即使没显式给出)中有对基类的构造函数的调用代码。
- 当派生类对象消亡时,
- 是先调用本身类的析构函数,本身类析构函数的函数体执行完之后,再去调用基类的析构函数!
- 也就是说,析构函数的函数体最后有对基类析构函数的调用代码。
- 如果一个类D既有基类B、又有成员对象类M,则
- 在创建D类对象时,构造函数的执行次序为:B->M->D
- 当D类的对象消亡时,析构函数的执行次序为:D->M->B
派生类对象的赋值
派生类拷贝构造函数
- 派生类的隐式拷贝构造函数(由编译程序提供):
- 对派生类中新定义的成员进行拷贝初始化外。
- 调用基类的拷贝构造函数实现对基类成员的初始化。
- 派生类自定义的拷贝构造函数:
- 在默认情况下调用基类的默认构造函数对基类成员初始化。
- 需要在“成员初始化表”中显式地指出调用基类的拷贝构造函数来实现对基类成员的初始化。
派生类对象的赋值操作
- 派生类隐式的赋值操作:
- 对派生类新定义的成员进行赋值。
- 调用基类的赋值操作对基类成员进行赋值。
- 派生类自定义的赋值操作:
- 不会自动调用基类的赋值操作。
- 需要在自定义的赋值操作符重载函数中显式地指出调用基类的赋值操作。
虚函数与消息的动态绑定
消息的多态性
- 相同的一条消息可以发送到不同类的对象,从而会得到不同的解释(处理)。例如,下面的A和B类都有对消息f的处理函数
一般而言,采用静态绑定,也就是说在编译时刻根据对象的类型来决定采用哪一个消息处理函数。
可是在继承的情况下,如果是
public类的继承,可能出现冲突,就像下面事实上,在上面的问题中,默认采用静态绑定,所以都采用
A::f ,为此我们有必要采用动态绑定- 在C++中,需要在基类中用虚函数来指出动态绑定。例如,下面A类中的f是个虚函数,对该函数的调用将采用动态绑定
也就是写成
virtual void f(); //虚函数 虚函数
- 两个作用:
指出消息采用动态绑定
指出基类中可以被派生类重定义的成员函数
- 在派生类中重定义基类的成员函数
对于基类中的一个虚函数,在派生类中定义的、与之具有相同型构的成员函数是对基类该成员函数的重定义(或称覆盖,override)
- 相同的型构是指: 派生类中定义的成员函数的名字、参数个数和类型与基类相应成员函数相同;其返回值类型与基类成员函数返回值类型或者相同,或者是基类成员函数返回值类型的public派生类。
- 消息的动态绑定将绑定到派生类中与基类同型构的成员函数!
- 对虚函数几点说明
- 只有类的成员函数才可以是虚函数,但静态成员函数不能是虚函数。
- 构造函数不能是虚函数,析构函数可以(往往)是虚函数。
- 只要在基类中说明了虚函数,在派生类、派生类的派生类、...中,与基类同型构的成员函数都是虚函数(virtual可以不写)。
- 只有通过基类的指针或引用访问基类的虚函数时才进行动态绑定
- 基类的构造函数和析构函数中对虚函数的调用不进行动态绑定。(为什么?)
看下面的例子
因为在此情况下,A( )函数先调用,如果直接动态绑定到B::f( ),极有可能涉及到一些在B( )中初始化的数据成员;同时由后一个例子,析构函数往往是virtual
- 通过基类指针访问派生类中新定义的成员
又看一个例子
- 什么时候需要
virtual - 在设计基类时,有时虽然给出了某些成员函数的实现,但实现的方法可能不是最好,今后可能还会有更好的实现方法。
- 在基类中根本无法给出某些成员函数的实现,它们必须由不同的派生类根据实际情况给出具体的实现。(抽象类与纯虚函数)
虚函数动态绑定的实现

抽象类
纯虚函数
- 纯虚函数是没给出实现的虚函数,函数体用“=0”表示, 例如:
虚函数需要在派生类中给出实现。
抽象类
- 包含纯虚函数的类称为抽象类。
- 抽象类不能用于创建对象。 例如:
- 抽象类的作用:为派生类提供一个基本框架和一个公共的对外接口
- 图形数据的输入:
- 图形输出
看一个跟图形相关的例子
- 即使今后增加了图形的种类,上述代码(属于高层代码)不需要改动!只需要增加相应的类(属于低层代码)就行。这体现了多态带来的好处:便于高层代码的复用。
用抽象类实现类的真正抽象作用
- 由于在C++中使用某个类时必须要见到该类的定义,
- 因此,使用者能够见到该类的一些实现细节(如:非public成员),这样会使得类的抽象作用大打折扣。
- 有手段绕过类的访问控制而使用类的非public成员,类的封装作用可能被破坏,带来安全问题!


多继承
多继承的需要性
如果尝试用一个C来继承A,B的所有特性,可能出现以下问题
- 用单继承实现
- 概念混乱:导致A和B之间增加了层次关系。
- 易造成不一致:A中的m、fa(或B中的n、fb)与C中的m、fa(或n、fb)
- 不能完全实现子类型:只有A和B中某一类对象能被C类对象替代。
- 用对象成员实现
- 不能实现子类型:程序中需要A和B类对象的地方都不能用C类对象去替代
- 完美的解决方法:用多继承实现
优雅安全
多继承的类定义
多继承是指派生类可以有一个以上的直接基类。多继承的派生类定义格式为
- 继承方式及访问控制的规定同单继承
- 派生类拥有所有基类的所有成员
- 基类的声明次序决定:
- 对基类数据成员的存储安排
- 对基类构造函数/析构函数的调用次序
解释一下

public多继承的派生类与它的各个基类之间是子类型关系。把多继承派生类对象的地址赋给基类的指针时会自动进行地址调整。在此处,
c.fa() 也就是A::fa(this) ,查看上述代码的输出结果,我们发现,pa就是A下面的this指针,满足我们的理论
通过pa只能访问到A类和C类中的内容,但不会访问到B类中的内容。 通过pb只能访问到B类和C类中的内容,但不会访问到A类中的内容。 通过pa或pb访问C类中的内容一般要通过虚函数动态绑定来实现。
多继承带来的问题
- 多继承使得语言特征复杂化、加大了编译程序的难度以及使得消息绑定复杂化等,从而给正确使用多继承带来困难。因此,有些面向对象语言(如Smalltalk、Java等)放弃了多继承机制。
- 多继承带来的两个主要问题
- 名冲突问题
- 重复继承问题
- 名冲突问题
- 如果基类中有两个名字一模一样的函数,子类怎么知道到底是谁的函数?
- 最好的方法是加上类名限制
- 重复继承问题
- 如果说子类继承的两个类,又分别继承自一个相同的类,那岂不是最后一个类继承了两次最开始的基类的成员?
- 如果要求类D中只有一个x,则应把A定义为B和C的虚基类:
这样,D类的对象d就只有一个x了。
虚基类
虚基类的构造函数
- 对于拥有虚基类的类,创建该类的对象时:
- 虚基类的构造函数由该类的构造函数直接调用。
- 虚基类的构造函数优先非虚基类的构造函数执行。
虚基类的实现

聚合和组合
继承不是类代码复用的唯一方式
- 在面向对象程序设计中,有些代码复用不宜用继承来实现:
- 继承除了支持代码复用外,还体现了类之间在概念上的一般与特殊关系(is-a-kind-of)。
- 如果两个类之间没有这个关系,纯粹是为了代码复用而用继承把它们关联起来,这样会造成概念上的混乱。
- 例如:“飞机”类复用“发动机”类的功能就不宜用继承来实现。
类之间的整体与部分关系
- 类之间除了继承关系外,还存在一种整体与部分的关系(is-a-part-of),即一个类的对象包含了另一个类的对象。例如,“飞机”类与“发动机”类之间就是整体与部分的关系。
- 在整体与部分的关系中,新的类(整体)通过成员类(部分)复用已有类的功能。
- 类之间的整体与部分的关系可以分为: 聚合(aggregation)关系 组合(composition)关系
聚合
- 在聚合关系中,被包含的对象与包含它的对象独立创建和消亡,被包含的对象可以脱离包含它的对象独立存在。例如,
- 一个公司与它的员工之间是聚合关系
- 实现上,聚合类的成员对象一般是采用对象指针表示,用于指向被包含的成员对象,而被包含的成员对象是在外部创建,然后加入到聚合类对象中来的。
组合
- 在组合关系中,被包含的对象随包含它的对象创建和消亡,被包含的对象不能脱离包含它的对象独立存在。例如,
- 一个人与他的头、手和脚之间则是组合关系。
- 实现上,组合类的成员对象一般直接是对象,有时也可以采用对象指针表示,但不管是什么表示形式,成员对象一定是在组合类对象内部创建并随着组合类对象的消亡而消亡。
- 用指针表示成员对象时,既可以实现组合关系,也可以实现聚合关系,它们的区别在于: 在组合关系中,成员对象是由包含它的对象创建和撤销。 在聚合关系中,成员对象不由包含它的对象创建和撤销。
- 继承与封装存在矛盾,聚合/组合与封装则不存在这个矛盾。
在基于继承的代码复用中,一个类向外界提供两种接口:
public:对象(实例)用户public+protected:派生类用户 在基于聚合/组合的代码复用中,一个类对外只需一个接口:public
- 继承更容易实现子类型:
在C++中,
public继承的派生类往往可以看成是基类的子类型。 在需要基类对象的地方可以用派生类对象去替代。 发给基类对象的消息也能发给派生类对象。 具有聚合/组合关系的两个类不具有子类型关系!
