带有枚举的结构在 C 和 C++ 中是不同的,为什么?
Structs with enums are different in C and C++, why?
任务是通过I2C从Arduino向STM32发送数据。
所以我使用 C++ 在 Arduino 中定义了结构和枚举:
enum PhaseCommands {
PHASE_COMMAND_TIMESYNC = 0x01,
PHASE_COMMAND_SETPOWER = 0x02,
PHASE_COMMAND_CALIBRATE = 0x03
};
enum PhaseTargets {
PHASE_CONTROLLER = 0x01,
// RESERVED = 0x02,
PHASE_LOAD1 = 0x03,
PHASE_LOAD2 = 0x04
};
struct saatProtoExec {
PhaseTargets target;
PhaseCommands commandName;
uint32_t commandBody;
} phaseCommand;
uint8_t phaseCommandBufferSize = sizeof(phaseCommand);
phaseCommand.target = PHASE_LOAD1;
phaseCommand.commandName = PHASE_COMMAND_SETPOWER;
phaseCommand.commandBody = (uint32_t)50;
另一方面,我使用 C:
得到了相同的定义
typedef enum {
COMMAND_TIMESYNC = 0x01,
COMMAND_SETPOWER = 0x02,
COMMAND_CALIBRATE = 0x03
} MasterCommands;
typedef enum {
CONTROLLER = 0x01,
// RESERVED = 0x02,
LOAD1 = 0x03,
LOAD2 = 0x04
} Targets;
struct saatProtoExec {
Targets target;
MasterCommands commandName;
uint32_t commandBody;
} execCommand;
uint8_t execBufferSize = sizeof(execCommand);
execCommand.target = LOAD1;
execCommand.commandName = COMMAND_SETPOWER;
execCommand.commandBody = 50;
然后我逐字节比较这个结构:
=====================
BYTE | C++ | C
=====================
Byte 0 -> 0x3 -> 0x3
Byte 1 -> 0x0 -> 0x2
Byte 2 -> 0x2 -> 0x0
Byte 3 -> 0x0 -> 0x0
Byte 4 -> 0x32 -> 0x32
Byte 5 -> 0x0 -> 0x0
Byte 6 -> 0x0 -> 0x0
Byte 7 -> 0x0 -> 0x0
那么为什么字节 1 和 2 不同?
这真是个糟糕的主意。
您永远不应该依赖结构的二进制表示在两种 C 实现之间是相同的,更不用说从 C 到 C++ 了!
您应该编写一些适当的 serialization/deserialization 代码,以在结构的外部表示的字节级别进行控制。
也就是说,这可能是由于填充所致。您最终通过外部 link 发送填充(这只是编译器添加的内容以保持其主机 CPU 快乐)是这种方法有多么糟糕的另一个迹象。
在C版本中明确表示sizeof(Targets) == 1
。看起来结构的第二个字段是 2 字节对齐的,所以你有一个填充字节,内容未定义。
现在,在 C++ 中,sizeof(PhaseTargets)
可能是 1
或 2
。如果它是 1
(可能),一切都很好,并且你有相同的填充 space,只是碰巧有不同的垃圾值。如果它是 2
... 那么,您将得到一个错误的枚举值!
初始化结构的简单方法是在变量的定义中。如果您还没有值,只需添加一个 0,所有结构都将初始化为 0。
struct saatProtoExec execCommand = {0};
如果无法做到这一点,您可以在使用前memset()
将其归零。
一种可移植的替代方法是将结构的字段声明为适当大小的整数,并使用 enum
类型作为常量集合。
struct saatProtoExec {
uint8_t target;
uint8_t commandName;
uint8_t padding[2];
uint32_t commandBody;
} execCommand;
好吧,如前所述,您不应该依赖具有特定格式的结构。但是,有时使用结构而不是序列化会很方便,因为如果不需要转换并且表示在给定体系结构上的 运行 时间兼容且高效。
因此,这里有一些使用结构的建议:
- 使用适当的编译指示或属性来确保布局不依赖于项目选项。
- 添加检查以验证最终大小是否符合预期。在 C++ 中,您可以使用
static_assert
.
- 同时添加检查字节顺序是否符合预期。
- 通过单元测试来确认格式也是一个好主意。
无论如何,我还建议避免在通用代码和序列化中使用相同的 struct
。有时,增加未序列化的成员或做一些调整是有用的。
另一件重要的事情是计划在某些时候,您可能需要添加额外的字段并转换旧数据,或者可能会发现一些错误并需要更正数据。
您可能还想考虑如果在旧软件中打开新文件会发生什么情况。在很多情况下,它可能会被拒绝。
任务是通过I2C从Arduino向STM32发送数据。
所以我使用 C++ 在 Arduino 中定义了结构和枚举:
enum PhaseCommands {
PHASE_COMMAND_TIMESYNC = 0x01,
PHASE_COMMAND_SETPOWER = 0x02,
PHASE_COMMAND_CALIBRATE = 0x03
};
enum PhaseTargets {
PHASE_CONTROLLER = 0x01,
// RESERVED = 0x02,
PHASE_LOAD1 = 0x03,
PHASE_LOAD2 = 0x04
};
struct saatProtoExec {
PhaseTargets target;
PhaseCommands commandName;
uint32_t commandBody;
} phaseCommand;
uint8_t phaseCommandBufferSize = sizeof(phaseCommand);
phaseCommand.target = PHASE_LOAD1;
phaseCommand.commandName = PHASE_COMMAND_SETPOWER;
phaseCommand.commandBody = (uint32_t)50;
另一方面,我使用 C:
得到了相同的定义typedef enum {
COMMAND_TIMESYNC = 0x01,
COMMAND_SETPOWER = 0x02,
COMMAND_CALIBRATE = 0x03
} MasterCommands;
typedef enum {
CONTROLLER = 0x01,
// RESERVED = 0x02,
LOAD1 = 0x03,
LOAD2 = 0x04
} Targets;
struct saatProtoExec {
Targets target;
MasterCommands commandName;
uint32_t commandBody;
} execCommand;
uint8_t execBufferSize = sizeof(execCommand);
execCommand.target = LOAD1;
execCommand.commandName = COMMAND_SETPOWER;
execCommand.commandBody = 50;
然后我逐字节比较这个结构:
=====================
BYTE | C++ | C
=====================
Byte 0 -> 0x3 -> 0x3
Byte 1 -> 0x0 -> 0x2
Byte 2 -> 0x2 -> 0x0
Byte 3 -> 0x0 -> 0x0
Byte 4 -> 0x32 -> 0x32
Byte 5 -> 0x0 -> 0x0
Byte 6 -> 0x0 -> 0x0
Byte 7 -> 0x0 -> 0x0
那么为什么字节 1 和 2 不同?
这真是个糟糕的主意。
您永远不应该依赖结构的二进制表示在两种 C 实现之间是相同的,更不用说从 C 到 C++ 了!
您应该编写一些适当的 serialization/deserialization 代码,以在结构的外部表示的字节级别进行控制。
也就是说,这可能是由于填充所致。您最终通过外部 link 发送填充(这只是编译器添加的内容以保持其主机 CPU 快乐)是这种方法有多么糟糕的另一个迹象。
在C版本中明确表示sizeof(Targets) == 1
。看起来结构的第二个字段是 2 字节对齐的,所以你有一个填充字节,内容未定义。
现在,在 C++ 中,sizeof(PhaseTargets)
可能是 1
或 2
。如果它是 1
(可能),一切都很好,并且你有相同的填充 space,只是碰巧有不同的垃圾值。如果它是 2
... 那么,您将得到一个错误的枚举值!
初始化结构的简单方法是在变量的定义中。如果您还没有值,只需添加一个 0,所有结构都将初始化为 0。
struct saatProtoExec execCommand = {0};
如果无法做到这一点,您可以在使用前memset()
将其归零。
一种可移植的替代方法是将结构的字段声明为适当大小的整数,并使用 enum
类型作为常量集合。
struct saatProtoExec {
uint8_t target;
uint8_t commandName;
uint8_t padding[2];
uint32_t commandBody;
} execCommand;
好吧,如前所述,您不应该依赖具有特定格式的结构。但是,有时使用结构而不是序列化会很方便,因为如果不需要转换并且表示在给定体系结构上的 运行 时间兼容且高效。
因此,这里有一些使用结构的建议:
- 使用适当的编译指示或属性来确保布局不依赖于项目选项。
- 添加检查以验证最终大小是否符合预期。在 C++ 中,您可以使用
static_assert
. - 同时添加检查字节顺序是否符合预期。
- 通过单元测试来确认格式也是一个好主意。
无论如何,我还建议避免在通用代码和序列化中使用相同的 struct
。有时,增加未序列化的成员或做一些调整是有用的。
另一件重要的事情是计划在某些时候,您可能需要添加额外的字段并转换旧数据,或者可能会发现一些错误并需要更正数据。
您可能还想考虑如果在旧软件中打开新文件会发生什么情况。在很多情况下,它可能会被拒绝。