问题描述
我想弄清楚在 C 中是否有一种解决方法可以在结构中拥有一个灵活的数组成员,这不是最后一个。例如,这会产生编译错误:
typedef struct __attribute__((__packed__))
{
uint8_t slaveAddr; /*!< The slave address byte */
uint8_t data[]; /*!< Modbus frame data (Flexible Array
Member) */
uint16_t crc; /*!< Error check value */
} rtuHead_t;
这不会产生错误:
typedef struct __attribute__((__packed__))
{
uint8_t slaveAddr; /*!< The slave address byte */
uint8_t data[]; /*!< Modbus frame data (Flexible Array
Member) */
} rtuHead_t;
typedef struct __attribute__((__packed__))
{
rtuHead_t head; /*!< RTU Slave addr + data */
uint16_t crc; /*!< Error check value */
} rtu_t;
但是不起作用。如果我有一个字节数组:data[6] = {1,2,3,4,5,6};
并将其转换为 rtu_t
,那么 crc
成员将等于 0x0302
,而不是 0x0605
。
有什么办法可以在结构体(或结构体中的结构体)中间使用灵活的数组成员?
解决方法
灵活数组成员必须是结构体的最后一个成员,包含灵活数组成员的结构体不能是数组或其他结构体的成员。
这种结构的预期用途是动态分配它,为其他成员以及灵活成员的 0 个或多个元素留出足够的空间。
您试图做的是将一个结构覆盖到一个内存缓冲区上,该缓冲区包含您想通过访问成员来解析的数据包数据。在这种情况下这是不可能的,而且由于对齐和填充问题,通常这样做不是一个好主意。
做你想做的正确方法是编写一个函数,一次反序列化数据包一个字段,并将结果放在用户定义的结构中。
,它不能在 ISO C 中完成。但是...
GCC 有一个扩展,允许在结构中定义可变修改类型。所以你可以这样定义:
#include <stddef.h>
#include <stdio.h>
int main() {
int n = 8,m = 20;
struct A {
int a;
char data1[n];
int b;
float data2[m];
int c;
} p;
printf("offset(a) = %zi\n",offsetof(struct A,a));
printf("offset(data1) = %zi\n",data1));
printf("offset(b) = %zi\n",b));
printf("offset(data2) = %zi\n",data2));
printf("offset(c) = %zi\n",c));
return 0;
}
除了一些关于使用非 ISO 功能的警告外,它可以很好地编译并产生预期的输出。
offset(a) = 0
offset(data1) = 4
offset(b) = 12
offset(data2) = 16
offset(c) = 96
问题是这种类型只能在函数体范围内定义,因此不能用于将参数传递给其他函数。
但是,它可以传递给嵌套函数,这是另一个 GCC 扩展。示例:
int main() {
... same as above
// nested function
int fun(struct A *a) {
return a->c;
}
return fun(&p);
}
,
灵活的数组成员只能放在结构体的末尾。这正是 C 标准 6.7.2.1 定义它们的方式:
作为一种特殊情况,具有多个命名成员的结构的最后一个元素可能 有一个不完整的数组类型;这称为灵活数组成员。
但对于特定情况,它们也是错误问题的错误解决方案。错误的问题是“如何在 C 结构中存储可变大小的 Modbus 数据协议帧”? struct
通常最好一开始就避免。不幸的是,我们的 C 程序员几乎被洗脑了,在每一种情况下都使用 struct
,以至于我们不假思索地声明了一个。
结构存在各种问题,最显着的是对齐/填充问题,只能通过非标准扩展如 gcc __attribute__((__packed__))
或 #pragma pack(1)
来解决。但即使你使用这些,你最终会得到一个编译器可能仍然访问未对齐的块 - 你只是告诉它放弃填充“我知道我在做什么”。但是,如果您继续进行字访问该内存,则可能是未对齐的访问。
然后是可变大小协议的问题。除了膨胀和程序执行开销外,根据接收到的数据量一遍又一遍地调整该内存块的大小实际上并没有太大的影响。这样做可以节省多少内存?大约 10 到 100 个字节?即使在低端 MCU 中,这也不算什么。因为您只需要同时在 RAM 中保留几帧。
事实证明,您将不得不分配足够的内存来存储有史以来出现的最大帧,因为您的程序必须处理最坏的情况。然后你也可以开始静态分配那么多内存。更快、更安全、更确定。
然后还有另一个您似乎没有解决的问题,即网络字节序。 Modbus 使用 big endian,CRC 以 big endian 计算。所以结构末尾的 uint16_t
成员只是坐在那里制造问题。即使您决定使用一些非标准的 GNU VLA 扩展来调整每个帧的大小。
我建议你忘记所有这些结构。
快速、便携和安全的解决方案是简单地使用 uint8_t frame [MAX];
,其中 MAX
是一个帧可以拥有的最大字节大小。使用结构体只是为帧中的一个特定字节指定一个变量名,实际上并没有添加任何东西。您真正想要的是具有易于解释每个字节的作用的可读代码,而不是原始数据的匿名缓冲区。
这也可以在访问它时使用此 enum
数组的命名索引(例如 uint8_t
)来完成。在结构版本 frame.slave_addr = x;
和数组版本 frame[slave_addr] = x;
之间生成的可读性、用途或机器代码没有区别。 (除了前者可能会导致机器代码中的访问不对齐。)
无论如何,您都需要逐字节访问 CRC,因为您首先需要使用 CPU 字节序计算它,然后将其转换为网络字节序。例如:
frame[fcs_high] = checksum >> 8;
frame[fcs_low] = checksum & 0xFF;
与结构体不同,此代码不依赖于 CPU 字节序,它只能在大字节序 CPU 上按预期工作。