参考:
1. (Boolan) C++ 类型大小和内存分布(虚函数指针、虚表、内存对齐问题)
声明:本文是在Win32编译器上进行的测试!!!
1.常用数据的大小
数据类型 | 大小(Byte) |
---|---|
char | 1 |
short | 2 |
int | 4 |
long | 4 |
float | 4 |
double | 8 |
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就是你要指定的“对齐系数”。