专栏首页嵌入式知识C++多态的实现原理剖析

C++多态的实现原理剖析

1、虚函数表

首先放上结论:C++的多态是通过一张虚函数表(Virtual Table) 来实现的, 简称为 V-Table。 在这个表中, 主要是一个类的虚函数的地址表, 这张表解决了继承、 覆写的问题, 保证其真实反应实际的函数。 这样, 在有虚函数的类的实例中这个表被分配在了这个实例的内存中, 所以, 当我们用父类的指针来操作一个子类的时候, 这张虚函数表就显得由为重要了, 它就像一个地图一样, 指明了实际所应该调用的函数。这里我们着重看一下这张虚函数表C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下) 。 这意味着我们通过对象实例的地址得到这张虚函数表, 然后就可以遍历其中函数指针, 并调用相应的函数。

所以在有虚函数的类的对象,用sizeof查看的时候会多出来一个4,就是因为这个虚函数表的指针在这里

画个图解释一下。 如下所示

注意: 在上面这个图中, 在虚函数表的最后多加了一个结点, 这是虚函数表的结束结点, 就像字符串的结束符'0'一样, 其标志了虚函数表的结束。 这个结束标志的值在不同的编译器下是不同的。

通过一段代码了解一下:注意下方用了很多C语言中的指针问题,我在代码中进行注释说明了。

class Base 
{
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g() { cout << "Base::g" << endl; }
    virtual void h() { cout << "Base::h" << endl; }
};

typedef void (*FUNC)();

int main()
{
    cout<<"Base size:"<<sizeof(Base)<<endl;
    Base b;
    cout<<"对象的起始地址: "<<&b<<endl;
    //这是在取对象的地址,取出来的就是对象的首地址
    //对象的起始地址,就类似一个函数指针数组的指针(就是虚函数表的指针)
    
    cout<<"虚函数表的地址: "<<(int**)*(int *)&b<<endl;
    //在取出起始地址&b之后,我们只想取得前四个字节,所以将上面的地址强制类型转为int*型,再解引用,就是虚函数表的地址,虚函数表的起始地址,再进行强制类型转化成int**
    
    cout<<"虚函数表第一个函数的地址:"<<*((int**)*(int *)&b)<<endl;
    //这个就比上面的多了一个*,就是解引用,对虚函数表首地址解引用,得到虚函数表的第一个函数的地址
    
    cout<<"虚函数表第二个函数的地址:"<<*((int**)*(int *)&b+1)<<endl;
    //这个比上边的多了一个+1,可以看出是指针加1,所以地址实际+4,指向下一个函数
    
    //注意不要转为 FUNC 来打印, cout 没有重载
    FUNC pf = (FUNC)(*((char**)*(int *)&b));
    pf();
    pf = (FUNC)(*((void**)*(int *)&b+1));
    pf();
    pf = (FUNC)(*((void**)*(int *)&b+2));
    pf();
    return 0;
}

2、一般继承(无虚函数覆写)

当Derive 类继承了Base类的时候,且没有覆写Base类中的虚函数,用图表示就是这样的:

此时对于实例 Derive d; 的虚函数表如下:

3、一般继承(有虚函数覆写)

覆盖父类的虚函数是很显然的事情, 不然, 虚函数就变得毫无意义。 下面, 我们来看一下, 如果子类中有虚函数重载了父类的虚函数, 会是一个什么样子?

例如下方的继承关系:

此时对于实例 Derive d; 的虚函数表如下:

覆写的 f()函数被放到了虚表中原来父类虚函数的位置。 没有被覆盖的函数依旧

这样, 我们就可以看到对于下面这样的程序:

Base *b = new Derive();
b->f();

由 b 所指的内存中的虚函数表的 f()的位置已经被 Derive::f()函数地址所取代, 于是在实际调用发生时, 是 Derive::f()被调用了。 这就实现了多态。

静态代码发生了什么 当编译器看到这段代码的时候, 并不知道 b 真实身份。 编译器能作的就是用一段代码代替这段语句。

Base *b = new Derive();
b->f();

1, 明确 b 类型。 2, 然后通过指针虚函数表的指针 vptr 和偏移量,匹配虚函数的入口。 3, 根据入口地址调用虚函数。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • C++之模板、IO流、异常

    泛型(Generic Programming),即是指具有在多种数据类型上皆可操作的含意。泛型编程的代表作品 STL 是一种高效、泛型、可交互操作的软件组件。泛...

    用户5426759
  • C++的前世今生

    1979 年,美国 AT&T 公司贝尔实验室的 Bjarne Stroustrup 博士在 C 语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。...

    用户5426759
  • C++类的拓展

    对于类的大小,发现成员函数并不用类的存储空间。 只用一段空间来存放这个共同的函数代码段,在调用各对象的函数时,都去调用这个公用的函数代码。 所有的对象都调用共用...

    用户5426759
  • 深入解析golang编程中函数的用法

    函数是一组一起执行任务的语句。每Go程序具有至少一个函数,它一般是main(),以及所有的最琐碎程序可以定义附加函数。 你可以将代码放到独立的功能。如何划分代码...

    李海彬
  • 深度学习中的激活函数一览

    激活函数概念 所谓激活函数(Activation Function),就是在人工神经网络的神经元上运行的函数,负责将神经元的输入映射到输出端。

    glm233
  • 深入解构iOS系统下的全局对象和初始化函数

    事件源于接入了一个第三方库导致应用出现了大量的crash记录,很奇怪的是这么多的crash居然没有收到用户的反馈信息! 在这个过程中每个崩溃栈的信息都明确的指向...

    欧阳大哥2013
  • 【玩转腾讯云】万物皆可Serverless之关于云函数冷热启动那些事儿

    然后我们再来看一下腾讯云云函数文档里的简介 https://cloud.tencent.com/document/product/583/9199

    乂乂又又
  • 原 PostgreSQL的系统函数分析记录

    王果壳
  • Python 函数3000字使用总结

    我们把一些经常或反复被使用的任务放在一起,创建一个函数,而不是为不同的输入反复编写相同的代码。

    double
  • 机器学习(4)——逻辑回归Logistic回归softmax回归

    前言:这里是分类问题,之所以放到线性回归的下面介绍,是因为逻辑回归的步骤几乎是和前面一样的,写出目标函数,找到损失函数,求最小值,求解参数,建立模型,模型评估。...

    DC童生

扫码关注云+社区

领取腾讯云代金券