从多个派生时了解虚函数 类
Understanding Virtual Functions when Deriving from Multiple Classes
我已经开始了解 C++
中虚函数的工作原理并遇到了以下代码。这是我对虚函数的理解:
- 每个定义了虚函数的 class 都会为其创建一个 vtable。
- 创建class的实例时,会创建
vptr
,它指向class的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();
}
我认为,由于 C12
对象被分配给类型 I2
,对象 o
只能访问它在 C12
中的方法 g()
(因为 g 被覆盖了)。现在,由于 o
被类型转换为 I1
,我认为 C12
中的 f()
会被调用。
实际输出:
C12g
我想了解以下内容:
- C12的内存布局以及I1,I2指向什么
- 如何将 C12g 打印为输出。
- 当一个对象在两个不相关的接口之间进行类型转换时会发生什么?
这里首先要明白的是,创建的实际对象,*o
的类型是C12
- 因为那是你用 new C12()
.
构建的
接下来,对于虚函数,实际对象的成员将被调用,无论什么'type'你投了一个指针。因此,当您转换为 I2 *o = new C12()
中的 I2
指针时,底层对象并不重要,例如,如果您随后调用 o->g(),因为对象会 'know' 调用它的覆盖函数。
但是,当您将指针指向 'unrelated' I1*
时,您会进入一个奇怪的地方。请记住,classes I1
和 I2
本质上具有相同的内存布局,然后在一个中调用 f()
将指向相同的 'offset'就像在另一个中调用 g()
一样。但是,由于 o
实际上是指向 I2
的指针,调用结束的 v-table 条目是 I2 中 g
的偏移量 - 它被 C12
.
同样值得注意的是,您使用了 C 风格的转换从 I2*
到 I1*
(但您也可以使用 reinterpret_cast
)。这很重要,因为这两个 对指针或指向的 object/memory 完全没有做任何事情。
可能听起来有点乱,但我希望它能提供一些见解!
这是一个 可能的 内存 layout/scenario - 但它将是特定于实现的,并且使用 class 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)
现在,当您在 I2 *o = new C12();
中进行从 C12*
到 I2*
的转换时,编译器会理解两个 class 之间的关系,因此 o
将指向 C12 中的 0x0008
偏移量(导出的 class 已正确 'sliced')。 但是 从 I2*
到 I1*
的 C 风格转换没有改变任何东西,所以编译器 'thinks' 它指向 I1
但它仍然指向 C12
的实际 I2
切片 - 而这个 'looks' 就像真正的 I1
class.
家庭作业
您可能会发现有趣的(并且可能同意也可能不同意我所描述的内存布局)是在 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;
请 - 任何尝试过的人 - 让我们知道这些值是什么,以及 platform/compiler 你正在使用什么。在 Visual Studio 2019 年,为 x64 平台编译时,我得到了这些指针值:
000002688A9726E0
000002688A9726E0
000002688A9726E8
000002688A9726E0
... 这(有点)与我描述的内存布局一致(除了在其他地方有 v-tables,而不是 'in-block')。
我已经开始了解 C++
中虚函数的工作原理并遇到了以下代码。这是我对虚函数的理解:
- 每个定义了虚函数的 class 都会为其创建一个 vtable。
- 创建class的实例时,会创建
vptr
,它指向class的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();
}
我认为,由于 C12
对象被分配给类型 I2
,对象 o
只能访问它在 C12
中的方法 g()
(因为 g 被覆盖了)。现在,由于 o
被类型转换为 I1
,我认为 C12
中的 f()
会被调用。
实际输出: C12g
我想了解以下内容:
- C12的内存布局以及I1,I2指向什么
- 如何将 C12g 打印为输出。
- 当一个对象在两个不相关的接口之间进行类型转换时会发生什么?
这里首先要明白的是,创建的实际对象,*o
的类型是C12
- 因为那是你用 new C12()
.
接下来,对于虚函数,实际对象的成员将被调用,无论什么'type'你投了一个指针。因此,当您转换为 I2 *o = new C12()
中的 I2
指针时,底层对象并不重要,例如,如果您随后调用 o->g(),因为对象会 'know' 调用它的覆盖函数。
但是,当您将指针指向 'unrelated' I1*
时,您会进入一个奇怪的地方。请记住,classes I1
和 I2
本质上具有相同的内存布局,然后在一个中调用 f()
将指向相同的 'offset'就像在另一个中调用 g()
一样。但是,由于 o
实际上是指向 I2
的指针,调用结束的 v-table 条目是 I2 中 g
的偏移量 - 它被 C12
.
同样值得注意的是,您使用了 C 风格的转换从 I2*
到 I1*
(但您也可以使用 reinterpret_cast
)。这很重要,因为这两个 对指针或指向的 object/memory 完全没有做任何事情。
可能听起来有点乱,但我希望它能提供一些见解!
这是一个 可能的 内存 layout/scenario - 但它将是特定于实现的,并且使用 class 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)
现在,当您在 I2 *o = new C12();
中进行从 C12*
到 I2*
的转换时,编译器会理解两个 class 之间的关系,因此 o
将指向 C12 中的 0x0008
偏移量(导出的 class 已正确 'sliced')。 但是 从 I2*
到 I1*
的 C 风格转换没有改变任何东西,所以编译器 'thinks' 它指向 I1
但它仍然指向 C12
的实际 I2
切片 - 而这个 'looks' 就像真正的 I1
class.
家庭作业
您可能会发现有趣的(并且可能同意也可能不同意我所描述的内存布局)是在 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;
请 - 任何尝试过的人 - 让我们知道这些值是什么,以及 platform/compiler 你正在使用什么。在 Visual Studio 2019 年,为 x64 平台编译时,我得到了这些指针值:
000002688A9726E0
000002688A9726E0
000002688A9726E8
000002688A9726E0
... 这(有点)与我描述的内存布局一致(除了在其他地方有 v-tables,而不是 'in-block')。