3

我在处理虚拟继承时遇到了这个问题。我记得在非虚拟继承层次结构中,子类的对象持有其直接超类的对象。虚拟继承呢?在这种情况下,子类的对象是直接持有其超类的对象,还是仅仅持有指向其超类对象的指针?

顺便说一下,为什么下面代码的输出是:

sizeof(A): 8
sizeof(B): 20
sizeof(C): 20
sizeof(D): 36

代码:

#include <iostream>

using namespace std;

class A{
    char k[ 3 ];
    public:
        virtual void a(){};
};

class B : public virtual A{
    char j[ 3 ];
    public:
        virtual  void b(){};
};

class C : public virtual A{
    char i[ 3 ];
    public:
        virtual void c(){};
};

class D : public B, public C{
    char h[ 3 ];
    public:
        virtual void d(){};
};

int main( int argc, char *argv[] ){
    cout << "sizeof(A): " << sizeof( A ) << endl;
    cout << "sizeof(B): " << sizeof( B ) << endl;
    cout << "sizeof(C): " << sizeof( C ) << endl;
    cout << "sizeof(D): " << sizeof( D ) << endl;

    return 0;
}

提前致谢。亲切的问候。

4

5 回答 5

5

虚拟基础对象位于属于该对象的内存块中的某个位置(size = sizeof(object) 的内存)。由于多个不同类型的子对象可以通过多种方式组合,但必须共享同一个基础对象,因此每个子对象都需要一个偏移指针来找出虚拟基础对象。在没有虚拟继承的情况下,用于查找相应基础对象的偏移量在每个类类型的编译时都是固定的。

sizeof 值取决于您的编译器和机器,但以下假设很常见:

假设:指针大小为 4 个字节

假设:类大小四舍五入为 4 个字节的倍数

sizeof(A): 8  ->   1 pointer to vtable (virtual method) 
                 + 3 chars -> 4+3=7 
              -> round up to 8

sizeof(B): 20 ->   8 + 1 pointer to vtable (virtual method) 
                 + 1 offset pointer to virtual base 
                 + 3 chars -> 8 + 4 + 4 + 3 = 19 
              -> round up to 20

sizeof(C): 32 ->  20 + 1 pointer to vtable (virtual method) 
                 + 1 offset pointer to virtual base 
                 + 3 chars 
              -> 20 + 4 + 4 + 3 = 31 // this calculation refers to an older 
              -> round up to 32      // version of the question's example 
                                     // where C had B as base class

计算是猜测的,因为真正的计算必须确切地知道编译器是如何工作的。

问候,奥利弗

为什么需要额外的偏移量指针的更多细节:

例子:

class B  : virtual public A {...};
class C  : virtual public A {...};
class D1 : public B {...};
class D2 : public B, C {...};

D1 可能的内存布局:

A
B
D1

D2 可能的内存布局:

A
C
B
D2

在第二种情况下,子对象 B 需要另一个偏移量来找到它的基础 A

D2 类型的对象由一个内存块组成,其中包含所有父对象部分,即 D2 类型的对象的内存块有一个用于 A 成员变量、C 成员变量、B 成员变量和D2 成员变量。这些部分的顺序取决于编译器,但该示例显示,对于多重虚拟继承,需要一个偏移指针,该指针 对象的总内存块内指向虚拟基础对象。这是必需的,因为 B 类的方法只知道一个指向 B 的this指针,并且必须以某种方式计算 A 内存部分相对于this指针的位置。

计算sizeof(D):

sizeof(D): 36 ->   A:3 chars + A:vtable 
                 + B:3 chars + B:vtable + B:virtual base pointer
                 + C:3 chars + C:vtable + C:virtual base pointer
                 + D:3 chars + D:vtable
               =   3 + 4 
                 + 3 + 4 + 4 
                 + 3 + 4 + 4 
                 + 3 + 4 
                 = 36

上面的计算可能是错误的;-) ...

我不确定 D 部分是否有自己的 vtable 指针(这完全取决于编译器)。

我现在认为可能是 D 部分使用其父类的 vtable 指针条目,并且 4 个额外字节用于对齐每个部分(8 个字节的倍数):

所以这个计算可能更正确:

sizeof(D): 36 ->   A:3 chars + A:vtable + A:alignment
                 + B:3 chars + B:vtable + B:virtual base pointer + B:alignment
                 + C:3 chars + C:vtable + C:virtual base pointer + C:alignment
                 + D:3 chars + D:alignment
               =   3 + 4 + 1
                 + 3 + 4 + 4 + 1 
                 + 3 + 4 + 4 + 1
                 + 3 + 1
                 = 36
于 2010-05-02T17:17:00.410 回答
1

我看到上述问题的三点分析

一个。虚拟继承

“虚拟继承是一种机制,一个类指定它愿意共享其虚拟基类的状态。在虚拟继承下,对于给定的虚拟基,无论该类多少次,都只继承一个共享的基类子对象“作为派生层次结构中的虚拟基础出现。共享的基类子对象称为虚拟基类。” ...来自李普曼

虚拟继承只能避免从多重继承中继承的重复子对象。但这并不表示基类对象不会是子对象。相反,即使在虚拟继承期间,子对象(至少会存在一个副本 - 我的意思是会包含在 sizeof() 操作中)。

湾。虚函数

虚函数用于对层次结构中涉及的对象的成员函数进行动态绑定。因此,即使这对子对象排列也没有任何意义。

C。子对象的实现

这完全取决于编译器,并且出于各种原因很难确定 - 在其实现中。但是,我们可以确认对象的 sizeof() 也将包括基类(子)对象的大小 - 我们可以将它们可视化为嵌入了基类对象。

继承函数的每个对象肯定会包含子对象的空间。

高温高压

于 2010-05-02T17:34:19.583 回答
1

