带有枚举的结构在 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) 可能是 12。如果它是 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。有时,增加未序列化的成员或做一些调整是有用的。

另一件重要的事情是计划在某些时候,您可能需要添加额外的字段并转换旧数据,或者可能会发现一些错误并需要更正数据。

您可能还想考虑如果在旧软件中打开新文件会发生什么情况。在很多情况下,它可能会被拒绝。