【问题标题】:Low level details of inheritance and polymorphism继承和多态的底层细节
【发布时间】:2011-05-31 16:39:07
【问题描述】:

这个问题是萦绕在我脑海中的一大疑问,也很难用语言来描述。有时它似乎很明显,有时却很难破解。所以问题是这样的::

类基{ 民众: int a_number; 根据(){} 虚空函数 1() {} 虚空函数 2() {} 无效函数3(){} }; 类派生:公共基础{ 民众: 派生():基(){} void function1() {cout &lt&lt "从基础派生" &lt&lt endl; virtual void function4() {cout 函数4(); // 将给出编译错误!! b_ptr->函数1(); // 调用 Derived 类的重写方法 返回0; }

Q1。虽然 b_ptr 指向 Derived 对象,但它访问哪个 VTABLE 以及如何?因为 b_ptr -> function4() 给出了编译错误。还是 b_ptr 在派生的 VTABLE 中只能访问该大小的基类 VTABLE?

Q2。既然 Derived 的内存布局必须是 (Base,Derived) ,那么 Base 类的 VTABLE 是否也包含在 Derived 类的内存布局中?

Q3。既然Vtable基类的function1和function2指向Base类的实现,Derived类的function2指向Base类的function2,那么Base类中真的需要VTABLE吗? (这可能是我能问过的最愚蠢的问题,但在我目前的状态下我仍然对此表示怀疑,答案必须与 Q1 的答案相关:))

请评论。

谢谢你的耐心。

【问题讨论】:

  • 是的,是的,是的。只有一个“否”才会成为一个有趣的答案。

标签: c++ inheritance polymorphism vtable


【解决方案1】:

作为进一步的说明,这里是 C++ 程序的 C 版本,显示了 vtable 和所有内容。

#include <stdlib.h>
#include <stdio.h>

typedef struct Base Base;
struct Base_vtable_layout{
    void (*function1)(Base*);
    void (*function2)(Base*);
};

struct Base{
    struct Base_vtable_layout* vtable_ptr;
    int a_number;
};

void Base_function1(Base* this){}

void Base_function2(Base* this){}

void Base_function3(Base* this){}

struct Base_vtable_layout Base_vtable = {
    &Base_function1,
    &Base_function2
};

void Base_Base(Base* this){
    this->vtable_ptr = &Base_vtable;
};

Base* new_Base(){
    Base *res = (Base*)malloc(sizeof(Base));
    Base_Base(res);
    return res;
}

typedef struct Derived Derived;
struct Derived_vtable_layout{
    struct Base_vtable_layout base;
    void (*function4)(Derived*);
};

struct Derived{
    struct Base base;
};

void Derived_function1(Base* _this){
    Derived *this = (Derived*)_this;
    printf("Derived from Base\n");
}

void Derived_function4(Derived* this){
    printf("Only in derived\n");
}

struct Derived_vtable_layout Derived_vtable = 
{
    { &Derived_function1,
      &Base_function2},
    &Derived_function4
};

void Derived_Derived(Derived* this)
{
    Base_Base((Base*)this);
    this->base.vtable_ptr = (struct Base_vtable_layout*)&Derived_vtable;
}      

Derived* new_Derived(){
    Derived *res = (Derived*)malloc(sizeof(Derived));
    Derived_Derived(res);
    return res;
}



int main(){
      Derived *der_ptr = new_Derived();
      Base *b_ptr = &der_ptr->base;
      /* b_ptr->vtable_ptr->function4(b_ptr); Will Give Compilation ERROR!! */
      b_ptr->vtable_ptr->function1(b_ptr);
      return 0;
}

【讨论】:

  • 做得很好。除了我更喜欢Base *b_ptr = &amp;der_ptr-&gt;base;function1 虚拟呼叫应该是b_ptr-&gt;vtable_ptr-&gt;base.function1(b_ptr);
  • @aschepler:我同意演员表(已更改)。 vcall 必须保持原样:b_ptr 是一个基指针,因此它的 vtable 没有基字段。尝试编译它。
【解决方案2】:

所有这些都取决于实现。但这里是使用“vtables”的最简单方法的答案。

Base 类有一个 vtable 指针,因此底层表示类似于以下伪代码:

struct Base {
  void** __vtable;
  int a_number;
};
void* __Base_vtable[] = { &Base::function1, &Base::function2 };
void __Base_ctor( struct Base* this_ptr ) { this_ptr->__vtable = __Base_vtable; }

Derived 类包含一个 Base 类子对象。由于它有一个 vtable 的位置,Derived 不需要添加另一个。

struct Derived {
  struct Base __base;
};
void* __Derived_vtable[] =
  { &Derived::function1, &Base::function2, &Derived::function4 };
void __Derived_ctor( struct Derived* this_ptr ) {
  __Base_ctor( &this_ptr->__base );
  this_ptr->__base.__vtable = __Derived_vtable;
}

如果有人尝试new Base();Base obj;,则需要我的伪代码中的“基类vtable”__Base_vtable

当涉及多重继承或虚拟继承时,上述所有内容都会变得更加复杂......

对于b_ptr -&gt; function4(); 行,这是一个编译时错误,与vtables 没有太大关系。当您转换为 Base* 指针时,您只能以 class Base 定义的方式使用该指针(因为编译器不再“知道”它是否真的是 DerivedBase 或其他一些类)。如果Derived 有自己的数据成员,则不能通过该指针访问它。如果Derived 有它自己的成员函数,不管是不是虚拟的,你都不能通过那个指针访问它。

【讨论】:

    【解决方案3】:

    首先,也是最重要的一点,请记住,C++ 不会进行大量的任何类型的运行时自省。基本上,它需要在编译时了解对象的所有信息。

    Q1 - b_ptr 是一个指向 Base 的指针。因此它只能访问 Base 对象中存在的东西。没有例外。现在,实际的实现可能会根据对象的实际类型而改变,但是如果您想通过 Base 指针调用它,则无法绕过必须在 Base 中定义的方法。

    Q2 - 简单的答案是“是的,基础的 vtable 必须存在于派生中”,但是对于如何布局 vtable,有很多可能的策略,所以不要挂断它确切的结构。

    Q3 - 是的,Base 类中必须有一个 vtable。在类中调用虚函数的所有东西都将通过 vtable,因此如果底层对象实际上是 Derived,那么一切都可以工作。

    现在这不是绝对的,因为如果编译器可以绝对确定它知道它得到了什么(可能是在本地堆栈上声明的 Base 对象的情况),那么编译器就可以优化vtable 查找,甚至可能被允许内联函数。

    【讨论】:

      【解决方案4】:

      A1。 vtable 指针指向 Derived vtable,但编译器不知道这一点。你告诉它把它当作一个 Base 指针,所以它只能调用对 Base 类有效的方法,不管指针指向什么。

      A2。标准没有指定 vtable 布局,它甚至不是该类的正式一部分。这只是 99.99% 最常见的实现方法。 vtable 不是对象布局的一部分,但有一个指向 vtable 的指针,它是对象的隐藏成员。它将始终位于对象中的相同相对位置,以便编译器始终可以生成代码来访问它,无论它具有哪个类指针。多重继承会使事情变得更加复杂,但我们先不要去那里。

      A3。 Vtables 每个类存在一次,而不是每个对象存在一次。编译器需要生成一个,即使它从未被使用过,因为它不会提前知道这一点。

      【讨论】:

        【解决方案5】:

        b_ptr 指向 Derived vtable- 但编译器不能保证派生类中有一个 function_4,因为它不包含在基本 vtable 中,所以编译器不知道如何进行调用并抛出一个错误。

        不,vtable 是程序中其他地方的静态常量。基类只保存一个指向它的指针。一个 Derived 类可能拥有两个 vtable 指针,但也可能没有。

        在这两个类的上下文中,Base 需要一个 vtable 来查找 Derived 的 function1,它实际上是 虚拟的,即使你没有将它标记为虚拟,因为它覆盖了一个虚拟的基类功能。但是,即使不是这种情况,我也很确定编译器无论如何都需要生成 vtable,因为它不知道您在其他翻译单元中有哪些其他类可能会或可能不会从这些类继承并以不可知的方式覆盖它们的虚函数。

        【讨论】:

          【解决方案6】:

          Q1 - 名称解析是静态的。由于 b_ptr 是 Base* 类型,因此编译器看不到任何对 Derived 唯一的名称,以便访问它们在 v_table 中的条目。

          Q2 - 也许,也许不是。您必须记住,vtable 本身只是实现运行时多态性的一种非常常用的方法,实际上并不是任何地方的标准的一部分。无法就其所在位置做出明确声明。 vtable 实际上可以是程序中某处的某个静态表,它是从实例的对象描述中指向的。

          Q3 - 如果一个地方有一个虚拟条目,那么所有地方都必须有一个虚拟条目,否则就需要进行一系列困难/不可能的检查来提供覆盖功能。如果编译器知道您有一个 Base 并且正在调用一个重写的函数,则不需要访问 vtable,而可以直接使用该函数;如果需要,它甚至可以内联它。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2013-10-30
            • 2011-12-19
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-04-18
            • 1970-01-01
            相关资源
            最近更新 更多