这个黑客攻击是否符合标准?

8
这就像结构体黑客一样。根据C标准,它是有效的吗?
 // error check omitted!

    typedef struct foo {
       void *data;
       char *comment;
       size_t num_foo;
    }foo;

    foo *new_Foo(size_t num, blah blah)
    {
        foo *f;
        f = malloc(num + sizeof(foo) + MAX_COMMENT_SIZE );
        f->data = f + 1;            // is this OK?
        f->comment = f + 1 + num;
        f->num_foo = num;
        ...
        return f;

}
6个回答

6

是的,这是完全有效的。我强烈建议在避免不必要的额外分配(以及它们所带来的错误处理和内存碎片化)时这样做。其他人可能有不同的意见。

顺便说一句,如果您的数据不是 void * ,而是您可以直接访问的东西,那么声明您的结构体会更容易(并且更有效,因为它节省了空间并避免了额外的间接性),如下所示:

struct foo {
    size_t num_foo;
    type data[];
};

分配所需数据的空间。在C99中,[]语法是有效的,因此为了与C89兼容,您应该使用[1],但这可能会浪费一些字节。


如你所知,但也值得指出的是,在结构体末尾的可变长度数组成员只适用于具有一个可变长度项的情况。而对于这个问题,有两个可变长度的项(datacomment),无法直接使用该技术。 - Jonathan Leffler
在这种情况下,将“comment”存储为普通的“malloc()”分配的字符串。 - Donal Fellows
顺便说一句,在添加评论字段之前,我已经给出了这个答案。添加评论后,请遵循Donal的建议。 - R.. GitHub STOP HELPING ICE

4

你质疑的那行代码是有效的 - 正如其他人所说。

有趣的是,下一行代码在语法上是有效的,但并不能给你想要的答案(除非 num == 0 的情况下)。

typedef struct foo
{
   void *data;
   char *comment;
   size_t num_foo;
} foo;

foo *new_Foo(size_t num, blah blah)
{
    foo *f;
    f = malloc(num + sizeof(foo) + MAX_COMMENT_SIZE );
    f->data = f + 1;            // This is OK
    f->comment = f + 1 + num;   // This is !!BAD!!
    f->num_foo = num;
    ...
    return f;
}
< p > f + 1 的值是一个 foo *(通过赋值隐式地强制转换为 void *)。

f + 1 + num 的值也是一个 foo *;它指向第 num+1foo

你可能想到的是:

foo->comment = (char *)f->data + num;

或者:

foo->comment = (char *)(f + 1) + num;

请注意,虽然GCC允许您将num添加到void指针中,并将其视为sizeof(void)==1,但C标准并不允许您这样做。

1

那是一个旧游戏,通常形式如下

struct foo {
   size_t size
   char data[1]
}

然后分配所需大小的空间,并将数组用作所需大小。

这是有效的,但如果可能的话,我鼓励您找到另一种方法:有很多机会搞砸它。


这个老技巧根据标准来说并不严格有效 - Nyan
不,它不是,但它将与任何符合malloc的实现一起工作。 ::思考一下病态分段内存体系结构:: 几乎任何符合条件的malloc - dmckee --- ex-moderator kitten
1
这是标准的其他要求的结果,例如如果一个结构体与另一个结构体的初始部分匹配,则通过2访问这些元素是兼容的。 C99使用灵活的数组符号“[]”明确支持它,但旧的“[1]”方法也必定可以工作! - R.. GitHub STOP HELPING ICE
2
@dmckee,任何病态的东西都不可能符合标准。标准要求malloc返回的内存必须作为“unsigned char”数组可访问,并且它必须适当地对齐于任何类型。 - R.. GitHub STOP HELPING ICE
委员会曾经讨论过这个问题,并决定它在C89中并没有定义。访问超出数组定义大小的行为会导致未定义的行为。虽然很少见,但编译器可能进行范围检查,并在尝试访问whatever[0]之后的内存时抛出某种操作系统异常(或其他异常)。 - Jerry Coffin
显示剩余5条评论

