10

我在 C++ 中“玩”虚拟继承,我想知道一个类对象是如何布局的。我有这三个类:

class A {
private:
    int a;
public:
    A() {this->a = 47;}
    virtual void setInt(int x) {this->a = x;}
    virtual int getInt() {return this->a;}
    ~A() {this->a = 0;}
};

class B {
private:
    int b;
public:
    B() {b = 48;}
    virtual void setInt(int x) {this->b = x;}
    virtual int getInt() {return this->b;}
    ~B() {b = 0;}
};

class C : public A, public B {
private:
    int c;
public:
    C() {c = 49;}
    virtual void setInt(int x) {this->c = x;}
    virtual int getInt() {return this->c;}
    ~C() {c = 0;}
};

(我认为他们是正确的:p)

我用-fdump-class-hierarchyg ++,我得到了这个

Vtable for A
A::_ZTV1A: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1A)
16    A::setInt
24    A::getInt

Class A
   size=16 align=8
   base size=12 base align=8
A (0x10209fb60) 0
    vptr=((& A::_ZTV1A) + 16u)

Vtable for B
B::_ZTV1B: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1B)
16    B::setInt
24    B::getInt

Class B
   size=16 align=8
   base size=12 base align=8
B (0x1020eb230) 0
    vptr=((& B::_ZTV1B) + 16u)

Vtable for C
C::_ZTV1C: 8u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1C)
16    C::setInt
24    C::getInt
32    (int (*)(...))-0x00000000000000010
40    (int (*)(...))(& _ZTI1C)
48    C::_ZThn16_N1C6setIntEi
56    C::_ZThn16_N1C6getIntEv

Class C
   size=32 align=8
   base size=32 base align=8
C (0x1020f5080) 0
    vptr=((& C::_ZTV1C) + 16u)
  A (0x1020ebd90) 0
      primary-for C (0x1020f5080)
  B (0x1020ebe00) 16
      vptr=((& C::_ZTV1C) + 48u)

现在这些到底是(int (*)(...))-0x00000000000000010什么C::_ZThn16_N1C6setIntEi and (int (*)(...))0??有人可以解释转储吗?

谢谢你。

4

2 回答 2

6

我不是 100% 确定这个答案是正确的,但这是我最好的猜测。

当你有一个类继承了 multiply 和 non-virtual 时,类的布局通常是第一个基类型的完整对象,然后是第二个基类型的完整对象,然后是对象本身的数据。如果您查看 B,您可以看到 A 对象的 vtable 指针,如果您查看 C,您可以看到 A 和 B 对象的 vtable 指针。

因为对象是以这种方式布局的,这意味着如果你有一个B*指针指向一个C对象,那么指针实际上不会位于对象的基部;相反,它将指向中间的某个地方。这意味着,如果您需要将对象转换为A*,则需要将B*指针调整一些量以将其跳回对象的开头。为了做到这一点,编译器需要在某处编码您需要跳回以到达对象开头的字节数。我认为第一个(int(*)(...))实际上只是您需要查看的原始字节数才能到达对象的开头。如果您注意到,对于Avtable,此指针为 0(因为 A 的 vtable 位于对象的开头,对于Bvtable (因为它也存在于对象的开头。但是,请注意Cvtable 有两个部分 - 第一部分是 vtable for A,它的第一个疯狂条目也是零(因为如果你在Avtable 处,你不需要做任何调整)。然而,在这个表的前半部分之后似乎是Bvtable,并注意它的第一个条目是 hex value -0x10。如果你查看C对象布局,你会注意到vtable 指针在Bvtable 指针之后 16 个字节A。该-0x10值可能是您需要跳过Bvtable 指针以返回对象根的校正偏移量。

每个 vtable 的第二个疯狂条目似乎是指向 vtable 本身的指针。请注意,它始终等于 vtable 对象的地址(比较 vtable 的名称和它指向的内容)。如果您想要进行任何类型的运行时类型识别,这将是必要的,因为这通常涉及查看 vtable 的地址(或至少靠近它前面的东西)。

最后,至于为什么在Cvtable 的末尾有神秘命名的 setInt 和 getInt 函数,我很确定这是因为该C类型继承了两组不同的函数,名为setIntand getInt- 一个 throughA和一个 through B。如果我不得不猜测,这里的修改是为了确保编译器内部可以区分两个虚函数。

希望这可以帮助!

于 2011-01-05T22:16:51.960 回答
6

这是您通过 c++filt 运行的转储:

Vtable for A
A::vtable for A: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& typeinfo for A)
16    A::setInt
24    A::getInt

Class A
   size=16 align=8
   base size=12 base align=8
A (0x10209fb60) 0
    vptr=((& A::vtable for A) + 16u)

Vtable for B
B::vtable for B: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& typeinfo for B)
16    B::setInt
24    B::getInt

Class B
   size=16 align=8
   base size=12 base align=8
B (0x1020eb230) 0
    vptr=((& B::vtable for B) + 16u)

Vtable for C
C::vtable for C: 8u entries
0     (int (*)(...))0
8     (int (*)(...))(& typeinfo for C)
16    C::setInt
24    C::getInt
32    (int (*)(...))-0x00000000000000010
40    (int (*)(...))(& typeinfo for C)
48    C::non-virtual thunk to C::setInt(int)
56    C::non-virtual thunk to C::getInt()

Class C
   size=32 align=8
   base size=32 base align=8
C (0x1020f5080) 0
    vptr=((& C::vtable for C) + 16u)
  A (0x1020ebd90) 0
      primary-for C (0x1020f5080)
  B (0x1020ebe00) 16
      vptr=((& C::vtable for C) + 48u)

不知道(int (*)(...))-0x00000000000000010and(int (*)(...))0是什么。
该部分是此处C::_ZThn16_N1C6setIntEi/C::non-virtual thunk to C::setInt(int)描述的“在存在多重继承或虚拟继承的情况下优化虚拟函数调用” 。

于 2011-01-05T22:37:27.090 回答