首先,我认为您不能谈论不完整类型的对齐要求,因为对齐要求仅针对完整类型定义:
完整的对象类型具有对齐要求,这对可以分配该类型对象的地址进行了限制。 (§6.2.8/1;所有标准引用均取自 n1570.pdf,实际上是 C11)
但是在一个有效的程序中,一个指针必须指向一个完整类型(或 NULL)的对象。因此,即使指针的引用类型在某些翻译单元中不完整,该指针所引用的对象(如果有的话)也必须具有完整的类型,因此需要对齐。但是,在引用类型不完整的翻译单元中,无论是编译器还是编写代码的人都无法知道对齐要求是什么。
因此,可以合法地为指向不完整类型的指针分配非 NULL 值的唯一方法是从类型完整的某个地方(可能在另一个翻译单元中)复制指向相同类型的指针。这不是转换,因为这两种类型是兼容的,并且保证可以工作。类似地,指向不完整类型的指针的值的少数合法用途之一是将其传递给在类型完整的上下文中指向相同类型的指针。 (其他用途包括将其与另一个指针值比较是否相等或将其转换为整数并打印出来,但这些不涉及转换为不同的指针类型。)
因此,总而言之,指向不完整类型的指针在预期的用例中是有用且可精确使用的——其中引用的类型是不透明的——但不能可移植地转换为任何其他类型的指针,除了可能符合条件的 char*和void*。
该标准不保证将指针转换为指向对齐可能更严格的类型的指针的可能性。如果引用类型的对齐方式未知,则目标对齐方式不能更严格的指针只有char* 和void*。因此,任何指向不完整类型的指针到char* 或void* 以外的类型的任何转换都必须被视为不可移植。
确实,引用类型不完整这一事实并不重要。该标准没有指定复合类型的对齐方式。这样的对齐必须足以让编译器正确对齐成员,但它可以任意大。换句话说,不能保证类型:
typedef char oneChar;
struct oneChar_s { char x; };
union oneChar_u { char x; };
具有相同的对齐方式。这两种复合类型很可能具有比 1 更大的对齐方式。因此不能保证可以将 oneChar* 转换为 oneChar_s*(当然,除非 oneChar* 是由以前的相反方向的转换),并且可移植程序不会尝试。从这个意义上说,struct oneChar_s 的定义是否可见并没有区别。
并非巧合,该标准并不保证所有对象指针的大小都相同。基本理论是,在某些体系结构上,普通指针不足以精确地引用单个字节,但有可能通过添加例如位偏移量来增加指针。实际上,可能还有其他可以打包成单词的小对象,这也需要增强的指针表示,但精度低于位偏移。
在这样的架构中,不可能为小型复合对象利用不同的指针精度,因为标准坚持认为复合指针最多有两种表示形式,一种用于structs,一种用于@ 987654333@s:
所有指向结构类型的指针都应具有彼此相同的表示和对齐要求。所有指向联合类型的指针都应具有彼此相同的表示和对齐要求。 (§6.2.5/27) [注 2]
特别是,这意味着指向给定类型对象的指针具有相同的表示,无论该类型是否完整。
指针表示的差异并不是转换为更受约束的对齐方式可能失败的唯一原因。例如,实现可能会在转换后插入代码来验证对齐(可能是为了响应清理编译器选项)。在类型不完整的情况下,编译器将无法插入静态代码来进行检查(尽管可能会进行某种运行时检查),但编译器可能会省略验证代码这一事实不会改变未定义性结果。
对于它的价值,OP 中的标准引用是针对 C99;在 C11 中,稍作修改(添加强调以表示更改的措辞):
指向对象类型的指针可以转换为指向不同对象类型的指针。如果结果指针未正确对齐 referenced 类型,则行为未定义。否则,当再次转换回来时,结果将等于原始指针。当指向对象的指针转换为指向字符类型的指针时,结果指向对象的最低寻址字节。结果的连续增量,直到对象的大小,产生指向对象剩余字节的指针。 (§6.3.2.3/7)
在我看来,这种变化纯粹是编辑性的。它源于更改 §6.2.5/1 中“对象类型”定义的决定。在 C99 中,存在三种类型:对象类型、函数类型和不完整类型。在 C11 中,只有对象类型和函数类型两种,注释是“在翻译单元内的不同点,对象类型可能是不完整的……或完整的……”,这是一个更准确的描述。
注意事项
-
作为一个完全假设的示例,请考虑一台在概念上类似于 PDP-6/10 架构的机器。这是一个字地址大的机器;一个单词大到足以包含两个地址(假设的 LISP 实现可以利用这一事实,以便将由 car 和 cdr 字段组成的 cons 节点存储到一个单词中)。因为希望有效地表示小对象的向量,机器还具有可以提取或覆盖字中的位域的指令,其中位域指针由伴随偏移和长度信息的字指针组成。 (因此,一个字只能包含一个位域指针。)(硬件有一条指令可以通过将长度添加到偏移量并在必要时移动到从下一个字的 0 开始的位域来增加位域指针。)
所以可能存在三种不同的指针类型:
- 由地址和位域偏移/长度组成的全字字符指针。
- 一个普通的半字指针,指向任何字对齐的对象类型。
- 一个扩充的半字加一位指针,指向一个字对齐对象的指针,由地址和地址是在字的前半部分还是后半部分的指示组成。 (这种表示也可能需要一个全字,但编码更简单。但可能还有其他地方可以放置额外的位。这是一个假设的示例,请记住。)
在这个假设的架构中,转换规则变得相当复杂。例如,您可以将char** 转换为int*,因为int 的对齐方式与char* 的对齐方式相同。但是您不能将int** 转换为int*,因为int 的对齐方式大于int* 的对齐方式。
比起记住这些复杂的规则,程序员可能会选择简单地避免执行那些保证可移植的指针转换,即通过char* 或void* 往返。
-
即使没有必要,指向所有组合的指针也可以使用更大、更精确的指针类型。在我看来,如果不是所有复合对象,一个实现更有可能只是选择对structs 施加最小对齐。该标准的措辞将允许实现使用structs 的最小对齐和所有unions 的增强指针表示。