C 结构体的顺序是否有任何保证?

4

我广泛地使用了结构体,并看到了一些有趣的事情,特别是在指向结构体的指针中使用*value而不是value->first_value,其中first_value是第一个成员,那么*value是否安全?

此外,请注意由于对齐问题导致大小不能保证,对齐值是基于什么的,架构/寄存器大小?

我们对数据/代码进行对齐以实现更快的执行速度,我们可以让编译器不这样做吗?这样或许我们就可以保证某些关于结构体的属性,例如它们的大小?

当进行指向结构体成员的指针算术运算以查找成员偏移量时,如果是小端字节序则使用-,如果是大端字节序则使用+,或者这取决于编译器?

malloc(0)会分配什么?

以下代码仅用于教育/发现目的,不适用于生产环境。

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

int main()
{
    printf("sizeof(struct {}) == %lu;\n", sizeof(struct {}));
    printf("sizeof(struct {int a}) == %lu;\n", sizeof(struct {int a;}));
    printf("sizeof(struct {int a; double b;}) == %lu;\n", sizeof(struct {int a; double b;}));
    printf("sizeof(struct {char c; double a; double b;}) == %lu;\n", sizeof(struct {char c; double a; double b;}));

    printf("malloc(0)) returns %p\n", malloc(0));
    printf("malloc(sizeof(struct {})) returns %p\n", malloc(sizeof(struct {})));

    struct {int a; double b;} *test = malloc(sizeof(struct {int a; double b;}));
    test->a = 10;
    test->b = 12.2;
    printf("test->a == %i, *test == %i \n", test->a, *(int *)test);
    printf("test->b == %f, offset of b is %i, *(test - offset_of_b) == %f\n",
        test->b, (int)((void *)test - (void *)&test->b),
        *(double *)((void *)test - ((void *)test - (void *)&test->b))); // find the offset of b, add it to the base,$

    free(test);
    return 0;
}

运行gcc test.c,然后执行./a.out,我得到了以下结果:

sizeof(struct {}) == 0;
sizeof(struct {int a}) == 4;
sizeof(struct {int a; double b;}) == 16;
sizeof(struct {char c; double a; double b;}) == 24;
malloc(0)) returns 0x100100080
malloc(sizeof(struct {})) returns 0x100100090
test->a == 10, *test == 10 
test->b == 12.200000, offset of b is -8, *(test - offset_of_b) == 12.200000
更新 这是我的机器:

gcc --version

意思是查询gcc编译器的版本号。
i686-apple-darwin10-gcc-4.2.1 (GCC) 4.2.1 (Apple Inc. build 5666) (dot 3)
Copyright (C) 2007 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

uname -a

Darwin MacBookPro 10.8.0 Darwin Kernel Version 10.8.0: Tue Jun  7 16:33:36 PDT 2011; root:xnu-1504.15.3~1/RELEASE_I386 i386

6
啊!请一个问题一个问题地问。你在这里提出的两个问题没有任何关联,应该分别提出。"malloc(0)"分配什么? - dmckee --- ex-moderator kitten
5个回答

6

从6.2.5/20:

结构体类型描述了一组按顺序分配的非空成员对象(在某些情况下,还包括不完整数组),每个成员都有一个可选的名称和可能不同的类型。

回答:

特别是在指向结构体的指针value使用*value而不是value->first_value, 其中first_value是第一个成员,那么*value是安全的吗?

参见6.7.2.1/15:

15 在结构对象内部,非位域成员和位域所在的单元按照声明的顺序递增地具有地址。适当转换后,指向结构对象的指针指向其初始成员(如果该成员是位域,则指向其中包含的单位),反之亦然。结构对象内部可能有未命名的填充字节,但没有填充字节在其开头。1

然而,在结构体末尾或成员之间可能有填充字节。

在C语言中,malloc(0)是实现定义的。(作为旁注,这是C和C++之间有所不同的小事情之一。)

[1] 强调是我的。


