目录

  • 内存对齐
    • 内存对齐现象
    • 内存对齐规则
    • 为什么存在内存对齐
  • 空类大小为1
  • 存在成员函数or静态变量的类
    • 存在成员函数时的大小
    • 含有静态变量的类大小
  • 存在虚函数时的类
  • 存在继承关系的类
    • 普通派生类
    • 基类存在虚函数的派生类
    • 多重继承的派生类
    • 虚继承的派生类–虚基表!

内存对齐

内存对齐现象

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字节;

内存对齐规则

  1. 结构体的第一个成员在与结构体变量偏移量为0的地址处。
  2. 其他成员变量的偏移量要对齐到对齐数整数倍的地址处

对齐数: min(编译器默认的一个对齐数, 该成员类型大小 );

VS中默认的值为8 Linux中的默认值为4,这个默认对齐数可以用#pragma pack(num)预处理指令进行修改

  1. 结构体总大小最大对齐数每个成员变量确定选出的较小对齐数中最大的那个对齐数字)的整数倍

    注意,最大对齐数不一定包含VS平台那个默认8字节对齐数,假设每个成员大小都小于8,那么最大对齐数就是那些成员中最大的类型值

下面分析S结构体为何大小为16字节:

为什么存在内存对齐

性能原因:

CPU的优化规则

与CPU命中率有关,大致原则是这样的:对于n字节的元素(n=2,4,8,…),它的首地址能被n整除,才能获得最好的性能。

访问未对齐的内存,处理器需要作两次内存访问而对齐的内存访问仅需要一次访问

所以内存对齐本质上是一种空间换时间的优化;(现代内存空间大大的多,更偏向注重时间效率了)

小tips: 根据内存对齐的特征,设计结构体时,同样个数与类型的变量,让较小的成员聚集在一起可以节省空间!

空类大小为1

  1. 空类同样可以被实例化,为了区分空类实例化出来的不同对象, 那么就要保证每个实例能放入内存中的唯一地址;

  2. 如果这个类的大小为0的话无法放入内存!所以C++会强制给空类一个缺省成员,大小为1字节(跟个char一样);

如果有该空类有了自定义的成员变量,那么成员变量将取代顶替掉这个缺省成员

存在成员函数or静态变量的类

存在成员函数时的大小

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结构体一样遵守内存对齐规则,但是因为类可能具有虚函数表指针,这个需要拉出来单独分析;

含有静态变量的类大小

可以看到还是4字节,因为static变量整个类只有一份,和函数一样也是放在类外,在全局数据段;

存在虚函数时的类

原因:

C++中的虚函数是为了通过继承实现多态的一种设计;

如果一个类中存在虚函数,那么这个类的成员最开始的内存位置隐式的多了一个指针,这个指针叫虚表指针,它指向了一张虚表(函数数组)的首地址,因此大小是4or8字节(取决于32位还是64位);这个虚表里面存的是各个对应的虚函数的地址(多态的底层原理,这里不深究);

每个实例化对象存一份虚表指针的意义是,多态通过基类的指针或者引用调用不同的派生类方法时候,是通过通过这个虚表指针达到的多态的效果

通过观察可以发现,无所增加多少虚函数,该类的类型大小都不会变,只是多了一个虚表指针,因为增加的虚函数也是存放在.text代码只读段,地址可以通过虚表指针找到,不用在每个实例对象中都存一份,在这个类中有一份及OK了!

存在继承关系的类

普通派生类

派生类会存放基类中非静态数据成员(A中的a)的副本

基类存在虚函数的派生类

显然是多态场景:

那么B类的前四or八个字节放的是虚表指针,等价于存在虚函数时的类; 之后算上A::a的副本和B::b,一共就是12个字节大小!

多重继承的派生类

C类大小需要算上其基类B和C中非静态数据成员长度以及是否需要存在虚表指针的情况(看基类中是否存在虚函数)加和

虚继承的派生类–虚基表!

B中不止存放了A中a变量的副本还有B的b,还存在了一个虚基表的指针

(解决菱形继承用的,与虚表是俩概念)

关于虚基表的问题可以参照我的这篇博客