首页 文章

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

提问于
浏览
732
#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 %d\n", w++, ++w, w); // shouldn't this print 0 2 2

   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?
}

14 回答

  • 47

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

    据我所知,该标准没有明确说明为什么存在未定义行为的概念 . 在我看来,这仅仅是因为语言设计者希望在语义上有一些余地,而不是要求所有实现以完全相同的方式处理整数溢出,这很可能会带来严重的性能成本,他们只是留下了行为未定义,以便如果您编写导致整数溢出的代码,任何事情都可能发生 .

    那么,考虑到这一点,为什么这些"issues"?该语言明确指出某些事情会导致undefined behavior . 没有问题,没有"should"参与 . 如果未声明的行为在其中一个涉及的变量被声明为 volatile 时发生更改,则不会证明或更改任何内容 . 它是未定义的;你无法推理这种行为 .

    你看起来最有趣的例子

    u = (u++);
    

    是未定义行为的教科书示例(请参阅维基百科在sequence points上的条目) .

  • 45

    只需编译和反汇编您的代码行,如果您倾向于知道它是如何得到您正在获得的 .

    这就是我在我的机器上得到的,以及我的想法:

    $ 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指令是某种编译器优化?)

  • 21

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

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

    和6.5.16分配运算符,§4:

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

  • 9

    行为无法真正解释,因为它同时调用了unspecified behaviorundefined behavior,所以我们无法对此代码进行任何一般性预测,尽管如果您阅读Olve Maudal的工作,例如Deep CUnspecified and Undefined,有时您可以在非常具体的情况下做出正确的猜测使用特定的编译器和环境,但请不要在 生产环境 附近的任何地方这样做 .

    所以转向未指明的行为,在draft c99 standard部分 6.5 第3段说(强调我的):

    运算符和操作数的分组由语法表示.74)除了后面指定的(对于函数调用(),&&,||,?:和逗号运算符),子表达式的评估顺序和顺序发生副作用的都是未指明的 .

    所以,当我们有这样一条线:

    i = i++ + ++i;
    

    我们不知道是先评估 i++ 还是 ++i . 这主要是给编译器better options for optimization .

    我们在这里也有未定义的行为,因为程序在sequence points之间多次修改变量( iu 等等) . 从草案标准部分 6.5 第2段(强调我的):

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

    它引用以下代码示例为未定义:

    i = ++i + 1;
    a[i++] = i;
    

    在所有这些示例中,代码尝试在同一序列点中多次修改对象,这将在每种情况下以 ; 结束:

    i = i++ + ++i;
    ^   ^       ^
    
    i = (i++);
    ^    ^
    
    u = u++ + ++u;
    ^   ^       ^
    
    u = (u++);
    ^    ^
    
    v = v++ + ++v;
    ^   ^       ^
    

    draft c99 standard部分draft c99 standard中定义了未指定的行为:

    使用未指明的值或本国际标准提供两种或更多种可能性的其他行为,并且在任何情况下都不会对其进行任何进一步的要求

    和未定义的行为在 3.4.3 节中定义为:

    使用不可移植或错误的程序结构或错误数据时的行为,本国际标准不对此要求

    并注意到:

    可能的未定义行为范围从完全忽略不完全结果的情况,到翻译或程序执行期间的行为,以文件的特征环境(有或没有发出诊断消息),终止翻译或执行(使用发出诊断信息) .

  • 13

    这里引用的大多数答案来自C标准,强调这些结构的行为是不确定的 . 要理解 why the behavior of these constructs are undefined ,让我们首先根据C11标准理解这些术语:

    Sequenced: (5.1.2.3)

    给定任何两个评估A和B,如果A在B之前排序,则A的执行应在B的执行之前执行 .

    Unsequenced:

    如果A之前没有排序或在B之后,A和B没有排序 .

    评估可以是两件事之一:

    • value computations ,它解决了表达式的结果;和

    • side effects ,它们是对象的修改 .

    Sequence Point:

    在表达式A和B的评估之间存在序列点意味着在与B相关联的每个值计算和副作用之前,对与A相关联的每个值计算和副作用进行排序 .

    现在提出问题,对于像这样的表达式

    int i = 1;
    i = i++;
    

    标准说:

    6.5表达式:

    如果相对于同一标量对象的不同副作用或使用相同标量对象的值进行值计算,标量对象的副作用未被排序,则行为未定义 . [...]

    因此,上面的表达式调用UB,因为对同一对象 i 的两个副作用相对于彼此是无序的 . 这意味着没有对 i 的副作用是否在 ++ 的副作用之前或之后进行排序 .
    根据赋值是在增量之前还是之后发生,将产生不同的结果,这是 undefined behavior 的情况之一 .

    让我们重命名赋值左边的 iil 并且在赋值的右边(在表达式 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.
    

    An important point关于Postfix ++ 运算符是:

    只是因为变量之后并不意味着增量发生得晚 . 只要编译器确保使用原始值,增量就可以在编译器喜欢的时候发生 .

    这意味着表达式 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
    

    导致两个不同的结果 12 ,这取决于通过赋值和 ++ 的副作用的顺序,因此调用UB .

  • 75

    另一种回答这个问题的方法,不仅仅是陷入关于序列点和未定义行为的神秘细节,而是简单地问,它们应该是什么意思?程序员试图做什么?

    第一个片段询问, i = i++ + ++i ,在我的书中非常疯狂 . 没有人会在一个真实的程序中编写它,它不是一个可以想象的算法,有人本来可以尝试编码会导致这个特殊的操作序列操作 . 而且由于's not obvious to you and me what it'应该这样做,所以's fine in my book if the compiler can' t弄清楚它应该做什么 .

    第二个片段 i = i++ 更容易理解 . 有人显然试图增加i,并将结果分配给i . 但是有几种方法可以在C中执行此操作 . 向i添加1并将结果返回给i的最基本方法在几乎所有编程语言中都是相同的:

    i = i + 1
    

    当然,C有一个方便的捷径:

    i++
    

    这意味着,“向i添加1,并将结果返回给i” . 因此,如果我们通过写作构建两者的大杂烩

    i = i++
    

    我们真正要说的是“向i添加1,并将结果返回给i,并将结果返回给i” . 我们很困惑,所以如果编译器也感到困惑,它也不会让我感到困扰 .

    实际上,这些疯狂的表达式写作的唯一时间是人们使用它们作为应该如何工作的人为例子 . 当然,理解工作原理很重要 . 但是使用的一个实际规则是,“如果使用表达式表达不明显,请不要写它 . ”

    我们曾经花了不少时间在comp.lang.c上讨论像这样的表达式以及为什么它们未定义 . 我的两个较长的答案,试图真正解释原因,在网上存档:

  • 5

    虽然任何编译器和处理器都不太可能实际这样做,但在C标准下,编译器使用序列实现“i”是合法的:

    In a single operation, read `i` and lock it to prevent access until further notice
    Compute (1+read_value)
    In a single operation, unlock `i` and store the computed value
    

    虽然我不认为任何处理器支持硬件以允许有效地完成这样的事情,但是可以很容易地想象这样的行为会使多线程代码更容易的情况(例如,如果两个线程试图执行上述操作,它将保证同时序列, i 将增加2)并且一些未来的处理器可能提供类似的功能并不是完全不可思议的 .

    如果编译器如上所述编写 i++ (在标准下是合法的)并且在整个表达式的评估期间(也是合法的)散布上述指令,并且如果没有注意到其他指令之一碰巧访问 i ,编译器生成一系列会死锁的指令是可能的(也是合法的) . 可以肯定的是,如果在两个地方使用相同的变量 i ,但是如果例程接受对两个指针的引用,则编译器几乎肯定会检测到该问题 . pq ,并在上面的表达式中使用 (*p)(*q) (而不是使用 i 两次),如果为 pq 传递了相同的对象地址,则不需要编译器识别或避免发生的死锁 .

  • 3

    通常这个问题被链接为与代码相关的问题的副本

    printf("%d %d\n", i, i++);
    

    要么

    printf("%d %d\n", ++i, i++);
    

    或类似的变种 .

    虽然这也是undefined behaviour已经如上所述,但在与以下语句进行比较时涉及 printf() 时会有细微差别:

    x = i++ + i++;
    

    在以下声明中:

    printf("%d %d\n", ++i, i++);
    

    printf()order of evaluation的参数是unspecified . 这意味着,可以按任何顺序评估表达式 i++++i . C11 standard对此有一些相关的描述:

    Annex J, unspecified behaviours

    在函数调用中评估参数中函数指示符,参数和子表达式的顺序(6.5.2.2) .

    3.4.4, unspecified behavior

    使用未指明的值或本国际标准提供两种或更多种可能性的其他行为,并且在任何情况下都不会对其进行任何进一步的要求 . 示例未指定行为的示例是计算函数参数的顺序 .

    未指定的行为本身不是问题 . 考虑这个例子:

    printf("%d %d\n", ++x, y++);
    

    这也有未指定的行为,因为未指定 ++xy++ 的评估顺序 . 但它's perfectly legal and valid statement. There'在此声明中没有未定义的行为 . 因为修改( ++xy++ )是针对不同的对象完成的 .

    是什么呈现以下声明

    printf("%d %d\n", ++i, i++);
    

    因为未定义的行为是这两个表达式修改同一个对象 i 而没有干预sequence point的事实 .


    另一个细节是printf()调用中涉及的逗号是分隔符,而不是comma operator .

    这是一个重要的区别,因为逗号运算符确实在其操作数的评估之间引入了一个序列点,这使得以下内容合法:

    int i = 5;
    int j;
    
    j = (++i, i++);  // No undefined behaviour here because the comma operator 
                     // introduces a sequence point between '++i' and 'i++'
    
    printf("i=%d j=%d\n",i, j); // prints: i=7 j=6
    

    逗号运算符从左到右计算其操作数,并仅生成最后一个操作数的值 . 所以在 j = (++i, i++); 中, ++i 增量 i6i++ 产生 i6 )的旧值,该值被分配给 j . 由于后增量, i 变为 7 .

    因此,如果函数调用中的逗号是逗号运算符,那么

    printf("%d %d\n", ++i, i++);
    

    不会有问题 . 但是它会调用未定义的行为,因为这里的逗号是一个分隔符 .


    对于那些对未定义行为不熟悉的人来说,阅读What Every C Programmer Should Know About Undefined Behavior可以从中理解C中未定义行为的概念和许多其他变体 .

    这篇文章:Undefined, unspecified and implementation-defined behavior也是相关的 .

  • 22

    C标准规定变量最多只能在两个序列点之间分配一次 . 例如,分号是序列点 .
    所以表格的每一个陈述:

    i = i++;
    i = i++ + ++i;
    

    等违反了这条规则 . 该标准还表示行为未定义且未指定 . 有些编译器会检测到这些并产生一些结果,但这不符合标准 .

    但是,两个不同的变量可以在两个序列点之间递增 .

    while(*src++ = *dst++);
    

    以上是复制/分析字符串时的常见编码习惯 .

  • 30

    虽然像 a = a++a++ + a++ 这样的表达式的 syntax 是合法的,但这些结构的 behaviourundefined ,因为不符合C标准中的 shall . C99 6.5p2

    在前一个和下一个序列点之间,对象的存储值最多只能通过表达式的计算修改一次 . [72]此外,先前的值应只读以确定要存储的值[73]

    随着footnote 73进一步澄清

    此段落呈现未定义的语句表达式,例如i = i 1;
    a [i] = i;
    同时允许i = i 1;
    a [i] = i;

    各种序列点列于C11(和C99)的附件C中:

    以下是5.1.2.3中描述的序列点:函数指示符的评估与函数调用中的实际参数和实际调用之间 . (6.5.2.2) . 在以下运算符的第一个和第二个操作数的计算之间:逻辑AND &&(6.5.13);逻辑OR || (6.5.14);逗号,(6.5.17) . 在条件的第一个操作数的评估之间? :运算符和第二个和第三个操作数中的任何一个(6.5.15) . 完整声明者的结尾:声明者(6.7.6);在评估完整表达式和下一个要评估的完整表达式之间 . 以下是完整表达式:不属于复合文字的初始化程序(6.7.9);表达式中的表达式(6.8.3);选择语句的控制表达式(if或switch)(6.8.4); while或do语句的控制表达式(6.8.5);每个(可选)表达式for a声明(6.8.5.3); return语句中的(可选)表达式(6.8.6.4) . 紧接库函数返回之前(7.1.4) . 在与每个格式化的输入/输出函数转换说明符(7.21.6,7.29.2)相关联的操作之后 . 紧接在每次调用比较函数之前和之后,以及对比较函数的任何调用和作为参数传递给该调用的对象的任何移动之间(7.22.5) .

    同一个paragraph in C11的措辞是:

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


    您可以通过例如使用最新版本的GCC与 -Wall-Werror 来检测程序中的此类错误,然后GCC将完全拒绝编译您的程序 . 以下是gcc(Ubuntu 6.2.0-5ubuntu12)6.2.0 20161005的输出:

    % gcc plusplus.c -Wall -Werror -pedantic
    plusplus.c: In function ‘main’:
    plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
        i = i++ + ++i;
        ~~^~~~~~~~~~~
    plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    plusplus.c:10:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
        i = (i++);
        ~~^~~~~~~
    plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
        u = u++ + ++u;
        ~~^~~~~~~~~~~
    plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    plusplus.c:18:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
        u = (u++);
        ~~^~~~~~~
    plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
        v = v++ + ++v;
        ~~^~~~~~~~~~~
    plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
    cc1: all warnings being treated as errors
    

    重要的是要知道what a sequence point is -- and what is a sequence point and what isn't . 例如,逗号运算符是序列点,所以

    j = (i ++, ++ i);
    

    是明确的,并将 i 增加一个,产生旧值,丢弃该值;然后在逗号操作员,解决副作用;然后将 i 递增1,结果值成为表达式的值 - 也就是说这只是一种人为的写 j = (i += 2) 的方式,这又是一种"clever"写的方式

    i += 2;
    j = i;
    

    但是, , in函数参数列表不是逗号运算符,并且在不同参数的计算之间没有序列点;相反,他们的评价对彼此没有考虑;所以函数调用

    int i = 0;
    printf("%d %d\n", i++, ++i, i);
    

    具有未定义的行为,因为 there is no sequence point between the evaluations of i++ and ++i in function arguments ,因此 i 的值在前一个和下一个序列点之间由 i++++i 两次修改 .

  • 3

    https://stackoverflow.com/questions/29505280/incrementing-array-index-in-c,有人问起如下声明:

    int k[] = {0,1,2,3,4,5,6,7,8,9,10};
    int i = 0;
    int num;
    num = k[++i+k[++i]] + k[++i];
    printf("%d", num);
    

    打印7 ... OP预计它打印6 .

    在其余计算之前,不保证 ++i 增量全部完成 . 实际上,不同的编译器在这里会得到不同的结果 . 在您提供的示例中,执行了前两个 ++i ,然后读取了 k[] 的值,然后是最后的 ++i 然后是 k[] .

    num = k[i+1]+k[i+2] + k[i+3];
    i += 3
    

    现代编译器将很好地优化它 . 事实上,可能比你最初编写的代码更好(假设它按照你希望的方式工作) .

  • 56

    关于这种计算中发生的事情的一个很好的解释在n1188的文件n1188中提供 .

    我解释了这些想法 .

    在这种情况下适用的标准ISO 9899的主要规则是6.5p2 .

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

    i=i++ 这样的表达式中的序列点位于 i= 之前和 i++ 之后 .

    在我上面引用的论文中,解释了你可以把程序想象成由小盒子组成,每个盒子包含2个连续序列点之间的指令 . 序列点在标准的附录C中定义,在 i=i++ 的情况下,有2个序列点来界定完整表达 . 这样的表达式在语法上与语法的Backus-Naur形式的 expression-statement 条目相同(语法在附录A中提供) .

    因此,框内的指令顺序没有明确的顺序 .

    i=i++
    

    可以解释为

    tmp = i
    i=i+1
    i = tmp
    

    或者作为

    tmp = i
    i = tmp
    i=i+1
    

    因为所有这些形式来解释代码 i=i++ 都是有效的,并且因为两者都生成不同的答案,所以行为是未定义的 .

    因此,组成程序的每个框的开头和结尾都可以看到序列点[框中是C中的原子单元],并且在框内,所有情况下都没有定义指令的顺序 . 更改该订单有时可以更改结果 .

    编辑:

    解释这种含糊不清的其他好的来源是来自c-faq网站(也发布as a book)的条目,即herehere以及here .

  • 529

    原因是程序正在运行未定义的行为 . 问题在于评估顺序,因为根据C 98标准没有所需的序列点(根据C 11术语,没有操作在其他操作之前或之后排序) .

    但是,如果你坚持使用一个编译器,你会发现行为是持久的,只要你不添加函数调用或指针,这会使行为更加混乱 .

    • 首先是海湾合作委员会:使用Nuwen MinGW 15 GCC 7.1你会得到:
    #include<stdio.h>
    int main(int argc, char ** argv)
    {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2
    
    i = 1;
    i = (i++);
    printf("%d\n", i); //1
    
    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 2
    
    u = 1;
    u = (u++);
    printf("%d\n", u); //1
    
    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); //2
    

    }

    海湾合作委员会如何运作?它右侧(RHS)按从左到右的顺序计算子表达式,然后将值分配给左侧(LHS) . 这正是Java和C#的行为和定义标准的方式 . (是的,Java和C#中的等效软件已经定义了行为) . 它按照从左到右的顺序在RHS声明中逐个评估每个子表达式;对于每个子表达式:首先评估c(预增量),然后将值c用于操作,然后是后增量c) .

    根据GCC C++: Operators

    在GCC C中,运算符的优先级控制各个运算符的计算顺序

    GCC理解的定义行为C中的等效代码:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
        int i = 0;
        //i = i++ + ++i;
        int r;
        r=i;
        i++;
        ++i;
        r+=i;
        i=r;
        printf("%d\n", i); // 2
    
        i = 1;
        //i = (i++);
        r=i;
        i++;
        i=r;
        printf("%d\n", i); // 1
    
        volatile int u = 0;
        //u = u++ + ++u;
        r=u;
        u++;
        ++u;
        r+=u;
        u=r;
        printf("%d\n", u); // 2
    
        u = 1;
        //u = (u++);
        r=u;
        u++;
        u=r;
        printf("%d\n", u); // 1
    
        register int v = 0;
        //v = v++ + ++v;
        r=v;
        v++;
        ++v;
        r+=v;
        v=r;
        printf("%d\n", v); //2
    }
    

    然后我们去Visual Studio . Visual Studio 2015,您将获得:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
        int i = 0;
        i = i++ + ++i;
        printf("%d\n", i); // 3
    
        i = 1;
        i = (i++);
        printf("%d\n", i); // 2 
    
        volatile int u = 0;
        u = u++ + ++u;
        printf("%d\n", u); // 3
    
        u = 1;
        u = (u++);
        printf("%d\n", u); // 2 
    
        register int v = 0;
        v = v++ + ++v;
        printf("%d\n", v); // 3 
    }
    

    visual studio如何工作,它采用另一种方法,它在第一遍中评估所有预增量表达式,然后在第二遍中使用操作中的变量值,在第三遍中从RHS分配到LHS,然后在最后一遍中它评估所有的一次传递后增量表达式 .

    因此,作为Visual C的定义行为C中的等价物理解:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
        int r;
        int i = 0;
        //i = i++ + ++i;
        ++i;
        r = i + i;
        i = r;
        i++;
        printf("%d\n", i); // 3
    
        i = 1;
        //i = (i++);
        r = i;
        i = r;
        i++;
        printf("%d\n", i); // 2 
    
        volatile int u = 0;
        //u = u++ + ++u;
        ++u;
        r = u + u;
        u = r;
        u++;
        printf("%d\n", u); // 3
    
        u = 1;
        //u = (u++);
        r = u;
        u = r;
        u++;
        printf("%d\n", u); // 2 
    
        register int v = 0;
        //v = v++ + ++v;
        ++v;
        r = v + v;
        v = r;
        v++;
        printf("%d\n", v); // 3 
    }
    

    正如Visual Studio文档在Precedence and Order of Evaluation处所述:

    如果多个运算符一起出现,它们具有相同的优先级,并根据它们的相关性进行评估 . 表中的运算符在Postfix Operators开头的部分中描述 .

  • 11

    你的问题可能不是,"Why are these constructs undefined behavior in C?" . 你的问题可能是,“为什么这段代码(使用 ++ )没有给我预期的 Value ?”,有人将你的问题标记为重复,并将你发送到这里 .

    这个答案试图回答这个问题:为什么你的代码没有给你你想要的答案,你怎么能学会识别(并避免)不能按预期工作的表达式 .

    我现在假设您've heard the basic definition of C' s ++-- 运算符,以及 ++x 的前缀与后缀形式 x++ 的不同之处 . 但是这些操作符很难想到,所以为了确保你理解,也许你写了一个小小的测试程序,包括类似的东西

    int x = 5;
    printf("%d %d %d\n", x, ++x, x++);
    

    但是,令你惊讶的是,这个程序并没有帮助你理解 - 它打印出一些奇怪的,意想不到的,莫名其妙的输出,暗示也许 ++ 做了一些完全不同的事情,而不是你认为它做的事情 .

    或者,也许你正在看一个难以理解的表达

    int x = 5;
    x = x++ + ++x;
    printf("%d\n", x);
    

    也许有人给你这个代码作为一个谜题 . 这段代码也毫无意义,特别是如果你运行它 - 如果你在两个不同的编译器下编译和运行它,你可能会得到两个不同的答案!那是怎么回事?哪个答案是对的? (答案是他们两个都是,或者都不是 . )

    正如你现在所听到的那样,所有这些表达式都是未定义的,这意味着C语言不能保证它们不是那么'll do. This is a strange and surprising result, because you probably thought that any program you could write, as long as it compiled and ran, would generate a unique, well-defined output. But in the case of undefined behavior, that' .

    什么使表达式未定义?涉及 ++-- 的表达式是否始终未定义?当然不是:这些是有用的操作符,如果你正确使用它们,它们就是完美的定义 .

    对于表达式,我们谈论的是什么使得它们未被定义是当什么时候有太多的事情发生,当我们不确定将发生什么样的订单时,但是当订单对我们获得的结果很重要时 .

    让我们回到我在这个答案中使用的两个例子 . 我写的时候

    printf("%d %d %d\n", x, ++x, x++);
    

    问题是,在调用 printf 之前,编译器是先计算 x 的值,还是 x++ ,或者 ++x ?但事实证明我们不知道 . 有's no rule in C which says that the arguments to a function get evaluated left-to-right, or right-to-left, or in some other order. So we can' t表示编译器是先执行 x ,然后是 ++x ,然后是 x++ ,还是 x++ 然后是 ++x 然后 x ,或其他一些顺序 . 但顺序显然很重要,因为根据编译器使用的顺序,我们将清楚地得到 printf 打印的不同结果 .

    这个疯狂的表达怎么样?

    x = x++ + ++x;
    

    此表达式的问题在于它包含三种不同的尝试来修改x的值:(1) x++ 部分尝试将1添加到x,将新值存储在 x 中,并返回旧值 x ; (2) ++x 部分尝试将1添加到x,将新值存储在 x 中,并返回 x 的新值; (3) x = 部分尝试将其他两个的总和分配回x . 这三个尝试作业中的哪一个将"win"?这三个值中的哪一个实际上会分配给 x ?再一次,也许令人惊讶的是,C中没有任何规则可以告诉我们 .

    您可能会想到优先级或关联性或从左到右的评估会告诉您事情发生的顺序,但事实并非如此 . 你可能不相信我,但请接受我的话,我会再说一遍:优先级和结合性不会决定C中表达式的评估顺序的每个方面 . 特别是,如果在一个表达式中有多个我们尝试的不同地点为 x ,优先级和关联性等事物分配新值不会告诉我们哪些尝试首先发生,或最后发生,或任何事情发生 .


    因此,如果您想确保所有程序都定义明确,可以编写哪些表达式,哪些表达式可以编写?

    这些表达式都很好:

    y = x++;
    z = x++ + y++;
    x = x + 1;
    x = a[i++];
    x = a[i++] + b[j++];
    x[i++] = a[j++] + b[k++];
    x = *p++;
    x = *p++ + *q++;
    

    这些表达式都是未定义的:

    x = x++;
    x = x++ + ++x;
    y = x + x++;
    a[i] = i++;
    a[i++] = i;
    printf("%d %d %d\n", x, ++x, x++);
    

    最后一个问题是,如何判断哪些表达式定义明确,哪些表达式未定义?

    正如我之前所说的那样,未定义的表达式是那些一次性过多的表达式,在那里你无法确定发生了什么顺序,以及顺序的重要性:

    • 如果's one variable that'在两个或多个不同的地方被修改(分配给),你怎么知道哪个修改首先发生?

    • 如果's a variable that'在一个地方被修改,并且在另一个地方使用了它的值,你怎么知道它是使用旧值还是新值?

    作为#1的一个例子,在表达式中

    x = x++ + ++x;
    

    有三次尝试修改`x .

    作为#2的一个例子,在表达式中

    y = x + x++;
    

    我们都使用 x 的值,并修改它 .

    这就是答案:确保在您编写的任何表达式中,每个变量最多被修改一次,如果修改了变量,您也不会尝试在其他地方使用该变量的值 .

相关问题