1. 背景
-
在看Lua源码的时候,很多地方都用到了union(共用体或者联合体),在定义lua类型的时候,为了以一个结构来包含所有的数据类型,设计了一个 TValue类型,TValue类型最终关联到 Value类型,而这个Value类型就是一个union。如下图:
-
可以看出,基础数据类型(除了string)之后都包含了。
-
联合体允许定义任意一种数据类型,这些数据共享同一段内存,以达到节省空间的目的。union变量所占用的内存长度等于最长的成员的内存长度。这个联合体内的数据只能取其一(俗称 “n选1”)。
-
问题来了,联合体占用的内存大小是怎么算的?和struct占用内存的大小算法区别在哪里?
2. Struct占用内存大小的算法
-
前置
- 对于32位CPU,CPU每次都是以 4 的倍数来读取内存地址的。64位CPU以 8 的倍数来读取内存地址。(16位以 2 的倍数来读取内存地址)
- 一个内存地址存储的是一个字节(1 byte),即 8 位(8 bit)。
- Struct在内存中存储的时候要做 字节对齐。
-
Struct在内存中的存放规则
- 结构体成员的首地址(开始地址)必须要是这个成员所占空间 的整数倍。
- 结构体占用空间的总大小必须要是 占用空间最大成员所占空间的整数倍。
- 结构体的首地址必须要是 占用空间最大成员所占空间的整数倍。
-
对上面规则的解释
- 第一个就是字节对齐。比如一个 int 变量 在32位系统上占4个字节,double占8个字节,那么这个变量的存放地址就必须要是4/8的整数倍。
- 第二个就是计算完最后一个成员地址后,要看总大小是不是 占用地址最大成员 所占空间的整数倍。
- 第三个因为预设结构体首地址是0,所以无论最大成员所占空间数是多少,肯定都是整数倍。
-
看下面的例子:
-
- 上图右侧的例子计算步骤如下:
- char a1占1个字节,首地址是 0,0%1==0(规则第一条),所以不需要填充,已分配大小:1字节
- Int b1 占4个字节,首地址是1,1%4 !=0 (不满足第一条,这个成员的首地址不是占用空间(4字节)的倍数),所以需要填充3,已分配大小:8字节
- char c1占1个字节,首地址是8,8%1==0(满足规则第一条),所以不需要填充,已分配大小:9字节
- 最后一个成员计算完毕,已分配9字节,最大成员占用空间是4(int b1),但是 9%4!=0(不满足第二条 —总大小是最大成员空间的整数倍),所以需要填充3。总大小:12字节。
- 上图右侧的例子计算步骤如下:
-
看一个稍微复杂一点的例子,包含了嵌套。
- 如上图,Example4里面包含了 struct Example2。根据规则, struct Example2 占用了24个字节(计算步骤略)。
- 重点看一下 Example4的计算步骤:
1) char a 占 1 个字节。首地址是0,0%10,无需填充,已分配大小:1字节
2) struct Example2 占24个字节,首地址是1,struct Example2 的最大成员所占空间是 8 (规则第二条),1%8 != 0,需要填充7个字节(注意:不是23个字节),已分配大小:32字节
3) int c 占4个字节,首地址是32,32%4 == 0,无需填充,已分配大小:36字节
4) double d 占8个字节,首地址36,36%8 != 0,需要填充4,已分配大小:48字节。
5) 最后一个成员计算完毕,已分配48字节,最大成员占用空间是8(注意:这里不是 sizeof(struct Example2)=24),48%80,无需填充,总大小:48字节。
3. Union内存大小的算法
- 啰嗦一遍 union 的定义:叫 共用体,也叫联合体,在里面可以定义多种不同的数据类型,这些数据可以共享同一段内存,以达到节省空间的目的。union定义的变量所占用的内存长度等于最长的成员的内存长度。
- 看一个简单的例子:
-
- sizeof(union test)的结果为4。 Char mark,long num,float score这三个变量存放在同一个地址开始的内存单元中。三个变量所占用的字节数是不一样的,也就是说,三个变量相互覆盖。如下示意图:
-
- 如上图,三个变量的地址都是从 0x0000开始,也就是从0开始,到0x0003结束(红色条标注),占用 4 个字节。
- 看一个稍微复杂一点的例子:
-
- 上图中,myun里包含两个变量,一个是struct u(占12个字节),一个是int k(4个字节),所以sizeof(union myun)的结果是12。
- 同时,由于union 是共享内存的,所以strtuct u的起始地址是 0,结束地址是11,int k的起始地址是0,结束地址是3。
- 在main里面,设置了struct u的 x,y,z分别是4,5,6。设置了int k 是 0。所以内存里面的变化是这样的(如下图):
- 如上图,可以看出main里面对union myun赋值的过程。后面对 k 的赋值也是从首地址开始,会把原来设置的 4 覆盖掉。所以上图打印的结果应该是 0,5,6。
4. 大小端的问题
- 既然union里面的变量共享内存,都是从某一个内存地址开始,那么就可以利用这种特性来测试CPU是大端模式还是小端模式。
- 主要思想是:定义一个union变量,设置这个union变量里面的某一个成员的值,然后看union里面char类型的变量的值是不是1,如果是1则是小端。否则是大端。
- 实现如下:
- 上图定义了一个 union MyUnion,通过设置union里面的成员变量 int a 之后,在去查看 char c 变量的值,如果是小端(从低地址到高地址依次存放数据的低位字节到高位字节),则c的值应该是1。如果是大端(从低地址到高地址依次存放数据的高位字节到低位字节),则c的值肯定不等于1。
- 如下图,看看变量在内存里分别对应大小端的情况
- 上图中 0X1234abcd,分别以大小端存储时的异同。可以看出对于一个变量的值 0X1234abcd,从高位字节到低位依次是 1234abcd,如果是大端,则低地址放高位字节,高地址放低位字节,则排列顺序如上图。
- 这里面有一个小小的问题,可能有的人会问:为什么 0X1234ABCD 在内存中会以 0X12,0X34,0XAB,0XCD这样去存放?为什么不是 0X1, 0X2, 0X3,0X4,0XA,0XB,0XC,0XD这样的方式去存放?
- 解释上面的问题
- 首先 0X1234ABCD 是一个16进制的数值。这个16进制的数值转换为2进制是 0001 0010 0011 0100 1010 1011 1100 1101,可以看出,16进制的这个数的每一位对应 4 个bit(因为是16进制,2的4次方,所以一个16进制的位转换为2进制就是 4 bit)。
- 其次,需要知道的是,一个内存地址对应一个内存空间,上图中的 0X0000 号内存地址就是一个内存空间,而一个内存空间存储一个字节,一个字节有8bit,所以一个内存地址存放的是2个16进制位(一个16进制位占4bit,2个就是 2 * 4bit = 8bit),所以一个内存地址里面存放的是 0X12,0x34等等,而不是 0x1,0x2。
- 回到上图的解释中,如果是大端,则低地址存放高字节,高地址存放低字节,所以此时从低地址到高地址依次存放的是 0x12,0x34,0xab,0xcd。而如果是小端,则低地址存放低字节,高地址存放高字节,所以从低地址到高地址依次存放的是 0xcd,0xab,0x34,0x12。
- 看看内存里实际对变量是如何存储的,如下图:
- 看代码里面定义的变量 TestNum1 = 0X1234abcd,在内存里面这个变量的起始地址是 0x0000001869379270,存放的顺序是 cd,ab,34,12。结束地址应该是 0x0000001869379273 。(由这个图我们还能看出,这里是 little endian“小端”)
- 然后我们再看蓝色箭头的内容, TestNum2 = 0X00000001,这个变量的起始地址是 0x0000001869379274,存放顺序是 01,00,00,00。这里我们能得出几个信息,首先,显然 TestNum1 和 TestNum2 都占了 4 个字节(对应 int 占4个字节)。其次,这两个变量在内存中的存放方式表明是== little endian模式==。
- 上图中 0X1234abcd,分别以大小端存储时的异同。可以看出对于一个变量的值 0X1234abcd,从高位字节到低位依次是 1234abcd,如果是大端,则低地址放高位字节,高地址放低位字节,则排列顺序如上图。
5. Reference
- https://developer.aliyun.com/article/369914 c++中union的使用,看高手们如何解释的
- https://eysent.com/others/struct-padding/【C】如何计算结构体占用内存大小
- https://blog.csdn.net/HJS020828/article/details/123729229 结构体在内存中的存储