【问题标题】:Weird behaviour with variable length arrays in struct in CC中结构中可变长度数组的奇怪行为
【发布时间】:2016-11-07 03:34:21
【问题描述】:

我遇到了一些人称之为“结构黑客”的概念,我们可以在结构中声明一个指针变量,如下所示:

struct myStruct{
    int data;
    int *array;
};

之后,当我们在main() 函数中使用mallocstruct myStruct 分配内存时,我们可以在同一步骤中同时为int *array 指针分配内存,如下所示:

struct myStruct *p = malloc(sizeof(struct myStruct) + 100 * sizeof(int));

p->array = p+1;

而不是

struct myStruct *p = malloc(sizeof(struct myStruct));

p->array = malloc(100 * sizeof(int));

假设我们想要一个大小为 100 的数组。

据说第一个选项更好,因为我们将获得一个连续的内存块,我们可以通过一次调用 free() 来释放整个块,而在后一种情况下调用两次。

实验,我写了这个:

#include<stdio.h>
#include<stdlib.h>

struct myStruct{
    int i;
    int *array;
};

int main(){
    /* I ask for only 40 more bytes (10 * sizeof(int)) */

    struct myStruct *p = malloc(sizeof(struct myStruct) + 10 * sizeof(int)); 

    p->array = p+1; 

    /* I assign values way beyond the initial allocation*/
    for (int i = 0; i < 804; i++){
        p->array[i] = i;
    }

    /* printing*/
    for (int i = 0; i < 804; i++){
        printf("%d\n",p->array[i]);
    }

    return 0;
}

我能够毫无问题地执行它,没有任何分段错误。我觉得很奇怪。

我还了解到 C99 有一条规定,我们可以在结构中声明 int *array,而不是在结构中声明 int array[],而我这样做了,仅对结构使用 malloc(),例如

struct myStruct *p = malloc(sizeof(struct myStruct));

并像这样初始化数组[]

p->array[10] = 0; /* I hope this sets the array size to 10 
                    and also initialises array entries to 0 */

但是又一次奇怪的是,我能够访问和分配超出数组大小的数组索引并打印条目:

for(int i = 0; i < 296; i++){ // first loop
    p->array[i] = i;
}

for(int i = 0; i < 296; i++){ // second loop
    printf("%d\n",p->array[i]);
}

在打印p-&gt;array[i]i = 296 之后,它给了我一个分段错误,但显然它在分配i = 9 之外没有问题。 (如果我在上面的第一个 for 循环中将 'i' 增加到 300,我会立即遇到分段错误,并且程序不会打印任何值。)

关于发生了什么的任何线索?是未定义的行为还是什么?

编辑:当我使用命令编译第一个 sn-p 时

cc -Wall -g -std=c11 -O    struct3.c   -o struct3

我收到了这个警告:

 warning: incompatible pointer types assigning to 'int *' from
  'struct str *' [-Wincompatible-pointer-types]
    p->array = p+1;

【问题讨论】:

  • 你仍然需要为整数分配内存。
  • 您的代码中没有可变长度数组。您所做的称为 flxeible 数组成员 (FAM)。你认为malloc 应该如何知道你希望这个数组包含多少个元素?
  • 我不会给你怎么做,因为你可以很容易地确定你是否考虑一下而不是专心询问。就是这样:您的问题中已经显示了所有必要的信息。
  • @WeatherVane:数组的长度为 1 个条目。超越边界的解除引用绝对是 UB。
  • @tectonicfury:如果您不了解所有含义,则永远不要仅仅为了使编译器静音而进行强制转换。如果你认为 UB 会因演员而神奇地消失:好吧,你错了!

标签: c struct


【解决方案1】:

是的,您在此处看到的是未定义行为的示例。

写入超出分配数组的末尾(又名缓冲区溢出)是未定义行为的一个很好的例子:它通常看起来“正常工作”,而其他时候它会崩溃(例如“分段错误”)。

一个低级的解释:内存中的控制结构与分配的对象有一定距离。如果您的程序发生大缓冲区溢出,则更有可能损坏这些控制结构,而对于较小的溢出,它将损坏一些未使用的数据(例如填充)。然而,在任何情况下,缓冲区溢出都会引发未定义的行为。

第一种形式的“struct hack”也调用未定义的行为(如警告所示),但属于特殊类型 - 在大多数编译器中,几乎可以保证它始终正常工作。但是,它仍然是未定义的行为,因此不建议使用。为了批准它的使用,C 委员会发明了这种“灵活的数组成员”语法(您的第二种语法),它保证可以工作。

只是为了说清楚-分配给数组的元素永远不会为该元素分配空间(至少在C中不是)。在 C 中,当分配给一个元素时,它应该已经被分配了,即使数组是“灵活的”。您的代码在分配内存时应该知道要分配多少。如果您不知道要分配多少,请使用以下技术之一:

  • 分配一个上限: struct myStruct{ int data; int array[100]; // you will never need more than 100 numbers };
  • 使用realloc
  • 使用链表(或任何其他复杂的数据结构)

【讨论】:

  • 谢谢。我在编译第一个 sn-p 时得到的警告在我投射 p-&gt;array = (int *)(p + 1); 时就消失了,所以显然它更多是由于 UB 而不是警告。
  • 警告永远不会引起问题;他们表明存在问题。当您“知道自己在做什么”时,使用强制转换是消除警告的好方法。如果你有 C99 编译器,最好使用不需要强制转换的语法——这是更安全的代码;也不那么难看。
  • 详细说明:兼容现代标准(目前为 2011 年,又名 C11)的编译器也可以。无需使用旧的 1999 版本(又名 C99)标准。
【解决方案2】:

您所说的“结构黑客”确实是一种黑客行为。这不值得 IMO。

p-&gt;array = p+1;

会在许多需要显式转换的编译器上给您带来问题:

p-&gt;array = (int *) (p+1);

我能够毫无问题地执行它,没有任何分段错误。我觉得很奇怪。

这是未定义的行为。您正在访问堆上的内存,许多编译器和操作系统不会阻止您这样做。但是使用它是非常糟糕的做法。

【讨论】:

  • 问题是我尝试了另一种选择(我知道),它不涉及指针但使用了灵活的数组成员。我不是很想使用这个 hack,但是因为我遇到的第二种选择也不是很有帮助,所以我很感兴趣。
猜你喜欢
  • 2014-09-10
  • 2014-11-14
  • 1970-01-01
  • 2023-03-04
  • 1970-01-01
  • 2014-04-30
  • 2014-03-14
相关资源
最近更新 更多