• 虚拟表


    为了实现虚函数,C ++使用了一种称为虚表的特殊形式的后期绑定。虚拟表是用于解决在动态/后期绑定方式的函数调用函数的查找表。虚拟表格有时会以其他名称,如“vtable”,“虚拟功能表”,“虚拟方法表”或“调度表”。

    因为了解虚拟表的工作原理并不需要使用虚拟功能,所以这部分可以被认为是可选的阅读。

    虚拟表格其实很简单,尽管用文字描述有点复杂。首先,每个使用虚函数的类(或者从使用虚函数的类派生)都被赋予它自己的虚表。这个表只是编译器在编译时设置的一个静态数组。一个虚拟表包含一个可以被类的对象调用的虚拟函数的条目。这个表中的每个条目都只是一个函数指针,指向那个类可以访问的派生最多的函数。

    其次,编译器还向基类添加了一个隐藏指针,我们将调用* __ vptr。* __ vptr在创建类实例时自动设置,以便指向该类的虚拟表。与* this指针不同,它实际上是编译器用来解析自引用的函数参数,* __ vptr是一个真正的指针。因此,它使每个类对象的大小分配一个指针的大小。这也意味着* __ vptr被派生类继承,这很重要。

    到目前为止,您可能对这些东西如何融合在一起感到困惑,所以我们来看一个简单的例子:

    因为这里有3个类,所以编译器会设置3个虚拟表:一个用于Base,一个用于D1,另一个用于D2。

    编译器还为使用虚函数的最基类添加了一个隐藏的指针。虽然编译器会自动执行此操作,但我们将把它放在下一个示例中,以显示它的添加位置:

    创建类对象时,* __ vptr被设置为指向该类的虚拟表。例如,当创建Base类型的对象时,* __ vptr被设置为指向Base的虚拟表格。当构造D1或D2类型的对象时,* __ vptr被设置为分别指向D1或D2的虚拟表。

    现在,我们来讨论一下这些虚拟表格是如何填充的。因为这里只有两个虚拟函数,每个虚拟表将有两个入口(一个用于函数1(),另一个用于函数2())。请记住,填写这些虚拟表时,每个条目都填写了该类类型的对象可以调用的派生最多的函数。

    基础对象的虚拟表格很简单。Base类型的对象只能访问Base的成员。基地不能访问D1或D2功能。因此,函数1的条目指向Base :: function1(),函数2的条目指向Base :: function2()。

    D1的虚拟表格稍微复杂一些。D1类型的对象可以访问D1和Base的成员。但是,D1重写了function1(),使得D1 :: function1()比Base :: function1()更加派生。因此,函数1的条目指向D1 :: function1()。D1没有重写function2(),所以函数2的入口将指向Base :: function2()。

    D2的虚拟表与D1类似,除了函数1的条目指向Base :: function1(),并且函数2的条目指向D2 :: function2()。

    这是一张图片:

    虽然这张图看起来很疯狂,但实际上很简单:每个类中的* __ vptr都指向该类的虚拟表。虚拟表中的条目指向允许调用该类的函数对象的派生最多的版本。

    所以考虑一下当我们创建一个D1类型的对象时会发生什么:

    因为d1是D1对象,所以d1将其* __ vptr设置为D1虚拟表。

    现在,我们设置一个基地指针到D1:

    请注意,因为dPtr是一个基址指针,所以它只指向d1的基本部分。但是,还要注意* __ vptr是在类的基本部分,所以dPtr有权访问这个指针。最后请注意,dPtr - > __ vptr指向D1虚拟表!因此,即使dPtr是Base类型,它仍然可以访问D1的虚拟表(通过__vptr)。

    那么当我们尝试调用dPtr-> function1()时会发生什么呢?

    首先,程序认识到function1()是一个虚函数。其次,程序使用dPtr - > __ vptr去D1的虚拟表。第三,它查找在D1的虚拟表中调用哪个版本的function1()。这已被设置为D1 :: function1()。因此,dPtr-> function1()解析为D1 :: function1()!

    现在,你可能会说:“但是如果Base真的指向一个Base对象而不是一个D1对象。它仍然会打电话给D1 :: function1()?“。答案是不。

    在这种情况下,当创建b时,__vptr指向Base的虚拟表,而不是D1的虚拟表。因此,bPtr - > __ vptr也将指向Base的虚拟表。function1()的基本虚拟表项指向Base :: function1()。因此,bPtr-> function1()解析为Base :: function1(),它是Base1对象应该能够调用的function1()的派生版本。

    通过使用这些表,编译器和程序能够确保函数调用解析到适当的虚拟函数,即使您只使用指针或对基类的引用!

    调用虚拟函数比调用非虚函数要慢,原因如下:首先,我们必须使用* __ vptr来获取适当的虚拟表。其次,我们必须索引虚拟表来找到正确的调用函数。只有这样我们才能调用这个函数。因此,我们必须执行3个操作才能找到要调用的函数,而不是针对正常的间接函数调用的2个操作,或者针对直接函数调用的一个操作。但是,用现代计算机,这个额外的时间通常是微不足道的。

    另外值得一提的是,任何使用虚拟函数的类都有一个__vptr,因此该类的每​​个对象都会被一个指针放大。虚拟功能是强大的,但他们确实有一个性能成本。

  • 相关阅读:
    c 语言 运算符 优先级
    回文字符串个数
    最小操作数
    将一个二叉树转化为双向链表,不开辟新空间
    两个整数集合的交集 ———— 腾讯2014软件开发笔试题目
    python download
    Spring5.2.X源代码编译-问题-Unable to locate Spring NamespaceHandler for XML schema namespace [http://www.springframework.org/schema/context]
    Spring5.2.X源代码编译-问题-找不到CoroutinesUtils
    Spring5.2.X源代码编译
    入行四年的思考
  • 原文地址:https://www.cnblogs.com/weekbo/p/8378287.html
Copyright © 2020-2023  润新知