我的编程空间,编程开发者的网络收藏夹
学习永远不晚

详解C++中多态的底层原理

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

详解C++中多态的底层原理

前言

要了解C++多态的底层原理需要我们对C指针有着深入的了解,这个在打印虚表的时候就可以见功底,理解了多态的本质我们才能记忆的更牢,使用起来更加得心应手。

1.虚函数表

(1)虚函数表指针

首先我们在基类Base中定义一个虚函数,然后观察Base类型对象b的大小:

class Base
{
public:
    virtual void Func1()
    {
        cout << "Func1" << endl;
    }
    virtual void Func2()
    {
        cout << "Func2" << endl;
    }
    void f()
    {
        cout << "f()" << endl;
    }
protected:
    int b = 1;
    char ch = 1;
};
int main()
{
    Base b;
    cout << sizeof(b);
    return 0;
}

我们发现,如果按照对齐数原则来计算b的大小时,得到的结果是8,而我们打印的结果是:

这说明带有虚函数的类所定义的对象中,除了成员变量之外还有其他的东西被加入进去了(成员函数默认不在对象内,在代码段)。

我们可以通过调试来观察b中的内容:

我们发现对象中多了一个__vfptr,即为虚函数表指针。简称为虚表指针。

(2)虚函数表

仍然看上图,我们发现虚函数表指针下方有两个地址,这两个地址分别对应的就是Base中两个虚函数的地址,构成了一个虚函数表。所以虚函数表本质是一个指针数组,数组中每一个元素是一个虚函数的地址。

VS2019封装更为严密,在底层的汇编代码中,虚函数表中的地址并不一定是虚函数的地址,可能存放的是跳转到虚函数的地址的指令的地址。这个在后面会加以演示。

因此当我们调用普通函数和虚函数时,它们的本质是不同的:

    Base* bb=nullptr;
    bb->f();
    bb->Func1();

其中bb调用f()的过程没有发生解引用操作,非虚函数在公共代码段中,直接对其进行调用即可。而bb调用Func1()的过程中,需要通过虚表指针来找到Func1(),而拿到虚表指针需要对bb进行解引用操作,而bb是空,因此程序会崩溃。

我们知道对象中只存储成员变量,成员函数存储在公共代码段中,其实虚函数也是一样存储在公共代码段,只不过寻找虚函数需要通过虚表来确定位置。普通函数直接就可以确定位置。

2.虚函数表的继承–重写(覆盖)的原理

还拿上一节中买票的例子举例,其中父类中有两个虚函数,子类重写了其中的一个,子类中还有自己的函数。

class Person
{
public:
    virtual void BuyTicket()
    {
        cout << "全价" << endl;
    }
    virtual void Func1()
    {
        cout << "Func1" << endl;
    }
protected:
    int _a;
};
class Student :public Person
{
public:
    virtual void BuyTicket()
    {
        cout << "半价" << endl;
    }
    virtual void Func2()
    {
        cout << "Func2" << endl;
    }
protected:
    int _b;
};
int main()
{
    Person a;
    Student b;
    return 0;
}

我们可以通过调试来观察一下他们的虚表和虚表指针。

显然父类对象__vfptr[0]中存放的是BuyTicket的地址,__vfptr[1]中存放的是Func1()的地址。子类对象中__vfptr[0]中存放的是继承并重写的BuyTicket的地址,__vfptr[1]中存放的是继承下来但没有进行重写的Func1()的地址。通过对比我们发现:对于没有进行重写的Func1()来说,子类中虚表中的地址和父类中的是一样的,可以说是直接拷贝下来的。而对于进行了重写的BuyTicket来说,子类中虚表的地址与父类中明显不一样,其实是在拷贝了父类的地址后又进行了覆盖的。因此重写从底层的角度来说又叫做覆盖。

同时我们又发现了一个问题,那就是子类对象的虚表中为什么没有写它自己的虚函数地址Func2()呢?

其实是写了的,只不过通过VS的监视窗口并不能看到,我们可以通过内存来进行观察:

3.观察虚表的方法

(1)内存观察

我们可以通过观察内存来观察虚函数表的情况,这里观察的是父类对象,会发现在虚函数指针的地址存放的是父类对象中两个虚函数的地址。

我们也可以观察一下子类对象:

与父类对象中存储的相同,唯一有区别的地方就是紫色的部分,存放的其实是子类虚函数Func2()的地址。这说明Func2()也在虚表中只不过在监视窗口没有看不到而已。

(2)打印虚表

虚表的地址

通过观察内存,对于单继承来说,我们只需要打印对象的首元素的地址即可找到虚表,并进行打印。

我们发现对象的前四个字节存储的就是虚表的地址。可以通过这一点来打印虚表。

我们关闭一下调试来重新写一下代码(关闭调试后再进行运行地址会发生变化,但是规律是不变的)

