内存的存储细节 - ShenYj/ShenYj.github.io GitHub Wiki
简单来说,就是指的超过一个字节的数据类型在内存中存储的顺序
其实大部分人在实际的开发中都很少会直接和字节序打交道。唯有在跨平台以及网络程序中字节序才是一个应该被考虑的问题
大端(Big Endian)、小端(Little Endian)这两个词是从《格列佛游记》里出来的。
《格列佛游记》有一段讲的是吃鸡蛋是从大的那头敲开还是小的那头敲开的问题,书中把从大头敲开的那种叫做Big Endian,把从小头敲开的那种叫Little Endian。
国外的计算机专家很文艺,直接把这两个词拿来给计算机的数据存储模式命名。鸡蛋大的那头表示数据权值高的位,小的那头表示权值低的位,而把低地址(比如0x00000000)看作我们敲鸡蛋的地面(或墙面、桌面),于是有了下面这张图。与鸡蛋类比,于是数据存储模式也分出了大端和小端。
所以
大端模式(Big Endian)就是大的那头敲地面(从大端敲破鸡蛋),即高位放到低地址
小端模式(Little Endian)就是小的那头敲地面(从小端敲破鸡蛋),即低位放到低地址
- 大端字节序:
高位字节数据存放在低地址处,低位数据存放在高地址处;
这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。
- 小端字节序:
高位字节数据存放在高地址处,低位数据存放在低地址处;
这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低
- 网络字节序:
TCP/IP协议规定接收到得第一个字节是高字节,存放到低地址,所以发送时会首先去低地址取数据的高字节
所以TCP/IP协议传输数据时,字节序默认大端.
以unsigned int value = 0x12345678
为例,分别看看在两种字节序下其存储情况
用unsigned char buf[4]
来表示value
-
Big-Endian: 低地址存放高位,如下:
低地址 buf[0] (0x12) -- 高位字节 buf[1] (0x34) buf[2] (0x56) buf[3] (0x78) -- 低位字节 高地址
-
Little-Endian: 低地址存放低位,如下:
低地址 buf[0] (0x78) -- 低位字节 buf[1] (0x56) buf[2] (0x34) buf[3] (0x12) -- 高位字节 高地址
-
什么是字节对齐,为什么要对齐
计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐
对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多
- 阶段尤其是iOS开发32位设备已经很少见了,但用32位与64位作对比, 更容易观测、理解内存对齐的作用
-
内存对齐原则
-
结构体的总大小,必须要是其内部最大成员的整数倍,不足的要补齐
typedef struct S1 { char a; ======> 1 [0, 1, 2, 3] int b; ======> 4 [4, 5, 6, 7] double c; ======> 8 (max)[8, 9, 10, 11, 12, 13, 14, 15] // 补齐8的最小倍数 char d; ======> 1 [16, 17, 18, 19, 20, 21, 22, 23] } s1; 结构体s1总大小: 4+4+8+8 = 24
-
结构体或联合的数据成员,第一个数据成员是要放在offset == 0的地方,如果遇上子成员,要根据子成员的类型存放在对应的整数倍的地址上
typedef struct S2 { char array[2]; ======> 2 [0, 1, 2, 3] int b; ======> 4 [4, 5, 6, 7] double c; ======> 8 (max)[8, 9, 10, 11, 12, 13, 14, 15] // 原则1 float d; ======> 4 [16, 17, 18, 19, 20, 21, 22, 23] } s2; 结构体s2总大小: 4+4+8+8 = 24 最后的 float d 如果只有4字节,那么整体20字节,不足以满足8的整数倍 所以补了4个字节后扩容到24字节, 正好是8的3倍
-
如果结构体作为成员,则要找到这个结构体中的最大元素,然后从这个最大成员的整数倍地址开始存储
typedef struct S3 { char a; ======> 1 [0, 1, 2, 3] int b; ======> 4 [4, 5, 6, 7] double c; ======> 8 [8, 9, 10, 11, 12, 13, 14, 15] // 原则3 ,下面是个结构体,其中最大成员为8,则需要从8的整数倍地址存放,所以变量d补齐到8 short d; ======> 2 [16, 17, 18, 19, 20, 21, 22, 23] // struct S2内存结构为: 2, 4, 8, 4 // char array[2] [24, 25] // int b [26, 27, 28, 29] // double c [30, 31, 32, 33, 34, 35, 36, 37] // float d [38, 39, 40, 41] S2 e; ======> 24 (max 8)[24, 25, 26, 27, 28, 29, 30, 31, 32, ...47] } s3; 结构体s3总大小: 4+4+8+8+24 = 48
-
联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”; 但优点是内存使用更为精细灵活,也节省了内存空间