对于具体类型 T
,sizeof
意味着两件事:
完整对象占用的字节不重叠:一个是另一个的主题并包含在其中,或者它们没有共同的字节。
您可以覆盖一个完整的对象(使用memset
),然后使用placement 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;
};
由于对齐intchar
的原因,即使存在未使用的字节,其大小也必须大于其大小:成员是一个完整的对象并占据其所有字节,并且在该对象中是允许的。ic
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>
它没有 type 的成员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
{};
与普通继承和成员关系不同,虚拟继承没有定义严格的、直接的所有权关系,而是共享的、间接的所有权,就像调用虚函数引入了间接关系一样。虚拟继承创建了两种类布局:基类子对象和完整对象布局。
当一个类被实例化时,编译器将使用为完整对象定义的布局,可以像 GCC 那样使用 vptr,Titanium ABI 规定:
struct Derived2 {
void *__vptr;
};
vptr 指向一个完整的vtable,包含所有的运行时信息,但是C++语言不认为这样的类是多态类,所以dynamic_cast
/typeid
不能用来判断动态类型。
AFAIK,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
}
可以翻译成(Titanium ABI-style)
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();
}
开销取决于实现,但只要动态类型未知,就会出现开销。