为什么(* p = * p)& (* Q = * Q); in C触发未定义的行为

时间:2015-06-28 13:29:07

标签: c pointers undefined-behavior sequence-points

如果(*p=*p) & (*q=*q);p相等,为什么C中的q会触发未定义的行为。

int f2(int * p, int * q)
{
  (*p=*p) & (*q=*q);
  *p = 1;
  *q = 2;
  return *p + *q;
}

来源(顺便说一句好文章):http://blog.frama-c.com/index.php?post/2012/07/25/On-the-redundancy-of-C99-s-restrict

5 个答案:

答案 0 :(得分:4)

如果*p*q指定相同的内存位置,则在没有插入序列点(或C11中的序列关系)的情况下写入它们会导致未定义的行为。

=&不会引入序列点。

代码相当于int i = 0; (i=i) & (i=i);,出于同样的原因,它有UB。另一个类似的例子是(*p = 1) & (*q = 2)

答案 1 :(得分:4)

C11标准对声明的裁决

(*p=*p) & (*q=*q);

是:

P1

  

<强>§6.5p3

     

运算符和操作数的分组由语法指示。 85)除了后面指出的以外,子表达的副作用和数值计算都没有顺序。

由于§6.5.10 按位AND运算符未能提及其操作数的排序,因此(*p=*p)(*q=*q)未被排序。

P2

  

<强>§6.5p2

     

如果相对于同一标量对象的不同副作用或使用相同标量对象的值进行值计算,标量对象的副作用未被排序,则行为未定义。如果表达式的子表达式有多个允许的排序,则如果在任何排序中发生这种未测序的副作用,则行为是不确定的。 84)

两项作业(*p=*p)(*q=*q)均未按顺序排列。相互§6.5p3,如果p==q对同一个对象产生副作用。因此,如果p==q,那么通过§6.5p2我们有UB。

P3

  

<强>§3.4.3

     

未定义的行为

     

行为,在使用不可移植或错误的程序结构或错误数据时,本国际标准不对其施加任何要求。

根据这一条款,我们知道该标准对UB没有任何要求。这通常被编译器解释为忽略这种行为发生的可能性的许可。

特别是,它允许编译器不处理案例p == q,这意味着它可以假设p != q

P1 + P2 + P3 - &gt; C1

因为组合的前提P1,P2和P3可以假设(*p=*p)(*q=*q)不调用UB,所以也可以假设它们是加载并存储到不同的存储位置。这也意味着f2的返回值必须为3而不是4。如果p == q,标准对发生的事情没有要求。

答案 2 :(得分:2)

简单来说,如果(*p = *p) & (*q = *q)p具有相同的值,则q未定义,因为:

  • 在未经测序的评估中,您不能将同一位置变异两次;和
  • 您无法在相同的未经评估的评估中从正在变异的位置进行阅读。

这在C和C ++中都是未定义的行为,虽然标准的措辞略有不同(并且上述文字并不准确地与任何一个标准相对应;它只是作为一个简化的解释。我确定你可以在SO上找到精确的文字。)

&运算符是一个简单的按位and,因此它不会强加任何评估顺序。可能看起来*p = *p显然是无操作,但无法保证以这种方式实现。编译器可以(例如)将其实现为tmp = *p; *p = 0; *p += tmp。它也可能无法一次设置*p的所有位,要求分配完成。

现在,一点个人的bugbear。表达式<something>&#34; 触发未定义的行为&#34;让它听起来像是某种类型的行为称为&#34;未定义的行为&#34;,也许是一种大的红色按钮,当按下时会开始向各个方向发射鼻子。对于正在发生的事情,这不是一个好模型。最好说&#34; <something>的行为未定义&#34;。

请注意,如果执行的程序的任何部分具有未定义的行为,则未定义整个程序的行为。 整个程序,而不是从具有未定义行为的部分开始的程序部分。

