【C/C++开发】内存对齐(内存中的数据对齐)、大端模式及小端模式
数据对齐,是指数据所在的内存地址必须是该数据长度的整数倍。DWORD数据的内存起始地址能被4除尽,WORD数据的内存起始地址能被2除尽。X86 CPU能直接访问对齐的数据,当它试图访问一个未对齐的数据时,会在内部进行一系列的调整。这些调整对于程序员来说是透明的,但是会降低运行速度,所以编译器在编译程序时会尽量保证数据对齐。
不同的编译器内存对齐的方式不同。
一个小例子:在32位的机器上,数据是以4字节为对齐单位,这两个类的输出结果为什么不同?(VS2008)
#include <iostream> using namespace std; class B { private: bool m_bTemp; int m_nTemp; bool m_bTemp2; }; class C { private: int m_nTemp; bool m_bTemp; bool m_bTemp2; }; int _tmain(int argc, _TCHAR* argv[]) { cout<<sizeof(B)<<endl; cout<<sizeof(C)<<endl; system("pause"); return 0; }
答案是:3*4=12,2*4=8
分析:在访问内存时,如果地址按4字节对齐,则访问的效率会高很多。
考虑到性能方面,编译器会对结构进行对齐处理,考虑下面的结构:
struct aStruct
{
char cValue;
int iValue;
};
直观地讲,这个结构的尺寸是sizeof(char)+sizeof(int)=6,但是在实际编译下, 这个结构尺寸默认是8,因为第二个域iValue会被对齐到第4个字节。
注意:字节对齐是编译时决定的,一旦决定则不会再改变,因此即使有对齐的因素存在,也不会出现一个结构在运行时尺寸发生变化的情况。
在本题中:第一种类的数据对齐是下面的情况:
bool ---- ---- ----
------- int ---------
bool ----- ---- ----
第二种类的数据对齐是下面的情况:
------- int ----------
bool bool ---------
所以类的大小分别3*4和2*4
一般在VC++中加上#pragma pack(n)设置内存对齐。
我们可以利用#pragma pack()来改变编译器的默认对齐方式。
#pragma pack(n) //编译器将按照n字节对齐
#pragma pack() //编译器将取消自定义字节对齐方式
在#pragma pack(n)和#pragma pack()之间的代码按n字节对齐。
但是成员对齐有一个重要的条件,即每个成员按照自己的对齐方式对齐;也就是说虽然指定了按n字节对齐,但并不是所有的成员都以n字节对齐。
对齐的规则是:每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是n字节)中较小的一个对齐,即min(n,sizeof(item)),并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。
#pragma pack(8) struct TestStruct4 { char a; long b; }; struct TestStruct5 { char c; TestStruct4 d; long long e; }; int _tmain(int argc, _TCHAR* argv[]) { cout<<sizeof(TestStruct4)<<endl; cout<<sizeof(TestStruct5)<<endl; system("pause"); return 0; }
运行结果为:8,24
分析:
TestStruct4 中,成员a是1字节,默认按照1字节对齐,指定对齐参数是8,这两个值中取1,a按1字节对齐;
成员b是4字节,默认是按4字节对齐,这时就按4字节对齐,所以sizeof(TestStruct4)应该是8.
TestStruct5 中,c和TestStruct4中的a一样,按1字节对齐;而d是个结构,它是8字节,对于结构来说,它的默认对齐方式就是其所有成员使用的对齐参数中最大的一个,TestStruct4就是4,所以成员d就按照4字节对齐。成员e是8字节,它是默认的8字节对齐,和指定的一样,所以它对齐到8自己的边界上,这时,已经使用了12字节了,所以又添加了4字节的空间,从第16字节开始放置成员e。这时长度为24,已经可以被8整除(成员e按8字节对齐)。这样一共使用了24字节。
内存布局图如下:
TestStruct4 的内存布局:
a b
1*** 1111
TestStruct5 的内存布局:
c d.a d.b e
1*** 1*** 1111 **** 11111111
注意3点:
(1)每个成员按照自己的方式对齐,并能最小化长度
(2)复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度。
(3)对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐。
补充:对于数组,比如说char a[3],它的对齐方式和分别写3个char是一样的,也就是说它还是按1字节对齐;如果写成typedef char Arrary3[3],Arrary3这种类型的对齐方式还是按1字节对齐,而不是按它的长度。
但是不论类型是什么,对齐的边界一定是1、2、4、8、16、32、64......中的一个。
下面来说下大端模式和小端模式
大端模式:认为第一个字节是最高位字节,也就说按照从低地址到高地址的顺序存放数据的高位字节到低位字节。
小端模式:认为第一个字节是最低位字节,也就是说按照从低地址到高地址的顺序存放数据的低位字节到高位字节。
假设从内存地址0x0000开始有以下数据:
内存地址:0x0000 0x0001 0x0002 0x0003
对应数据:0x12 0x34 0x56 0x78
如果我们去读取一个地址为0x0000的4字节变量
若字节序位为小端模式,读出为:0x78563412
若字节序位为大端模式,读出为:0x12345678
一般来说:X86系列的CPU都是小端字节序,powerPC通常是大端字节序。
int _tmain(int argc, _TCHAR* argv[]) { char *sz = "0123456789"; int *p = (int *)sz; cout<<hex<<*++p<<endl; }
运行结果为:37363534
分析:这里是小端字节序
地址从0x0000开始,那么sz在内存中的存储为:
内存地址: 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09
对应的值: 0 1 2 3 4 5 6 7 8 9
对应的值: 48 49 50 51 52 53 54 55 56 57
对应的16进制:0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39
sz ++p
所以读取为:0x37363534
参考资料:《程序员面试宝典》第三版 电子工业出版社 P50-P45
《C语言深度解剖》 北京航空航天大学出版社 P39-P40,P71-P75