一.大端模式和小端模式
计算机系统中,每个地址单元对应一个字节(8bit),一种数据类型可能占用多个字节,如何安排这种数据类型中的每个字节,哪个字节在低地址,哪个在高地址,以及每个字节中比特序在内存中的位置,这就涉及到大小端模式。一般情况下,字节序和比特序的排列规则是一致的。我们在书写和计算中数据数据分为高位和低位,高位在左边,低位在右边,例如0x6401中,0x64为高位,0x01为低位。
大端模式:低位(字节/比特)放在高地址中,高位(字节/比特)放在低地址中。
小端模式:低位(字节/比特)放在低地址中,高位(字节/比特)放在高地址中。
示例:以整数0x0a0b0c0d为例来说明该数据在大端和小端系统中内存的位置
大端模式系统:
byte addr 0 1 2 3
bit offset 01234567 01234567 01234567 01234567
binary 00001010 00001011 00001100 00001101
hex 0a 0b 0c 0d
小端模式系统:
byte addr 3 2 1 0
bit offset 76543210 76543210 76543210 76543210
binary 00001010 00001011 00001100 00001101
hex 0a 0b 0c 0d
二、检测大端模式和小端模式
C语言联合体中各个成员共享同一段内存空间,一个联合体变量的内存长度等于各成员中内存最长的长度。而且联合体和结构体的成员变量的内存都是按照成员变量顺序从低地址到高地址分布。可以利用联合体的这些特性来检测系统使大端模式还是小端模式。
检测代码如下:
BOOL IsBigEndian()
{
union NUM
{
int a;
char b;
}num;
num.a = 0x1234;
if( num.b == 0x12 )
{
return TRUE;
}
return FALSE;
}
三、常见的大小端
一般操作系统都是小端模式;而通讯协议是大端模式;java和平台无关,默认是大端模式。常见的cpu的大小端:
大端:PowerPC、IBM、Sun
小端:x86
ARM既可以工作在大端模式,也可以工作在小端模式
四、结构体内成员的内存分布
结构体内成员的内存分布即和编译器相关,也和硬件cpu相关。编译器有时为了优化CPU访问内存的效率,在生成结构体内成员起始地址时遵循特定的规则,即结构体成员的“内存对齐”。硬件CPU会影响结构体成员的是采用大端模式还是小端模式来进行内存分配。
五、结构体中普通成员变量的内存分布规则
1、结构体(联合体)的成员所占内存地址依次增高,第一个成员位于低地址处,最后一个成员位于高地址处。但是结构体成员的内存并不是连续的,为了提高CPU访问内存的效率,编译器会对成员做“对齐”处理。
2、通常编译器可以设置一个对齐参数n,结构体中每个成员实际对齐参数N根据N=min(sizeof(成员类型),n)来得到。结构体所有成员的对齐参数N的最大值称为结构体的对齐参数。
3、结构体成员的内存偏移地址x,满足条件x%N=0。结构体A中包含结构体B成员变量b,结构体B的对齐参数为NB,则b在A中的内存偏移地址y,满足条件y%NB=0。
4、整个结构体的长度必须是结构体对齐参数的最小整数倍。
注:编译器对齐参数可以通过指令控制,例如#param pack(2)两字节对齐。vs2010IDE还可以通过"项目属性"->"C/C++"->"代码生成"->“结构成员对齐”来设置。编译器默认对齐参数为8个字节。
示例:
struct A
{
char c; //1byte
double d; //8byte
short s; //2byte
int i; //4byte
};
int main(int argc, char*argv[])
{
A strua;
printf("len:%d\n",sizeof(A));
printf("%d,%d,%d,%d",&strua.c,&strua.d,&strua.s,&strua.i);
return 0;
}
输出结果为:
len:24
1506156,1506164,1506172,1506176
六、结构体中特殊成员变量的内存分布规则
1、结构体成员变量为数组时,是将数组的每个元素当一个成员来分配,并不是将整个数组当成一个成员来对待。其他成员的分配规则按照上述规则。
2、当结构体成员是位段时,存储是按其类型分配空间的,如int型的位段就分配4个字节的存储单元。相邻的同类型的两个位段,如果该类型的长度够用,就将两位段连续存放,共用存储单元,如果不够用,就另起一个该类型长度的存储空间。相邻的不同类型的两个位段,分别为这两个位段分配它们所属类型长度的存储空间。其他成员的分配规则按照上述规则。
示例1:
struct bit
{
int a:3;
int b:2;
int c:3;
};
int main(int argc,char* argv[])
{
bit s;
char *c = (char*)&s;
*c = 0x98;
cout<<s.a<<endl<<s.b<<endl<<s.c<<endl;
return 0;
}
输出结果: 0 -1 -4
分析:
hex 0x98
bin 100 11 000
c b a
内存地址 高 --> 低
cpu为小端模式,高位比特存储在高地址中,低位比特存储在低地址中。而且计算机中存储的是数的补码形式。
示例2:
struct bit
{
char a:5;
char b:4;
char c:7;
};
int main(int argc,char* argv[])
{
bit s;
int *c = (int*)&s;
*c = 0x99E00000;
cout<<sizeof(bit)<<endl<<(int)s.a<<endl<<(int)s.b<<endl<<(int)s.c<<endl;
return 0;
}
输出结果:3 0 0 -32
hex 0x99 0xE0 0x00 0x00
bin 10011001 11100000 00000000 00000000
c b a
内存地址:高 <-------------------- 低
七、结构体基础数据类型成员的默认对齐方式
在编译 32 位 x86 时:
一个char(一个字节)将按 1 字节对齐。
short(两个字节)将按 2 字节对齐。
int (四个字节)将是 4 字节对齐的。
long (四个字节)将是 4 字节对齐的。
float(四个字节)将按 4 字节对齐。
double(八字节)在 Windows 上将是 8 字节对齐,在 Linux 上将是 4 字节对齐(使用-malign-double编译时选项的 8 字节)。
long long(八个字节)在 Windows 上将是 8 字节对齐,在 Linux 上将是 4 字节对齐(带有-malign-double编译时选项的 8 字节)。
long double(C++Builder 和 DMC 为 10 个字节,Visual C++ 为 8 个字节,GCC 为 12 个字节)将与 C++Builder 8 字节对齐,与 DMC 对齐 2 字节,与 Visual 对齐 8 字节C++,与 GCC 4 字节对齐。
任何指针(四个字节)都将是 4 字节对齐的。(例如:字符*、整数*)
与 32 位系统相比, LP64 64 位系统在对齐方面唯一显着的差异是:
long (八个字节)将是 8 字节对齐的。
双精度数(八字节)将按 8 字节对齐。
long long(八个字节)将是 8 字节对齐的。
long double(对于 Visual C++ 为 8 个字节,对于 GCC 为 16 个字节)将与 Visual C++ 8 字节对齐,与 GCC 16 字节对齐。
任何指针(八个字节)都将是 8 字节对齐的。
八、通信协议中的数据采用结构体进行定义
网络通信或者音视频协议中往往会定义一些数据结构,数据结构中包含多个字段,在C语言中可以通过结构体来定义这些数据。例如下图所示,为H.265视频流在RTP中的负载头部NALHeader,可以看到LayerId在书写和计算中为连续的比特序列,但是在内存中该字段却是不连续的比特序列。
在x86的cpu架构下,数据存储为小端模式,低位字节和比特对应低地址,高位对应高地址。首先NALHeader的第一个字节(高位)存储在低地址中,第二个字节(低位)存储在高地址中。而每个字节中高位比特序存储在低地址中,低位比特序存储在高地址中,如下图所示:
可以看到LayerId在书写和计算中为连续的数据位,但是存储位置却是不连续的,该结构在C语言的结构体中定义为(结构体中成员变量的存储位置为按照成员的先后顺序由低地址到高地址):
struct H265NALHeader
{
unsigned short layerid1 : 1;
unsigned short naltype : 6;
unsigned short f_bit : 1;
unsigned short tid : 3;
unsigned short layerid2 : 5;
};