为什么char* 可以与任何对象指针别名,而不是相反?

92vpleto  于 2023-10-16  发布在  其他
关注(0)|答案(4)|浏览(109)

What is the strict aliasing rule?的公认答案提到,您可以使用char *来别名另一种类型,但不能以其他方式。
这对我来说没有意义-如果我们有两个指针,一个类型为char *,另一个类型为struct something *,指向同一个位置,怎么可能第一个别名第二个,但第二个不别名第一个?

xsuvu9jc

xsuvu9jc1#

如果我们有两个指针,一个类型为char *,另一个类型为struct something *,指向同一个位置,怎么可能第一个指针别名了第二个指针,而第二个指针没有别名了第一个指针呢?
是的,但这不是重点。
重点是,如果你有一个或多个struct something s,那么你可以使用char*来读取它们的组成字节,但是如果你有一个或多个char s,那么你不能使用struct something*来读取它们。

x7rlezfr

x7rlezfr2#

参考答案中的措辞略有错误,所以让我们先解决这个问题:一个对象永远不会给另一个对象别名,但是两个指针可以给同一个对象“别名”(这意味着,指针指向同一个内存位置-如M. M。这不是100%正确的措辞,但你得到的想法)。此外,该标准本身(据我所知)实际上根本没有谈到严格的别名,而只是列出了一些规则,这些规则控制了可以访问或不访问对象的表达式。像-fno-strict-aliasing这样的标记告诉编译器它是否可以假设程序员遵循这些规则(因此它可以基于该假设执行优化)。
现在回答你的问题:任何对象都可以通过指向char指针访问,但是char对象(尤其是char数组)可能无法通过大多数其他指针类型访问。基于此,编译器需要做出以下假设:
1.如果不知道实际对象本身的类型,char*T*指针可能总是指向同一个对象(彼此别名)- * 对称关系 *。
1.如果类型T1T2不“相关”,并且char也不“相关”,那么T1*T2*可能永远不会指向同一个对象- * 对称关系 *。

  1. char*指针可以指向char对象任何类型的T对象。
    1.一个T*指针可能指向一个char对象- * 一个对称关系 *。
    我相信,关于通过指针访问 object
    非对称**规则背后的主要理由是,char数组可能不满足例如int的对齐要求。
    因此,即使没有基于严格别名规则的编译器优化,例如,将int写入地址为0x 1,0x 2,0x 3,0x 4的4字节char数组的位置,在最好的情况下,将导致性能低下,在最坏的情况下,将访问不同的内存位置,因为CPU指令在写入4字节值时可能会忽略最低的两个地址位(因此,这可能会导致写入0x 0、0x 1、0x 2和0x 3)。
    请注意,C和C++中“相关”的含义不同,但这与您的问题无关。
mklgxw1f

mklgxw1f3#

如果我们有两个指针,一个类型为char *,另一个类型为struct something *,指向同一个位置,怎么可能第一个指针别名了第二个指针,而第二个指针没有别名了第一个指针呢?
指针之间不使用别名;你用词太草率了别名是当一个 * 左值 * 被用来访问一个不同类型的 * 对象 *。(解引用指针给出左值)。
在您的示例中,重要的是被别名化的对象的类型。举一个具体的例子,假设对象是double。通过解引用指向double的char *来替换double是可以的,因为严格的别名规则允许这样做。但是,不允许通过取消引用struct something *来访问double(除非结构体以double开头!).
如果编译器正在查看一个接受char *struct something *的函数,并且它没有关于所指向对象的信息(这实际上是不可能的,因为别名传递是在整个程序优化阶段完成的);那么它必须考虑对象实际上可能是struct something *的可能性,因此在此函数内无法进行优化。

xcitsw88

xcitsw884#

C标准的许多方面都源自C标准,需要在编写它时的历史背景下理解。如果编写C标准是为了描述一种包含基于类型的别名的新语言,而不是描述一种围绕访问左值就是访问存储在内存中的位模式这一思想设计的现有语言,那么就没有理由给予任何特权状态给用于存储字符串中字符的类型。使用显式操作将存储区域视为位模式将允许优化同时更有效和更安全。如果C标准是以这种方式编写的,那么C标准大概也会是这样。
然而,实际上,该标准是为了描述一种语言而编写的,在这种语言中,一个非常常见的习惯用法是通过复制对象的所有字节来复制对象的值,并且该标准的作者希望允许这样的结构在可移植程序中使用。
此外,该标准的作者希望实现在有用的情况下“以环境特有的文档化方式”处理许多不可移植的构造,但放弃了何时应该发生的管辖权,因为编译器作者被期望比委员会更好地理解他们的客户和潜在客户的需求。
假设在一个编译单元中,有这样的函数:

void copy_thing(char *dest, char *src, int size)
{
  while(size--)
    *(char volatile *)(dest++) = *(char volatile*)(src++);
}

在另一个编译单元中:

float f1,f2;
float test(void)
{
  f1 = 1.0f;
  f2 = 2.0f;
  copy_thing((char*)&f2, (char*)&f1, sizeof f1);
  return f2;
}

我认为委员会成员之间应该有一个共识,那就是没有一个高质量的实现应该把copy_thing从不写入float类型的对象这一事实视为假设返回值总是2.0f的邀请。关于上面的代码,有许多事情应该防止或阻止实现将f2的读取与前面的写入合并,有或没有关于字符类型的特殊规则,但是不同的实现会有不同的原因。
很难描述一组规则,要求所有实现正确处理上述代码,而不会阻止某些现有或合理的实现实现有用的优化。将所有模块间调用视为不透明的实现将正确处理此类代码,即使它忽略了从T1到T2的转换是对T2的访问可能影响T1的标志,或者忽略了volatile访问可能以编译器不应该期望理解的方式影响其他对象的事实。一个执行跨模块内联并且不注意类型转换或volatile含义的实现,如果它避免对通过字符指针的访问做出任何别名假设,那么它将正确地处理这样的代码。
委员会希望在上述结构中认识到一些东西,编译器需要认识到这意味着f2可能会被修改,因为另一种选择是将这样的结构视为未定义行为,尽管它应该在可移植程序中可用。事实上,他们选择通过字符指针进行访问的事实是迫使这个问题的一个方面,从来没有打算暗示编译器忽略其他一切,即使不幸的是,一些编译器作者将标准解释为邀请这样做。

相关问题