typedef void(*vfptr)();
void Printvfptr(vfptr* table)
{
    for (int i = 0; table[i] != nullptr; i++)
    {
        printf("%d:%p\n",i,table[i]);
    }
    cout << endl;
}
int main()
{
    Person a;
    Student b;
    Printvfptr((vfptr*)*(void**)&a);
    Printvfptr((vfptr*)*(void**)&b);
    return 0;
}

下面来解释一下如何打印的虚表,分为两部分,一部分是函数,一部分是传参:

函数

首先我们明确,虚函数指针是一个函数指针,因此为了简便我们可以将函数指针重命名为vfptr。

通过接收虚表指针,并依次打印指针数组中的内容(虚函数的地址)。

传参

拿父类对象a举例,我们要找到a的前四个字节的内容,即为虚表指针,然后再传入函数中。

首先使用(void**)对a的地址进行强制类型转换,这其中发生了切割。使用(void**)的原因在于,由于不知道是使用的32位还是64位系统,但我们可以通过指针的大小来判断。首先将&a转换成一个指针,再将其转换成一个指针类型,再进行解引用就得到了a的前4或者8个字节。但同时我们需要传递的是一个vfptr类型的函数指针,所以还需要进行(vfptr*)类型的强制转换。

有了前面的解释,我们就可以理解打印虚表的原理了,我们把这段代码运行一下:

发现分别打印出了a和b的虚函数表。

如果打印的虚函数数量不对,这是VS编译器的bug,我们可以重新生成解决方案,再重新运行代码。

(3)虚表的位置

我们还可以观察一下虚表的位置,在哪个区域:

使用其他区域的变量进行对比:

    Person per;
    Student std;
    int* p = (int*)malloc(4);
    printf("堆:%p\n", p);
    int a = 0;
    printf("栈:%p\n", &a);
    static int b = 1;
    printf("数据段:%p\n", &b);
    const char* c = "aaa";
    printf("常量区:%p\n", &c);
    printf("虚表:%p\n", *(void**)&std);

打印的结果是:

我们发现虚表的位置在数据段和常量区之间。大致属于数据段。

4.多态的底层过程

class Person
{
public:
    virtual void BuyTicket()
    {
        cout << "全价" << endl;
    }
    virtual void Func1()
    {
        cout << "Func1" << endl;
    }
protected:
    int _a;
};
class Student :public Person
{
public:
    virtual void BuyTicket()
    {
        cout << "半价" << endl;
    }
    virtual void Func2()
    {
        cout << "Func2" << endl;
    }
protected:
    int _b;
};
void F(Person& p)
{
    p.BuyTicket();
}
int main()
{
    Person per;
    Student std;
    F(per);
    F(std);
    return 0;
}

我们还使用这一段代码来举例,首先复习一下多态:使用父类的指针或者引用去接收子类或者父类的对象,使用该指针或者引用调用虚函数,调用的是父类或子类中不同的虚函数。

下面来分析原理:

父类对象原理:

首先用父类引用p来接收父类对象per,此时p中的虚表和per中的虚表一模一样,只需要访问__vfptr中的BuyTicket()的地址即可调用该函数。

子类对象的原理:

用p来接收子类对象std,发生切片处理,会将子类中的虚表内容拷贝到父类引用p中,然后再调用其中的__vfptr中的BuyTicket地址。此时的p不是新创建了一个父类对象,而是子类对象std切片后构成的,其中就将重写之后的BuyTicket()的地址也随之切入了p。可以把p看成原std的包含__vfptr的一部分。

总结:基类的指针或者引用,指向谁就去谁的虚函数表中找到对应位置的虚函数进行调用。

5.几个原理性问题

了解了多态原理之后,就可以分析出在上一节中出现的一些现象规律。

(1)虚表中函数是公用的吗?

虚表中的函数和类中的普通函数一样是放在代码段的,只是虚函数还需要将地址存一份到虚表,方便实现多态。这也就说明同一类型的不同对象的虚表指针是相同的,我们还可以通过调试观察:

    Person per;
    Person pper;

(2)为什么必须传入指针或引用而不能使用对象?

当我们使用父类对象去接收时,父类对象本身就具有一个虚表了,当子类对象传给父类对象的时候,其他内容会发生拷贝,但是虚表不会,C++这样处理的原因在于,如果虚表也会发生拷贝的话,那么该父类对象的虚表就存了子类对象的虚表,这是不合理的。

我们同样可以通过调试来进行观察:

void F(Person p)
{
    p.BuyTicket();
}
int main()
{
    Person per;
    Student std;
    F(std);
}

这是std中的虚表内容。

这是p中的虚表内容,而且在调试过程中,程序是进入父类中进行调用函数的。

(3)为什么私有虚函数也能实现多态?

