了解从多个类派生时的虚函数

use*_*993 4 c++ virtual-functions multiple-inheritance undefined-behavior reinterpret-cast

我已经开始了解虚拟函数的工作原理C++并遇到了以下代码。以下是我对虚函数的理解:

  1. 每个定义了虚函数的类都会为其创建一个虚函数表。
  2. 当创建类的实例时,vptr会创建 ,它指向该类的vtable 。

根据我的理解,我试图分析以下代码的输出,但我无法破译代码如何打印“C12g”。

class I1 {
  public: 
    virtual void f(){cout << "I1" << endl;}
};
class I2 {
  public: 
    virtual void g(){cout << "I2" << endl;}
};
class C12 : public I1, public I2 {
  public:
    virtual void f(){cout << "C12f" << endl;}
    virtual void g(){cout << "C12g" << endl;}
};
int main(int argc, char *argv[]) {
  I2 *o = new C12();
  ((I1*)o)->f();
}
Run Code Online (Sandbox Code Playgroud)

我认为,由于C12对象被分配给 type I2,对象o只能访问它的方法g()C12因为 g 被重写)。现在,由于o是 类型转换为 an I1,我想f()inC12会被调用。

实际产量:C12g

我想了解以下事项:

  1. C12的内存布局以及I1、I2指向的内容
  2. C12g 如何打印为输出。
  3. 当一个对象在两个不相关的接口之间进行类型转换时会发生什么?

Adr*_*ica 5

这里您首先必须了解的是,创建的实际对象*o是类型C12- 因为那是您使用 构造的new C12()

接下来,使用虚函数,无论您将指针强制转换为什么“类型”,都将调用实际对象的成员。因此,当您转换为I2中的指针I2 *o = new C12()时,如果您随后调用 o->g(),则对底层对象并不重要,因为该对象会“知道”调用其重写的函数。

然而,当您将指针强制转换为“不相关”时,I1*您就会陷入奇怪的境地。请记住,类I1I2本质上具有相同的内存布局,那么调用f()一个类将指向与调用g()另一个类相同的“偏移量”。但是,由于o实际上是指向 的指针I2,调用最终的 v 表条目是gI2 中的偏移量 - 它被 覆盖C12

还值得注意的是,您使用了 C 风格的转换来从I2*to获取I1*(但您也可以使用 a reinterpret_cast)。这很重要,因为这两者对指针或指向的对象/内存绝对没有任何作用。

可能听起来有点乱,但我希望它能提供一些见解!

这是一个可能的内存布局/场景 - 但它将是特定于实现的,并且在 C 风格转换之后使用类指针很可能会构成未定义的行为!

可能的内存映射(简化,假设所有组件都是 4 字节):

class I1:
0x0000: (non-virtual data for class I1)
0x0004: v-table entry for function "f"

class I2:
0x0000: (non-virtual data for class I2)
0x0004: v-table entry for function "g"

class C12:
0x0000: (non-virtual data for class I1)
0x0004: v-table entry for function "f"
0x0008: (non-virtual data for class I2)
0x000C: v-table entry for function "g"
0x0010: (class-specific stuff for C12)
Run Code Online (Sandbox Code Playgroud)

C12*现在,当您进行从 a到I2*in的转换时I2 *o = new C12();,编译器了解两个类之间的关系,因此o将指向0x0008C12 中的偏移量(派生类已被正确“切片”)。但是I2*从to进行的 C 风格转换I1*不会改变任何内容,因此编译器“认为”它指向 an,I1但它仍然指向实际的I2切片C12- 并且这“看起来”就像一个真正的I1类。

家庭作业

您可能会发现有趣的(并且可能同意也可能不同意我描述的内存布局)是在末尾添加以下代码main()

C12* properC12 = new C12();// Points to the 'origin' of the class
I1* properI1 = properC12; // Should (?) have same value as above?
I2* properI2 = properC12; // Should (?) have an offset to 'slice'
I1* dodgyI1 = (I1*)properC12; // Will (?) have same value as properI2!
cout << std::hex << properC12 << endl;
cout << std::hex << properI1 << endl;
cout << std::hex << properI2 << endl;
cout << std::hex << dodgyI1 << endl;
Run Code Online (Sandbox Code Playgroud)

请 - 任何尝试过的人 - 让我们知道这些值是什么,以及您正在使用什么平台/编译器。在 Visual Studio 2019 中,针对 x64 平台进行编译,我得到以下指针值:

000002688A9726E0
000002688A9726E0
000002688A9726E8
000002688A9726E0
Run Code Online (Sandbox Code Playgroud)

...这(在某种程度上)与我描述的内存布局一致(除了将 v 表放在其他地方,而不是“块内”)。