目录
前言
本文首先介绍了字节序的种类以及各类字节序在内存中的存储方式,然后介绍了结构体中的位域申明方法和数据存储方式,然后位域数据的实际存储结果和分析结果进行内存数据比对,最后给出了大端机器和小端机器间的数据转换方法。
1. 字节序
1.1 什么是字节序?
字节序是指多字节数据在内存中的存放顺序。
字节序(Byte Order)分为大端字节序(Big-Endian)和小端字节序(Little-Endian)。
在处理多字节数据时,以字节为单位,大端序指的是数据高位字节存储在低位地址,数据低位字节存储在高位地址,即数据表示和数据存储按字节反序排列。小端序指的是数据高位字节存储在高位地址,低位字节存储在低位地址,即数据表示和数据存储按字节正序排列。
1.2 内存中的存储方式
对于整型数据0x12345678,大端序和小端序在内存中的存储方式如下表如示(假设内存起始地址为0x10000000)。
内存地址 | 0x10000000 | 0x10000001 | 0x10000002 | 0x10000003 |
---|---|---|---|---|
大端序 | 0x12 | 0x34 | 0x56 | 0x78 |
小端序 | 0x78 | 0x56 | 0x34 | 0x12 |
2. 位域
2.1 什么是位域
位域是一种数据结构的定义方式,可以将多个字段(成员)组合在一起,每个字段使用特定的比特位数来存储数据。位域可以用来节省内存空间,特别适用于存储一些开关类型的数据。
位域的定义可以通过使用特定的语法来指定每个字段所占用的位数。例如,下面的代码定义了一个具有三个位域字段的结构体:
struct Flags {
unsigned int flag1: 1;
unsigned int flag2: 2;
unsigned int flag3: 3;
};
在这个例子中,flag1占用了1位,flag2占用了2位,flag3占用了3位。每个字段都可以存储的值的范围取决于它所占用的位数。
使用位域可以方便地读取和修改特定的位字段,如下所示:
struct Flags flags;
flags.flag1 = 1;
flags.flag2 = 2;
flags.flag3 = 4;
这样我们就可以通过访问flags.flag1
、flags.flag2
和flags.flag3
来读取和修改位域的值。
需要注意的是,位域的使用也有一些限制。因为位域的存储顺序和大小取决于编译器的实现,所以在不同的编译器和不同的机器上可能会有不同的结果。此外,位域的总位数不能超过字段的数据类型的大小。
2.2 内存中的存储方式
定义了位域的数据结构不是简单的按照大端序和小端序的存储方式进行有效存储。接下来通过实例来解析位域这种数据结构在内存中的存储方式,测试平台如下所示。
操作系统 | Window 11 |
---|---|
编译器 | MS Visual Studio 2022 |
首先,我们定义如下的结构体ST_BIT_FIELD,其中包含一个unsigned int型数据的位域成员和一个unsigned short型数据的位域成员,共6字节数据。
typedef struct _ST_BIT_FIELD
{
// int
unsigned int ulPartA : 3;
unsigned int ulPartB : 3;
unsigned int ulPartC : 8;
unsigned int ulPartD : 8;
unsigned int ulPartE : 10;
// short
unsigned short usPartA : 4;
unsigned short usPartB : 6;
unsigned short usPartC : 6;
}ST_BIT_FIELD;
对该结构体进行如下的赋值操作。
ST_BIT_FIELD* stField = new ST_BIT_FIELD;
memset(stField, 0, sizeof(ST_BIT_FIELD));
stField->ulPartA = 1;
stField->ulPartB = 2;
stField->ulPartC = 3;
stField->ulPartD = 4;
stField->ulPartE = 5;
stField->usPartA = 1;
stField->usPartB = 2;
stField->usPartC = 3;
位域只有在设置的基本数据类型长度内才有意义,因此上述内存中的内容可以分为两个部分,第一部分为4字节的unsigned int型变量,第二部分为2字节的unsigned short型变量。
针对于代码中的赋值结果,可以得出每个基本数据类型下的每个位域数值的二进制表现形式,如下表所示。
ulPartA | ulPartB | ulPartC | ulPartD | ulPartE |
0b001 | 0b010 | 0b00000011 | 0b00000100 | 0b0000000101 |
usPartA | usPartB | usPartC |
0b0001 | 0b000010 | 0b000011 |
位域数据又是如何组成完整数据的呢?又是如何在内存中存储的呢?
同一数据类型下定义的所有位域成员,遵循按顺序从低地址向高地址依次相邻存储的规则。
位域是按照比特位存放数据的,那么比特序又遵循什么原则呢?
比特序与字节序保持相同的存储规则,即大端序为数据高比特位对应内存低地址,小端序为数据高比特位对应内存高地址。
根据上述两个规则,小端序下位域的存储方式总结如下:
- 提取定义了位域的基本数据类型长度大小,以unsigned int为例。
- 按照大小端规则,将内存数据按照数据表现形式排列,如下表1所示。
- 按照位域成员按照定义顺序,从低地址向高地址依次存储的规则进行存储。
- 按照地址排列顺序将数据存储到内存中,内存中结果为0xD1004101,结果如下表所示。
内存地址 | 0x10000000 | 0x10000001 | 0x10000002 | 0x10000003 |
---|---|---|---|---|
按照数据表示形式排列后的地址顺序 | 0x10000003 | 0x10000002 | 0x10000001 | 0x10000000 |
按比特位排列的顺序 | BIT31-BIT0 | |||
小端序 | ulPartA(BIT31-BIT22): 0b0000000101 ulPartB(BIT21-BIT14): 0b00000100 ulPartC(BIT13-BIT6): 0b00000011 ulPartD(BIT5-BIT3): 0b010 ulPartE(BIT2-BIT0): 0b001 组合后:0b00000001 01000001 00000000 11010001 十六进制表示:0x01 41 00 D1 |
内存 | 0x10000000 | 0x10000001 | 0x10000002 | 0x10000003 |
---|---|---|---|---|
小端序 | 0xD1 | 0x00 | 0x41 | 0x01 |
大端序同上,具体步骤见表3和表4。
内存地址 | 0x10000000 | 0x10000001 | 0x10000002 | 0x10000003 |
---|---|---|---|---|
按照数据表示形式排列后的地址 | 0x10000000 | 0x10000001 | 0x10000002 | 0x10000003 |
比特位按地址顺序排列 | BIT0-BIT31 | |||
小端序 | ulPartA(BIT22-BIT31): 0b0000000101 ulPartB(BIT14-BIT21): 0b00000100 ulPartC(BIT6-BIT13): 0b00000011 ulPartD(BIT3-BIT5): 0b010 ulPartE(BIT0-BIT2): 0b001 组合后:0b00101000 00001100 00010000 00000101 十六进制表示:0x28 0C 10 05 |
内存 | 0x10000000 | 0x10000001 | 0x10000002 | 0x10000003 |
---|---|---|---|---|
小端序 | 0x28 | 0x0C | 0x10 | 0x05 |
2.3 实际结果与分析结果对比
在visual studio 2022下,我们得到如下图所示的内存存储结果,存储结果为0xd1004101210c,总共6字节。(此处,由于字节对齐,结构体ST_BIT_FIELD正常情况应该占8字节,作者在测试时设置了1字节对齐,因此只占用6字节,感兴趣的朋友可以搜索字节对齐相关知识)
第一部分(unsigned int) | 第二部分(unsigned short) |
---|---|
0xd1004101 | 0x210c |
从图中可以看出,第一部分对于unsigned int数据类型的内存结果与分析结果一致,说明2.2节中分析结果的正确性。
2.4 数据在大小端机器下的转换
对于上述提到的结构体ST_BIT_FIELD,为了确保大端序和小端序下对于所有的位域成员和基本成员得到相同的取值结果,需要对结构体定义做些许处理。
处理方法:
步骤1: 将隶属于同一数据类型的所有位域成员按照小端序下的声明顺序进行反序申明即可,反之亦可,如下图所示。
步骤2:在数据发送或者接收时,还需要按照基本数据类型的大小端转换方式进行字节的倒序操作,该操作在发送端或者接收端处理都可以,一般建议在发送端处理。
// 小端序下的定义
typedef struct _ST_BIT_FIELD
{
// int
unsigned int ulPartA : 3;
unsigned int ulPartB : 3;
unsigned int ulPartC : 8;
unsigned int ulPartD : 8;
unsigned int ulPartE : 10;
// short
unsigned short usPartA : 4;
unsigned short usPartB : 6;
unsigned short usPartC : 6;
}ST_BIT_FIELD;
// 大端序下的定义
typedef struct _ST_BIT_FIELD_BIGEND
{
// int
unsigned int ulPartE : 10;
unsigned int ulPartD : 8;
unsigned int ulPartC : 8;
unsigned int ulPartB : 3;
unsigned int ulPartA : 3;
// short
unsigned short usPartC : 6;
unsigned short usPartB : 6;
unsigned short usPartA : 4;
}ST_BIT_FIELD_BIGEND;