struct包装是确定性的吗?

例如,假设我在不同的项目中有两个等效的结构ab

 typedef struct _a { int a; double b; char c; } a; typedef struct _b { int d; double e; char f; } b; 

假设我没有使用像#pragma pack这样的指令,并且这些结构在同一个体系结构上的同一个编译器上编译,它们在variables之间是否有相同的填充?

编译器是确定性的; 如果不是,分开编译将是不可能的。 具有相同struct声明的两个不同的翻译单元将一起工作; 这由§6.2.7/ 1保证:兼容types和复合types 。

此外,同一平台上的两个不同的编译器应该互操作,尽pipe这不是标准保证的。 (这是一个执行问题的质量。)为了允许互操作性,编译器编写者就平台ABI(应用程序二进制接口)达成一致,该平台将包括如何表示复合types的精确规范。 这样,用一个编译器编译的程序就可以使用用不同的编译器编译的库模块。

但是你不只是对决定论感兴趣, 你也希望两种不同types的布局是一样的。

根据标准,两个structtypes是兼容的,如果它们的成员(按顺序)是兼容的,并且它们的标签和成员名称是相同的。 由于您的示例structs具有不同的标签和名称,即使它们的成员types是不兼容的,所以您不能使用另一个所需的名称。

标准允许标签和成员名称影响兼容性似乎很奇怪。 该标准要求结构的成员按声明顺序排列,所以名称不能改变结构内成员的顺序。 那么为什么它们会影响填充? 我不知道任何编译器在哪里,但是标准的灵活性是基于这样一个原则,即要求应该是保证正确执行的最低要求。 在翻译单元中不允许对不同标记的结构进行别名,因此不需要在不同的翻译单元之间宽恕它。 所以标准不允许。 (对于一个实现来说,在一个struct的填充字节中插入关于该types的信息是合理的,即使它需要确定性地添加填充来为这些信息提供空间。唯一的限制是填充不能放在第一个成员之前的struct 。)

平台ABI可能会指定一个组合types的布局,而不参考它的标签或成员名称。 在一个特定的平台上,有一个平台ABI,它有这样一个规范和一个编译器来logging符合平台ABI,你可以摆脱别名,尽pipe它在技术上是不正确的,显然前提条件使它不可移植。

C标准本身没有提到,所以根据原则,你不能确定。

但是 :很可能你的编译器会遵守某些特定的ABI,否则与其他库和操作系统的通信将是一场噩梦。 在最后一种情况下,ABI通常会规定包装如何工作。

例如:

  • 在x86_64 Linux / BSD上, SystemV AMD64 ABI是参考。 这里(§3.1)针对每一个原始处理器数据types详细说明了与Ctypes,其大小和alignment要求的对应关系,并说明了如何使用这些数据来组成位域,结构和联合的存储器布局; 一切 (除了填充的实际内容) 都是确定性的 。 对于许多其他体系结构也是如此,请参阅这些链接 。

  • ARM 推荐其EABI用于其处理器,而Linux和Windows通常都遵循这个原则。 在“ARM体系结构文档的过程调用标准”(§4.3)中指定了聚合alignment方式。

  • 在Windows上没有跨厂商的标准,但是VC ++实际上决定了ABI,几乎所有的编译器都遵循这个标准。 它可以在这里findx86_64, 这里是ARM(但对于这个问题的兴趣部分,它只是指的是ARM EABI)。

任何理智的编译器都会为这两个结构产生相同的内存布局。 编译器通常被写成完全确定性的程序。 非确定性需要明确而刻意地join,而我却没有看到这样做的好处。

但是,这不允许您将struct _a*转换为struct _b*并通过两者来访问其数据。 Afaik,即使内存布局是相同的,这仍然会违反严格的别名规则,因为它将允许编译器通过struct _a*对访问进行重新sorting,并通过struct _b*进行访问,这会导致不可预知的未定义行为。

他们将有相同的variables之间的填充?

在实践中,他们大多喜欢有相同的内存布局。

从理论上讲,由于标准并没有多说如何在对象上使用填充,所以不能在元素之间的填充上做任何事情。

另外,我什至不知道你为什么要知道/假设有关结构成员之间的填充。 只需编写标准的,兼容的C代码,你会没事的。

你不能在不同的系统上用C语言确定地确定结构或联合的布局。

虽然很多时候,不同编译器生成的布局看起来都是一样的,但是必须考虑这样一种情况:由编译器devise的实用性和function性方便性在标准的程序员留给select自由的范围内,有效。

C11标准ISO / IEC 9899: 201x与以前的标准几乎没有变化,在第6.7.2.1节中明确指出结构和联合说明符

结构体或联合体对象中的每个非位域成员都按照适合其types的实现定义的方式alignment。

即使最糟糕的情况是位域的情况下,程序员仍然有很大的自主权:

一个实现可以分配任何足够大的可寻址存储单元来容纳一个位域。 如果剩余足够的空间,紧接在结构中的另一个比特字段之后的比特字段应被打包到相同单元的相邻比特中。 如果剩余空间不足,则将不合适的比特字段放入下一个单元,或与相邻单元重叠是实现定义的。 一个单元内的位域分配顺序(从高到低或从低到高)是实现定义的。 未指定可寻址存储单元的alignment方式。

只要计算术语“实现定义”和“未指定”的次数。

同意检查编译器版本,机器和目标体系结构每个运行之前使用在不同的系统上生成的结构或联合是难以承受的,你应该得到一个体面的答案你的问题。

现在让我们说,是的,有一个解决办法。

