OP 这么问:
我认为问题是字节序(我的机器是小字节序),我不知道如何优雅地解决问题。
也许位域不是适合这项工作的工具?
尽管在使用位域或联合时,将机器的字节序考虑在内始终是一个很好的考虑因素,并且不应该忘记这一点。但是,在您目前的情况下,我看不出字节序是导致任何问题的原因或问题。至于问题的第二部分,这完全取决于具体的需求。如果要写入的代码专门用于特定的架构/操作系统/平台并且不太可能是可移植的,那么如果正确构造位域,则使用位域应该没有任何问题。即使您决定移植到其他机器,您仍然可以使用位域,但您必须更加小心,并且可能必须使用预处理器指令或控制 switch 和 case 语句编写更多代码才能使用代码并做一件事在一台机器上而不是另一台机器上。
当使用位域时,我认为在混合类型时会考虑字节序。
struct Bitfield {
unsigned a : 10,
b : 10,
c : 16;
int x : 10,
y : 10,
z : 16;
};
可能需要将字节序考虑在内。
通过查看您的位域结构,我看到的是对位域内位对齐与结构本身对齐的误解。
你当前的结构是:
#pragma pack(push, 1)
struct vlan_header {
// uint16_t = 2bytes: - 16bits to work with
uint16_t PCP : 3, // bit(s) 0-2
DEI : 1, // bit(s) 3
ID : 12; // bit(s) 4-15
};
#pragma pack(pop)
您正在将对齐打包到1 byte 的最小可能大小,因此该结构内的边界对于每个边界都应位于8 bits。没什么大不了的,而且很不言自明。然后,您将使用 uint16_t 的类型,它是 typedef 的 unsigned short,其大小为 2 bytes,或使用 16 bits。 unsigned short 的值范围为 [0,65535]。
然后在结构中,您将位域成员 PCP、DEI 和 ID 分别设置为具有位数:3、1、12。我将 cmets 添加到您的结构中以显示此模式。
现在就像在你的 main 函数中一样,你要声明一个指向 uint8_t 类型的指针,然后创建上述结构的实例,然后为数组大小为 [2] 的指针创建动态内存。这里uint8_t 是typedef 的unsigned char,它的大小是1 byte 或8 bits,因为你有2,所以你总共有2 bytes 或16 bits。好的,因此您的 bitfield struct 和 data[] 数组之间的内存总大小匹配。
然后您通过索引和设置十六进制值来填充您的指针数组。然后,通过将其转换为该类型,将 array 中的值分配给您的 bitfield。但是,我认为您的假设是 data[0] 应该适用于位域的 1st2 成员,而 data[1] 应该适用于最后一个 ID 值。然而事实并非如此:
这里发生的事情是在您的这部分代码中:
data[0] = 0;
data[1] = 0x14; // data is 00 14
上面没有做你认为它应该做的事情。
我将制作一个图表,只是为了向您展示示例:但是它太大,无法在此处显示;所以我能做的就是为你提供一些代码,让你在你的机器上运行,生成一个日志文件,让你查看模式。
#include <iostream>
#include <fstream>
#pragma pack(push, 1)
struct vlan_header {
// uint16_t = 2bytes: - 16bits to work with
uint16_t PCP : 3, // bit(s) 0-2
DEI : 1, // bit(s) 3
ID : 12; // bit(s) 4-15
};
#pragma pack(pop)
int main() {
uint8_t* data; // sizeof(uint8_t) = 1byte - 8bits
vlan_header* vlanHeader;
data = new uint8_t[2];
std::ofstream log;
log.open( "results.txt" );
for ( unsigned i = 0; i < 256; i++ ) {
for ( unsigned j = 0; j < 256; j++ ) {
data[0] = j;
data[1] = i;
std::cout << "data[0] = " << static_cast<unsigned>(data[0]) << " ";
std::cout << "data[1] = " << static_cast<unsigned>(data[1]) << " ";
log << "data[0] = " << static_cast<unsigned>(data[0]) << " ";
log << "data[1] = " << static_cast<unsigned>(data[1]) << " ";
vlanHeader = reinterpret_cast<vlan_header*>(data);
std::cout << "PCP: " << std::hex << vlanHeader->PCP << " ";
std::cout << "DEI: " << std::hex << vlanHeader->DEI << " ";
std::cout << "ID: " << std::hex << vlanHeader->ID << std::endl;
log << "PCP: " << std::hex << vlanHeader->PCP << " ";
log << "DEI: " << std::hex << vlanHeader->DEI << " ";
log << "ID: " << std::hex << vlanHeader->ID << std::endl;
}
}
log.close();
delete[] data;
std::cout << "\nPress any key and enter to quit." << std::endl;
char q;
std::cin >> q;
return 0;
}
如果您查看这些模式,就会非常清楚正在发生的事情。
让我们看一下生成的文件的前几次迭代,在这里进行了简化。
// Values are represented in hex
// For field member PCP: remember that 3 bits can only hold a max value of 7
// 8-bits 8-bits 3-bits 1-bit 12-bits
// data[0] data[1] PCP DEI ID
0x00 0x00 0 0 0
0x01 0x00 1 0 0
0x02 0x00 2 0 0
0x03 0x00 3 0 0
0x04 0x00 4 0 0
0x05 0x00 5 0 0
0x06 0x00 6 0 0
0x07 0x00 7 0 0 // PCP at max value since 3 bits only has 2^3 digit combinations
0x08 0x00 0 1 0
0x09 0x00 1 1 0
0x0a 0x00 2 1 0
0x0b 0x00 3 1 0
0x0c 0x00 4 1 0
0x0d 0x00 5 1 0
0x0e 0x00 6 1 0
0x0f 0x00 7 1 0 // the next iteration is where the bit carries into ID
0x10 0x00 0 0 1
// And this pattern repeats through out until ID has max value.
您的位域在内存中发生的情况是第 1st 字节或 8 bits 正在同时消耗 PCP、DEI 以及ID 的第一个st4 bits,我认为这就是你感到困惑的地方。正如SoronelHaetir 在他们的简短回答中所说,如果你希望你的3 位域的值是十进制的{0,0,20},那么你需要将data array 分别设置为data[0] = 0x40 和data[1] = 0x01。来自data[0] 的位溢出到其他位域成员中,当该成员不能再包含足够高的值而不是分配的位数量可以支持时。
这基本上意味着PCP 有3 可用bits 并且它的最大组合位数是2^3 = 8 所以PCP 可以存储来自[0,7] 的值。由于DEI 只有1 bit,这充当了一个单比特布尔标志,它只能存储[0,1] 的值,最后ID 有12 bits 可用,其中1st4来自data[0],最后一个8 都来自data[1],这为您提供2^12 = 4096 组合数字,其值范围为[0,4095],十六进制中的最大值为FFF .这都可以在日志或结果文件中看到。
我还将显示您的 data[] 数组与您的 bitfield 的对齐方式
First Byte | Second Byte
data[0] | data[1]
data[n]: ([0][0][0]) ([0])-([0][0][0][0] | [0][0][0][0]-[0][0][0][0])
|
PCP DEI ID |
bitfield: ([0][0][0]) ([0])-([0][0][0][0] | [0][0][0][0]-[0][0][0][0])
编辑
OP 在对此答案的评论中提到了这些陈述:
我没有得到“当该成员不能再包含足够高的值时,来自 data[0] 的位溢出到其他位域成员”,我看不到溢出发生在哪里 – Lior Sharon
和
也根据您在答案底部的对齐方式,我所做的应该可以工作,因为当 ID 为 20 时,位域仅使用 data1 的第二个字节
我在这里尝试做的是显示data[0] 和data[1] 的位模式,其值为0x40 和0x01
Byte 1 Byte 2
data[0] = 0x40 data[1] = 0x01
[0][1][0][0] [0][0][0][0] | [0][0][0][0] [0][0][0][1]
这是data 在将其转换为位域结构之前的位模式。现在让我们看看在转换之前所有0s 的位域,然后让我们看看与位域成员相关的十六进制值以及它们可以存储的值。我已经说过PCP 可以存储来自[0-7] 的值,DEI 可以存储值[0,1],ID 可以存储来自[0-4095] 的十进制值。您将十六进制值分配到两个字节或 16 位内存中。您希望PCP 和DEI 的值是0,而ID 的值是十进制的20。您认为第一个字节的0x00 将同时为PCP 和DEI 提供0 的值,而0x14 应该为ID 提供20 的值。这是行不通的。十六进制值的0x14 表示内存中的一个字节,但是ID 有12 bits 或1.5 bytes 要存储。如果您参考上面的图表,成员 PCP 只有 3 位要存储,因此如果我们将值 7 添加到 data[0] PCP 将如下所示:[1][1][1] 二进制。即使不使用data[1] 的字节,我们也可以将值推送到DEI 和ID 成员中。
Byte1 = Byte 2 =
============================|==========================
PCP DEI ID
0x00 0x00
[0][0][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
0x01 0x00
[0][0][1] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
0x02 0x00
[0][1][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
0x03 0x00
[0][1][1] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
0x04 0x00
[1][0][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
0x05 | 0x00
[1][0][1] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
0x06 0x00
[1][1][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
0x07 0x00
[1][1][1] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
0x08 0x00
[0][0][0] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
0x09 0x00
[0][0][1] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
0x0A 0x00
[0][1][0] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
0x0B 0x00
[0][1][1] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
0x0C 0x00
[1][0][0] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
0x0D 0x00
[1][0][1] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
0x0E 0x00
[1][1][0] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
0x0F 0x00
[1][1][1] [1] [0][0][0][0] | [0][0][0][0] [0][0][0][0]
// When we increment the hex value from 0x0F to 0x10 with a decimal value of 16
// this is where the overflow into the ID member happens and as of right now
// PCP has a max value of 7 and DEI has a max value of 1 where all bits are full.
// Watch what happens on the next iteration. Also note that we never gave any values
// to data[1] or byte 2 we only gave values to byte 1. This next value will
// populate a result into the bitfield's member ID.
0x10 0x00
[0][0][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][1]
// then for the next iteration it'll be like this and so on...
0x11 0x00
[0][0][1] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][1]
0x12 0x00
[0][1][1] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][1]
// while this pattern continues we seen that `0x10` gave us a bit at the right end
// of member ID so lets look at values 0x20, 0x30 & 0x40 in the first byte
// if 0x10 =
[0][0][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][0][1]
// then 0x20 should be
[0][0][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][1][0]
// and 0x30 should be
[0][0][0] [0] [0][0][0][0] | [0][0][0][0] [0][0][1][1]
// finally 0x40 should be
[0][0][0] [0] [0][0][0][0] | [0][0][0][0] [0][1][0][0]
// This is all without touching byte.
// Remember we want both PCP & DEI to have values of 0 but we
// need a value of 0x16 or 20 in decimal in ID. Because of this
// overflow of bits due to the nature of bit fields, we can not
// just set the bytes directly with regular hex values as normal
// because member PCP only has 3 bits, member DEI has only 1, and
// the rest belong to ID. In order to get to the value we want
// we would have to iterate 0x40 all the way up to 0xFF before we would
// ever use byte 2 making it have a value of 0x01
// Another words: 0xFF 0x00 comes before 0x00 0x01 in this sequence
// bit patterns, but since we have the value of 0x40 already in the first
// byte of data[n] giving us a bit pattern of
[0][0][0] [0] [0][0][0] | [0][0][0][0] [0][1][0][0]
// what does making byte 2 with a value of 0x01 do to this pattern?
// It does this:
[0][0][0] [0] [0][0][0] | [0][0][0][1] [0][1][0][0]
// Okay so data[0] = 0x40 and data[1] = 0x01 so how does this
// give us the values of {0,0,20} or {0x00,0x00,0x14} ?
// Let's see from the far left going right the first 3 bits
// are PCP and all bits are 0 giving it a value of 0
// Next is the single bit for DEI which has a value of 0.
// Finally the next 12 bits are for ID and when we look at this 12 bit
// pattern we have [0][0][0][0] | [0][0][0][1] [0][1][0][0]
// Let's ignore the left 4 since they are all 0s or padding at this moment
// So we can see that [0][0][0][1] [0][1][0][0] = 0x14 in hex with a
// a decimal value of 20.
现在唯一的问题是:我在运行 Win7 x64 家庭高级版的英特尔四核处理器上的 MS Visual Studio 2017 CE 中执行此操作,并将其编译为 x86 应用程序。位的实际存储位置也会因编译器、操作系统和架构而异。我只是展示了从左到右的纯数学位表示,其中大多数机器将以从右到左的顺序存储它们的位。如果您在小端机器上运行并使用 Visual Studio 编译器,您应该会得到类似的结果。
这里有一些关于位域的写得很好的文章;如果我遇到更多,我会在这里发布: