8wDlpd.png
8wDFp9.png
8wDEOx.png
8wDMfH.png
8wDKte.png

为什么这些构造使用前置和后置增量未定义的行为?

novski 2月前

201 0

#包括int main(void){ int i = 0; i = i++ + ++i; printf(\'%d\n\', i); // 3 i = 1; i = (i++); printf(\'%d\n\', i); // 2 应该是 1,不是吗? volatile int u = 0; ...

#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

   i = 1;
   i = (i++);
   printf("%d\n", i); // 2 Should be 1, no ?

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d\n", ++w, w); // shouldn't this print 1 1

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}
帖子版权声明 1、本帖标题:为什么这些构造使用前置和后置增量未定义的行为?
    本站网址:http://xjnalaquan.com/
2、本网站的资源部分来源于网络,如有侵权,请联系站长进行删除处理。
3、会员发帖仅代表会员个人观点,并不代表本站赞同其观点和对其真实性负责。
4、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
5、站长邮箱:yeweds@126.com 除非注明,本帖由novski在本站《function》版块原创发布, 转载请注明出处!
最新回复 (0)
  • @Jarett,不,只需要一些指向“序列点”的指针。在工作时,我发现一段代码中 i = i++,我认为“这并没有修改 i 的值”。我测试了一下,想知道为什么。后来,我删除了这个语句,并用 i++ 替换了它;

  • 我认为有趣的是,每个人都总是假设问这样的问题是因为提问者想要使用相关构造。我的第一个假设是 PiX 知道这些不好,但很好奇为什么它们在所使用的编译器上会这样表现……是的,正如 unWind 所说……它是未定义的,它可以做任何事情……包括 JCF(Jump and Catch Fire)

  • 我很好奇:如果结果未定义,为什么编译器似乎不会对诸如 \'u = u++ + ++u;\' 之类的构造发出警告?

  • C 具有未定义行为的概念,即某些语言结构在语法上有效,但您无法预测代码运行时的行为。

    据我所知,标准并没有明确说明 原因 。在我看来,这只是因为语言设计者希望在语义上有一些余地,而不是要求所有实现都以完全相同的方式处理整数溢出,这很可能会带来严重的性能损失,他们只是让行为保持未定义状态,这样如果你编写的代码导致整数溢出,任何事情都可能发生。

    那么,考虑到这一点,为什么会出现这些“问题”?语言明确指出,某些事情会导致 未定义行为 。没有问题,没有“应该”涉及。如果在声明所涉及的变量之一时未定义行为发生变化 volatile ,则不能证明或改变任何事情。它是 未定义的 ;您无法推断该行为。

    你最有趣的例子是

    u = (u++);
    

    是未定义行为的教科书示例(请参阅维基百科关于 序列点 )。

  • @PiX:事物未定义的原因有很多。这些包括:没有明确的“正确结果”,不同的机器架构会强烈倾向于不同的结果,现有的实践不一致,或者超出了标准的范围(例如,哪些文件名是有效的)。

  • 只是为了让大家感到困惑,一些这样的例子现在在 C11 中有明确的定义,例如 i = ++i + 1; 。

  • Avi 2月前 0 只看Ta
    引用 8

    通过阅读标准和已发布的理由,我们很清楚为什么存在 UB 概念。标准从未打算完全描述 C 实现必须做的所有事情以适合任何特定用途(参见“一个程序”规则的讨论),而是依赖于实现者的判断和产生有用的高质量实现的愿望。适合低级系统编程的高质量实现需要定义在高端数字运算应用程序中不需要的操作行为。与其试图使标准复杂化……

  • ...通过详细阐述哪些极端情况是已定义或未定义的,标准的作者认识到,实施者应该能够更好地判断他们预期支持的程序类型需要哪些类型的行为。超现代主义编译器假装将某些操作设为 UB 是为了暗示任何高质量程序都不需要它们,但标准和基本原理与这种假定的意图不一致。

  • @jrh:在我意识到超现代主义哲学已经变得多么失控之前,我就写了这个答案。让我恼火的是,从“我们不需要正式承认这种行为,因为需要它的平台无论如何都可以支持它”到“我们可以删除这种行为而不提供可用的替代品,因为它从未被识别,因此任何需要它的代码都被破坏了”。许多行为早就应该被弃用,取而代之的是各方面都更好的替代品,但这需要承认它们的合法性。

  • oHo 2月前 0 只看Ta
    引用 11

    这里的大多数答案都引用了 C 标准,强调这些构造的行为是未定义的。要理解 为什么这些构造的行为是未定义的 ,让我们首先根据 C11 标准来理解这些术语:

    顺序: (5.1.2.3)

    给定任意两个求值 A B ,如果 A 在之前排序 B ,则的执行 A 应先于的执行 B .

    未排序:

    如果 A 在 之前 或 之后 没有排序 B ,则 A B 是无序的。

    评估可以是以下两种情况之一:

    • 值计算 ,计算出表达式的结果;
    • 副作用 ,即对对象的修改。

    序列点:

    表达式求值和之间存在序列点 A ,这 B 意味着 相关的 值计算副作用 A 相关的 值计算 副作用 进行排序 B .

    现在回到问题,对于这样的表达

    int i = 1;
    i = i++;
    

    标准规定:

    6.5 表达方式:

    如果对标量对象的副作用相对于对同一标量对象的其他副作用或使用同一标量对象的值进行的值计算无序 行为 未定义 。[...]

    因此,上述表达式调用了 UB,因为同一对象上的两个副作用 i 相对于彼此是无序的。这意味着,赋值给的副作用是在赋值给的 i 副作用之前还是之后完成, ++ .
    未定义行为 的情况之一 .

    让我们将 i 赋值语句左边的重命名为 il ,将赋值语句右边的(在表达式中 i++ )重命名为 ir ,那么表达式将是这样的

    il = ir++     // Note that suffix l and r are used for the sake of clarity.
                  // Both il and ir represents the same object.  
    

    关于后缀 一个重要点 ++ 是:

    just because the ++ comes after the variable does not mean that the increment happens late 只要编译器确保使用原始值, 增量就可以尽早发生 .

    这意味着表达式 il = ir++ 可以被评估为

    temp = ir;      // i = 1
    ir = ir + 1;    // i = 2   side effect by ++ before assignment
    il = temp;      // i = 1   result is 1  
    

    或者

    temp = ir;      // i = 1
    il = temp;      // i = 1   side effect by assignment before ++
    ir = ir + 1;    // i = 2   result is 2  
    

    导致两个不同的结果 1 ,并且 2 取决于赋值的副作用序列, ++ 因此会导致未定义的行为。

  • \'只要编译器确保使用原始值。\'。这句话是什么意思?在哪里使用?

  • 引用 13

    我认为 C99 标准的相关部分是 6.5 表达式,§2

    在前一个和下一个序列点之间,对象的存储值最多应通过表达式求值修改一次。此外,应只读取先前的值以确定要存储的值。

    和 6.5.16 赋值运算符,§4:

    操作数的求值顺序未指定。如果尝试修改赋值运算符的结果或在下一个序列点之后访问它,则行为未定义。

  • @Zaibis:我喜欢在大多数地方使用的理由规则适用于:理论上,多处理器平台可以实现类似 A=B=5; 的内容为 \'写锁定 A;写锁定 B;将 5 存储到 A;将 5 存储到 B;解锁 B;解锁 A;\',以及类似 C=A+B; 的语句为 \'读锁定 A;读锁定 B;计算 A+B;解锁 A 和 B;写锁定 C;存储结果;解锁 C;\'。这将确保如果一个线程执行 A=B=5; 而另一个线程执行 C=A+B; ,后者要么认为两个写入都已发生,要么认为两个写入都未发生。这可能是一个有用的保证。但是,如果一个线程执行 I=I=5;,...

  • ... 并且编译器没有注意到两次写入都是针对同一位置(如果一个或两个左值涉及指针,这可能很难确定),则生成的代码可能会死锁。我认为任何现实世界的实现都不会将这种锁定作为其正常行为的一部分,但根据标准,这是允许的,如果硬件可以廉价地实现此类行为,它可能会很有用。在今天的硬件上,这种行为作为默认行为实现的成本太高了,但这并不意味着它会一直如此。

  • @supercat 但仅凭 c99 的序列点访问规则是否足以将其声明为未定义行为?所以硬件在技术上可以实现什么并不重要?

  • 如果您很想知道您得到的结果究竟是如何的,只需编译和反汇编您的代码行即可。

    这是我在我的机器上得到的结果以及我认为正在发生的事情:

    $ cat evil.c
    void evil(){
      int i = 0;
      i+= i++ + ++i;
    }
    $ gcc evil.c -c -o evil.bin
    $ gdb evil.bin
    (gdb) disassemble evil
    Dump of assembler code for function evil:
       0x00000000 <+0>:   push   %ebp
       0x00000001 <+1>:   mov    %esp,%ebp
       0x00000003 <+3>:   sub    $0x10,%esp
       0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
       0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
       0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
       0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
       0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
       0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
       0x0000001d <+29>:  leave  
       0x0000001e <+30>:  ret
    End of assembler dump.
    

    (我……假设 0x00000014 指令是某种编译器优化?)

  • 我如何获取机器代码?我使用 Dev C++,并在编译器设置中尝试了“代码生成”选项,但没有额外的文件输出或任何控制台输出

  • @ronnieaka gcc evil.c -c -o evil.bin 和 gdb evil.bin → 反汇编 evil,或者其他与它们在 Windows 上等价的程序 :)

  • 另外,编译为汇编会更容易(使用 gcc -S evil.c),这里只需要这样做。汇编然后反汇编只是一种迂回的方法。

返回
作者最近主题: