发件人: esr@netaxs.com (Eric Raymond)
新闻组: comp.lang.c,comp.std.c,comp.arch
主题: 欢迎对 C-FAQ 提交的内容提出批评
后续: comp.lang.c
日期: 1994 年 9 月 21 日 22:52:16 GMT
消息 ID: <35qdf0$7iq@netaxs.com>

我正在为 Steve Summit 的 C-FAQ 编写这个。欢迎批评。交叉发布到 comp.arch 是因为它对各种架构上的标准对齐条件做了一些断言。所有更正都乐于接受。


问:什么是“对齐”,它如何影响我的复合 C 类型的布局?

答:在大多数较旧的字(word)导向型机器的编译器上,除了 char 数组的组成部分之外,每个 C 数据项都必须从一个是机器字大小倍数的字节地址开始(也就是说,每种类型都必须“字对齐”)。随着古老的 16 位和 36 位机器的消亡,这种实现方式正变得越来越少见。char数组都必须从一个是机器字大小倍数的字节地址开始(也就是说,每种类型都必须“字对齐”)。随着古老的 16 位和 36 位机器的消亡,这种实现方式正变得越来越少见。

在像 SPARC 或 Intel [34]86 这样的现代 32 位机器上,或者 68020 及以上的 Motorola 芯片上,每个数据项通常都必须是“自对齐”的,即从其类型大小的倍数的地址开始。因此,32 位类型必须从 32 位边界开始,16 位类型从 16 位边界开始,8 位类型可以从任何地方开始,struct/array/union 类型具有其最严格成员的对齐方式。每组连续的位字段的第一个成员通常是字对齐的,所有后续成员都会被连续地打包到后续的字中(尽管 ANSI C 仅要求后者对小于字大小的位字段组)。

这些规则是由于您的编译器使用机器的原生寻址模式所致。消除对齐要求通常会减慢内存访问速度,因为它需要生成跨字边界或从较慢访问的奇数地址进行字段访问的代码。一些编译器可以通过 pragma、非默认编译器选项或指令来强制实现这一点,该指令表示“优化空间”。除非您被要求匹配外部定义的布局,并且也确定不会受到大端序与小端序不兼容性等问题的影响,否则使用此选项几乎都不是一个好主意。

问:当一个类型出现在 struct 或 array 中时,我如何计算它的“真实大小”(包含尾部填充)?

答:通过将其可见大小向上舍入到其对齐倍数。使一般情况棘手的是,必须依次将此规则应用于 struct 或 array 的每个元素,以计算内部填充;当 struct 或 array 包含 struct 或 array 元素时,必须递归地应用此规则。

在“自对齐”要求下,任何标量类型的真实大小与其可见大小相同。复合情况可以通过示例最容易理解(假设一个 32 位架构,在这些架构上,long、int、short 和 char 的典型 C 大小分别为 4、4、2 和 1,除非另有说明)。

(1) struct {char *; long;}没有尾部填充,大小为 (4 + 4) = 8 字节,或者在 64 位架构上没有填充,大小为 (8 + 8) = 16 字节。但 (2)struct {char *; short;}也将是两个机器字长,即使short只是半个字长,因为前面的char *会强制后续实例的字对齐。因此,(3)struct {char *; short; short;}将具有与示例 2 相同的两字大小;最后一个short仅仅占用了本该是填充的空间。

(4) struct {char *; short; long;}short后面会有半个字节的填充,因为long必须是字对齐的,因此大小与struct {char *; short; short; long;}. (5) struct {short; char;}将有一个字节的尾部填充;(6)struct {short; char; char;}将与示例 5 大小相同,但没有填充。另一方面,(7)struct {long; char;}将有 3 个字节的尾部填充,而 (8)struct {long; char; short}将在char后有一个字节的填充,大小与示例 7 相同,没有尾部填充。

问:如何从结构体中挤出填充以使其更小?

