【C++ 学习 ⑲】- 多态(下)

目录

一、虚函数表和多态的原理

1.1 - 虚函数表

1.2 - 多态的原理

二、单继承和多继承关系中的虚函数表

2.1 - 单继承关系中的虚函数表

2.2 - 多继承关系中的虚函数表

三、纯虚函数和抽象类



一、虚函数表和多态的原理

1.1 - 虚函数表

  1. 问:sizeof(b) 是多少?

    #include <iostream>
    using namespace std;
    ​
    class Base
    {
    public:
        virtual void func1() { cout << "Base::func1()" << endl; }
        virtual void func2() { cout << "Base::func2()" << endl; }
        void func3() { cout << "Base::func3()" << endl; }
    protected:
        int _i = 1;
    };
    ​
    int main()
    {
        Base b;
        cout << sizeof(b) << endl;
        return 0;
    }

    通过调试可以发现,在 b 对象内存模型中,除了 _i 成员,还有一个名为 _vfptr 的成员,它是虚函数表指针,所以 sizeof(b) 是 8 或 16 字节

    一个含有虚函数的类对象至少有一个指向虚函数表的指针,虚函数表本质上是一个存放虚函数地址的函数指针数组,一般情况下在这个数组的最后面还放了一个 nullptr

    虚函数表可以简称为虚表。因为 func3 不是虚函数,所以没有放进虚表中。

  2. 提一个很容易混淆的问题:虚函数存在哪里?虚表又存在哪里?虚函数和普通函数一样,都是存在代码段的;而虚表存在哪里可以通过以下代码得知

    #include <iostream>
    using namespace std;
    ​
    class Base
    {
    public:
        virtual void func1() { cout << "Base::func1()" << endl; }
        virtual void func2() { cout << "Base::func2()" << endl; }
        void func3() { cout << "Base::func3()" << endl; }
    protected:
        int _i = 1;
    };
    ​
    typedef void(*VFPTR)();
    ​
    int main()
    {
        int m = 0;
        printf("栈:%p\n", &m);
    ​
        int* p1 = new int;
        printf("堆:%p\n", p1);
    ​
        static int n = 0;
        printf("静态区:%p\n", &n);
    ​
        const char* str = "abcdef";
        printf("常量区:%p\n", str);
    ​
        Base b;
        // 通过对象的地址获取虚函数表的地址
        VFPTR* p2 = (VFPTR*)*(int*)&b;  
        printf("虚表:%p\n", p2);
        return 0;
    }

    根据输出结果,我们有理由相信虚表也是存在代码段的

  3. 让派生类 Derive 继承自 Base,然后在派生类中重写基类虚函数 func1:

    #include <iostream>
    using namespace std;
    ​
    class Base
    {
    public:
        virtual void func1() { cout << "Base::func1()" << endl; }
        virtual void func2() { cout << "Base::func2()" << endl; }
        void func3() { cout << "Base::func3()" << endl; }
    protected:
        int _i = 1;
    };
    ​
    class Derive : public Base
    {
    public:
        virtual void func1() { cout << "Derived::func1()" << endl; }
    protected:
        int _j = 2;
    };
    ​
    int main()
    {
        Base b;
        Derive d;
        return 0;
    }

    因为在派生类中重写了基类的虚函数 func1,所以基类对象 b 和派生类对象 d 的虚表是不一样的

    d 的虚表中存的是重写的 Derive::func1,所以虚函数的重写也叫作覆盖,覆盖就是虚表中虚函数的覆盖。重写是语法上的叫法,覆盖是原理层的叫法

1.2 - 多态的原理

#include <iostream>
using namespace std;
​
class Base
{
public:
    virtual void func1() { cout << "Base::func1()" << endl; }
    virtual void func2() { cout << "Base::func2()" << endl; }
    void func3() { cout << "Base::func3()" << endl; }
protected:
    int _i = 1;
};
​
class Derive : public Base
{
public:
    virtual void func1() { cout << "Derive::func1()" << endl; }
protected:
    int _j = 2;
};
​
int main()
{
    Base b;
    Base* pb = &b;
    pb->func1();  // Base::func1()
​
    Derive d;
    pb = &d;
    pb->func1();  // Derive::func1()
    return 0;
}

当基类指针 pb 指向基类对象 b 时,pb->func1(); 就是在 b 的虚表中找到虚函数 Base::func1

当基类指针 pb 指向派生类对象 d 时,pb->func1(); 就是在 d 的虚表中找到虚函数 Derive::func1

这样就让基类指针表现出了多种形态

注意:不满足多态的函数调用是编译时确定好的,满足多态的函数调用是运行时去对象中找的