清楚的是,这不是绝对的解决scheme ,但是当数据结构交换在不同系统之间共享时,可以find一种常见的方法:在值1(标准字符大小)上打包结构元素。

使用包装和精确的结构定义可以产生足够可靠的声明,可以在不同的系统上使用。 包装强制编译器删除实现定义的alignment,从而减less了由于标准而导致的最终不兼容。 此外避免使用位域,您可以删除残留实现相关的不一致。 最后,由于缺lessalignment,访问效率可以通过在元素之间手动添加一些虚拟声明来重新创build,这样就可以强制每个字段以正确alignment的方式进行。

作为一个剩余的情况,你必须考虑一些编译器添加的结构上的填充,但是由于没有关联的有用数据,你可以忽略它(除非dynamic空间分配,但是你可以处理它)。

ISO C说,如果不同翻译单元中的两个structtypes具有相同的标签和成员,则它们是兼容的。 更确切地说,这里是C99标准的确切文字:

6.2.7兼容types和复合types

如果types相同,则两种types具有兼容types。 用于确定两种types是否兼容的其他规则在types说明符的6.7.2节中,types限定符的6.7.3节以及用于声明符的6.7.5节中进行了描述。 此外,如果标签和成员满足以下要求,则在单独的翻译单元中声明的两种结构,联合或枚举types是兼容的:如果一个标签声明,另一个标签声明相同。 如果两者都是完整的types,那么下列附加要求适用:它们的成员之间应该是一对一的对应关系,使得每对对应的成员被声明为兼容types,并且如果对应成员中的一个成员是用名称声明,另一个成员声明为相同的名称。 对于两个结构,相应的成员应按照相同的顺序进行声明。 对于两个结构或联合,相应的位域应具有相同的宽度。 对于两个枚举,相应的成员应具有相同的值。

如果我们从“标签或成员名称可能影响填充是什么”的angular度来解释它,这似乎很奇怪。 但是,基本上,规则是尽可能严格的,尽可能允许常见的情况:多个翻译单元通过头文件共享结构声明的确切文本 。 如果程序遵循更宽松的规则,那么他们没有错; 他们只是不依赖于标准的行为要求,而是来自其他地方。

在你的例子中,你违反了语言规则,只有结构上的等价,而不是等价的标签和成员的名字。 实际上,这并没有被强制执行。 不同翻译单元中具有不同标签和成员名称的结构types事实上在物理上是兼容的。 各种各样的技术依赖于这个,比如从非C语言到C库的绑定。

如果你的项目都是用C(或C ++)编写的,那么尝试把定义放到一个通用的头文件中可能是值得的。

对版本控制问题(如大小字段)采取一些防御措施也是一个好主意:

 // Widely shared definition between projects affecting interop! // Do not change any of the members. // Add new ones only at the end! typedef struct a { size_t size; // of whole structure int a; double b; char c; } a; 

这个想法是,构造一个实例的人必须初始化size字段为sizeof (a) 。 然后,当对象被传递到另一个软件组件(可能来自另一个项目)时,它可以根据 sizeof (a)来检查大小。 如果size字段较小,那么它知道构造a的软件正在使用较less成员的旧声明。 因此,不存在的成员不能被访问。

任何特定的编译器都应该是确定性的,但是在任何两个编译器之间,甚至在具有不同编译选项的同一个编译器之间,甚至在相同编译器的不同版本之间,所有的注释都是closures的。

如果你不依赖于结构的细节,或者如果你这样做,你会更好,你应该embedded代码来在运行时检查结构实际上是否依赖于你。

一个很好的例子就是最近从32位体系结构转换到64位体系结构,即使你没有改变结构中使用的整数大小,部分整数的缺省打包也会改变。 先前连续3个32位整数将完美包装,现在他们打包到两个64位插槽。

你不可能预测未来可能发生的变化。 如果您依赖于语言无法保证的细节,例如结构打包,则应该在运行时validation您的假设。

是。 你应该总是假定你的编译器有确定的行为。

[编辑]从下面的评论,很明显有很多Java程序员阅读上面的问题。 让我们清楚:C结构不会在对象文件,库或dll中生成任何名称,散列或类似内容。 C函数签名也不会引用它们。 这意味着,成员的名字可以随时改变 – 真的! – 提供了成员variables的types和顺序是一样的。 在C中,这个例子中的两个结构是等价的,因为包装不会改变。 这意味着下面的滥用在C中是完全有效的,而且在一些最广泛使用的库中肯定存在更糟的滥用。

没有人敢在C ++中做下面的任何事情

 /* the 3 structures below are 100% binary compatible */ typedef struct _a { int a; double b; char c; } typedef struct _b { int d; double e; char f; } typedef struct SOME_STRUCT { int my_i; double my_f; char my_c[1]; } struct _a a = { 1, 2.5, 'z' }; struct _b b; /* the following is valid, copy b -> a */ *(SOME_STRUCT*)&a = *(SOME_STRUCT*)b; assert((SOME_STRUCT*)&a)->my_c[0] == bf); assert(ac == bf); /* more generally these identities are always true. */ assert(sizeof(a) == sizeof(b)); assert(memcmp(&a, &b, sizeof(a)) == 0); assert(pure_function_requiring_a(&a) == pure_function_requiring_a((_a*)&b)); assert(pure_function_requiring_b((b*)&a) == pure_function_requiring_b(&b)); function_requiring_a_SOME_STRUCT_pointer(&a); /* may generate a warning, but not all compiler will */ /* etc... the name space abuse is limited to the programmer's imagination */