c++ 类的大小计算
前言
一个空类的大小是多少? 0个字节还是一个字节?本节分析一个类的大小是多少, 怎么计算。 本节没有涉及类中有静态成员的情况, 后面会单独对静态成员做一个分析, 现在只要知道静态成员变量不在类的内存布局中。
环境
实验环境只要能编译运行的都可以, 只有86的环境指针的大小为4字节, 64环境指针大小是8字节。
一个空类的大小
一个空类也是有大小的, 其大小为1个字节。我们可以很简单的完成这个实验
class A{}; A a;
cout << sizeof(a); // 1
但是需要明白的是为什么空类的大小是1, 而不是0, 这才是目的。
我们先来考虑如果大小是0. 如果空类实例化的对象的大小为0, 那么我们不管实例化多少个这样的对象都不会占内存, 反过来就是说空类的实例化并没有在内存中实际分配地址哦, 那是不是我们无法用指针指向这样的实例化。 指针无法指向空类实例化就有问题, 所以大小为0就是假命题。
实际上真正的原因是 : 一个空类在编译时会被编译器插入一个char字节的变量, 这只是使得空类在实例化时不同的实例化对象在内存中有独一无二的地址。 反过来可以说char用来标识类的不同实例化。
无虚指针的类的大小
一个类有了虚函数也就多了一个虚表, 我们先来分析没有虚函数时类的大小。
class A
{
int i, j;
};
class B
{
char ch;
int i;
};
class C
{
char ch1, ch2;
int i;
};
class D
{
char ch1;
int i;
char ch2;
};
类A的大小为8, 同样类B, C的大小也为8, 而类D的大小却为12.
在解释上面的例子之前要先明白什么时内存对齐。
1. 内存对齐
现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
2. 内存对齐的原因
- 平台原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。 比如,有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐。
- 性能原因:内存对齐可以提高存取效率。 比如,有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。
简单的来讲 : 在类和结构体中, 他们的实际占内存大小是最大成员变量的大小的n倍。
明白这个之后就明白上面例子的原因了。 类A, B, C, D中最大成员变量为int类型, 4字节, 则类的大小肯定是4的倍数。
下面这是类B的内存分布, char只占一个字节, 但是为了与int对齐, 所以char后面3个字节都填充为0, 之后再存放int类型成员, 所以最终为8个字节。
同样类C也是一样,两个 char只占两个字节, 但是为了与int对齐, 所以char后面2个字节都填充为0, 之后再存放int类型成员, 所以最终为8个字节。
那么类D的内存分布就是下面这样。
有vptr类的大小
有vptr的类的大小如同上面的分析一样, 只是多个了一个在类前端的vptr
虚指针而已。
class A
{
virtual void fun() {}
int i, j;
}; // 16字节
class B
{
virtual void fun() {}
char ch;
int i;
}; // 16字节
class C
{
virtual void fun() {}
char ch1, ch2;
int i;
}; // 16字节
class D
{
virtual void fun() {}
char ch1;
int i;
char ch2;
}; // 24字节
继承时子类的大小
1. 首先是基类如果是空类
class A {};
class B : A {}; // 1字节
类B之所以是一字节, 因为B还是空类啊, 就只能是一字节,而不是2字节。这叫做EBO空白基类最优化
,子类优化掉基类所占的1 byte。EBO并不是c++标准所规定必须的,但是大部分编译器都会这么做。
而EBO
优化技术节省了对象不必要的空间,提高了运行效率,因此成为某些强大技术的基石, 比如在STL中5种迭代器。
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};
空白基类最优化无法被施加于多重继承上只适合单一继承, 还要注意:在继承链中,对空基类的优化不能传递到后代。
2. 无虚函数基类的子类
class A
{
char i;
};
class B : A
{
char ch;
int i;
}; // 8字节
同样内存对齐的原则来解释。
3. 有虚指针的基类
class A
{
virtual void fun() {}
char i;
}; // 16字节
class B : A
{
char ch;
int i;
}; // 24字节
类B之所以是24字节, 因为类B的成员刚好是8字节, 刚好对齐。
4. 有虚指针的子类
class A
{
char i;
}; // 1字节
class B : A
{
virtual void fun() {}
char ch;
int i;
}; // 16字节
这里要解释一下B为什么是16字节,而不是24字节。
在类的内存分布中我们分析到, 虚指针是在类的前端, 而成员变量都在虚指针之后存放,所以B真正的内存分布是这样的
继承而来的i在vptr
下, 挨着ch变量, 所以总共为16字节。
5. 基类子类都有虚函数
实际上这种情况与基类有虚函数是一样。只要记住, 子类的虚指针个数为继承的个数 - 1 就行。
所以实际上B只有一个虚指针。
class A
{
virtual void fun() {}
char i;
}; // 16字节
class B : A
{
virtual void fun() {}
char ch;
int i;
}; // 24字节
总结
本节只是分析了怎样来计算一个类的实际的大小, 主要的要明白一个空类的大小和原因, 另一个是内存对齐。当然对扩展的EBO
能有所了解也是一个收获。