对于一个具体类型
T
,
sizeof
有两个含义:
完整对象占用的字节不重叠:要么一个是另一个的主体并包含在其中,要么它们没有共同的字节。
您可以使用 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
中有任何尾随字节,大多数编译器将在未使用的空间中分配
dummy
;
has_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();
当动态类型已知时,可以使用已知的偏移量,如此处所示,或者使用运行时信息来确定偏移量:
void foo (DerV &d) {
d.f();
}
可以翻译为(钛合金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();
}
实现方式不同,开销也会有所差异,但只要动态类型未知,就会存在开销。