继承
- 继承的概念
- 继承体系中对象赋值转换
- 继承方式对继承后的访问限定(重定义-同名隐藏)
- 继承体系中派生类的默认成员函数
- 友元函数、静态成员在继承中的特点
- 菱形继承和菱形虚拟继承
继承的概念
为了让代码可以复用,当前类可以继承其他类的成员变量以及成员函数,这样就能使得当前类更加复杂,足以描述更加复杂的情况,当前增加新功能的类,我们称之为派生类,也叫子类,继承自哪个类,哪个类就是基类,也叫父类。这样的话,继承了父类的成员,子类中就有了父类所有的成员,同时还有自己新增加的成员。
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
从代码中就能看到,B类继承了A类,并且是以public的方式继承的。这样B类中不仅包含了父类A的变量,也有自己新增的变量。(注意:针对不同的继承方式,继承后对父类的访问权限是不同的)
下面的表就展示了当使用不同继承方式时,子类访问父类成员的权限有什么区别
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | public | protected | private |
基类的protected成员 | protected | protected | private |
基类的private成员 | private(子类不可见) | private(子类不可见) | private(子类不可见) |
这个表为了更好记忆,可以这样去理解:
首先将可见性划分等级public>protected>private
要知道当前继承方式继承之后,子类对父类成员的访问权限,我们需要先看当前是什么方式继承的
继承方式为A,基类中成员权限等级如果存在比A等级高的,就会降级为当前等级。如果比A等级低的,则不进行权限的降级。
举例:
继承方式为protected,基类中fun1的访问权限为public,fun2访问权限为protected,fun3访问权限为private,那么子类中的继承下来的fun1的权限就被降级为protected,fun2的访问权限不变,fun3的访问权限不变。
注意:对于一个类中成员,使用class定义,如果没有指明访问权限,那么默认是private权限。使用struct定义,没有指明访问权限,默认是public权限。
继承体系中对象赋值转换
派生类对象是可以给基类对象赋值的,反之则不行
例如图中学生类给person类进行赋值是可以的,因为学生类中本来就包含了两部分变量,基类继承和子类新增。
而基类继承下来的可以顺理成章的赋值给基类,子类继承的不进行赋值即可。
为什么子类对象可以给基类对象赋值,而基类对象不能给子类对象赋值
但是如果反过来,让基类对象给子类对象赋值时,基类对象的变量赋值给了子类中继承自基类的,但是子类中子类新增就无法进行赋值了。
继承方式对继承后的访问限定(重定义-同名隐藏)
由于继承体系中基类和派生类都有独立的作用域,当子类和父类中有同名成员时,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)。
如果是成员函数的隐藏,只需要成员函数名一样即可。
继承体系中派生类的默认成员函数
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
构造函数:
对于基类来说,如果基类没有显式定义构造函数,那么在子类的构造函数的初始化列表中是否调用基类的构造函数都可以。
如果基类显示定义构造函数了,那么在子类的构造函数的初始化列表中一定要调用基类的构造函数。
拷贝构造函数:
子类的拷贝构造函数中,一定要显式调用基类的拷贝构造函数,以完成基类部分的拷贝初始化。
赋值运算符重载:
子类的赋值运算符重载,一定要显式调用基类的赋值运算符重载,以完成基类部分的赋值。
析构函数:
子类的析构函数中,先调用子类的析构函数,子类的析构函数最后一句会自动添加基类的析构函数,用于调用基类的析构函数清理基类成员,这样就保证先清理子类对象成员再清理基类对象成员。
派生类对象的初始化是先调用派生类对象的构造函数,销毁派生类对象是先调用派生类对象的析构函数。
友元函数、静态成员在继承中的特点
一句话:友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
菱形继承和菱形虚拟继承
首先我们要知道什么是单继承,什么是多继承:
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
如图即为菱形继承,其中BC类继承A类,D类继承BC类。
这样的继承方式会存在一个问题,数据冗余和二义性。
由于B和C都继承了一份A类的成员,那么D将B和C继承过来的时候会存在两份A类中的成员。这样的话,数据就被保存了两份,而且当需要通过D类对象访问A类成员时,就出现问题了,由于有两份A类的成员,那么到底是访问哪个类中的成员呢?
我们可以通过指定作用域来解决访问二义性的问题,但是无法解决数据冗余的问题。
因此就出现了菱形虚拟继承
什么是菱形虚拟继承?
专门用于解决由于菱形继承导致的数据冗余和访问二义性的问题。
我们直接来看普通的菱形继承和菱形虚拟继承之间,对象成员模型的区别。
//普通继承
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
//菱形虚拟继承
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
上述代码展示了两种继承方式的写法,下面我们来看下这两种继承方式对成员对象模型有什么区别
可以看到,普通继承方式中,就是按照继承顺序进行继承,存在多个_a。
可以看到,在虚拟继承中,对象模型发生了变化,基类A被置于最下方,而其余的B类C类和子类的相对顺序没有变化,但是B类C类中除了自己新增的成员,还多了一个ptr指针,我们称这个指针为虚基表指针。
这个虚基表指针指向的是一个虚基表。
而虚基表是这样的结构
虚基表通过查看子类对象相对于基类部分的偏移量后,就能够准确的找到基类部分,从而去修改基类部分的成员。
例如上述的继承体系中,
ptr1指向的内容中是这样的值
ptr1指向的内容中是这样的值