多继承、菱形继承、虚继承、多态


前言

多继承、菱形继承、虚继承、多态。

一、多继承

派生类都只有一个基类,称为单继承。除此之外,C++也支持多继承,即一个派生类可以有两个或多个基类
多继承的语法也很简单,将多个基类用逗号隔开。
例如已声明了类A、类B和类C,那么可以这样来声明派生类D:

class D: public A, private B, protected C
{
    
    
    //类D新增加的成员
}

D 是多继承形式的派生类,它以公有的方式继承 A 类,以私有的方式继承 B 类,以保护的方式继承 C 类。
D 根据不同的继承方式获取 A、B、C 中的成员,确定它们在派生类中的访问权限。

【注】执行基类的构造函数,再执行派生类的构造函数。
多继承形式下析构函数的执行顺序和构造函数的执行顺序相反。

【注】多重继承时会有多少张虚函数表?
多张表。
多重继承(无虚函数覆盖):
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中(所谓的第一个父类是按照声明顺序来判断的)。

多重继承(有虚函数覆盖):
多个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。

【参考】C++虚函数表与重载

二、菱形继承

菱形继承是多继承的一种特殊情况。
菱形继承有数据冗余二义性的问题。显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决。针对上述,C++引入了虚继承来解决这个问题。

using namespace std;
class Person
{
    
    
public:
	string _name; // 姓名
};
class Student : virtual public Person
{
    
    
protected:
	int _num; //学号
};
class Teacher : virtual public Person
{
    
    
protected:
	int _id; // 编号
};
class A : public Student, public Teacher
{
    
    
protected:
	int _a;
};
【注】优先使用对象组合,而不是类继承 。

三、虚继承

虚基类指针指向一张表,称为虚基类表,表中保存了当前获取到唯一的数据的偏移量。

虚继承是解决C++多重继承问题的一种手段,虚继承的底层实现原理与C++编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4(8)字节)和虚基类表(不占用类对象的存储空间)(虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

在虚继承情况下,底层子类对象的布局不同于普通继承,需要多出一个指向中间层父类对象的虚基类表指针vbptr。

vbptr是虚基类表指针(virtual base table pointer),vbptr指针指向一个虚基类表(virtual table),虚基类表存储了虚基类相对于直接继承类的偏移地址;通过偏移地址可以找到虚基类成员,虚继承不用像普通多继承维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

A类中有一个虚函数,所以必须有一个对应的虚函数表来记录对应的函数入地址。
有虚函数的类生成对象的前四个字节是一个指针,指向的是虚函数生成的虚函数表,所以A类的大小是4个字节。

对于B类,虚继承了A类,同时还拥有自己的虚函数,那么B类有一个B类vfptr指针指向自己的虚函数表,这里大小是4个字节。
可虚继承需要通过加入一个虚类指针记为vfptr_B_A来指向父类,这里大小也是4个字节,
同时还有继承父类的所有内容sizeof(A)是4个字节大小,所以B类的大小是12个字节。

C类的话也有一个自己的vfptr_C指向自己的虚函数表,然后是一个虚类指针vfptr_C_B_A,
同时还继承了B类的所有内容,所以大小是12字节+4+4 = 20字节。

【注】纯虚函数和纯虚继承:
先继承自一个父类,然后实现多个接口
类是抽象类,可将该类的构造函数说明为保护的访问控制权限

四、多态

多态分为静态多态动态多态。静态多态包括函数重载,泛型编程。动态是虚函数的使用。

【注】 泛型编程(Generic Programming) 指在多种数据类型上皆可操作

静态多态是指编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),
可推断出要调用的那个函数,如果有对应的函数就调用该函数,否则会出现编译错误。
动态多态是在程序执行期间(非编译器)判断所引用对象的实际类型,根据其实际类型调用相应的方法。

我们在这里主要说明的是动态多态:
父类的指针或引用可以指向子类对象。

实现多态

1 必须通过基类的指针或者引用调用虚函数
2 被调用的函数是虚函数,且必须完成对基类虚函数的重写

class Person //成人
{
    
    
  public:
	  virtual void fun()
	   {
    
    
	       cout << "全价票" << endl; //成人票全价
	   }
};
class Student : public Person //学生
{
    
    
   public:
   	   //子类不写virtual可以构成重写
	   virtual void fun() //子类完成对父类虚函数的重写
	   {
    
    
	       cout << "半价票" << endl;//学生票半价
	   }
};
void BuyTicket(Person* p)
{
    
    
   p->fun();
}

