为什么Derived4的sizeof为8字节?我认为应该是5个字节。

4
这是给定程序的输出结果:
sizeof(Empty) 1
sizeof(Derived1) 1
sizeof(Derived2) 4
sizeof(Derived3) 1
sizeof(Derived4) 8
sizeof(Dummy) 1

这是程序:
#include <iostream>
using namespace std;

class Empty
{};

class Derived1 : public Empty
{};

class Derived2 : virtual public Empty
{};

class Derived3 : public Empty
{    
    char c;
};

class Derived4 : virtual public Empty
{
    char c;
};

class Dummy
{
    char c;
};

int main()
{
    cout << "sizeof(Empty) " << sizeof(Empty) << endl;
    cout << "sizeof(Derived1) " << sizeof(Derived1) << endl;
    cout << "sizeof(Derived2) " << sizeof(Derived2) << endl;
    cout << "sizeof(Derived3) " << sizeof(Derived3) << endl;
    cout << "sizeof(Derived4) " << sizeof(Derived4) << endl;    
    cout << "sizeof(Dummy) " << sizeof(Dummy) << endl;

    return 0;
}

Derived3的大小为1字节。那么为什么Derived4的大小是8字节?如果对齐是答案,那么为什么Derived3没有对齐?


1
听起来这完全取决于实现,换句话说很难回答编译器制造商在想什么 :) - Joachim Isaksson
虚拟继承确保编译器无法优化基类的v-table指针。这个指针的对齐方式为4,强制添加3个字节的填充。4 + 1 + 3 = 8。 - Hans Passant
@JoachimIsaksson 这是实现相关的,受CPU大小和对齐方式以及C++语言规则的限制。 - curiousguy
2个回答

5

这取决于类中数据成员的对齐方式。如果一个类有一个虚基类,则它的实现包含对该虚基类的引用,该引用在你的情况下等于4个字节。当你添加char类型的数据成员时,它会填充3个字节,以提供对基本虚拟类的引用的对齐。


虚基类可以通过指向子对象的指针或vptr(例如GCC)来实现。 - curiousguy

1
对于一个具体类型 Tsizeof 有两个含义:
  • 类型 T 的完整对象 a 的表示仅占用 [(char*)&a, (char*)&a + sizeof(T)) 中的 sizeof(T) 字节;

  • T 的数组存储在第二个对象后面 sizeof(T)

完整对象占用的字节不重叠:要么一个是另一个的主体并包含在其中,要么它们没有共同的字节。

您可以使用 memset 覆盖完整对象,然后使用放置 new 重新构建它(或者对于没有有意义构造的对象,只需赋值),如果析构函数不重要,则一切都会很好(如果析构函数负责释放资源,请勿这样做)。您不能只覆盖基类子对象,因为它会损坏完整对象。 sizeof 告诉您可以覆盖多少字节而不破坏其他对象。

类的数据成员是完整的对象,因此类的大小始终至少是其成员大小之和

有些类型是“满”的:对象中的每个位都是有意义的;特别是unsigned char。一些类型有未使用的位或字节。许多类具有这样的“空隙”用于填充。一个空类没有任何有意义的位:没有任何位是状态的一部分,因为没有状态。一个空类是一个具体的类,但是不能实例化;每个实例都有一个标识,因此具有不同的地址,因此即使标准允许sizeof为零,其大小也不能为零。一个空类是纯填充。

考虑:

struct intchar {
    int i;
    char c;
};

intchar的对齐方式与int相同。在通常的系统中,sizeof(int)为4,这些基本类型的对齐方式等于大小,因此intchar的对齐方式为4,大小为8,因为大小对应于两个数组元素之间的距离,所以3个字节未被用于表示。

给定intchar_char

struct intchar_char {
    intchar ic;
    char c;
};

因为对齐的原因,即使ic中存在未使用的字节,大小也必须大于intchar的大小:成员ic是一个完整的对象,并占据了它所有的字节,在此对象中允许使用memset

