【C++】多态(下)

简介: 【C++】多态(下)

1.单继承中的虚函数

整体代码

#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;
    }
private:
    int _b = 1;
};
class Derive : public Base
{
public:
    virtual void Func1()
    {
        cout << "Derive::Func1()" << endl;
    }
    virtual void Func4()
    {
        cout << "Base::Func4()" << endl;
    }
private:
    int _d = 2;
};
typedef void(*VF_PTR)();    
//  typedef void(*)() VF_PTR;
void PrintVFTable(VF_PTR table[])//函数指针数组
{
    int i = 0;
    for (i = 0; table[i] != nullptr; i++)
    {
        printf("[%d]:%p\n",i, table[i]);
        VF_PTR f = table[i];
        f();
    }
}
int main()
{
    Base b;
    Derive d;
    PrintVFTable((VF_PTR*)*(int*)&b);
    cout << endl;
    PrintVFTable((VF_PTR*)*(int*)&d);
    return 0;
}

在子类中实现一个虚函数Func4,但是不构成重写



Func4函数并没有进入虚表中


通过查询内存发现,虚表指针中存在三个地址,而其中两个正好为监视中的两个地址

猜测 0x00c4146a 就是Func4的地址

用程序打印虚表

虚表本质是一个函数指针数组

VS中在数组最后放了一个nullptr,这样就可以解决在不同虚表中的个数不同的问题


typedef一个函数指针 为VF_PTR


正常来说 要写成将VF_PTR放在后面

但是由于函数指针的特殊性,定义变量都要放在中间

如何寻找到虚表地址

想要虚表的地址,就可以通过找到虚表的指针

而这个虚表指针在对象中,这个指针在对象的前4个(32位)或者8个字节(64位)上面


以32位为例,如何取对象的前4个字节

强制转换为int*


* (int* )&b

首先取到Base* ,将其强制转换为int*,代表前四个字节的地址,再解引用是int类型,把前四个字节取出来

但是由于PrintVFTable函数参数是 函数指针数组

(VF_PTR*) * (int *)&b

如果这个数组是int类型,就需要 一个int * 指针去指向

同理 ,该数组为 VF_PTR类型,需要一个VF_PTR *指针去指向

所以需将 int 类型 再次强制转换为 VF_PTR * ,使其指向这个数组


缺陷

但是这种写法具有一定的局限性,只能在32位跑,因为32位指针大小为4个字节

而64位下就不行了,64位下指针大小为8个字节

运行程序打印虚表,确实了解到多了一个地址



把虚表的地址拿出来赋给函数指针,用函数指针去调用函数

这里发现 监视中没有出现的地址确实是Func4函数的地址

虚表存在哪里?

由于常量区地址与虚表的地址最为接近,所以说明虚表在常量区/代码段上

2.多继承中的虚函数表

整体代码

class Base1 {
public:
    virtual void func1() { cout << "Base1::func1" << endl; }
    virtual void func2() { cout << "Base1::func2" << endl; }
private:
    int b1;
};
class Base2 {
public:
    virtual void func1() { cout << "Base2::func1" << endl; }
    virtual void func2() { cout << "Base2::func2" << endl; }
private:
        int b2;
};
class Derive : public Base1, public Base2 {
public:
    virtual void func1() { cout << "Derive::func1" << endl; }
    virtual void func3() { cout << "Derive::func3" << endl; }
private:
    int d1;
};
typedef void(*VF_PTR)();    
//  typedef void(*)() VF_PTR;
void PrintVFTable(VF_PTR table[])//函数指针数组
{
    int i = 0;
    for (i = 0; table[i] != nullptr; i++)
    {
        printf("[%d]:%p->", i, table[i]);
        VF_PTR f = table[i];
        f();
    }
}
int main()
{
    Derive d;
    PrintVFTable(  (VF_PTR*) *(int*) &d);
    cout << endl;
    /*PrintVFTable((VF_PTR*)*(int*)( (char*)&d+ sizeof(Base1) ) );*/
    Base2* ptr2 = &d;
    PrintVFTable((VF_PTR*)*(int*)(ptr2));
    return 0;
}

寻找虚表地址

