基本上,当启用严格别名时,此代码是否合法?
void f(int *pi) {
void **pv = (void **) π
*pv = NULL;
}
这里,我们通过另一种类型的指针(指向<code>void*</code>的指针)访问一种类型(<code<int*</code>)的对象,因此我认为这确实是一种严格的别名冲突。
但是,试图突出未定义行为的样本使我怀疑(即使它不能证明它是合法的)。
首先,如果我们将<code>int*</code>和<code>char*</code>别名,我们可以根据优化级别获得不同的值(因此这绝对是一种严格的别名冲突):
#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: %i\n", **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: %i\n", **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**
的标准中是否有明确的例外?
或者可能只是编译器专门处理< code>void **,因为实际上< code>(void **)
虽然char*
和void*
需要具有匹配的表示,但某些平台对int*
使用不同的表示。因此,任何依赖于使用取消引用的void**
来互换访问所有指针类型的能力的代码都不会移植到此类机器上,并且从标准的角度来看是“不可移植的”。因此,标准放弃了任何特定实现是否应该支持此类构造的管辖权。这样做的实现比不这样做的实现更适合低级编程,因此设计和配置为适合该目的的高质量实现也会这样做。然而,请注意,clang和gcc都不是为了特别适合低级编程而设计的,除非使用-fno-严格-混淆现象
标志。
为了澄清为什么平台可能对 int*
和 char*
使用不同的表示形式,某些硬件平台不允许以小于 16 位的块直接寻址内存。该标准将允许此类平台的编译器以各种方式存储内容,在性能,存储效率和与期望char
为8位的代码的兼容性之间进行不同的权衡:
>
只需使char与
最小的直接存储单元的大小相匹配(例如,使char
和int
都为16位)。我使用了一个编译器来做到这一点。这种方法可能会提供最佳性能,但是使用大量无符号字符
数组来保存八位字节的代码将浪费其一半的存储空间。
在每个char
中存储8位有用的数据,剩下8位未使用。将16位值拆分为两个字,将32位值拆分为四个字。这将提供出色的兼容性,但性能和存储效率很差。
将< code>char*实现为一个指向16位字的指针、一个指示它应该标识该字的哪一半的位以及15个填充位的组合,但是将< code>int*实现为一个指向16位字的指针。
如上实现char*
,但在int*
中添加一个填充字节。这将提高兼容性,但浪费一些存储空间。
没有一种方法对所有应用程序都是最好的,但是该标准允许实现选择对客户最有用的一种或多种方法(可能可以通过命令行开关选择)。
是的,虚空 *
和字符 *
是特殊的。
void**是严格别名规则的例外吗?
您没有通过虚空**
类型混叠;您正在别名通过虚空*
。在 *pv = 空
中,*pv
的类型为空 *
。
通常,C标准允许不同类型的指针有不同的表示。它们甚至可以有不同的大小。但是,它需要一些指针类型具有相同的表示。C 2018 6.2.5 28说[为了清楚起见,我分成要点]:
脚注49说:
相同的表示和对齐要求意味着作为函数的参数、函数的返回值和联合成员的互换性。
注释不是标准规范部分的一部分。也就是说,它不形成实现必须遵守的规则。但是,该注释似乎告诉我们,无论正式规则如何,您都应该能够在某些地方使用无效*
代替char *
,反之亦然。声明两件事应该是可互换的看起来像一个规则。我的解释是,该文本的作者打算至少在一定程度上将无效*
和char*
互换,但没有适合放入C标准规范部分的正式措辞。事实上,C标准对混叠的处理存在缺陷,比如这个,所以C标准确实需要重写规则。
因此,尽管这不是标准的规范部分,但编译器开发人员可能会尊重它,并支持将char*
与void*
进行别名,反之亦然。这可以解释为什么您看到使用<code>char*</code>进行别名时,它的行为好像是受支持的,而使用<code>int*</code>进行别名则不受支持。
基本上,当启用严格别名时,此代码是否合法?
不。pi
的有效类型是int*
,但您可以通过void*
左值访问指针变量。取消引用指针以提供与对象的有效类型不对应的访问权限是一种严格的混淆现象——除了一些例外,这不是。
在第二个示例中,函数的两个参数都被设置为指向有效类型< code>int*的对象,这是在下面完成的:< code>f(
在第三个示例中,您做了同样的事情,但是使用了< code>void*。这也是一种严格的混淆违规。在这种情况下,< code>void*或< code>void**没有什么特别之处。
为什么您的编译器在某些情况下表现出某种形式的未定义行为,推测起来没有多大意义。尽管根据定义,void*
必须可转换为任何其他对象指针类型,因此它们很可能在内部具有表示形式,即使这不是标准的明确要求。
此外,您还使用-fno strict aliasing
来关闭各种基于指针别名的优化。如果你想引起奇怪和意外的结果,你不应该使用这个选项。
我正在阅读关于reinterpret_cast的笔记,它是混淆现象(http://en.cppreference.com/w/cpp/language/reinterpret_cast)。 我写了代码: 我认为这些规则在这里不适用: T2是对象的(可能是cv限定的)动态类型 T2和T1都是指向相同类型T3的指针(可能是多级的,可能在每个级别都是cv限定的)(因为C11) T2是一个聚合类型或并集类
示例代码: 我认为这是常见的,被认为是可以接受的。该标准确实保证结构中没有初始填充。但是,这种情况未在严格别名规则中列出(C 17 [basic.lval]/11): 如果程序尝试通过下列类型之一以外的 gl 值访问对象的存储值,则行为未定义: (11.1)物体的动态类型, (11.2) 对象的动态类型的 cv 限定版本, (11.3) 与对象的动态类型相似的类型(如 7.5 中所定义), (11
我的理解是,由于所谓的“严格混淆现象规则”,以下代码在C中具有未定义的行为。 特别是,C编译器可能会完全忽略赋值,因为它可以假定
我无法解释此程序的执行行为: 因此,当使用-O3和gcc编译它,并使用参数25运行时,它会引发一个segfault。如果没有优化,它可以正常工作。我已经对它进行了反汇编:它正在进行矢量化,编译器假设数组以16字节对齐,因此它使用。显然是UB,尽管我无法解释。我知道严格别名规则,但情况并非如此(我希望如此),因为据我所知,严格别名规则不适用于s。为什么gcc假设这个指针是对齐的?即使进行了优化,Cl
在这些注释中,user @Deduplicator坚持认为,如果别名指针或别名指针是指向字符的指针类型(限定或非限定,有符号或无符号< code>char *),则严格的别名规则允许通过不兼容的类型进行访问。所以,他的观点基本上是 和 符合并具有定义的行为。 然而,在我看来,只有第一种形式是有效的,即当别名指针是指向char的指针时;然而,在另一个方向上却不能,即当别名指针指向不兼容的类型(而不是
下面是C 17形式的规则([basic.lval]/8),但它在其他标准中看起来很相似(C 98中是“lvalue”而不是“glvalue”): 8如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义: (8.4)-对应于对象动态类型的有符号或无符号类型 这条规则听起来像是“除非你做X,否则你会得到UB”,但这并不意味着如果你做了X,你就不会得到UB,正如人们所期望的那样