连续分配的非空成员对象集,那么我可以安全地假设成员的顺序是有保证的吗?因为集合没有排序,对吧? - Samy Vilar
@samy.vilar:是的,顺序是有保证的。这里的术语“集合”并不是严格数学意义上的,而是表示一组具有类型的成员。 - dirkgently
结构体末尾可能会有填充字节(但成员之间不会有)。这是真的吗?为什么我的偏移量是8个字节?是因为它是小端序吗?数据对齐呢,编译器不会为了更快的访问而适当地填充每个成员吗? - Samy Vilar
是的,这是为了遵守对齐约束而做的。字节序和对齐是两个不同的概念。请参考【数据结构对齐】(http://en.wikipedia.org/wiki/Data_structure_alignment) 以获取详细信息。 - dirkgently
是的,我知道,但是-8字节的偏移量让我感到惊讶,因为第一个成员是int类型,我原以为会是4或-4。 - Samy Vilar
显示剩余2条评论

3
调用 malloc(0) 将返回一个指针,该指针可以安全地传递给 free() 至少一次。如果多次调用 malloc(0) 返回相同的值,则可以为每个这样的调用释放一次。显然,如果它返回 NULL,那么可以无限次数地将其传递给 free() 而不会产生影响。每次调用返回非空的 malloc(0) 应该通过使用返回的值调用 free() 来平衡。

没有回答问题。即使在“malloc(0)分配什么?”被编辑掉之前,这个回答也只提到了调用返回的内容。 - cHao
1
@cHao:它可能会分配一些东西,也可能不会。如果它返回NULL,则不会分配任何内容,但我认为没有确定的方法可以知道它是否分配了任何内容。将每个对malloc(0)的调用与返回指针的free()平衡,将确保malloc(0)分配的任何内容都将被释放,但是如果free()的实现方式是这样的,即当给定某个特定的非空指针时它会无害地什么也不做,而该指针并不代表实际分配,则malloc(0)可以返回这样的指针而不分配任何内容。 - supercat
看,现在这回答了问题。至少在编辑之前删除那部分的问题以及像那样的问题可以被回答。 :) - cHao
既然他回答了这个问题,我会把它加回来,谢谢。我猜这取决于操作系统如何处理0字节分配,这仍然很奇怪,假设malloc是连续分配的,我们也不能确定它分配了多少字节,malloc(0))返回0x100100080malloc(sizeof(struct {}))返回0x100100090,虽然我们不能确定这里到底发生了什么。 - Samy Vilar
@samy.vilar:我相信在任何允许这样做的实现中,sizeof(struct{})至少为1,不是吗? - supercat
@supercat 在Max OS X Snow Leopard上,使用gcc 4.2.1是0。虽然空结构体不是标准的C ;) ... - Samy Vilar

3
我曾广泛使用结构体,并看到了一些有趣的东西,特别是在指向结构体的指针value中,使用*value而不是value->first_value,其中first_value是第一个成员,那么*value是否安全? 是的,*value是安全的;它会产生一个结构体的副本,该结构体指向value。 但几乎可以肯定,它与*value->first_value具有不同的类型,因此*value的结果几乎总是与*value->first_value不同。

反例:

struct something { struct something *first_value; ... };
struct something data = { ... };
struct something *value = &data;
value->first_value = value;

在这种相对有限的情况下,*value*value->first_value将得到相同的结果。在这种情况下,类型将是相同的(即使值不同)。在一般情况下,*value*value->first_value的类型是不同的。

还要注意,由于对齐而不能保证大小,但对齐总是在寄存器大小上吗?

由于“寄存器大小”不是C语言中定义的概念,所以不清楚你在问什么。在没有使用编译指示(如#pragma pack或类似指示)的情况下,当读取(或写入)值时,结构的元素将被对齐以获得最佳性能。

我们对数据/代码进行对齐以实现更快的执行;我们可以告诉编译器不要这样做吗?因此,也许我们可以保证某些关于结构体的东西,比如它们的大小?

编译器负责struct类型的大小和布局。您可以通过精心设计和可能的#pragma pack或类似指示来影响它们。

当人们担心序列化数据(或者说,试图通过逐个处理结构元素来避免序列化数据)时,这些问题通常会出现。通常,我认为您最好编写一个函数来执行序列化,并从组件部分构建它。

在进行指向结构体成员的指针算术运算以定位成员偏移量时,如果是小端字节序,则进行减法;如果是大端字节序,则进行加法,还是这取决于编译器?

最好不要对struct成员进行指针算术运算。如果必须这样做,请使用<stddef.h>中的offsetof()宏正确处理偏移量(这意味着您不直接进行指针算术运算)。无论是大端字节序还是小端字节序,第一个结构元素始终位于最低地址。实际上,字节序与结构中不同成员的布局无关,它仅影响结构的(基本数据类型)成员内值的字节顺序。

C标准要求结构的元素按照它们定义的顺序排列;第一个元素位于最低地址,下一个元素位于更高的地址,以此类推。编译器不允许更改顺序。结构的第一个元素之前不能有填充。在结构的任何元素之后都可以有填充,以使编译器认为适当对齐。结构的大小是这样的,您可以分配(N×size)字节,这些字节已适当地对齐(例如通过malloc()),并将结果视为结构的数组。


你假设 *value->first_value 是一个指针,但它可能不是,而且它的类型也会有所不同,因此它总是被强制转换为适当的类型。我问这个问题是为了教育目的,理解底层工作原理总是一个好主意 :) ... - Samy Vilar
1
如果你有:struct something { struct something *first_value; ... };struct something data = { ... }; 以及 struct something *value = &data;value->first_value = value; ,那么通过 *value*value->first_value 会得到相同的结果。在这个方案下,类型将是相同的。总的来说,*value*value->first_value 的类型是不同的。 - Jonathan Leffler
顺便问一下,Dirk Gently的回答和评论怎么样了?“结构体末尾可能会有填充字节(但成员之间不会有填充字节)。”这是真的吗? - Samy Vilar
1
不带括号注释的句子是正确的。括号注释是虚假的。证人:struct InteriorPad { char c; double d; }; 在大多数系统上,cd之间将有7个字节的填充。 - Jonathan Leffler
好的,我认为你已经得到了答案,请更新这个句子:“是的,value是安全的,但是它保证value->first_value有不同的类型,因此结果会有所不同。” 我认为你使用嵌套结构证明它是错误的了? - Samy Vilar
显示剩余4条评论

