【问题标题】:Is using flexible array members in C bad practice?在 C 中使用灵活的数组成员是不好的做法吗?
【发布时间】:2010-09-19 19:19:51
【问题描述】:

我最近读到,在 C 语言中使用灵活的数组成员是一种糟糕的软件工程实践。但是,该声明没有任何论据支持。这是公认的事实吗?

(Flexible array members 是 C99 中引入的一项 C 功能,可以将最后一个元素声明为未指定大小的数组。例如:)

struct header {
    size_t len;
    unsigned char data[];
};

【问题讨论】:

    标签: c arrays data-structures flexible-array-member


    【解决方案1】:

    使用 goto 是一种糟糕的软件工程实践,这是一个公认的“事实”。这并不成立。有时 goto 很有用,特别是在处理清理和从汇编程序移植时。

    灵活的数组成员让我印象深刻,它有一个主要用途,在我脑海中浮现,它是在 RiscOS 上映射遗留数据格式,如窗口模板格式。大约 15 年前,它们在这方面会非常有用,而且我敢肯定,在处理此类事情的人仍然会发现它们很有用。

    如果使用灵活的数组成员是不好的做法,那么我建议我们都去告诉 C99 规范的作者。我怀疑他们可能有不同的答案。

    【讨论】:

    • goto 在我们想要使用非递归实现来实现算法的递归实现时也很有用,因为递归可能会增加编译器的额外开销。
    • @pranavk 那么你应该使用while
    • 网络编程是另一种,你有一个结构体的头部,而数据包(或你所在的层中调用的......)作为灵活的数组。调用下一层,你剥离头部,并传递数据包。对网络堆栈中的每一层执行此操作。 (您将下层的数据从下层恢复到您所在层的结构)
    • @pranavk goto 不是 for 循环。
    • “有时候 goto 很有用” 瞧,这就是为什么我有时会在想一些刚刚学习编程的孩子会求助于 StackOverflow 来学习最佳实践时不寒而栗的原因。
    【解决方案2】:

    请仔细阅读此答案下方的评论

    随着 C 标准化的发展,没有理由再使用 [1]。

    我之所以不这样做,是因为为了使用此功能而将您的代码绑定到 C99 是不值得的。

    关键是你总是可以使用以下成语:

    struct header {
      size_t len;
      unsigned char data[1];
    };
    

    这是完全可移植的。然后在为数组data 中的n 个元素分配内存时可以考虑1:

    ptr = malloc(sizeof(struct header) + (n-1));
    

    如果您出于任何其他原因已经需要 C99 来构建您的代码,或者您的目标是特定的编译器,我认为没有什么坏处。

    【讨论】:

    • 谢谢。我留下了 n-1,因为它可能不会用作字符串。
    • 'following idiom' 不是完全可移植的,这就是灵活数组成员被添加到 C99 标准的原因。
    • @Remo.D: 次要问题:n-1 没有准确说明额外分配,因为对齐:在大多数 32 位机器上,sizeof(struct header) 将为 8(到保持 4 的倍数,因为它有一个 32 位字段,它喜欢/需要这种对齐方式)。 “更好”的版本是:malloc(offsetof(struct header, data) + n)
    • 在 C99 中使用 unsigned char data[1] 是不可移植的,因为 ((header*)ptr)->data + 2 -- 即使分配了足够的空间 -- 创建一个指向长度为 1 的数组对象之外的指针(而不是哨兵对象)结束)。但是根据 C99 6.5.6p8,“如果指针操作数和结果都指向同一个数组对象的元素,或者超过数组对象的最后一个元素,则评估不会产生溢出;否则,行为未定义”(强调)。灵活数组(6.7.2.2p16)就像一个填充分配空间的数组,不会在这里碰到 UB。
    • *警告: 使用[1] 已被证明会导致 GCC 生成错误代码:lkml.org/lkml/2015/2/18/407
    【解决方案3】:

    不,在 C 语言中使用 flexible array members 是不错的做法。

    此语言功能首先在 ISO C99 6.7.2.1 (16) 中标准化。在随后的修订版 ISO C11 中,它在第 6.7.2.1 (18) 节中指定。

    你可以这样使用它们:

    struct Header {
        size_t d;
        long v[];
    };
    typedef struct Header Header;
    size_t n = 123; // can dynamically change during program execution
    // ...
    Header *h = malloc(sizeof(Header) + sizeof(long[n]));
    h->n = n;
    

    或者,您可以这样分配:

    Header *h = malloc(sizeof *h + n * sizeof h->v[0]);
    

    请注意,sizeof(Header) 包括最终的填充字节,因此,以下分配不正确,可能会导致缓冲区溢出:

    Header *h = malloc(sizeof(size_t) + sizeof(long[n])); // invalid!
    

    具有灵活数组成员的结构将其分配数量减少了 1/2,即,您只需 1 个结构对象而不是 2 个分配。这意味着更少的工作量和更少的内存分配器簿记开销占用的内存。此外,您可以为一个额外的指针保存存储空间。因此,如果您必须分配大量此类结构实例,则可以显着提高程序的运行时和内存使用率(通过一个常数因子)。

    与此相反,对产生未定义行为的灵活数组成员使用非标准化构造(例如long v[0];long v[1];)显然是不好的做法。因此,应该避免任何未定义的行为。

    自从 ISO C99 于 1999 年发布以来,20 多年前,争取 ISO C89 兼容性是一个软弱的论据。

    【讨论】:

      【解决方案4】:

      你的意思是……

      struct header
      {
       size_t len;
       unsigned char data[];
      }; 
      

      在 C 中,这是一个常见的习惯用法。我想很多编译器也接受:

        unsigned char data[0];
      

      是的,这很危险,但话又说回来,它实际上并不比普通的 C 数组更危险 - 即非常危险 ;-) 。小心使用它,并且仅在您真正需要未知大小的数组的情况下使用。确保你 malloc 并正确释放内存,使用类似:-

        foo = malloc(sizeof(header) + N * sizeof(data[0]));
        foo->len = N;
      

      另一种方法是使数据只是指向元素的指针。然后,您可以根据需要将数据重新分配到正确的大小。

        struct header
          {
           size_t len;
           unsigned char *data;
          }; 
      

      当然,如果您问的是 C++,这两种方法都是不好的做法。然后,您通常会改用 STL 向量。

      【讨论】:

      • 前提是您在支持 STL 的系统上编码!
      • C++ 但没有 STL...这不是一个令人愉快的想法!
      • 命名一个接受零长度数组的编译器。 (如果答案是 GCC,现在换个名字。)它不受 C 标准的认可。
      • 我曾在 C++ 环境中工作,但没有 STL 环境——我们有自己的容器,它提供了常用功能,但没有 STL 迭代器系统的全部通用性。它们更容易理解并且具有良好的性能。然而,这是在 2001 年。
      • @JonathanLeffler 被 GCC 和 Clang 接受,它涵盖了当今使用的三个主要编译器中的两个。 (MSVC 是另一个大平台,它只在一个平台上真正相关——公认非常常见的平台。)
      【解决方案5】:

      我见过这样的事情: 来自 C 接口和实现。

        struct header {
          size_t len;
          unsigned char *data;
      };
      
         struct header *p;
         p = malloc(sizeof(*p) + len + 1 );
         p->data = (unsigned char*) (p + 1 );  // memory after p is mine! 
      

      注意:数据不必是最后一个成员。

      【讨论】:

      • 确实有这样的好处,data 不必是最后一个成员,但每次使用 data 时都会产生额外的取消引用。灵活的数组将取消引用替换为与主结构指针的恒定偏移量,这在一些特别常见的机器上是免费的,而在其他地方很便宜。
      • @R.. 虽然考虑到目标地址必然是直接在指针后面的字节,但它大约 100% 保证已经在 L1 缓存中,给出整个取消引用类似半个周期的开销。然而,这里的重点是灵活数组是一个更好的主意。
      • 使用unsigned char *p->data = (unsigned char*) (p + 1 ) 可以。然而,double complex *p->data = (double complex *) (p + 1 ) 可能会导致对齐问题。
      • 这个答案在技术上是无关紧要的,因为它做了一些不同的事情(它在内存中以不同的方式排列数据)。虽然它描述的模式通常很有用,但这并不意味着它可以替代其他模式。
      【解决方案6】:

      附带说明,为了 C89 兼容性,应该像这样分配这样的结构:

      struct header *my_header
        = malloc(offsetof(struct header, data) + n * sizeof my_header->data);
      

      或者使用宏:

      #define FLEXIBLE_SIZE SIZE_MAX /* or whatever maximum length for an array */
      #define SIZEOF_FLEXIBLE(type, member, length) \
        ( offsetof(type, member) + (length) * sizeof ((type *)0)->member[0] )
      
      struct header {
        size_t len;
        unsigned char data[FLEXIBLE_SIZE];
      };
      
      ...
      
      size_t n = 123;
      struct header *my_header = malloc(SIZEOF_FLEXIBLE(struct header, data, n));
      

      将 FLEXIBLE_SIZE 设置为 SIZE_MAX 几乎可以确保这将失败:

      struct header *my_header = malloc(sizeof *my_header);
      

      【讨论】:

      • 过于复杂,如果需要的话,使用 [1] 来兼容 C89 并没有任何好处......
      • 优化编译器可以正确假设长度为 1 的数组的索引必须为零。轰隆隆!
      【解决方案7】:

      结构体的使用方式有时存在一些缺点,如果您不考虑其中的含义,可能会很危险。

      对于你的例子,如果你启动一个函数:

      void test(void) {
        struct header;
        char *p = &header.data[0];
      
        ...
      }
      

      然后结果是不确定的(因为没有为数据分配存储空间)。您通常会意识到这一点,但在某些情况下,C 程序员可能习惯于将值语义用于结构,这会以各种其他方式分解。

      例如,如果我定义:

      struct header2 {
        int len;
        char data[MAXLEN]; /* MAXLEN some appropriately large number */
      }
      

      然后我可以简单地通过赋值复制两个实例,即:

      struct header2 inst1 = inst2;
      

      或者如果它们被定义为指针:

      struct header2 *inst1 = *inst2;
      

      但这不适用于灵活的数组成员,因为它们的内容不会被复制。您想要的是动态 malloc 结构的大小并使用 memcpy 或等效项复制数组。

      struct header3 {
        int len;
        char data[]; /* flexible array member */
      }
      

      同样,编写一个接受struct header3 的函数将不起作用,因为函数调用中的参数再次按值复制,因此您将得到的可能只是灵活数组成员的第一个元素。

       void not_good ( struct header3 ) ;
      

      这并不是一个坏主意,但您必须牢记始终动态分配这些结构,并且只将它们作为指针传递。

       void good ( struct header3 * ) ;
      

      【讨论】:

        猜你喜欢
        • 2011-02-10
        • 1970-01-01
        • 1970-01-01
        • 2013-04-25
        • 2011-03-04
        • 1970-01-01
        • 1970-01-01
        • 2012-08-17
        • 1970-01-01
        相关资源
        最近更新 更多