答:在较旧的、严格字导向的架构上,可能需要采取极端措施(见下文)。在“自对齐”情况下可以最大程度地减少填充的简单规则(在大多数其他情况下也不会造成损害)是按大小递减的顺序排列您的 struct 成员。在 16 位机器以及较旧的字导向和 36 位机器上,您需要明确了解指针的长度;由于内存模型复杂性或奇特的字符指针格式,它们的长度可能与标量类型不同。同一大小的成员组内将不需要填充,任何组都不需要前导填充;唯一会损失空间的地方是结构体的末尾。

上述规则假定您的成员都是正常的标量或指针类型,因此它们的大小按 8、4、2 和 1 的递减顺序排列。如果您的成员是位字段、结构体、数组或联合体,情况会稍微复杂一些。您需要考虑成员的“真实大小”(见上文),它可能不是 2 的幂。按真实大小递减的顺序排列您的成员,然后将成员从末尾移到每个非标量类型之后,以填充最后一个组件的尾部填充。在最常见的情况下,您会希望在 17 到 24 位成员(例如 18 位位字段、3 字节数组或struct {short x; char y})之后跟着一个char而不是一个short.

作为一种极端的措施,您可以将所有成员声明为位字段(参见 <转向下一个问题的引用>)。

但是,您应该三思而后行,即使是第一个措施,因为除非您的结构体在数组或磁盘文件中重复了数千次,否则空间节省通常非常微不足道。您的时间最好花在优化您使用的算法或表示法上。

问:我如何完全控制 struct 的位级布局?

答:这就是位字段的用途。将您所有成员声明为位字段。结构体可能仍然有不可见的尾部填充(除非您的最后一个位字段恰好填满最后一个机器字的末尾),但在现代架构上,这将消除所有其他填充,并让您完全控制已声明部分的位布局(尽管会显著增加访问时间)。然而,ANSI 并*不*保证这种良好的行为,所以要小心潜在的陷阱。

关于位字段的注意事项:尽管现代编译器默认将它们视为无符号,但 ANSI 并不要求这样做,这会导致在没有符号性声明的 1 位字段的情况下产生一些潜在的意外行为。

在一些实现不佳的 C 编译器和非常旧的 36 位字导向架构上,即使这样可能也不够;一个跨越字边界的位字段可能会迫使下一个位字段从下一个字的开头开始,留下高达 35 位的不可见间隙。在这些机器上,您唯一的选择是将结构体声明为单个 char 数组,并在 C 中自行执行字段访问。

参见 K&R II,A8.3,第 213 页。

问:为什么我无法在我的网络上的异构机器之间传递结构体?它们不都是 C 语言吗?

答:首先,您必须确保所有机器/编译器组合都具有相同的标量类型大小。如果它们都是 32 位机器,这很可能,大小为 long=4,int=4,short=2。即使这样也不是确定的——一些 680x0 编译器使用 2 字节的 int 以获得更快的算术代码。如果您的任何机器是(64 位)DEC Alphas 或(16 位)286 机器,那就别想了。

然后,您必须确保您不会受到大端序与小端序不兼容性的影响。而且,在许多应用中,还可能存在浮点数格式不兼容性。也许还有字符符号性的问题。

然后,您必须确保所有编译器都具有相同的对齐和结构填充要求。这很难,因为它们通常不被文档化。事实上,这通常是症结所在。

要通过网络传递结构数据,您通常需要像 ASN.1 或 SUN RPC 这样的工具。


还有一个免费的额外答案

问:我从 Pascal 转到 C,我怀念的一个特性是 Pascal 的 `with' 结构。C 不应该有一个等价物吗?

答:不,它太危险了。让我们给 `with' 一个显而易见的语法,并看一个例子

   struct symbol
   {
       char *name;
       int  class;
   };

   struct symbol mysym;
   int value = 17;

   with mysym
   {
      name = "sample"
      class = value;
   }

   if (value != 17)
	die_horribly();

到目前为止,一切都很好。但是,现在假设 struct 符号定义存在于一个#include文件中,离这个使用处很远——有人在那里添加了一个 `value' 成员,而没有注意到 `with'。`value' 引用的含义会突然而悄无声息地改变,使得 `class' 的最终值变得随机,并可能导致上述代码出现可怕的错误。