目录
- C语言
- 内存对齐现象
- 内存对齐规则
- 为什么存在内存对齐
- 如果struct or class中存在成员函数时的大小
- C++
- class存在虚函数时的大小
C语言
内存对齐现象
C语言中结构体的大小往往不是结构体中各种数据类型的加和,因为存在内存对齐;
struct S
{
double d;//8字节
char c;//1字节
int i;//4字节
};
int main() {
cout << sizeof(S) << endl;//输出S结构体的大小
return 0;
}
运行结果:
S结构体大小是16字节,显然不是结构体内部各类型加和的8+1+4=13字节;
内存对齐规则
- 结构体的第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量的偏移量要对齐到对齐数的整数倍的地址处。
对齐数: min(编译器默认的一个对齐数, 该成员类型大小 );
VS中默认的值为8 Linux中的默认值为4,这个默认对齐数可以用#pragma pack(num)预处理指令进行修改
-
结构体总大小为最大对齐数(每个成员变量确定选出的较小对齐数中最大的那个对齐数字)的整数倍
注意,最大对齐数不一定包含VS平台那个默认8字节对齐数,假设每个成员大小都小于8,那么最大对齐数就是那些成员中最大的类型值
下面分析S结构体为何大小为16字节:
为什么存在内存对齐
性能原因:
CPU的优化规则:
与CPU命中率有关,大致原则是这样的:对于n字节的元素(n=2,4,8,…),它的首地址能被n整除,才能获得最好的性能。
访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
所以内存对齐本质上是一种空间换时间的优化;(现代内存空间大大的多,更偏向注重时间效率了)
小tips: 根据内存对齐的特征,设计结构体时,同样个数与类型的变量,让较小的成员聚集在一起可以节省空间!
如果struct or class中存在成员函数时的大小
struct S //class S
{
char c;//1字节
int i;//4字节
double d;//8字节
void fun() { cout << 1 << endl; }
void fun2() { cout << 1 << endl; }
void fun3() { cout << 1 << endl; }
};
int main() {
cout << sizeof(S) << endl;
return 0;
}
运行结果:
可以看到大小还是16字节,这是因为不管有多少成员函数,每个函数的地址都是只有一份且存放在内存中的.text只读代码段的,这是一种省空间的做法;
试想,如果函数的地址和普通变量一样开空间放在每个实例化的对象中,那么同一个结构体或者类实例化n个对象,难道要多出n个指向相同函数的函数指针来调用吗,显然这样是浪费空间的,各个实例化的对象因为是同一种类型,调用的都是同一个函数,一份就够了;
C++中引入了class类的概念,类同样与struct结构体一样遵守内存对齐规则,但是因为类可能具有虚函数表指针,这个需要拉出来单独分析;
空类大小为1
-
空类同样可以被实例化,为了区分空类实例化出来的不同对象, 那么就要保证每个实例能放入内存中的唯一地址;
-
如果这个类的大小为0的话无法放入内存!所以C++会强制给空类一个缺省成员,大小为1字节(跟个char一样);
如果有该空类有了自定义的成员变量,那么成员变量将取代顶替掉这个缺省成员
C++
class存在虚函数时的大小
原因:
C++中的虚函数是为了通过继承实现多态的一种设计;
如果一个类中存在虚函数,那么这个类的成员隐式的多了一个指针,这个指针叫虚表指针,它指向了一张虚表(数组)的首地址,因此大小是4or8字节(取决于32位还是64位);这个虚表里面存的是各个对应的虚函数的地址(多态的底层原理,这里不深究);
每个实例化对象存一份虚表指针的意义是,多态通过基类的指针或者引用调用不同的派生类方法时候,是通过通过这个虚表指针达到的多态的效果
通过观察可以发现,无所增加多少虚函数,该类的类型大小都不会变,只是多了一个虚表指针,因为增加的虚函数也是存放在.text代码只读段,地址可以通过虚表指针找到,不用在每个实例对象中都存一份!