关于类的虚函数表

简介:     多态是面向对象的特性,通俗说,即用父类指针调用某成员函数时,针对不同对象调用的是不同的函数。从语言层面上看的统一形式的调用(相同名称的虚函数),体现出个性化的行为。 C++ 对象的多态通过虚函数表来实现的,虚函数表属于 C++ 对象模型层面的东西。

    多态是面向对象的特性,通俗说,即用父类指针调用某成员函数时,针对不同对象调用的是不同的函数。从语言层面上看的统一形式的调用(相同名称的虚函数),体现出个性化的行为。 C++ 对象的多态通过虚函数表来实现的,虚函数表属于 C++ 对象模型层面的东西。

    对于一个具体类型来说,它的行为是确定的,举一个简单例子,父类是几何形状,它有一个虚方法是计算自己的面积。对于具体类型例如矩形,正方形,圆形等,他的计算面积的方法是确定的。即对于同一个类型的所有实例,它们的虚函数表的内容相同,在编译时可以确定。

    因此,如果把虚函数表的全部内容附着在对象实例上,这样的对象模型显然是浪费内存的。因此,在对象实例起始处,放的是一个指针,指向其虚函数表,因此每个对象的虚函数表在实例中仅占一个指针(4 bytes)空间。如果一个类型有多个实例,它们指向的是同一份虚函数表(典型情况是位于进程空间的 .rdata section)。虚函数表指针的初始化由编译器生成的各种构造函数负责。如果一个函数不包含虚函数,则对象实例中不包含虚函数表指针。

    虚函数表可以认为是由函数指针组成的数组,数组元素由该类型的所有虚函数的地址组成,用 NULL 表示结尾(取决于编译器对模型的实现)。如果该类型实现了自己的虚函数,它将覆盖从父类继承下来的元素。

    编译器知道表中每个元素对应是那个虚函数,因此调用时取出元素,通过 call 指令实现调用。观察 VC6 debug 版本的汇编代码,虚函数表的内容被编译到只读的 section(和其他常量字符串一起),每个元素代表的是函数的地址(这些元素是代码段起始部的一组 jump 语句的地址,类似中断向量表,用于跳到真正的函数体)。由于虚函数表位于只读 section 中,所以其元素(函数指针)是不能直接改写的。

 

    下面的代码演示,交换两个实例的虚函数表,从而这两个对象的行为表现将被“交换”。

test_code
#include <stdio.h>

class ClassA
{
public:
    int a;

    ClassA()
    {
        printf("constructor(void).\n");
    }

    ClassA(ClassA& t)
    {
        a = t.a;
        printf("copy constructor.\n");
    }

    ClassA& operator =(ClassA& t)
    {
        a = t.a;
        printf("asignment operator.\n");
        return *this;
    }

    virtual void say()
    {
        printf("ClassA::say();\n");
    }

    virtual void say2()
    {
        printf("hello world2.\n");
    }
};

class ChildClassB : public ClassA
{
public:
    virtual void say()
    {
        printf("B::say();\n");
    }
};

class ChildClassC : public ClassA
{
public:
    virtual void say()
    {
        printf("C::say();\n");
    }
};


int main(int argc, char* argv[])
{
    ClassA *p1 = new ChildClassB();
    ClassA *p2 = new ChildClassC();

    p1->say();
    p2->say();

    unsigned int *p3 = (unsigned int*)p1;
    unsigned int *p4 = (unsigned int*)p2;

    printf("p1's vt: %08X\n", *p3);
    printf("p2's vt: %08X\n", *p4);

    //交换两个对象的虚函数表地址
    unsigned int tmp = *p3;
    *p3 = *p4;
    *p4 = tmp;

    p1->say();
    p2->say();


    delete p1;
    delete p2;
    return 0;
}

 

    下图是根据 VC6 Debug 版本的代码给出的是 ChildClassB 的对象模型(从《Inside The C++ Object Model》图1.3 修改而来):

    

 

    从实现机制来看,调用虚函数需要经过运行时的寻址,因此效率不如可以在编译时解析完成的非虚函数。如果不使用多态,即不使用虚函数,仅仅使用 class 进行数据封装和继承的程序风格,则称为“基于对象”(object-based)。

目录
相关文章
|
域名解析 监控 网络协议
分享40个主机域名PHP源码,总有一款适合你
分享40个主机域名PHP源码,总有一款适合你
624 1
|
消息中间件 存储 SQL
跨系统数据一致性方案的思考(上)
本文主要意在总结沉淀现有问题解决经验过程,整理解决跨系统数据不一致问题的经验方法。 跨系统数据一致性,比较优秀的解决方案就是微服务化,不同应用系统采用统一数据源方式,这样可以有效避免数据一致性问题。 但是我们很多系统由于历史原因或者业务缘由,导致非服务化情况下,又要采取数据一致性方案。
跨系统数据一致性方案的思考(上)
|
前端开发 数据可视化 JavaScript
【第50期】一文读懂React可视化
【第50期】一文读懂React可视化
297 0
|
SQL 监控 Oracle
|
JavaScript Java 测试技术
基于SpringBoot+Vue的大学生在线论坛系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的大学生在线论坛系统的详细设计和实现(源码+lw+部署文档+讲解等)
264 6
|
存储 编解码 弹性计算
阿里云服务器2核4G、4核8G、8核16G配置实例规格选择参考
2核4G、4核8G、8核16G配置的云服务器在阿里云目前的活动中目前有经济型e、通用算力型u1、计算型c7和计算型c8y四种实例可选,虽然配置相同,但是这些实例规格之间的价格差别是很大的,以2核4G配置为例,活动价格最便宜的经济型e实例2核4G配置只要30.06元/3个月,年付的价格为1089.53元/1年,而计算型c7实例2核4G3M则要3770.99元/1年,因此,我们有必要弄清楚他们之间的差别,这样才能根据自己的需求选择最适合自己的实例。
1413 1
阿里云服务器2核4G、4核8G、8核16G配置实例规格选择参考
|
Java 持续交付 开发者
使用 Docker 容器化 Java Web 应用:提高开发和部署效率
【4月更文挑战第4天】Docker 作为轻量级容器技术,提升了 Java Web 应用的开发和部署效率。它提供类似生产环境的本地开发体验,减少环境配置时间,保证应用隔离性与稳定性。Docker 改善了部署流程,实现跨环境的无缝迁移,支持自动化构建、部署和扩展,并促进持续集成和持续部署,助力企业实现更高效、可靠的软件生命周期管理。
328 8
|
弹性计算 云计算 虚拟化
GPU云服务器_GPU云计算_异构计算_弹性计算-阿里云
阿里云提供多种GPU服务器,包括NVIDIA V100、T4、A10和A100计算卡,其中gn6i实例享有最高3折优惠。包年包月价格如:gn6v实例(16G V100)从4685.20元/月起,gn6i实例(16G T4)从1878.40元/月起。学生无特定GPU服务器优惠,但新用户有折扣活动。GPU服务器计费模式有包年包月和按小时计费,按需选择。详细价格及活动规则见官方链接。
489 0
GPU云服务器_GPU云计算_异构计算_弹性计算-阿里云
|
存储 监控 Java
理解线程池的原理与最佳实践
理解线程池的原理与最佳实践
|
监控 Java Docker
【Spring Cloud Sleuth 分布式链路跟踪】 —— 每天一点小知识
【Spring Cloud Sleuth 分布式链路跟踪】 —— 每天一点小知识
357 0