关于虚拟继承层次结构的问题

发布于 2024-08-30 16:56:57 字数 984 浏览 6 评论 0原文

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

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

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;
}

提前致谢。 亲切的问候。

I encounter this problem when tackling with virtual inheritance. I remember that in a non-virtual inheritance hierarchy, object of sub-class hold an object of its direct super-class. What about virtual inheritance? In this situation, does object of sub-class hold an object of its super-class directly or just hold a pointer pointing to an object of its super-class?

By the way, why the output of the following code is:

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

Code:

#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;
}

Thanks in advance.
Kind regards.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

倾城°AllureLove 2024-09-06 16:57:01

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

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

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

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

I remember that in a non-virtual inheritance hierarchy, object of sub-class hold an object of its direct super-class.
That is not correct. Several implementations are going to do it this way, but it's not defined that way by Standard C++. Standard C++ does not specify how any of these things are to be implemented.

Virtual Inheritance is used only for some cases of multiple inheritance where a derived class multiply inherits from two base classes which themselves inherit from a common base class. An example of this is the iostream library, where istream and ostream inherit from basic_ios, and iostream inherits from istream and ostream (and so one iostream would have two basic_ios without virtual inheritance).

Unless you are in this specific scenario, you should not use virtual inheritance.

What about virtual inheritance? In this situation, does object of sub-class hold an object of its super-class directly or just hold a pointer pointing to an object of its super-class?
That is implementation defined. You do not need to know nor should you ever make any assumptions about this. Suffice to say that there is a runtime penalty for virtual inheritance, which is why you should avoid it when it is not needed.

一抹微笑 2024-09-06 16:57:01

比较:

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
};

Compare:

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
};
故人爱我别走 2024-09-06 16:57:00

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

的,无论继承是否为虚继承,都是如此。然而,我会使用“包含”和“保留”这个词。

如果您的层次结构如下所示,并且任何地方都没有虚拟继承:

#     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-->BD-->B,因为这是菱形在继承层次结构中“合并”的地方:

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

当然,如果您还有其他未显示的从 A 和 B 继承的类,则情况会发生变化 - 如果您还有另一个菱形,则 B-->A 继承确实需要是虚拟的没有告诉我们。

does object of sub-class hold an object of its super-class directly

Yes, that is how it works whether the inheritance is virtual or not. I would use the word "contain" vs. "hold" however.

If your hierarchy looked like this, with no virtual inheritances anywhere:

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

Then Z will have two copies of W. But if you make the X-->W and Y-->W inheritances virtual, then Z will only have one copy of W because Z's two superclasses share their common base class.

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

In your example:

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{...};

Having B inherit virtually from A isn't necessary. The only virtual inheritances you need are C-->B and D-->B, since that is where the diamond "merges" going up the inheritance hierarchy:

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

Of course if you have other classes not shown that inherit from A as well as B, that changes things -- maybe the B-->A inheritance does need to be virtual if there is another diamond you didn't tell us about.

时光清浅 2024-09-06 16:56:59

虚拟基础对象位于属于该对象的内存块中的某个位置(大小 = 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

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

问候,
Oliver

更多详细说明为什么需要额外的偏移指针:

示例:

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

The virtual base object is somewhere in the memory block that belongs to the object (the memory with size = sizeof(object)). Because several sub objects of different types can be combined in various ways but must share the same base object, a offset pointer is needed for each sub object to find out the virtual base object. Without virtual inheritance, the offset to find out the corresponding base object is fixed at compile time for each class type.

The sizeof values depend on your compiler and machine, but the following assumptions are very common:

assumption: pointer size is 4 bytes

assumption: class size is rounded up to multiple of 4 bytes

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

The calculations are guessed because the real calculation must exactly know how the compiler works.

Regards,
Oliver

More details why an extra offset pointer is needed:

Example:

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

possible memory layout for D1:

A
B
D1

possible memory layout for D2:

A
C
B
D2

in the second case sub object B needs another offset to find its base A

An object of type D2 consists of a memory block, where all the parent object parts are contained, i.e. the memory block for an object of type D2 has a section for the A member variables, the C member variables, the B member variables and the D2 member variables. The order of these sections is compiler dependent, but the example shows, that for multiple virtual inheritance a offset pointer is needed, that points within the object's total memory block to the virtual base object. This is needed because the methods of class B know only one this pointer to B and must somehow calculate where the A memory part is relative to the this pointer.

Calculation 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

The above calculation is probably wrong ;-) ...

I'm not sure whether the D part has its own vtable pointer or not (this is all highly compiler dependent).

I now think that it could be that the D part use the vtable pointer entry of its parent classes and that the 4 extra bytes are used for alignment each part (multiple of 8 bytes):

So this calculation is probably more correct:

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
太阳公公是暖光 2024-09-06 16:56:59

对于上面的问题我看到三点分析

。虚拟继承

“虚拟继承是一种机制,通过该机制,类指定它愿意共享其虚拟基类的状态。在虚拟继承下,对于给定的虚拟基,仅继承一个共享基类子对象,无论其如何该类在派生层次结构中作为虚拟基出现了多少次共享基类子对象称为虚拟基类。 ... 来自 Lippman

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

b.虚函数

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

c.子对象的实现

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

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

华泰

I see three point analysis for the above question

a. Virtual Inheritance

"Virtual inheritance is a mechanism whereby a class specifies that it is willing to share the state of its virtual base class. Under virtual inheritance, only one, shared base-class subobject is inherited for a given virtual base regardless of how many times the class occurs as a virtual base within the derivation hierarchy. The shared base-class subobject is called a virtual base class." ... From Lippman

Virtual inheritance only avoids duplicate sub-objects inherited from multiple inheritance. But this does not indicate in any way that the base class objects will not be sub-objects. On the contrary, the sub-object (atleast one copy would be present - I mean would be included in sizeof() operation) even during the virtual inheritance.

b. virtual function

Virtual function is for dynamic binding of member functions of objects involved in hierarchy. So even this does not have any significance towards sub-object arrangements.

c. Implementation of the sub-objects

This is totally compiler dependent, and for all reasons would be very difficult to determine - in its implementation. However, we can confirm that the sizeof() of the object would include the size of the base class (sub) objects also - and we can visualize them as having the base class object embedded in them.

Each object of the inherited function will definitely contain space for the sub-objects.

HTH

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文