C++之多态总结(多态的定义及实现,抽象类,多态原理,单继承,多继承中的虚函数表)

Posted _BitterSweet

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++之多态总结(多态的定义及实现,抽象类,多态原理,单继承,多继承中的虚函数表)相关的知识,希望对你有一定的参考价值。

1.多态的概念

1.1概念

  • 多态的概念:通俗来说,就是多种形态具体点就是去完成某个行为当不同的对象去完成时会产生出不同的状态
  • 举个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票

2.多态的定义及实现

2.1多态构成的条件

  • 多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价Student对象买票半价
    在这里插入图片描述
  • 在继承中要构成多态还有两个条件
  • .派生类中,必须对基类的虚函数进行重写,被调用的函数必须是虚函数
  • .必须通过基类的指针或者引用调用虚函数
    在这里插入图片描述

2.2虚函数

  • 虚函数:即被virtual修饰的类成员函数称为虚函数
    在这里插入图片描述

2.3虚函数的重写

  • 虚函数的重写:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型函数名字参数列表完全相同),称子类的虚函数重写了基类的虚函数
    在这里插入图片描述
  • 这里有一个注意的点:在重写基类的虚函数时,派生类的虚函数不加virtual关键字,也可以构成重写,因为基类的虚函数被继承下来依然保持着虚函数的属性,但是这样的写法不太规范,并不建议使用
    在这里插入图片描述

2.3.1虚函数重写的两个例外

  • .协变(基类与派生类虚函数返回值类型不同)
  • 派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用时,称为协变
    在这里插入图片描述
  • .析构函数的重写:(基类与派生类析构函数的名字不同
  • 如果基类的析构函数为虚函数,此时派生类析构函数只要定义无论是否加virtual关键字都与基类的析构函数构成重写,即使基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
    在这里插入图片描述

2.4 C++11 overrride 和 final

  • 从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了overridefinal两个关键字,可以帮助用户检测是否重写
  • final:修饰虚函数,表示该虚函数不能再被继承
    在这里插入图片描述
  • override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
    在这里插入图片描述

2.5重载、覆盖(重写)、隐藏(重定义)的对比

在这里插入图片描述

3.抽象类

3.1概念

  • 在虚函数的后面加上 =0 ,则这个函数为纯虚函数包含纯虚函数的类叫做抽象类(也叫接口类)抽象类不能实例化出对象派生类继承后也不能实例化出对象只有重写纯虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
    在这里插入图片描述

3.2接口继承和实现继承

  • 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现
  • 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口
  • 所以如果不实现多态,不要把函数定义成虚函数

4.多态的的原理

4.1虚函数表

  • 我们先看一道常见的笔试题:求sizeof(class)的大小

  • 普通函数
    在这里插入图片描述

  • 虚函数
    在这里插入图片描述

  • 测试完我们发现是8bytes,我们打开监视窗口会发现
    在这里插入图片描述

  • 我们再增加一个派生类去探一探这虚函数表
    在这里插入图片描述

  • 我们打开监视窗口看一看
    在这里插入图片描述

  • 总结

  • .派生类的虚表生成过程a.先将基类的虚表内容拷贝一份到派生类虚表中b.如果派生类重写了基类中的某个虚函数,用派生类自己的虚函数覆盖基类的虚函数 c.派生类新增加的虚函数按其再派生类中的声明顺序依次加到虚表的后面

  • .虚函数表的本质其实是一个指针数组,数组以nullptr结束,a.数组中每一个元素为虚函数指针,b.子类会继承父类的虚函数表,c.普通函数指针不会放在虚表中,d.虚函数指针在虚表的存放顺序和声明/定义的顺序一致

  • .虚表指针存在对象中,虚表存在代码段,虚函数指针存在虚表中,虚函数再代码段,

  • .虚函数指针指向虚函数的首地址,它是一个二级指针,因为指向的虚表是指针数组

4.2多态实现的原理

  • 我们还是拿买票来举例子:

在这里插入图片描述

  • 打开监视窗口,我们一探究竟
    在这里插入图片描述
  • 此时我们转到汇编,去看看函数调用的过程
    在这里插入图片描述
  • 总结
  • 多态调用的过程(通过指针/引用调用虚函数)
  • a.首先通过实际指向的实体获取虚表指针
  • b.通过虚表指针找到虚表
  • c.从虚表中找到执行函数的实际地址
  • d.执行对应地址的函数指令,完成多态行为

4.3动态绑定与静态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载模板编程
  • 动态绑定又称后期绑定(晚绑定),是在程序运行期间根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态,比如:继承

5.单继承和多继承关系中的虚函数表

5.1单继承中的虚函数表

  • 我们举一个单继承的栗子:

在这里插入图片描述

在这里插入图片描述

  • 那么我可以写一段代码来查看d的虚表
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
 // 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
 cout << " 虚表地址>" << vTable << endl;
 for (int i = 0; vTable[i] != nullptr; ++i)
 {
 printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
 VFPTR f = vTable[i];
 f();
 }
 cout << endl; 
 }
// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
 // 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
 // 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
 // 4.虚表指针传递给PrintVTable进行打印虚表
 // 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。
int main()
{
 Base b;
 Derive d;
 VFPTR* vTableb = (VFPTR*)(*(int*)&b);
 PrintVTable(vTableb);
 VFPTR* vTabled = (VFPTR*)(*(int*)&d);
 PrintVTable(vTabled);
 return 0; 
 }
  • 运行结果:
    在这里插入图片描述

  • 虚表中的位置:

在这里插入图片描述

  • 单继承虚表总结
  • .子类继承父类的虚表
  • .子类重写的虚函数,其虚函数指针会覆盖父类中的虚函数指针
  • .子类型定义的虚函数,其虚函数指针会按照定义/声明的顺序存放在继承的父类的虚表末尾

5.2多继承中的虚函数表

  • 举一个多继承的栗子:
    在这里插入图片描述

  • 我们打开监视窗口:
    在这里插入图片描述

  • 我们依然写一段代码来查看虚表地址:

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
 cout << " 虚表地址>" << vTable << endl;
 for (int i = 0; vTable[i] != nullptr; ++i)
 {
 printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
 VFPTR f = vTable[i];
 f();
 }
 cout << endl; 
 }
int main()
{
 Derive d;
 VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
 PrintVTable(vTableb1);
 VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
 PrintVTable(vTableb2);
 return 0; 
}
  • 运行结果:
    在这里插入图片描述

  • 多继承虚表总结

  • .虚表的个数与直接父类的个数相同

  • .子类继承每一个直接的父类

  • .子类重写的虚函数,其虚函数指针会覆盖对应父类中的虚函数指针

  • .子类新定义的虚函数,其虚函数指针会按照声明或定义的顺序依次存放在继承的第一个直接父类的虚表末尾

以上是关于C++之多态总结(多态的定义及实现,抽象类,多态原理,单继承,多继承中的虚函数表)的主要内容,如果未能解决你的问题,请参考以下文章

C++多态

C++进阶:多态多态的构成条件 | 虚函数的重写 | 抽象类 | 多态的原理 | 多继承的虚函数表

C++进阶:多态多态的构成条件 | 虚函数的重写 | 抽象类 | 多态的原理 | 多继承的虚函数表

C++进阶:多态多态的构成条件 | 虚函数的重写 | 抽象类 | 多态的原理 | 多继承的虚函数表

C++进阶:多态多态的构成条件 | 虚函数的重写 | 抽象类 | 多态的原理 | 多继承的虚函数表

C++ 面向对象程序三大特性之 多态