sizeof仅对具体类型(可实例化)和完整对象定义良好。因此,如果要创建这样的数组,则需要sizeof来确定空类的大小;但是对于基类子对象,sizeof无法给出所需信息。

C++中没有运算符可以测量表示类时使用了多少字节,但您可以尝试使用派生类:

template <class Base, int c=1>
struct add_chars : Base {
    char dummy[c];
};

template <class T>
struct has_trailing_unused_space {
    static const bool result = sizeof (add_chars<T>) == sizeof (T);
};

请注意,add_chars<T>没有类型为T的成员,因此没有T完整对象,memset不允许在intchar子对象上使用。 dummy是一个完整对象,不能与任何其他完整对象重叠,但它可以与基类子对象重叠。 派生类的大小并不总是至少等于其子对象的大小之和。 成员dummy占用一字节;如果Base中有任何尾随字节,大多数编译器将在未使用的空间中分配dummyhas_trailing_unused_space测试此属性。
int main() {
    std::cout << "empty has trailing space: ";
    std::cout << has_trailing_unused_space<empty>::result;
}

输出

空的末尾有空格:1

虚继承

在考虑涉及虚函数和虚基类的类的布局时,您需要考虑隐藏的vptr和内部指针。它们在典型实现中将具有与void*相同的属性(大小和对齐方式)。

class Derived2 : virtual public Empty
{};

与普通的继承和成员不同,虚拟继承并没有定义严格的直接所有权关系,而是共享间接所有权关系,就像调用虚函数引入了一个间接性。虚拟继承创建两种类布局:基类子对象布局和完整对象布局。
当一个类被实例化时,编译器将使用为完整对象定义的布局,这可以使用vptr(如GCC所做)和Titanium ABI规定的方式。
struct Derived2 {
    void *__vptr;
};

vptr指向完整的虚函数表,包含所有运行时信息,但是C++语言不认为这样的类是多态类,因此不能使用dynamic_cast/typeid来确定动态类型。
据我所知,Visual C++不使用vptr而是使用子对象指针。
struct Derived2 {
    Empty *__ptr;
};

其他编译器可以使用相对偏移量:
struct Derived2 {
    offset_t __off;
};

Derived2 是一个非常简单的类;Derived2 的子对象布局与其完整对象布局相同。

现在考虑一个稍微复杂一些的情况:

struct Base {
    int i;
};

struct DerV : virtual Base {
    int j;
};

这里是完整的 DerV 布局(Titanium ABI 风格):
struct complete__DerV {
    void *__vptr;
    int j;
    Base __base;
};

子对象布局是:
struct DerV {
    void *__vptr;
    int j;
};

所有类型为DerV的完整或不完整对象都具有此布局。

vtable包含虚基类的相对偏移量:offsetof(complete__DerV,__base),对于动态类型为DerV的对象而言。

通过在运行时查找覆盖函数或者按照语言规则知道动态类型,可以调用虚函数。

向上转型(将指针转换为虚基类),通常在调用基类的成员函数时会隐式发生:

struct Base {
    void f();
};

struct DerV : virtual Base {
};

DerV d;
d.f(); // involves a derived to base conversion

当动态类型已知时,可以使用已知的偏移量,如此处所示,或者使用运行时信息来确定偏移量:
void foo (DerV &d) {
    d.f(); // involves a derived to base conversion
}

可以翻译为(钛合金ABI风格)

void foo (DerV &d) {
    (Base*)((char*)&d + d.__vptr.off__Base)->f();
}

或者按照Visual C++的风格:
void foo (DerV &d) {
    d.__ptr->f();
}

甚至连

都不例外

void foo (DerV &d) {
    (Base*)((char*)&d + d.__off)->f();
}

实现方式不同,开销也会有所差异,但只要动态类型未知,就会存在开销。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接