新C++(7):多态那些事儿_下
创始人
2024-05-23 12:45:25
0

"当人类悬浮到腐朽,有谁愿追随彗星漂流哦~"

一、多态原理

(1)虚函数表指针(虚表指针)

紧接上一篇sizeof(Base)这一小段说起。

class Base1
{
public:void func(){}
private:int _a;
};class Base2
{
public:virtual void func() {}
private:int _a;
};

我们知道,两个Base虽然在成员上相差无几,但是因为虚函数的存在,Base2一定是 > Base1的。

前篇说了这里的原因在于,一旦有虚函数,类里面就会自动生成一个虚函数指针。那么什么是虚函数指针呢?

虚函数表指针是能够实现动态多态的根本原因,一个有虚函数的类里,会多出一个_vfptr即虚函数表指针(虚表指针),它指向的是一个函数指针数组_vtable,而这个指针数组,存储的是类里虚函数的地址。

(2)虚函数表

那么这个虚函数表在哪里呢?这个虚函数表有几份呢?

在图示中,我们清晰地看到,相同的类它的虚表指针是固定的,即它们共享一份虚函数表。不同的类,有不同的虚表指针。

但这些表存储在哪个地方呢?它们是在编译时生成还是在构造时生成呢?

原来虚函数表是存储在静态区、代码段区域。

虚函数表存储在常量、代码区域附近
虚函数表在编译时就已经存在。
虚函数表指针在构造函数初始化列表出初始化。

(3)重写覆盖

为什么说子类对基类虚函数的定义叫做重写,这个行为又被叫做覆盖呢?

class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}
private:int _a;
};class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};

这份份代码只对基类的func1虚函数进行了重写。

因此,一定程度上,就是可以这么理解。建立_vftble子类虚函数表的时候,是把基类的虚函数表拷贝一份过去,完成重写的部分,则"覆盖"式地填写进子类的虚函数表中。重写是语法的叫法,覆盖是原理层的叫法。

(4)静态绑定vs动态绑定

从概念上这两个定义很简单。

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态。

动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

我们从反汇编的角度来看看呢,

从指令的复杂程度,也就知道它们之间的差异其实挺大的。

对于静态的动态而言,如:函数重载。函数实现在代码区,编译期间就可以找到函数的地址,当执行到该函数时,直接call该保存的 地址即可(如图所示)。

但是对于动态的多态,如:虚函数重写,虽然虚函数表在代码段\静态区,但是你不知道调用子类的虚函数还是父类的虚函数。因为起决定的是,父类指针、引用接收的对象,由此当父类指针、引用接收到对象时,会根据该对象去虚函数表中找到适合的虚函数,再进行调用,从而实现动态的多态。

(5)子类虚函数

子类也定义一个虚函数,那是否会进入虚函数表呢?

我们写一个打印虚函数表的函数;

void PrintVFTable(VFPtr vft[])
{for (int i = 0; vft[i] != nullptr; ++i){printf("[%d]:%p->", i, vft[i]);//我们拿到了函数的地址 就可以去调用函数vft[i]();}cout << endl;
}

(6)经典题目

我们以一道面试题来开启这一小段。

class A{
public:A(const char *s) { cout << s << endl; }~A(){}
};
class B :virtual public A
{
public:B(const char *s1,const char*s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:C(const char *s1,const char*s2) :A(s1) { cout << s2 << endl; }
};
class D :public C, public B
{
public:D(const char *s1,const char *s2,const char *s3,const char *s4) : B(s1, s2), C(s1, s3),A(s1){cout << s4 << endl;}
};

这段打印什么?

初始化列表初始化数据的顺序:
①声明顺序
②继承顺序

为什么这里需要D去显式调用A的构造?

因为在B、C中A的部分都是一个。因此,让B、C其中哪一个去构造都不太合适,因为两人都合适,并且A的部分是公共的。所以,这个任务就交由D一定要去显式调用A的构造。完成对那部分的初始化。

此时我们把虚继承去掉,此时也就是菱形继承了:

class A{
public:A(const char *s) { cout << s << endl; }~A(){}
};
class B :public A
{
public:B(const char *s1,const char*s2) :A(s1) { cout << s2 << endl; }
};
class C :public A
{
public:C(const char *s1,const char*s2) :A(s1) { cout << s2 << endl; }
};
class D :public C, public B
{
public:D(const char *s1,const char *s2,const char *s3,const char *s4) : B(s1, s2), C(s1, s3),A(s1){cout << s4 << endl;}
};

这种题很考人,如果对多态与继承学得不是很扎实时,总不免会踩坑。

(7)为什么多态的条件要求是父类的指针或引用?

这也是为什么,多态的条件是基类的指针或者引用,而非对象。

二、多继承关系里的多态

需要注意的是,虚函数表与虚继承表无 任何关系,虽然它们都使用了一个 同样的关键字"virtual"。

(1)单继承中的虚函数表

从观察窗口看,我们看不见func3,这是vs调试窗口做了特殊处理进行了隐藏。那我们如何看到子类的func3虚函数呢? 我们就只好用之前写好的打印函数。

(2)多继承中的虚函数表

此外,多继承的派生类为重写的虚函数,会放在第一个继承的基类部分的虚函数表中。

菱形继承、菱形虚拟继承产出的虚函数表更加地吓人,本节不会对此做过多赘述。你要设计菱形继承又要以此设计多态出来,只能奉劝你 "耗子尾汁"。

三、经典面试问答

(1)重载、重写(覆盖)、重定义(隐藏)

(2)inline函数可以是虚函数吗?

inline函数就是在函数调用处给出展开,但是我们虚函数是需要写入虚函数表的。因此不适宜展开。

虽然这个在语法上来说编译器不会报错,但是一旦构成多态,那么内联就没什么用了。毕竟inline只是给编译器提"建议"。而如果是普通调用,那么内联展开也是行得通的。

(3)静态成员函数可以是虚函数吗

我们写出来编译器就立马报错。静态成员函数最显著的一个特征时,可以不需要类对象的创建,就可以调用的函数,也就是该函数没有this指针。没this指针你怎么访问虚函数表,调用虚表指针呢?

(4)构造函数可以是虚函数?

在前面也说过,虚函数表是在编译期间就已经存在了。但是虚表指针是在构造函数的初始化列表中完成初始化的。虚表指针都没有,你怎么让构造函数是虚函数。

(5)析构函数可以是虚函数?

父类指针、引用 子类对象时,如果父类的虚函数没有完成重写,那么它就只会去调用它自己的析构函数,而不会去调用子类的析构函数。只有将父类的析构函数变为虚函数,才能正确地析构子类对象。

(6)对象访问普通函数快还是虚函数更快?

如果是普通调用。两个一样的块。难道声明了virtual的虚函数,每次调用都会去查找虚表?

只要你不构成多态的条件,对类里虚函数的调用跟普通调用没什么区别。当然,构成多态从反汇编的都知道它要去虚表里面查找合适的虚函数,肯定对效率有一定的影响。

总结:

①类里一旦有虚函数,就会自动生成虚函数表指针。

②虚函数表指针在初始化列表初始化,虚函数表是在编译阶段就生成的,一般情况

下存在代码段(常量区)的。

③多态分为静态的多态和动态的多态。一个是在编译期间确定的,一个是运行期间才能确定的。

本篇到此结束,感谢你的阅读

祝你好运,向阳而生~

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...