类的存储结构

参考:
1. (Boolan) C++ 类型大小和内存分布(虚函数指针、虚表、内存对齐问题)

声明:本文是在Win32编译器上进行的测试!!!

1.常用数据的大小

数据类型大小(Byte)
char1
short2
int4
long4
float4
double8

2.设置字节对齐提高存取速度

通过合理的内存对齐可以提高访问效率。为使CPU能够对数据进行快速访问,数据的起始地址应具有“对齐”特性。比如4字节数据的起始地址应位于4字节边界上,即起始地址能够被4整除。此时可通过位操作快速取到相应地址处的值,否则的话用的是加法运算。
为了提高数据的存取速度,现代计算机都使用了 Cache技术。Cache可以看成一些可以用非常快的速度进行访问的临时内存。但是Cache的容量一般不大,比如一级Cache只有几K到几十K,二级Cache也就几百K到几M。这点容量相对于数G的内存来说,就显得微不足道了。
因为CPU对内存的直接访间是非常慢的,所以一般硬件会将经常使用到的内容存放到Cache里面。而Cache是通过Cache Line来组织的,每一条 Cache Line包含16字节、32字节或64字节等。比如某计算机的Cache Line是32字节的,那么每段Cache Line总是会包含32个字节对齐的一段内存。
现假设有一4字节的整型变量,如果它的地址不是4字节对齐的,那么就有可能在访回它的时候需要使用两条Cache Line,这增加了总线通讯量,而且增加了对Cache的使用量。并且会造成欲使用的数据出现在Cache里面的可能性减小,不能提前放置到Cache里的直接后果就是还需另花费时间将数据从内存加载到Cache中,这就极大地降低了程序的运行速度。

3.类(或结构体)对齐准则

准则1:成员变量相对于类的偏移地址必须为该变量所占用字节的整数倍。
准则2:类的大小为拥有最大字节成员变量的整数倍。

例1:

class B
{
    char c;       //成员c的偏移地址为0,0为char型变量的整数倍
    short d;      //未进行对齐之前,成员d的偏移地址为0+1 = 1,因1不是short型变量的整数倍,需调整。,调整后偏移地址为2
    int b;        //未进行对齐之前,成员b的偏移地址为2+2 = 4,因4是int型变量的整数倍,故无需调整
    double a;     //未进行对齐之前,成员a的偏移地址为4+4 = 8,因8是double型变量的整数倍,故无需调整
};                //类中最大字节的成员变量占用8个字节,因8+8 = 16恰为8的整数倍,故sizeof(B) = 16

例2:

class B
{
    double a;     //成员a的偏移地址为0,0为double型变量的整数倍
    char c;       //未进行对齐之前,成员c的偏移地址为0+8 = 8,因8是char型变量的整数倍,故无需调整
    short d;      //未进行对齐之前,成员d的偏移地址为8+1 = 9,因9不是short型变量的整数倍,需调整,调整后偏移地址为10
    int b;        //未进行对齐之前,成员b的偏移地址为10+2 = 12,因12是int型变量的整数倍,故无需调整
};                //类中最大字节的成员变量占用8个字节,因12+4 = 16恰为8的整数倍,故sizeof(B) = 16

例3:

class B
{
    char c;       //成员c的偏移地址为0,0为char型变量的整数倍
    double a;     //未进行对齐之前,成员a的偏移地址为0+1 = 1,因1不是double型变量的整数倍,需调整,调整后偏移地址为8
    short d;      //未进行对齐之前,成员d的偏移地址为8+8 = 16,因16是short型变量的整数倍,故无需调整
    int b;        //未进行对齐之前,成员b的偏移地址为16+2 = 18,因18不是int型变量的整数倍,需调整,调整后偏移地址为20
};                //类中最大字节的成员变量占用8个字节,因20+4 = 24恰为8的整数倍,故sizeof(B) = 24

4.拥有静态成员变量的类

静态成员变量存储在全局数据区,不占用类的字节数。

举例:

class B           //sizeof(B) = 24
{
    char c;      
    double a;     
    short d;      
    int b;        
    static int m; //静态成员变量存储在全局数据区,不占用类的字节数
};                

5.含有子类的类

class A          //sizeof(B) = 24
{
	int c;
	double d;
	char e;
};
class B         //sizeof(B) = 40
{
	A a;
	char f;		//这里从24开始存放
	int g;
	char h;
};				//类B依照A的8字节进行对齐
class B        //sizeof(B) = 40
{
	char f;
	A a;	   //这里从8开始存放
	char h;	   //这里从32开始存放
};

6.含有继承关系的类

现假设父类的对齐系数为4,子类的对齐系数为8。当父类被继承到子类中的时候,父类依旧按照4字节进行对齐。对齐之后父类将作为一个整体存在于子类中,并按照子类的对齐系数8进行继续的对齐!
例1:

class A             //sizeof(A) = 6
{
    short i;
    short k;
    char n;
};                  //类A按照2字节对齐