子类的对象是否直接持有其超类的对象

是的,无论继承是不是虚拟的,这都是它的工作原理。然而,我会使用“包含”与“持有”这个词。

如果您的层次结构看起来像这样,并且在任何地方都没有虚拟继承:

#     W    <--- base class
#    / \
#   X   Y  <--- subclasses of W
#    \ /
#     Z    <--- most derived class

然后Z会有两个副本W。但是,如果您将X-->WY-->W继承设为虚拟,Z则将只有一份副本,W因为Z的两个超类共享它们的公共基类。

#     W
#    / \   <--- make these two virtual to eliminate duplicate W in Z.
#   X   Y
#    \ /
#     Z

在您的示例中:

class A{...};
class B : public virtual A{...};
class C : public virtual B{...}; // Edit: OP's code had this typo when I answered
class D : public B, public C{...};

没有必要让 B 虚拟地从 A 继承。您需要的唯一虚拟继承是C-->Band D-->B,因为这是菱形“合并”继承层次结构的地方:

#   What you have     |     What you want?
#             A       |               A
#            /        |              /
#           /v        |             /
#          /          |            /
#         B           |           B
#        / \          |          / \
#       /v  \         |         /v  \v
#      /     \        |        /     \
#     C       )       |       C       )
#      \     /        |        \     /
#       \   /         |         \   /
#        \ /          |          \ /
#         D           |           D

当然,如果您有其他未显示的从 A 和 B 继承的类,那会改变一些事情——B-->A如果还有另一个您没有告诉我们的菱形,那么继承可能确实需要是虚拟的。

于 2010-05-03T09:31:53.833 回答
0

我记得在非虚拟继承层次结构中,子类的对象持有其直接超类的对象。
这是不正确的。有几个实现会这样做,但标准 C++ 没有这样定义。标准 C++ 没有指定如何实现这些东西。

虚拟继承仅用于某些多重继承的情况,其中派生类从两个基类乘以继承,而这两个基类本身继承自一个公共基类。这方面的一个例子是 iostream 库,其中 istream 和 ostream 从 basic_ios 继承,而 iostream 从 istream 和 ostream 继承(因此一个 iostream 将有两个 basic_ios 而没有虚拟继承)。

除非您处于这种特定情况下,否则不应使用虚拟继承。

虚拟继承呢?在这种情况下,子类的对象是直接持有其超类的对象,还是仅仅持有指向其超类对象的指针?
那是实现定义的。您不需要知道也不应该对此做出任何假设。可以说虚拟继承存在运行时损失,这就是为什么在不需要它时应该避免它的原因。

于 2010-05-02T17:15:52.757 回答
0

相比:

struct A {
    void *vptr; // offset 0 size 4 alignment 4
    char k[3]; // offset 4 size 3 alignment 1
    char unnamed_padding; // offset 7 size 1
    // total size 8 alignment 4
};

// MS:
struct base_B {
    void *vptr; // offset 0 size 4 alignment 4
    char j[3]; // offset 4 size 3 alignment 1
    char unnamed_padding; // offset 7 size 1
    A &a_subobject; // offset 8 size 4 alignment 4
    // total size 12 alignment 4

    base_B (&a_subobject) :a_subobject(a_subobject) {}
};

struct B {
    base_B b; // offset 0 size 12 alignment 4
    A a_subobject; // offset 12 size 8 alignment 4
    // total size 20 alignment 4

    B () : b(a_subobject) {}
};

struct base_C {
    void *vptr; // offset 0 size 4 alignment 4
    char i[3]; // offset 4 size 3 alignment 1
    char unnamed_padding; // offset 7 size 1
    A &a_subobject; // offset 8 size 4 alignment 4
    // total size 12 alignment 4

    base_C (&a_subobject) : a_subobject(a_subobject) {}
};

struct C {
    base_C c;
    A a_subobject; // offset 12 size 8 alignment 4
    // total size 20 alignment 4

    C () : c(a_subobject) {}
};

struct D {
    // no new vptr!
    // base_B is used as primary base: b_subobject.vptr is used as vptr
    base_B b_subobject; // offset 0 size 12 alignment 4
    base_C c_subobject; // offset 12 size 12 alignment 4
    char h[3];  // offset 24 size 3 alignment 1
    char unnamed_padding; // offset 27 size 1
    A a_subobject; // offset 28 size 8 alignment 4
    // total size 36 alignment 4

    D (): b_subobject(a_subobject), c_subobject(a_subobject) {}
};

// GCC:
struct base_B {
    void *vptr; // offset 0 size 4 alignment 4
    char j[3]; // offset 4 size 3 alignment 1
    char unnamed_padding; // offset 7 size 1
    // total size 8 alignment 4
};

struct B {
    base_B b; // offset 0 size 12 alignment 4
    A a_subobject; // offset 8 size 8 alignment 4
    // total size 16 alignment 4
};

struct base_C {
    void *vptr; // offset 0 size 4 alignment 4
    char i[3]; // offset 4 size 3 alignment 1
    char unnamed_padding; // offset 7 size 1
    // total size 8 alignment 4
};

struct C {
    base_C b; // offset 0 size 12 alignment 4
    A a_subobject; // offset 8 size 8 alignment 4
    // total size 16 alignment 4
};

struct D {
    // no new vptr!
    // base_B is used as primary base: b_subobject.vptr is used as vptr
    base_B b_subobject; // offset 0 size 8 alignment 4
    base_C c_subobject; // offset 8 size 8 alignment 4
    char h[3];  // offset 16 size 3 alignment 1
    char unnamed_padding; // offset 19 size 1
    A a_subobject; // offset 20 size 8 alignment 4
    // total size 24 alignment 4
};
于 2012-08-05T02:54:12.237 回答