最后 - 这是链接文章的要点 - 允许编译器假定定义了程序的行为。因此,如果程序包含类似(*p = *p) & (*q = *q)的表达式,则编译器可以假定pq指向不同的非重叠对象。一旦做出这个假设,就可以更好地优化涉及* p和* q的表达式。一旦编译器做出这个假设,它也可能会消除(*p = *p) & (*q = *q)的整个计算,因为如果p和q是p和q,则* p和* q的中间值(如果有的话)是不可观察的不同。因此,您可以将该表达式视为一种声明:您承诺编译器已经做了必要的事情以保证p和q指向不同的非重叠对象。 (编译器不会,也可能不会,验证您的声明。它只会接受您的意见。)

然后,作者认为这个成语比(有点争议的)restrict关键字更强大。我毫不怀疑它是,并且很可能构造这样的表达式来涵盖一些用restrict无法轻易表达的限制。所以在这个程度上,这似乎是一个有趣的想法。另一方面,精确的表达方式至少可以说是模糊不清,容易出错。

答案 3 :(得分:2)

当编写C标准时,如果某个动作的效果在不同平台上有所不同,则特定平台并不总是能够保证任何特定的精确效果,并且如果可能存在可行的实施可能会触发一个硬件陷阱,其行为超出了C编译器的控制范围,标准对行为的说法几乎没有什么意义。即使没有任何硬件陷阱的可能性,“令人惊讶”行为的可能性足以将品牌行为视为未定义。

例如,考虑unsigned long x,*p; ... *p=(x++);。如果p==&x,则*p不仅可能最终不仅会保留x的旧值,或者值1更大,而是x例如p < q。 0x0000FFFF它也可能最终保持0x00000000或0x0001FFFF。即使没有机器会触发硬件陷阱,我也不认为标准的作者会考虑“不止一次修改的任何左值将保持Indeterminate Value,并且在同一表达式中读取左值以及以其他方式写入超出此处允许的数量可能会产生“不确定值”,而不是简单地将此类行为声明为未定义行为。此外,从标准作者的角度来看,标准未能在某些平台可以免费提供而其他平台无法提供特定行为的情况下不会对平台上此类行为的规范造成障碍。这可以提供他们。

在实践中,即使非常松散地指定的行为通常对于与今天编写的绝大多数程序共享以下两个要求的程序非常有用:

  1. 当给出有效输入时,产生正确的输出。
  2. 当输入无效输入时,不要发射核导弹。
  3. 不幸的是,有人提出这样的想法:如果C标准没有在特定情况Y中强制执行某些操作X的行为,即使大多数编译器恰好具有足以满足上述要求的程序的行为需求(例如,大多数编译器将为表达式p生成代码,该代码将产生0或1并且没有其他副作用,即使q(*p=*p) & (*q=*q)标识不相关的对象),动作X应该被视为编译器的一个指示,即程序永远不会收到任何会导致情况Y的输入。

    指示的p==q旨在表示这样的“承诺”。逻辑是,由于标准不会说明编译器在p==q时可以做什么,编译器应该假设程序员不介意程序是否会发出核导弹以响应任何可能的输入导致代码在x < y时执行。

    这个想法及其后果从根本上与C的本质和设计目标及其使用系统编程语言相对立。几乎所有系统都提供超出标准规定的一些功能和保证,但具体情况因系统而异。我认为通过重新定义{{1}}从“我愿意接受指针比较的任何方法来使用该程序实际将要运行的任何硬件”来更好地服务该语言的想法是荒谬的。 / em>“to”我很确定这两个指针将与我将生命放在它上面相关“,而不是通过添加指导编译器假设”x和y相关“的新方法指针“,但不知怎的,它似乎正在被接受。

答案 4 :(得分:1)

这个线程的问题以“如果p和q相等,为什么C中的(*p=*p) & (*q=*q);触发未定义的行为?”并且问题引用了一篇文章,该文章认为C(和C ++?)中的新restrict关键字是不必要的,因为我们可以通过编写表达式(*p=*p) & (*q=*q);来告诉编译器。

用户 Iwillnotexist Idonotexist 对此表达式的解释非常彻底......而且非常复杂。基本上,结论是这是一个指令而不是语句,因为该表达式不会产生任何使用的结果,只会产生副作用(赋值给自己)没有效果(自我保持不变,即使 p==q),所以任何好的编译器都可以优化它。

仍然没有完全理解解释,我选择了新关键字,而不是写错表达。