【CPlusPlusThings笔记】内存对齐(类大小计算)

内存对齐


什么是内存对齐?

假设我们同时声明两个变量:
char a;
short b;
用&(取地址符号)观察变量a,b的地址的话,我们会发现(以16位CPU为例):如果a的地址是0x0000,那么b的地址将会是0x0002或者是0x0004。

那么就出现这样一个问题:0x0001这个地址没有被使用,那它干什么去了? 答案就是它确实没被使用。 因为CPU每次都是从以2字节(16位CPU)或是4字节(32位CPU)的整数倍的内存地址中读进数据的。如果变量b的地址是0x0001的话,那么CPU就需要先从0x0000中读取一个short,取它的高8位放入b的低8位,然后再从0x0002中读取下一个short,取它的低8位放入b的高8位中,这样的话,为了获得b的值,CPU需要进行了两次读操作。

但是如果b的地址为0x0002,那么CPU只需一次读操作就可以获得b的值了。所以编译器为了优化代码,往往会根据变量的大小,将其指定到合适的位置,即称为内存对齐(对变量b做内存对齐,a、b之间的内存被浪费,a并未多占内存)。


结构体(类)内存对齐规则

结构体所占用的内存与其成员在结构体中的声明顺序有关,其成员的内存对齐规则如下(在没有#pragam pack宏的情况下):

  • 每个成员分别按自己的对齐字节数PPB(指定的对齐字节数,32位机默认为4)两个字节数最小的那个对齐,这样可以最小化长度。如在32bit的机器上,int的大小为4,因此int存储的位置都是4的整数倍的位置开始存储。

  • 复杂类型(如结构体)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,如结构体数组的时候,可以最小化长度。

  • 结构体对齐后的长度必须是成员中最大的对齐参数(PPB)的整数倍,这样在处理数组时可以保证每一项都边界对齐。

  • 结构体作为数据成员的对齐规则:在一个struct中包含另一个struct,内部struct应该以它的最大数据成员大小的整数倍开始存储。如 struct A 中包含 struct B, struct B 中包含数据成员 char, int, double,则 struct B 应该以sizeof(double)=8的整数倍为起始地址。


实例演示

struct A
{
	char a;   //内存位置: [0]
	double b; // 内存位置: [8]...[15]
	int c;   // 内存位置: [16]...[19]  ----  规则1
}; // 内存大小:sizeof(A) = (1+7) + 8 + (4+4) = 24, 补齐[20]...[23]  ----  规则3


struct B
{
	int a,    // 内存位置: [0]...[3]
	A b,       // 内存位置: [8]...[31]  ----  规则2
	char c,   // 内存位置: [32]
}; // 内存大小:sizeof(B) = (4+4) + 24 + (1+7) = 40, 补齐[33]...[39]

注释:(1+7)表示该数据成员大小为1,补齐7位;(4+4)同理。(1+7)的原因是:根据规则2,结构体A的最长的成员为double,所以默认对齐大小为8字节。


类大小计算


规则

首先来个总结,然后下面给出实际例子,实战!(字节即byte)

  • 空类的大小为1字节

  • 一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间。

  • 对于包含虚函数的类,不管有多少个虚函数,只有一个虚指针,vptr的大小。

  • 普通继承,派生类继承了所有基类的函数与成员,要按照字节对齐来计算大小。

  • 虚函数继承,不管是单继承还是多继承,都是继承了基类的vptr。(32位操作系统4字节,64位操作系统 8字节)!

  • 虚继承,继承基类的vptr。



案例

注意:gcc中默认#pragma pack(4),即默认4字节对齐。可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16来改变这一系数。


原则1

class A{}; // 空类的大小为1字节

原则2

class A
{
	public:
		char b;
		static int c;
		static int d;
		static int f;
}; // 内存大小:1字节,成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间

class A
{
	public:
		virtual void fun() {};
		static int c;
		static int d;
		static int f;
}; // 8字节,因为vptr;虚函数本身不占用类对象的存储空间。

class A
{
	public:
		char b;
		virtual void fun() {};
		static int c;
		static int d;
		static int f;
}; // 内存大小:16字节,由于vptr占8个字节,所以根据‘结构体内存对齐规则2,默认对齐大小为8字节,所以对char进行(1+7)。’

静态数据成员被编译器放在程序的一个global data members中,它是类的一个数据成员,但不影响类的大小。
不管这个类产生了多少个实例,还是派生了多少新的类,静态数据成员只有一个实例。静态数据成员,一旦被声明,就已经存在。


原则3

class A{
    virtual void fun();
    virtual void fun1();
    virtual void fun2();
    virtual void fun3();
}; //内存大小:8字节

对于包含虚函数的类,不管有多少个虚函数,只有一个虚指针,vptr的大小。


原则4和5

class A
{
    public:
        char a;
        int b;
}; //内存大小:(1+3)+ 4 = 8

/**
 * @brief 此时B按照顺序:
 * char a
 * int b
 * short a
 * long b
 * 根据字节对齐4+4+8+8=24
 * (Note:by nuo):上面是在64位下结果。32位下的实验结果:根据字节对齐4+4+(2+2)
 * 或编译器优化
 * char a
 * short a
 * int b
 * long b
 * 根据字节对齐2+2+4+8=16
 * (Note:by nuo):上面是在64位下结果。32位下的实验结果:根据字节对齐(1+1)+2+4+4=12
 */
class B:A
{
    public:
        short a;
        long b;
};
/**
* 把A的成员拆开看,char为1,int为4,所以是(1+3)+4+(1+3)=12
*/
class C
{
    A a;
    char c;
};

class A1
{
    virtual void fun(){}
};
class C1:public A
{
};
  1. 普通单继承,继承就是基类+派生类自身的大小(注意字节对齐)。
    注意:类的数据成员按其声明顺序加入内存,无访问权限无关,只看声明顺序。

  2. 虚单继承,派生类继承基类vptr


原则6

class A
{
    virtual void fun() {}
};

class B
{
    virtual void fun2() {}
};
class C : virtual public  A, virtual public B
{
    public:
        virtual void fun3() {}
};
/**
* @brief 8 8 16  派生类虚继承多个虚函数,会继承所有虚函数的vptr
*/
cout<<sizeof(A)<<" "<<sizeof(B)<<" "<<sizeof(C);

类型大小区别

32位编译器与64位编译器中类型大小区别

char char* short int int unsigned int float double long unsigned long long long
32位 1 4 2 4 4 4 8 4 4 8
64位 1 8 2 4 4 4 8 8 8 8

char*(即指针变量,只与地址寻址范围有关): 4个字节(32位的寻址空间是2^32, 即32个bit,也就是4个字节。同理64位编译器)

posted @ 2022-04-29 11:41  围城chen  阅读(71)  评论(0编辑  收藏  举报