int main()
{
    
    
   Student st;
   Person p;
   BuyTicket(&st);//子类对象切片过去
   BuyTicket(&p);//父类对象传地址
}

如果满足多态,编译器会调用指针指向对象的虚函数,而与指针的类型无关。如果不满足多态,编译器会直接根据指针的类型去调用虚函数。

【注】C++11新增了两个关键字。用final修饰的虚函数无法重写,用final修饰的类无法被继承。
被override修饰的虚函数,编译器会检查这个虚函数是否重写。如果没有重写,编译器会报错。

多态提高了代码的扩展性,维护方便。

纯虚函数和抽象类

在虚函数的后面加上=0就是纯虚函数,有纯虚函数的类就是抽象类,也叫做接口类。抽象类无法实例化出对象。抽象类的子类也无法实例化出对象,除非重写父类的虚函数。

class Car
{
    
    
 public:
    //有纯虚函数的类为抽象类,抽象类不能实例化创建对象
    virtual void fun() = 0; //不用实现,只写接口就行。
}

【注】纯虚函数:
子类必须重写;
先继承自一个父类,然后实现多个接口
类是抽象类,可将该类的构造函数说明为保护的访问控制权限。

【注】纯虚函数的使用场景?它的存在意义?
在实际开发中,可以先定义一个抽象类,只完成部分功能未完成的功能交给派生类来完成,这部分功能往往是基类是不需要的,虽然抽象基类没能完成,可是却要求派生类来完成。

多态的原理

_vftptr(virtual function table pointer),即虚函数表指针,简称虚表指针。在计算类大小的时候要加上这个指针的大小。
那么虚表是什么呢?虚表就是存放虚函数的地址的地方。每当我们去调用虚函数,编译器就会通过虚表指针去虚表里面查找

【注】虚表是怎么产生的?
C++中,一个类存在虚函数,那么编译器就会为这个类生成一个虚函数表,在虚函数表里存放的是这个类所有虚函数的地址(虚表从属于类)。

编译器会为包含虚函数的类加上一个成员变量,该成员变量是一个指向虚函数表的指针,
因此虚表指针是一个成员属性,也就是说,如果一个类含有虚表,那么类的每个对象都含有虚表指针。

当生成类对象的时候,编译器会自动的将类对象的前四个字节设置为虚表的地址(不一定哦,但是大部分是),而这四个字节就可以看作是一个指向虚函数表的指针。
虚函数表属于类,类的所有对象共享这个类的虚函数表。
虚函数表存储在只读数据段(.rodata),也就是说虚函数表在编译阶段就已经形成了,虚函数表指针是在构造函数中进行初始化赋值的。
虚函数表中保存了函数的入口地址。

【注】怎么实现多态中的动态调用?
首先是虚表的创建,
如果继承了虚函数,那么
1 子类先拷贝一份父类虚表,然后用一个虚表指针指向这个虚表
2 如果有虚函数重写,那么在子类的虚表上用子类的虚函数覆盖
3 子类新增的虚函数按其在子类中的声明次序增加到子类虚表的最后
创建父类的指针或引用,由于父类的指针或引用可以指向子类对象,那么每当我们调用虚函数时,编译器会根据指针指向对象去进行对应虚函数的调用,会通过虚表指针去虚表里面查找然后执行。

【注】虚函数是带有virtual的函数,虚函数表是存放虚函数地址的指针数组,虚函数表指针指向这个数组。对象中存的是虚函数指针,不是虚函数表。

【注】虚函数表属于类,不属于对象。所以虚函数表应该存在共有区:全局数据段

【注】虚析构:让子类在多态中也能进行析构。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数不调用派生类析构函数,这样就会造成派生类对象析构不完全。

【注】构造函数不能是虚函数的原因:
1、构造一个对象的时候,必须知道对象的实际类型,而虚函数是在运行期间确定实际类型的如果构造函数为虚函数,则在构造一个对象时,由于对象还未构造成功,编译器还无法知道对象的实际类型,是该类本身还是派生类,无法确定

2、虚函数的执行依赖于虚函数表,而虚函数表是在构造函数中初始化的,即初始化vptr,让它指向虚函数表。如果构造函数为虚函数,则在构造对象期间,虚函数表还没有被初始化,将无法进行

Hello C++(十六)——多继承
继承——多继承
C++ 多态 超详细讲解

猜你喜欢

转载自blog.csdn.net/XUfengge111/article/details/125553929