基本上,当启用严格别名时,此代码合法吗?
void f(int *pi) {
void **pv = (void **) π
*pv = NULL;
}
在这里,我们通过另一种类型的指针(指向void *
的指针)访问一个类型的对象(int*
),所以我认为这确实是一种严格的混叠冲突。
但一个试图强调未定义行为的样本让我怀疑(即使它不能证明它是合法的)。
首先,如果我们将int *
和char *
别名,我们可以根据优化级别获得不同的值(因此这肯定是一种严格的别名冲突):
#include <stdio.h>
static int v = 100;
void f(int **a, char **b) {
*a = &v;
*b = NULL;
if (*a)
// *b == *a (NULL)
printf("Should never be printed: %in", **a);
}
int main() {
int data = 5;
int *a = &data;
f(&a, (char **) &a);
return 0;
}
$ gcc a.c && ./a.out
$ gcc -O2 -fno-strict-aliasing a.c && ./a.out
$ gcc -O2 a.c && ./a.out
Should never be printed: 100
但使用void **
而不是char **
的同一样本并没有表现出未定义的行为:
#include <stdio.h>
static int v = 100;
void f(int **a, void **b) {
*a = &v;
*b = NULL;
if (*a)
// *b == *a (NULL)
printf("Should never be printed: %in", **a);
}
int main() {
int data = 5;
int *a = &data;
f(&a, (void **) &a);
return 0;
}
$ gcc a.c && ./a.out
$ gcc -O2 -fno-strict-aliasing a.c && ./a.out
$ gcc -O2 a.c && ./a.out
这只是偶然吗?或者void **
的标准中是否有明确的例外情况?
或者只是编译器专门处理void **
,因为实际上(void **) &a
在野外太常见了?
基本上,当启用严格别名时,此代码合法吗?
否。pi
的有效类型是int*
,但您可以通过void*
对指针变量进行lvalue访问。取消引用指针以提供与对象的有效类型不对应的访问是一种严格的别名冲突-除了一些例外,这不是一种。
在第二个示例中,函数的两个参数都设置为指向有效类型为int*
的对象,这在这里完成:f(&a, (char **) &a);
。因此,函数中的*b
确实是一个严格的混叠冲突,因为您使用的是char*
类型的访问。
在您的第三个示例中,您使用void*
执行相同的操作。这也是一种严格的混叠冲突。在这种情况下,void*
或void**
没有什么特别之处。
为什么编译器在某些情况下表现出某种形式的未定义行为,这并不是很有意义的推测。尽管void*
根据定义必须可以转换为任何其他对象指针类型,因此它们很可能在内部具有表示形式,尽管这不是标准中的明确要求。
此外,您正在使用-fno-strict-aliasing
,它关闭各种基于指针混叠的优化。如果你想引起奇怪和意想不到的结果,你不应该使用这个选项。
是的,void *
和char *
是特殊的。
void**是严格别名规则的例外吗?
您没有通过void **
类型进行混叠;您正在通过void *
进行混叠。在*pv = NULL
中,*pv
的类型是void *
。
通常,C标准允许不同类型的指针具有不同的表示。它们甚至可以有不同的尺寸。但是,它要求某些指针类型具有相同的表示形式。C 2018 6.2.5 28说[为了清晰起见,我将其分为要点]:
- 指向
void
的指针应具有与指向字符类型的指针相同的表示和对齐要求49)- 类似地,指向兼容类型的合格或不合格版本的指针应具有相同的表示和对齐要求
- 指向结构类型的所有指针应具有彼此相同的表示和对齐要求
- 指向并集类型的所有指针应具有彼此相同的表示和对齐要求
- 指向其他类型的指针不需要具有相同的表示或对齐要求
脚注49说:
相同的表示和对齐要求意味着作为函数的参数、函数的返回值和并集成员的互换性。
注释不是标准规范部分的一部分。也就是说,它并没有形成实现必须遵守的规则。然而,注释似乎告诉我们,无论形式规则如何,在某些地方都应该能够使用void *
来代替char *
,反之亦然。指出两种东西应该是可互换的看起来像是一条规则。我的解释是,本文本的作者希望void *
和char *
至少在某种程度上是可互换的,但没有适合放入C标准规范部分的正式措辞。事实上,C标准在处理混叠方面存在缺陷,比如这个,所以C标准确实需要重写规则。
因此,尽管这不是标准的规范部分,但编译器开发人员可能会尊重它,并支持将char *
与void *
混叠,反之亦然。这可以解释为什么您看到char *
的混叠表现得好像它是受支持的,而int *
的混叠则不受支持。
虽然char*
和void*
需要具有匹配的表示,但一些平台对int*
使用不同的表示。因此,任何依赖于使用去引用的void**
来可互换地访问所有指针类型的能力的代码都不可移植到这样的机器,并且从标准的角度来看是"可移植的";非便携式";。因此,本标准放弃了对任何特定实施是否应支持此类构造的管辖权。这样做的实现将比不这样做的更适合低级编程,因此设计和配置为适合此目的的高质量实现将这样做。但是,请注意,clang和gcc都没有被设计为特别适合低级编程(除非使用-fno-strict-aliasing
标志)。
为了澄清为什么平台可能对int*
和char*
使用不同的表示,一些硬件平台不允许以小于16位的块直接寻址存储器。该标准将允许这种平台的编译器以各种方式存储内容,在性能、存储效率和与期望char
为8位的代码的兼容性之间进行不同的权衡:
-
只需使
char
与最小的直接存储单元的大小相匹配(例如,使char
和int
都是16位)。我用过一个编译器。这种方法可能会提供最佳性能,但使用大型unsigned char
阵列来保存八位字节的代码将浪费其一半的存储空间。 -
在每个
char
中存储8位有用数据,其余8位未使用。存储拆分为两个字的16位值,以及拆分为四个字的32位值。这将提供出色的兼容性,但性能和存储效率较差。 -
将
char*
实现为指向16位字的指针、指示其应识别字的哪一半的位和15个填充位的组合,但将int*
实现为仅指向16位词的指针。 -
如上所述实现
char*
,但向int*
添加一个填充字节。这将提高兼容性,但会浪费一些存储空间。
没有一种方法对所有应用程序都是最好的,但标准允许实现选择对客户最有用的一种或多种方法(可能通过命令行开关进行选择)。