对于具体类型,T、sizeof 意味着两件事:
-
表示一个完整对象a,类型为T,在[(char*)&a,(char*)&a + sizeof(T))中只占用sizeof(T)字节;
李>
T 的数组 存储第一个对象sizeof(T) 之后的第二个对象。
完整对象占用的字节不重叠:要么是另一个的主体并包含在其中,要么它们没有共同的字节。
您可以覆盖一个完整的对象(使用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> 没有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;
}
outputs:
空有尾随空格: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();
}
开销取决于实现,但只要动态类型未知,就会出现开销。