1
是的,这个黑客的一般想法是有效的,但至少就我的阅读而言,你没有完全正确地实现它。你已经做得很对了:
    f = malloc(num + sizeof(foo) + MAX_COMMENT_SIZE );
    f->data = f + 1;            // is this OK?

但这是错误的:

    f->comment = f + 1 + num;

由于ffoo *,所以f+1+num是根据sizeof(foo)计算的——也就是说,它相当于说f[1+num]——它(试图)索引到数组中的第1+numfoo。我非常确定这不是你想要的。当你分配数据时,你传递的是sizeof(foo)+num+MAX_COMMENT_SIZE,所以你分配空间的大小是num char,而你(可能)想要的是将f->comment指向内存中f->data后面num char的位置,更像是这样:

f->comment = (char *)f + sizeof(foo) + num;

f转换为char *会强制使用char而不是foo进行数学计算。

另一方面,由于您始终为comment分配MAX_COMMENT_SIZE,我可能会简化事情(相当)并使用类似于此的东西:

typedef struct foo {
   char comment[MAX_COMMENT_SIZE];
   size_t num_foo;
   char data[1];
}foo;

然后像这样分配它:

foo *f = malloc(sizeof(foo) + num-1);
f->num_foo = num;

而且它可以在没有任何指针操作的情况下工作。如果您有一个C99编译器,可以稍微修改一下:

typedef struct foo {
   char comment[MAX_COMMENT_SIZE];
   size_t num_foo;
   char data[];
}foo;

并分配:

foo *f = malloc(sizeof(foo) + num);
f->num_foo = num;

这样做的额外优点是标准实际上认可它,尽管在这种情况下,优点相当小(我相信带有data [1]版本将与现有的每个C89/90编译器一起工作)。


你和我同时注意到了这一点——我们并行地撰写了答案。 - Jonathan Leffler
@Jonathan Leffler:是的——尤其是对于像我这样冗长的回答,这几乎是不可避免的… - Jerry Coffin

0

我宁愿使用一些函数来动态分配数据并正确释放它。

仅使用此技巧可以节省初始化数据结构的麻烦,但可能会导致非常严重的问题(请参见Jerry的评论)。

我会做类似这样的事情:

typedef struct foo {
       void *data;
       char *comment;
       size_t num_foo;
    }foo;
foo *alloc_foo( void * data, size_t data_size, const char *comment)
{
   foo *elem = calloc(1,sizeof(foo));
   void *elem_data = calloc(data_size, sizeof(char));
   char *elem_comment = calloc(strlen(comment)+1, sizeof(char));
   elem->data = elem_data;
   elem->comment = elem_comment;

   memcpy(elem_data, data, data_size);
   memcpy(elem_comment, comment, strlen(comment)+1);
   elem->num_foo = data_size + strlen(comment) + 1;
}

void free_foo(foo *f)
{
   if(f->data)
      free(f->data);
   if(f->comment)
      free(f->comment);
   free(f);
}

请注意,我没有对数据有效性进行检查,我的分配可以进行优化(通过存储的长度值替换strlen()调用)。
在我看来,这种行为更加安全... 但可能会导致数据散布。

0

另一个可能的问题可能是对齐。

如果您只是使用malloc分配内存给f->data,那么您可以安全地将void*转换为double*并用它来读写double(前提是num足够大)。不过在您的示例中,您不能再这样做,因为f->data可能未正确对齐。例如,要将double存储在f->data中,您需要使用memcpy等复制函数而不是简单的类型转换。


使用我的方法(将数组放在最后)可以自动解决对齐问题。也可以通过分配足够的额外空间来解决,以便可以将f->data向上舍入到下一个sizeof(double)或所需类型的倍数。 - R.. GitHub STOP HELPING ICE

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接