目录
概念及定义
概念:
1、继承机制是面向对象程序设计使代码可以复用的最重要的手段。
2、允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样实现的类称为派生类/子类。基于实现该类的原有类称为基类/父类
3、继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
4、继承是类层次设计的复用
定义:
定义方式:class 派生类:继承方式 继承类
继承方式可以是public、protected、private三种,他们在继承基类时,所具有的特性以及表现出的结果也有所不同,具体如下:
总结:
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式
- 在实际应用中一般使用的都是public继承,几乎很少用protect/private继承,也不提倡,因为protect/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
基类和派生类赋值转化
前提:一定要在public的继承方式下才满足
1、可以直接使用子类对象给父类对象赋值,反过来不行
2、可以使用基类的指针指向子类的对象,反过来不行。如果一定要指向,必须强转,不推荐,仅仅是能够通过编译,但是在使用的时候可能会造成程序崩溃。
3、可以使用基类的引用去引用子类的对象,反过来不行。在底层和指针是一个原理。
继承中的作用域问题
- 在继承体系中基类和子类都有独立的作用域
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫同名隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
- 注意在实际中继承体系中最好不要定义同名的成员。
派生类的默认成员函数
构造函数
1、基类没有显示定义任何构造函数
子类可以提供构造函数,也可以不提供构造函数,是否提供根据子类中完成的功能或者具体情况决定。
2、基类显式定义了构造函数
①基类的构造函数是无参或者全缺省的,此时和没有显示定义任何构造函数一样
②基类的构造函数是非默认构造函数
子类必须要定义自己的构造函数,在子类构造函数初始化列表位置显示调用基类的构造函数(完成从基类中继承下来的成员的初始化工作)
先后顺序:
创建哪个类的对象,编译器就会调用这个类的构造函数
创建子类对象,本质上调用的是子类的构造函数,但是子类的构造函数的初始化列表处会调用基类的构造方法来初始化从基类继承下来的对象。然后再去执行子类构造函数的函数体。
因此,基类对象的构造函数先执行完毕,子类构造函数后执行完毕。
拷贝构造函数:
1、基类的拷贝构造函数未定义
子类的拷贝构造函数可定义可不定义,根据子类实际情况决定。
2、基类的拷贝构造函数定义了
子类的也需要定义拷贝构造函数,并且需要在子类的拷贝构造函数初始化列表的位置显示调用基类的拷贝构造函数
赋值运算符重载
1、基类未定义:
子类可定义可不定义
2、基类的赋值运算符重载显示定义了
子类也需要定义,分为两个大步骤:
①调用基类的赋值运算符重载给基类部分成员赋值base::operator=(d)
②给子类自己新增的部分进行赋值
注意:基类的operator=与子类自己的operator=构成同名隐藏,因此要加作用域限定符,否则默认调用子类自己的operator=,就会陷入无限递归。
析构函数
编译器将子类的析构函数完成之后,会自动在子类析构函数的最后一条一句语句之后插上一条调用基类析构函数的汇编代码call ~base()
析构子类对象,本质上先调用子类的析构函数,在子类析构函数执行完最后一条语句时,还会去执行编译器加的一条调用基类析构函数的汇编指令。
基类中哪些成员被子类继承了?
成员变量:
普通成员变量,全部被继承。
静态成员变量也被继承了
注意:静态成员变量在整个继承体系中只有一份
成员方法:
普通成员方法被子类继承了。
静态成员方法也被子类继承了。
友元函数:
友元函数不是类的成员函数,它只是在一个类中进行声明,目的是打破类的封装去访问原本外部不可访问的成员。
友元函数不能被继承。
不同的继承体系
单继承
一个子类只有一个父类。这种继承方式是最为简单的:
多继承:
一个子类有两个或两个以上的直接父类,这样的继承关系称之为多继承。
注意:
- 在多继承的场景下,建议在继承的每一个基类名称前都加上继承方式,如果不加,可能无法达到我们的预期结果。
- 多继承的对象模型随着子类继承基类的顺序变化而变化。
①
②
菱形继承
存在的问题
1、存在二义性问题
最顶层基类成员在最底层子类中存在两份,如果直接通过最底层子类访问最顶层的基类成员时,会出现访问不明确问题。
2、存在数据冗余问题
最底层子类的对象模型:
可以看到base对象有两份。
解决方法:
- 在成员变量前加上它直接父类的名称。无法从根本上解决问题
采用菱形虚拟继承,让最顶层基类中的成员变量在最底层子类中只存在一份。(真正的解决方法)
菱形虚拟继承:
虚拟继承:
使用虚拟关键字virtual修饰的继承称之为虚拟继承:
虚拟继承存在的意义:为了解决菱形继承存在的二义性和数据冗余的情况
原理:
发现对子类对象d1初始化的时候,编译器将d1对应的内存空间前4个字节进行设置。可以查看该地址指向的内容,该地址其实是一个指针,指向的是一张偏移量表格,也称为虚基表。该指针称为虚基表指针/偏移量表格指针。
虚基表存在的意义:帮助编译器找到基类对象中成员变量在内存中存在的位置。
d1的对象模型:
虚拟继承和单继承的区别:
- 对象模型与单继承相反,子类在上,基类在下
- 子类对象比基类对象多4个字节
- 多出来的4个字节是在调用子类构造函数初始化子类对象的时候进行初始化的。
- 虚继承的初始化过程为:取对象前4个字节中的内容(偏移量表格 || 虚基表的地址);取该地址空间中往后偏移4字节之后的内容(当作偏移量使用);结合偏移量给基类中的成员变量赋值。
虚拟继承 + 菱形继承:
对象模型: