C语言 灵活的阵列成员,不必是最后一个

cetgtptt  于 2023-05-16  发布在  其他
关注(0)|答案(4)|浏览(101)

我试图弄清楚在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;

但不起作用。如果我有一个bytes数组:data[6] = {1, 2, 3, 4, 5, 6};并将其转换为rtu_t,则crc成员将等于0x0302,而不是0x0605
有没有什么方法可以在结构体中间(或结构体中的结构体)使用灵活的数组成员?

8yparm6h

8yparm6h1#

有一种方法可以得到你想要的东西。
您只需要多几个字节来存储字段的偏移量:

struct {
    uint8_t    slaveAddr;      /*!< The slave address byte */

    ptrdiff_t  modbus_off;
    ptrdiff_t  crc_off;

    uint8_t    data[];   /* this will hold all the magic stuff */
} rtuHead_t;

现在你需要自己做决定:你想把偏移量应用到结构体地址(a)上吗?或者可能是灵活数组(b)的地址?或者可能是偏移量本身的地址(c)?根据您的答案,访问数据的方式将略有不同。
(a)

crc = *(uint16_t *) ((char *) s + crc_off);

B)、

crc = *(uint16_t *) (s->data + crc_off);

c)、

crc = *(uint16_t *) ((char *) s + offsetof(rtuHead_t, crc_off) + crc_off);

由您来确保字段的对齐正确,并相应地设置偏移。
下面是一个示例程序,它利用这个(略有不同,但思想是相同的)在一个简单的结构中保存几个任意长的字符串。

$ cat flexi2.c 
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

struct s {
    int        _;
    ptrdiff_t  off[];
};

int
main(void)
{
    char      *p;
    struct s  *s;

    s = malloc(offsetof(struct s, off) +
               sizeof(ptrdiff_t) * 2 +
               sizeof("foobar") + sizeof("baz"));

    p = (char *) s + offsetof(struct s, off) + sizeof(ptrdiff_t) * 2;

    s->off[0] = p - (char *) s;
    p = stpcpy(p, "foobar") + 1;
    s->off[1] = p - (char *) s;
    p = stpcpy(p, "baz") + 1;

    puts((char *) s + s->off[0]);
    puts((char *) s + s->off[1]);

    free(s);
}
$ gcc-13 -Wall -Wextra -Werror -fanalyzer -O3 flexi2.c 
$ ./a.out 
foobar
baz
vfwfrxfs

vfwfrxfs2#

在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", offsetof(struct A, data1));
    printf("offset(b) = %zi\n", offsetof(struct A, b));
    printf("offset(data2) = %zi\n", offsetof(struct A, data2));
    printf("offset(c) = %zi\n", offsetof(struct A, c));
    return 0;
}

除了一些关于使用非ISO特性的警告外,它编译得很好,并产生预期的输出。

offset(a) = 0
offset(data1) = 4
offset(b) = 12
offset(data2) = 16
offset(c) = 96

问题是,此类型只能在block范围内定义,因此不能用于将参数传递给其他函数。
但是,它可以传递给一个嵌套函数,这是另一个GCC扩展。示例:

int main() {
   ... same as above

    // nested function
    int fun(struct A *a) {
        return a->c;
    }
    return fun(&p);
}
brccelvz

brccelvz3#

灵活数组成员必须是结构体的最后一个成员,包含灵活数组成员的结构体不能是数组或其他结构体的成员。
这种结构的预期用途是动态分配它,为其他成员加上灵活成员的0个或多个元素留出足够的空间。
您要做的是将一个结构体覆盖到内存缓冲区上,该缓冲区包含您希望通过访问成员来解析的数据包数据。在这种情况下这是不可能的,并且由于对齐和填充问题,通常这样做不是一个好主意。
正确的方法是编写一个函数,一次反序列化一个字段的数据包,并将结果放置在用户定义的结构中。

628mspwn

628mspwn4#

灵活数组成员只能放置在结构的末尾。这就是C标准6.7.2.1对它们的定义:
作为一种特殊情况,具有多个命名成员的结构的最后一个元素可能具有不完整的数组类型;这被称为“灵活阵列成员”。
但就具体情况而言,它们也是对错误问题的错误解决方案。错误的问题是“如何在C结构中存储可变大小的Modbus数据协议帧”?struct通常最好首先避免。不幸的是,我们C程序员几乎被洗脑了,在每一种情况下都使用struct,以至于我们毫不犹豫地声明了一个。
结构体有各种各样的问题,最值得注意的是对齐/填充问题,这只能通过非标准的扩展来解决,比如gcc __attribute__((__packed__))#pragma pack(1)。但是即使你使用了这些,你最终得到的是一个编译器仍然可能访问的块,而不是对齐的块--你只是告诉它删除填充“我知道我在做什么”。但是如果你继续用字访问内存,它可能是一个未对齐的访问。
然后是可变大小协议的问题。根据接收到的数据量一次又一次地调整内存块的大小,实际上除了膨胀和程序执行开销之外,并没有实现太多。这样做可以节省多少内存?大约10到100个字节?即使在低端MCU中也不算什么。因为你只需要同时在RAM中保存几帧。
事实证明,您将不得不分配足够的内存来存储出现过的最大帧,因为您的程序必须处理最坏的情况。然后你也可以从静态地分配那么多内存开始。更快,更安全,更确定。
然后还有一个问题,您似乎没有解决,即网络endianess。Modbus使用big endian,CRC以big endian计算。所以结构体末尾的uint16_t成员只是坐在那里制造问题。即使您决定使用一些非标准的GNUVLA扩展来调整每个帧的大小。
我建议你忘掉这些结构体。
快速,便携和安全的解决方案是简单地使用uint8_t frame [MAX];,其中MAX是帧可能具有的最大字节大小。使用一个结构体只是为了给帧中的一个特定字节给予一个变量名,实际上它本身并没有添加任何东西。您真正想要的是具有易于解释每个字节的作用的可读代码,而不是原始数据的匿名缓冲区。
这也可以在访问uint8_t数组时使用该数组的命名索引(例如enum)来完成。结构体版本frame.slave_addr = x;和数组版本frame[slave_addr] = x;在可读性、用途或生成的机器代码方面没有区别。(但前者可能会导致机器代码中的访问不对齐。)
无论如何,您都需要逐个字节地访问CRC,因为您首先需要使用CPU的endianess来计算它,然后将其转换为网络endianess。例如:

frame[fcs_high] = checksum >> 8; 
frame[fcs_low]  = checksum & 0xFF;

这段代码不像结构体那样依赖于CPU的字节序,结构体只在大端CPU上按预期工作。

相关问题