Base b;
b.func1();
// 00195182 lea     ecx,[b]
// 00195185 call    Person::func1 (01914F6h)
// 汇编代码分析:
// 虽然 func1 是虚函数,但是 b 是对象,不满足多态的条件,所以这里是普通函数的调用,
// 编译时就确定好了函数的地址,直接 call。
​
Base* pb = &b;
pb->func1();
// 注意:不相关的汇编代码被省去了
// 001940DE mov     eax,dword ptr [pb]
// 001940E1 mov     edx,dword ptr [eax]
// 00B823EE mov     eax,dword ptr [edx]
// 001940EA call    eax
// 汇编代码分析:
// 1、pb 中存的是 b 对象的地址,将 pb 移动到 eax 中
// 2、[eax] 就是取 eax 值指向的内容,相当于把 b 对象中的虚表指针移动到 edx
// 3、[edx] 就是取 edx 值指向的内容,相当于把虚表中第一个虚函数的地址移动到 eax
// 4、call eax 中存的虚函数地址
// 由此可以看出满足多态的函数调用,不是在编译时确定的,而是运行起来后去对象中找的。

 


二、单继承和多继承关系中的虚函数表

2.1 - 单继承关系中的虚函数表

#include <iostream>
using namespace std;
​
class Base
{
public:
    virtual void func1() { cout << "Base::func1()" << endl; }
    virtual void func2() { cout << "Base::func2()" << endl; }
protected:
    int _i = 1;
};
​
class Derive : public Base
{
public:
    virtual void func1() { cout << "Derive::func1()" << endl; }
    virtual void func3() { cout << "Derive::func3()" << endl; }
    virtual void func4() { cout << "Derive::func4()" << endl; }
protected:
    int _j = 2;
};
​
int main()
{
    Base b;
    Derive d;
    return 0;
}

在 d 的虚表中,我们看不到虚函数 func3 和 func4,这可能是监视窗口故意隐藏了这两个函数,也可能是一个小 bug,我们可以通过以下代码进行验证

typedef void(*VFPTR)();
​
void PrintVftable(VFPTR vftable[])
{
    for (size_t i = 0; vftable[i] != nullptr; ++i)
    {
        printf("第 %d 个虚函数地址:0X%p, -->", i, vftable[i]);
        vftable[i]();
    }
    cout << endl;
}
​
void test()
{
    Base b;
    VFPTR* p1 = (VFPTR*)*(int*)&b;
    PrintVftable(p1);
​
    Derive d;
    VFPTR* p2 = (VFPTR*)*(int*)&d;
    PrintVftable(p2);
}

 

2.2 - 多继承关系中的虚函数表

#include <iostream>
using namespace std;
​
class Base1
{
public:
    virtual void func1() { cout << "Base1::func1()" << endl; }
    virtual void func2() { cout << "Base1::func2()" << endl; }
protected:
    int _i1;
};
​
class Base2
{
public:
    virtual void func1() { cout << "Base2::func1()" << endl; }
    virtual void func2() { cout << "Base2::func2()" << endl; }
protected:
    int _i2;
};
​
class Derive : public Base1, public Base2
{
public:
    virtual void func1() { cout << "Derive::func1()" << endl; }
    virtual void func3() { cout << "Derive::func3()" << endl; }
protected:
    int _j;
};
​
typedef void(*VFPTR)();
​
void PrintVftable(VFPTR vftable[])
{
    for (size_t i = 0; vftable[i] != nullptr; ++i)
    {
        printf("第 %d 个虚函数地址:0X%p, -->", i, vftable[i]);
        vftable[i]();
    }
    cout << endl;
}
​
int main()
{
    Derive d;
    VFPTR* p1 = (VFPTR*)*(int*)&d;
    PrintVftable(p1);
​
    // VFPTR* p2 = (VFPTR*)*(int*)((char*)&d + sizeof(Base1));
    // PrintVftable(p2)
    // 或者:
    Base2* p2 = &d;
    PrintVftable((VFPTR*)*(int*)p2);
    return 0;
}

  1. 派生类中的虚函数 func3 放在第一个继承自基类部分的虚函数表中

  2. 假设有以下场景

    Derive d;
    Base1* p1 = &d;
    p1->func1();
    Base2* p2 = &d;
    p2->func1();

    首先要确定的是

    所以在语句 p2->func1(); 中,需要修正 this 指针

    这也是为什么在 d 的两个虚表中,重写的虚函数 func1 的地址不一样


三、纯虚函数和抽象类

纯虚函数是一种特殊的虚函数,在某些情况下,在基类中不能对虚函数给出有意义的实现,就可以把它声明为纯虚函数。纯虚函数只有函数名、参数和返回值类型,没有函数体,具体实现留给派生类去做。具体语法:virtual 返回值类型 函数名(参数列表) = 0;

含有纯虚函数的类被称为抽象类(或接口类),不能实例化对象,但可以创建指针和引用

派生类必须重写抽象类中的纯虚函数,否则也属于抽象类

#include <iostream>
using namespace std;
​
class Car
{
public:
    virtual void Drive() = 0;
};
​
class AITO : public Car
{
public:
    virtual void Drive() { cout << "Intelligent" << endl; }
};
​
class AVATR : public Car
{
public:
    virtual void Drive() { cout << "Comfortable" << endl; }
};
​
void func1(Car* p) { p->Drive(); }
​
void func2(Car& c) { c.Drive(); }
​
int main()
{
    AITO aito;
    AVATR avatr;
​
    func1(&aito);  // Intelligent
    func1(&avatr);  // Comfortable
​
    func2(aito);  // Intelligent
    func2(avatr);  // Comfortable
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值