这是因为编译器调用了父类的public接口,由于是父类的引用或者指针,因此编译器发现是public之后就不再进行检查了,只要在虚表中可以找到就能调用函数。

(4)VS中的虚表中存的是指令地址?

在VS2019中,为了封装严密,其实虚表中存入的是跳转指令,我们可以通过反汇编进行观察:

我们将虚表中的地址输入反汇编,看到的是这样的一条语句:

这是一条跳转指令,会跳转到BuyTicket()的实际地址处。

6.多继承中的虚表

谈到多继承就要谈到菱形虚拟继承,这是一个庞大而复杂的问题,需要更大的大佬来解释。

这里只介绍多继承中虚表的内容:

class Base1
{
public:
    virtual void Func1()
    {
        cout << "Func1" << endl;
    }
    virtual void Func2()
    {
        cout << "Func2" << endl;
    }
protected:
    int _a;
};
class Base2
{
public:
    virtual void Func3()
    {
        cout << "Func3" << endl;
    }
    virtual void Func4()
    {
        cout << "Func4" << endl;
    }
};
class Derive :public Base1, Base2
{
public:
    virtual void Func5()
    {
        cout << "Func5" << endl;
    }
};
int main()
{
    Derive a;
}

我们可以使用调试来观察a中的虚表内容:

通过调试我们可以看到a中有两个虚表指针分别存放的是Base1中虚函数的地址和Base2中虚函数的地址,那么a中特有的类Func5()存在哪个虚表呢?这需要通过内存进行观察:

我们发现它被存放在了第一个虚表指针指向的虚表中。

我们知道打印第一个虚表指针指向虚表的方法,那么第二个虚表指针的该怎样进行处理呢:

Printvfptr((vfptr*)*(void**)((char*)&a+sizeof(Base1));

注意需要先将&a转换成char*类型,这样对其加一,才代表加一个字节。

以上就是详解C++中多态的底层原理的详细内容,更多关于C++多态原理的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

详解C++中多态的底层原理

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

C++ 多态虚函数的底层原理深入理解

这篇文章主要介绍了C++ 多态虚函数的底层原理深入理解,多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为,通常是父类调用子类的重写函数,在C++中就是 父类指针指向子类对象,此时父类指针的向下引用就可以实现多态
2022-11-13

【C++】多态的实现及其底层原理

个人主页:🍝在肯德基吃麻辣烫 我的gitee:gitee仓库 分享一句喜欢的话:热烈的火焰,冰封在最沉默的火山深处。 文章目录 前言一、什么是多态?二、多态的构成条件2.1什么是虚函数?2.2虚函数的重
2023-08-17

Spring注解Autowired的底层实现原理详解

从当前springboot的火热程度来看,java config的应用是越来越广泛了,在使用java config的过程当中,我们不可避免的会有各种各样的注解打交道,其中,我们使用最多的注解应该就是@Autowired注解了。本文就来聊聊Autowired的底层实现原理
2022-11-13

C++中string底层原理的示例分析

小编给大家分享一下C++中string底层原理的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!一、深浅拷贝浅拷贝:在实现string时要是不实先strin
2023-06-25

深入详解Vue3ref底层实现原理

随着现在vue3越来越普及,相应的面试题也多了起来。说到vue3的面试题,有一个最经典的就是对于实现ref和reactive这两个方法的底层原理,本文就来和大家简单讲讲吧
2023-05-17

详解Golang中NewTimer计时器的底层实现原理

本文将主要介绍一下Go语言中的NewTimer,首先展示基于NewTimer创建的定时器来实现超时控制。接着通过一系列问题的跟进,展示了NewTimer的底层实现原理,需要的可以参考一下
2023-05-18

PHP数组在底层的实现原理详解

PHP数组由哈希表和顺序元素数组实现。哈希表将键映射到元素数组索引,元素数组顺序存储元素。数组元素通过键哈希为哈希表索引,再利用索引获取元素数组中元素位置,快速高效访问。插入和删除元素时,哈希表会调整索引,维持数组结构。数组性能受哈希函数、哈希表大小和访问模式影响。优化策略包括选择合适哈希函数、调整哈希表大小、使用有序数组和避免哈希表重新哈希。
PHP数组在底层的实现原理详解
2024-04-02

详解C++虚函数中多态性的实现原理

C++是一种面向对象的编程语言,在C++中,虚函数是实现多态性的关键。本文就来探讨一下C++虚函数中多态性的实现原理及其在面向对象编程中的应用吧
2023-05-19

Java@Autowired注解底层原理详细分析

@Autowired注解可以用在类属性,构造函数,setter方法和函数参数上,该注解可以准确地控制bean在何处如何自动装配的过程。在默认情况下,该注解是类型驱动的注入
2022-11-13

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录