Derive 作为Base1 和Base2的子类,所以Derive内部有两张虚表


正常来说,Derive内部还存在一个func3函数,这个函数放在哪里了呢?

借助打印虚表来查看,这里的打印虚表依旧可以使用单继承中的那个


base1的虚表指针 正好在对象的前4个字节处,直接可以使用求出虚表指针 去指向base1的虚表


方法1 : base2的虚表指针 需要加上base1的大小

但是这里要注意一个问题,若写成 PrintVFTable((VF_PTR*)(int)( &d+ sizeof(Base1) ) )

写的并不对,d本身是一个Derive类型,&d后变为Derive* 的一个指针,+1 跳转的是Derive类型的字节大小

而该设计想要每次+1跳转1个字节,所以需要强制转换char*


方法2 :切片自动偏移


两种方法的结果都是一样的

注意事项

多继承派生类增加的的虚函数在第一个虚表中

多继承重写后的func1地址为什么不同?


ptr1调用函数——一次jmp

找到 Base1虚表里的地址 0x00e21230 ,再call这个地址

只需要jmp一次 就可以找到实际真正执行的函数地址ptr1调用地址属于正常调用


ptr2 调用函数——多次jmp

ptr2调用地址,需要 多次jmp 才能找到真正的函数地址

ecx存的是this指针

ecx,8 目的是修正this指针的位置

最终Base1和Base2都是执行同一个函数的指令

  • ptr1->func1 调用的是子类的func1函数,ptr1指向调用对象的开始
  • ptr2并没有指向子类对象的开始,此时调用子类对象的func1函数,this指针指向中间的位置不对了,所以需要修正this指针,使之指向子类对象开始的地方
相关文章
|
28天前
|
C++
C++中的封装、继承与多态:深入理解与应用
C++中的封装、继承与多态:深入理解与应用
28 1
|
1月前
|
C++
【C++】从零开始认识多态(一)
面向对象技术(oop)的核心思想就是封装,继承和多态。通过之前的学习,我们了解了什么是封装,什么是继承。 封装就是对将一些属性装载到一个类对象中,不受外界的影响,比如:洗衣机就是对洗衣服功能,甩干功能,漂洗功能等的封装,其功能不会受到外界的微波炉影响。 继承就是可以将类对象进行继承,派生类会继承基类的功能与属性,类似父与子的关系。比如水果和苹果,苹果就有水果的特性。
41 4
|
1月前
|
C++
【C++】从零开始认识多态(二)
面向对象技术(oop)的核心思想就是封装,继承和多态。通过之前的学习,我们了解了什么是封装,什么是继承。 封装就是对将一些属性装载到一个类对象中,不受外界的影响,比如:洗衣机就是对洗衣服功能,甩干功能,漂洗功能等的封装,其功能不会受到外界的微波炉影响。 继承就是可以将类对象进行继承,派生类会继承基类的功能与属性,类似父与子的关系。比如水果和苹果,苹果就有水果的特性。
29 1
|
1月前
|
C++
c++多态
c++多态
26 0
|
7天前
|
C++
C++一分钟之-继承与多态概念
【6月更文挑战第21天】**C++的继承与多态概述:** - 继承允许类从基类复用代码,增强代码结构和重用性。 - 多态通过虚函数实现,使不同类对象能以同一类型处理。 - 关键点包括访问权限、构造/析构、菱形问题、虚函数与动态绑定。 - 示例代码展示如何创建派生类和调用虚函数。 - 注意构造函数初始化、空指针检查和避免切片问题。 - 应用这些概念能提升程序设计和维护效率。
20 2
|
2天前
|
编译器 C++ p3c
【c++】多态
【c++】多态
5 0
|
3天前
|
编译器 C++
【C++航海王:追寻罗杰的编程之路】多态你了解多少?
【C++航海王:追寻罗杰的编程之路】多态你了解多少?
7 0
|
3天前
|
编译器 C++
【C++】学习笔记——多态_2
【C++】学习笔记——多态_2
5 0
|
3天前
|
编译器 C++
【C++】学习笔记——多态_1
【C++】学习笔记——多态_1
5 0
|
1月前
|
存储 编译器 C语言
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题(下)
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题
35 1