2
如果你有一个内部结构体,那么如果它是外层结构体的第一个声明,则保证从相同地址开始。
所以在以下情况中,*valuevalue->first 访问的是同一地址的内存(但使用不同类型)。
struct St {
  long first;
} *value;

此外,结构体成员之间的顺序保证与声明顺序相同。
要调整对齐方式,可以使用编译器特定的指令或使用位域。
结构体成员的对齐通常基于在目标平台上访问各个成员的最佳方式。
另外,对于 malloc,它可能会在返回的地址附近保留一些记录,因此即使是零大小的内存,它也可以返回一个有效的地址(只是不要尝试通过返回的地址访问任何内容)。

这句话是C标准的一部分吗?此外,结构体成员之间的顺序保证与声明顺序相同。 - Samy Vilar
它必须是如此。C语言越低级,如果结构体在你使用的编译器和构建操作系统的编译器之间有不同的解释,那么它将会造成严重破坏。 - cHao
有趣的是,我一直认为结构体只是一种语言构造,与底层硬件架构无关,但我想当在操作系统和底层编译代码之间传输数据时,它们需要一些协调性。 - Samy Vilar
@samy.vilar - 在C语言(以及C++)中,语言结构往往与硬件架构密切相关(至少与它们创建时最常见的那些相关)。 - Attila
@Attila,那么结构体是否与底层硬件有共同的关系呢?因为我不明白,我可以看到char、short、int、address与硬件有关,因为我们有一个字节,我们可能有16、32、64位寄存器。 - Samy Vilar
1
@samy.vilar - 正如我之前提到的,对齐(因此“填充”)通常是基于硬件的。您正确地指出整个结构在硬件上没有特定的映射,但它的(最终原始的)成员确实有影响结构本身。 - Attila

0

了解结构体大小的工作方式非常重要。例如:

struct foo{
  int i;
  char c;
}

struct bar{
  int i;
  int j;
}

struct baz{
  int i;
  char c;
  int j;
}

sizeof(foo) = 8 bytes (32 bit arch)
sizeof(bar) = 8 bytes
sizeof(baz) = 12 bytes

这意味着结构体的大小和偏移量必须遵循两个规则:

1- 结构体必须是其第一个元素的倍数(为什么foo是8而不是5个字节)

2- 结构体元素必须从自身的倍数开始。(在baz中,int j不能从6开始,因此浪费了6、7和8字节的填充)


(我意识到这并不完全回答了你的问题,只是我经常在代码中看到的一个错误) - SetSlapShot
我非常确定C标准没有强制对齐要求。这意味着int j可以从第6个字节开始。这取决于体系结构是否允许这样做。 - cHao
哎呀,我的汇编语言课是骗人的吗?我正在阅读一些资料,如果证明是假的,我会删除它。 - SetSlapShot
1
我可能会提到这是“通常”这样做的方式(事实确实如此)。虽然不一定是这样,但足够普遍以值得一提。 - cHao
你有“结构体必须是其第一个元素的倍数”的来源吗?我认为struct { int64_t a; int32_t b; }可以是12个字节(不是8的倍数)。 - Matthew Flaschen
显示剩余2条评论

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