【问题标题】:How does the compiler know which entry in vtable corresponds to a called virtual function?编译器如何知道 vtable 中的哪个条目对应于调用的虚函数?
【发布时间】:2021-10-27 09:48:40
【问题描述】:

首先,这个问题听起来很重复,但我通过谷歌探索了这个问题,但作为外行没有得到满意的答案

问题: 假设我们在父类和派生类中有 3 个虚函数,如下例所示

class Base1 {
    public :
    virtual void Print1() {
       cout << "Print1 Base1" << endl;
    }
    virtual void Print2() {
       cout << "Print1 Base1" << endl;
    }
    virtual void Print3() {
       cout << "Print1 Base1" << endl;
    }
};

class Derived : public Base1 {
public:  
    void Print1() {
       cout << "Print1 Derived1" << endl;
    }
    void Print2() {
       cout << "Print1 Derived2" << endl;
    }
    void Print3() {
       cout << "Print1 Derived3" << endl;
    }
};

int main()
{
    Derived d;
    Base1 *bptr = &d;  
    bptr->Print2(); // -> Here Derived::Print2() shall be called
    return 0;
 }

如上例所示,在main()中调用Derived::Print2()

这里,基类和派生类vTable应由3个虚函数的3个条目组成

 [0] -> Address of Print1()
 [1] -> Address of Print2()
 [2] -> Address of Print3()

我想知道, 在运行时编译器如何在 Derived vTable 中搜索 Print2() ?

【问题讨论】:

  • 在大多数实现中,内存中的每个对象都有一个指向其类型信息的指针,作为其图像的一部分,包括 vtable。虚拟方法调用是通过链接的 vtable 通过 this 指针间接实现的。
  • [1] -&gt; Address of Print2() [2] -&gt; Address of Print2() 为什么会有两个Print2() 指针?还是打错字了?
  • 运行时不使用名称;变成了“调用表中的第三个虚函数”,很琐碎。
  • @KamilCuk,是错字
  • 仅供参考,在程序运行期间不会访问编译器。您可以在一台计算机上编译,然后在另一台没有编译器的计算机上运行程序。

标签: c++ function-pointers virtual-functions vtable


【解决方案1】:

它不搜索。 “Print2 is the second entry in the vtable”的对应关系是在编译时建立的,当编译器查看Base1类的定义时。

【讨论】:

  • 但是,那么它是如何跳转到第二个条目的,比如说,我们有 100 个 Print() 函数() ,我们正在调用 Print99() ,
  • vtable 包含一个数组,其中每个虚拟函数都有一个函数指针。如果 Print99() 是该类中的第 110 个虚函数,则 vtable[109] 是指向应调用的 Print99 实现的指针。
【解决方案2】:

没有搜索 - 表不是按名称而是按偏移量(即它是一个数组)。

在实践中,它的工作原理通常很像这个非常简化的类 C 实现:

// Simplified type; the actual implementation doesn't need to care about the type.
typedef void(*function)(void*);

// Base class
struct Base1 {
    function* vtable;
};

// Derived class
struct Derived
{
    Base1 parent; // The base class subobject    
};

// Virtual members
void Base1_Print1(void*) {
    cout << "Print1 Base1" << endl;
}

void Base1_Print2(void*) {
    cout << "Print2 Base1" << endl;
}

// v-table
function Base1_vtable[2] = { &Base1_Print1, &Base1_Print2 };

// Constructor
void Base1_create(Base1* self)
{
    self->vtable = Base1_vtable;
}

// Overridden members
void Derived_Print2(void*) {
    cout << "Print2 Derived" << endl;
}

// v-table
function Derived_vtable[2] = { &Base_Print1, &Derived_Print2 };

// Constructor
void Derived_create(Derived* self)
{
    Base1_create(&self->parent); // Construct the base subobject
    self->parent.vtable = Derived_vtable; // Adjust the vtable
}

int main()
{
    Derived d;
    Derived_create(&d); // Construct d
    Base1 *bptr = &d.parent; // Implicit conversion made explicit.
    (bptr->vtable[0])(bptr); // Virtual call to Base_Print1
    (bptr->vtable[1])(bptr); // Virtual call to Derived_Print2
 }

【讨论】:

  • 在最后一行 "bptr->vtable[1]->(bptr)" -> 这里编译器如何决定使用 vTable 的第一个索引来调用 Derived_Print2 ?
  • 编译器从Base1的定义中知道Print1是第一个虚函数,Print2是第二个虚函数。 (当然,索​​引就像 vtable 的存在一样,是一个实现细节,但这样实现起来既简单又高效。)
  • 谢谢,现在我通过你给定的代码 sn-p 得到了它
  • 编译器编译了它。对于基类,它只是决定它喜欢哪个虚函数去哪里的任何方式。按照虚拟函数的声明顺序,按字母顺序,任何它喜欢的方式。对于派生类,它使用相同的顺序,然后添加派生类中的任何新虚函数。编译器知道是因为编译器编译了代码。
猜你喜欢
  • 2016-01-05
  • 2010-09-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-27
  • 2014-07-10
  • 1970-01-01
相关资源
最近更新 更多