新C++(8):多态那些事儿_下

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

一、多态原理

(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的虚函数,每次调用都会去查找虚表?

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

总结:

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

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

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

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

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

祝你好运,向阳而生~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值