8

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

类基{
上市:
     int a_number;

     根据(){}
     虚空函数 1() {}
     虚空函数 2() {}
     无效函数3(){}
};

类派生:公共基础{
上市:
     派生():基(){}
     void function1() {cout << "从基础派生" << endl;
     virtual void function4() {cout << "仅在派生中" << endl;}
};

诠释主要(){

      派生 *der_ptr = new Derived();
      基 *b_ptr = der_ptr; // 因为只是地址被传递,b_ptr 指向派生对象

      b_ptr->函数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 的答案相关 :))

请评论。

谢谢你的耐心。

4

6 回答 6

8

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

#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;
}
于 2010-12-28T18:33:22.963 回答
3

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

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

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

于 2010-12-28T18:03:29.407 回答
3

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

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

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

于 2010-12-28T18:11:12.620 回答
1

所有这些都取决于实施。但这里是使用“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;
}

在我的伪代码中,需要“基类的 vtable”,__Base_vtable以防有人尝试new Base();Base obj;.

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

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

于 2010-12-28T18:20:44.653 回答
1

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

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

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

于 2010-12-28T18:06:17.033 回答
1

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

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

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

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

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

于 2010-12-28T18:13:29.957 回答