class B : public A  //sizeof(B) = 16
{
    char c;      //未进行对齐之前,成员c的偏移地址为0+6 = 6,因6是char型变量的整数倍,需无需调整
    double a;    //未进行对齐之前,成员a的偏移地址为6+1 = 7,因7不是double型变量的整数倍,需调整,调整后偏移地址为8
};               //继承到B中的A将按照8字节对齐

例2:

class A             //sizeof(A) = 8
{
    int j;
    char n;
};                  //类A按照4字节对齐

class B : public A  //sizeof(B) = 24
{
    char c;      //未进行对齐之前,成员c的偏移地址为0+8 = 8,因8是char型变量的整数倍,需无需调整
    double a;    //未进行对齐之前,成员a的偏移地址为8+1 = 9,因9不是double型变量的整数倍,需调整,调整后偏移地址为16
};               //继承到B中的A将按照8字节对齐

7.包含普通成员函数的类

成员函数存储在代码段,故不占用类的字节数。在编译时,编译器将成员函数提取出来发在代码段,同时做一些修改:
①.改函数名,即在函数名前方加上类名。构造函数更名为initialize。
②.添加一个函数形参this。调用a.getI()就相当于调用Test_getI(&a),这样就能区分到底是哪个对象去调用类的成员函数了。
③.静态成员函数不包含形参this。
编译器预处理过程

举例:

class B           //sizeof(B) = 24
{
    char c;      
    double a;    
    short d;     
    int b;        
    void fun();   //成员函数fun存储在代码段,不占用类的字节数
};              

8.包含虚成员函数的类

c++为了实现多态,引入了虚函数的概念。当类中含有虚函数的声明时,编译器会在类中生成一个对应的虚函数表,虚函数表是一个存储虚函数的函数指针的数据结构,并交由编译器维护。
当存在虚函数时,每个对象都有一个指向虚函数表的指针(VPTR,虚函数表指针),虚函数表指针的大小为4个字节,处在类存储空间的第一位置处,即前四个字节(64位编译器则为8字节)。
然,虚函数表指针的实际大小是依基类中的对齐系数来定的。

例1:

class B           //sizeof(B) = 4
{
    virtual void f();  //类B本身就是基类,其虚函数表指针大小等于对齐系数4
};

例2:

class B           //sizeof(B) = 32
{
    int b;
    double a;
    char c;
    short d;
    virtual void f();  //类B本身就是基类,其虚函数表指针大小等于对齐系数8
};

例3:

class A                //sizeof(A) = 4
{
    virtual void f();
};
class B : public A     //sizeof(B) = 24
{
    int b;             //未进行对齐之前,成员b的偏移地址为0+4 = 4,因4是int型变量的整数倍,故无需调整
    double a;          //未进行对齐之前,成员a的偏移地址为4+4 = 8,因8是double型变量的整数倍,故无需调整
    char c;
    short d;
    virtual void f();  //类B继承自A,类B的虚函数表指针的大小将依基类A的对齐系数来定,即4字节
};

例4:

class A                //sizeof(A) = 8
{
    char n;
    virtual void f();
};
class B : public A     //sizeof(B) = 32
{
    char o;            //未进行对齐之前,成员o的偏移地址为0+8 = 8,因8是char型变量的整数倍,故无需调整
    double a;          //未进行对齐之前,成员a的偏移地址为8+1 = 9,因9不是double型变量的整数倍,需调整,调整后偏移地址为16
    char c;            //未进行对齐之前,成员c的偏移地址为16+8 = 24,因24是char型变量的整数倍,需无需调整
    short d;
    virtual void f();  //类B继承自A,类B的虚函数表指针的大小将依基类A的对齐系数来定,即4字节
};

例5:

class A                //sizeof(A) = 4
{
    virtual void f1();
};
class C                //sizeof(C) = 4
{
    virtual void f2();
};
class B : public A, public C     //sizeof(B) = 8
{
    virtual void f3();  //类B同时继承自A和C,故将同时得到两张虚函数表,也即两个虚函数表指针,所以大小为8。
                        //此时类B中虽有自己的虚函数,但也会放在其中一张虚函数表当中,不会再增加对象大小。
};

9.空类和空结构体的大小

C++:

一个类能够实例化,编译器就需给它分配内存空间,来指示类实例的地址。这里编译器默认分配了一个字节(如:char,编译器相关),以便标记可能初始化的类实例,同时使空类占用的空间也最少(即1字节)。

如果空类大小为0,此时我们为该类声明一个对象数组,那么数组中的每个对象都拥有相同的地址,这显然是违背标准的。

C:

C语言下空结构体大小为0(当然这是编译器相关的)。

10.设置字节对齐的方法

方式一:
由编译器自信决定。对于我这次测试的平台来说,这个编译器的规则为,采用成员中最长的变量的长度作为对齐系数。

方式二:
程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值