了解虚拟派生的大小class
Understanding the size of virtual derived class
#include <iostream>
using namespace std;
class A {
int a;
};
class B1 : virtual public A {
int b1;
};
class B2 : virtual public A {
int b2;
};
class C : public B1, public B2 {
int c;
};
int main() {
A obj1; B1 obj2; B2 obj3; C obj4;
cout << sizeof(obj1) << endl;
cout << sizeof(obj2) << endl;
cout << sizeof(obj3) << endl;
cout << sizeof(obj4) << endl;
return 0;
}
输出:
4
16
16
40
在上面的c++程序中,
A 的大小是 4 因为它只有一个 int
B1 的大小为 16,因为 (int+ int + virtual pointer) B2
相同
但是C的大小怎么是40???
免责声明:None 这是 C++ 标准指定的,除了允许编译器添加填充这一事实外,我实际上没有检查任何汇编代码。
如果在 x64 架构上的 GCC 中编译,结构可能如下所示:
+-----------------------------------------------------------------------------------------------------+
| C |
| +------------------------------------+ +------------------------------------+ +----------+ |
| | B1 (16) | | B2 (16) | | A(4) | |
| | +--------------+ +------+ +------+ | | +--------------+ +------+ +------+ | +------+ | +------+ | |
| | | vptr (8) | | b1(4)| |pad(4)| | | | vptr (8) | | b2(4)| |pad(4)| | | c(4) | | | a(4) | | |
| | +--------------+ +------+ +------+ | | +--------------+ +------+ +------+ | +------+ | +------+ | |
| +------------------------------------+ +------------------------------------+ +----------+ |
+-----------------------------------------------------------------------------------------------------+
检查每个结构的对齐方式(https://wandbox.org/permlink/a4l99p6JwTrSdWx1),我们看到A
对齐到4字节,而所有剩余的结构在使用GCC编译器时都对齐到8字节。
我假设您的计算机是 x64 架构。这意味着指针必须至少为 8 个字节长,并且因为 vptr 存在于 B1
和 B2
中,所以整个结构对齐到 8 个字节。因此,编译器需要向两个结构添加填充,以保持 sizeof(B1) % alignof(B1) == 0
(或简单地说保持结构对齐)。
结果可能因您使用的编译器和系统架构而异。例如,msvc 19.28 x64
给出了这个不言自明的结果(使用选项 /d1reportAllClassLayout
得到它):
class C size(44):
+---
0 | +--- (base class B1)
0 | | {vbptr}
8 | | b1
| | <alignment member> (size=4)
| | <alignment member> (size=4)
| +---
16 | +--- (base class B2)
16 | | {vbptr}
24 | | b2
| | <alignment member> (size=4)
| | <alignment member> (size=4)
| +---
32 | c
| <alignment member> (size=4)
+---
+--- (virtual base A)
40 | a
+---
但对于 msvc 19.28 x86
,结果将是:
class C size(24):
+---
0 | +--- (base class B1)
0 | | {vbptr}
4 | | b1
| +---
8 | +--- (base class B2)
8 | | {vbptr}
12 | | b2
| +---
16 | c
+---
+--- (virtual base A)
20 | a
+---
更新
需要注意的是,上面的class布局清楚地展示了虚拟继承的特点,其中只有一个基础class实例(A
)的副本被继承孙子派生 class (C
)。如果 A::a
数据成员是 public,我们可以在 class C
的成员函数中使用以下语句:
void C::foo() {
B2::a = 1;
B1::a = 2;
std::cout << B2::a << " " << B1::a << " " << A::a; // Output: 2 2 2
}
但是如果你不使用虚拟继承(而只是public),那么class C
布局将是这样的:
class C size(20):
+---
0 | +--- (base class B1)
0 | | +--- (base class A)
0 | | | a
| | +---
4 | | b1
| +---
8 | +--- (base class B2)
8 | | +--- (base class A)
8 | | | a
| | +---
12 | | b2
| +---
16 | c
+---
我们将有两个基类成员变量的副本classA
void C::foo() {
B2::a = 1;
B1::a = 2;
std::cout << B2::a << " " << B1::a; // Output: 1 2. `A::a` ambiguity error
}
了解 C
的布局和填充方式并不重要,因为这是实现定义的,并且会因平台而异。但是有一个具体细节...
Base class A
,实际上是继承的,被提取到层次结构的顶部。所以它只是 B1
的一部分,只要 B1
被“单独使用”。当它在 C
中进一步继承时,A
不再是它的一部分。
这意味着我们不能依赖 sizeof(B1)
或 sizeof(B2)
,因为我们必须先从它们中减去 sizeof(A)
。但是从 B1
中删除 4 个字节不会将其大小从 16 减小到 12,因为由于 vbptr,它必须对齐并填充到 8 个字节(假设为 64 位)。
所以我们最终得到 sizeof(B1 less A)
+sizeof(B2 less A)
+sizeof(A)
+sizeof(C::c)
= 16+16+4+4 = 40.
如果我们将 A
变大,我们可以获得一些时髦的输出:
#include <iostream>
using namespace std;
class A {
int a1,a2,a3,a4,a5;
};
class B1 : virtual public A {
int b1;
};
class B2 : virtual public A {
int b2;
};
class C : public B1, public B2 {
int c;
};
int main() {
A obj1; B1 obj2; B2 obj3; C obj4;
cout << "sizeof(A) = " << sizeof(obj1) << endl;
cout << "sizeof(B1) = " << sizeof(obj2) << endl;
cout << "sizeof(B2) = " << sizeof(obj3) << endl;
cout << "sizeof(C) = " << sizeof(obj4) << endl;
return 0;
}
sizeof(A) = 20
sizeof(B1) = 32
sizeof(B2) = 32
sizeof(C) = 56
现在更清楚 sizeof(C)
< sizeof(B1)
+ sizeof(B2)
.
要了解更多信息,您可以 play with offsets 查看 C
的具体布局方式。
#include <iostream>
using namespace std;
class A {
int a;
};
class B1 : virtual public A {
int b1;
};
class B2 : virtual public A {
int b2;
};
class C : public B1, public B2 {
int c;
};
int main() {
A obj1; B1 obj2; B2 obj3; C obj4;
cout << sizeof(obj1) << endl;
cout << sizeof(obj2) << endl;
cout << sizeof(obj3) << endl;
cout << sizeof(obj4) << endl;
return 0;
}
输出:
4
16
16
40
在上面的c++程序中, A 的大小是 4 因为它只有一个 int B1 的大小为 16,因为 (int+ int + virtual pointer) B2
相同但是C的大小怎么是40???
免责声明:None 这是 C++ 标准指定的,除了允许编译器添加填充这一事实外,我实际上没有检查任何汇编代码。
如果在 x64 架构上的 GCC 中编译,结构可能如下所示:
+-----------------------------------------------------------------------------------------------------+
| C |
| +------------------------------------+ +------------------------------------+ +----------+ |
| | B1 (16) | | B2 (16) | | A(4) | |
| | +--------------+ +------+ +------+ | | +--------------+ +------+ +------+ | +------+ | +------+ | |
| | | vptr (8) | | b1(4)| |pad(4)| | | | vptr (8) | | b2(4)| |pad(4)| | | c(4) | | | a(4) | | |
| | +--------------+ +------+ +------+ | | +--------------+ +------+ +------+ | +------+ | +------+ | |
| +------------------------------------+ +------------------------------------+ +----------+ |
+-----------------------------------------------------------------------------------------------------+
检查每个结构的对齐方式(https://wandbox.org/permlink/a4l99p6JwTrSdWx1),我们看到A
对齐到4字节,而所有剩余的结构在使用GCC编译器时都对齐到8字节。
我假设您的计算机是 x64 架构。这意味着指针必须至少为 8 个字节长,并且因为 vptr 存在于 B1
和 B2
中,所以整个结构对齐到 8 个字节。因此,编译器需要向两个结构添加填充,以保持 sizeof(B1) % alignof(B1) == 0
(或简单地说保持结构对齐)。
结果可能因您使用的编译器和系统架构而异。例如,msvc 19.28 x64
给出了这个不言自明的结果(使用选项 /d1reportAllClassLayout
得到它):
class C size(44):
+---
0 | +--- (base class B1)
0 | | {vbptr}
8 | | b1
| | <alignment member> (size=4)
| | <alignment member> (size=4)
| +---
16 | +--- (base class B2)
16 | | {vbptr}
24 | | b2
| | <alignment member> (size=4)
| | <alignment member> (size=4)
| +---
32 | c
| <alignment member> (size=4)
+---
+--- (virtual base A)
40 | a
+---
但对于 msvc 19.28 x86
,结果将是:
class C size(24):
+---
0 | +--- (base class B1)
0 | | {vbptr}
4 | | b1
| +---
8 | +--- (base class B2)
8 | | {vbptr}
12 | | b2
| +---
16 | c
+---
+--- (virtual base A)
20 | a
+---
更新
需要注意的是,上面的class布局清楚地展示了虚拟继承的特点,其中只有一个基础class实例(A
)的副本被继承孙子派生 class (C
)。如果 A::a
数据成员是 public,我们可以在 class C
的成员函数中使用以下语句:
void C::foo() {
B2::a = 1;
B1::a = 2;
std::cout << B2::a << " " << B1::a << " " << A::a; // Output: 2 2 2
}
但是如果你不使用虚拟继承(而只是public),那么class C
布局将是这样的:
class C size(20):
+---
0 | +--- (base class B1)
0 | | +--- (base class A)
0 | | | a
| | +---
4 | | b1
| +---
8 | +--- (base class B2)
8 | | +--- (base class A)
8 | | | a
| | +---
12 | | b2
| +---
16 | c
+---
我们将有两个基类成员变量的副本classA
void C::foo() {
B2::a = 1;
B1::a = 2;
std::cout << B2::a << " " << B1::a; // Output: 1 2. `A::a` ambiguity error
}
了解 C
的布局和填充方式并不重要,因为这是实现定义的,并且会因平台而异。但是有一个具体细节...
Base class A
,实际上是继承的,被提取到层次结构的顶部。所以它只是 B1
的一部分,只要 B1
被“单独使用”。当它在 C
中进一步继承时,A
不再是它的一部分。
这意味着我们不能依赖 sizeof(B1)
或 sizeof(B2)
,因为我们必须先从它们中减去 sizeof(A)
。但是从 B1
中删除 4 个字节不会将其大小从 16 减小到 12,因为由于 vbptr,它必须对齐并填充到 8 个字节(假设为 64 位)。
所以我们最终得到 sizeof(B1 less A)
+sizeof(B2 less A)
+sizeof(A)
+sizeof(C::c)
= 16+16+4+4 = 40.
如果我们将 A
变大,我们可以获得一些时髦的输出:
#include <iostream>
using namespace std;
class A {
int a1,a2,a3,a4,a5;
};
class B1 : virtual public A {
int b1;
};
class B2 : virtual public A {
int b2;
};
class C : public B1, public B2 {
int c;
};
int main() {
A obj1; B1 obj2; B2 obj3; C obj4;
cout << "sizeof(A) = " << sizeof(obj1) << endl;
cout << "sizeof(B1) = " << sizeof(obj2) << endl;
cout << "sizeof(B2) = " << sizeof(obj3) << endl;
cout << "sizeof(C) = " << sizeof(obj4) << endl;
return 0;
}
sizeof(A) = 20
sizeof(B1) = 32
sizeof(B2) = 32
sizeof(C) = 56
现在更清楚 sizeof(C)
< sizeof(B1)
+ sizeof(B2)
.
要了解更多信息,您可以 play with offsets 查看 C
的具体布局方式。