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

PiX*_*PiX 793 c increment operator-precedence undefined-behavior sequence-points

#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?
}
Run Code Online (Sandbox Code Playgroud)

unw*_*ind 554

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

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

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

你最有趣的例子,有一个

u = (u++);
Run Code Online (Sandbox Code Playgroud)

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

  • 我知道它是未定义的,(在生产中查看这段代码的想法吓到了我:))但我试着理解这些结果的原因是什么.特别是为什么u = u ++增加了你.在java例如:u = u ++返回0作为(我的大脑)预期:)感谢序列点链接BTW. (38认同)
  • @PiX:由于许多可能的原因,事情未定义.这些包括:没有明确的"正确结果",不同的机器架构将强烈支持不同的结果,现有的实践不一致,或超出标准的范围(例如,哪些文件名有效). (8认同)
  • @MattMcNabb仅在[C++ 11中明确定义](http://stackoverflow.com/a/22616368/1708801),而不是在C11中. (6认同)
  • @rusty不确定你的意思.术语"未定义的行为"用于C标准.这意味着即使某些构造在语法上有效并且通常会编译,但它们会导致未定义的行为,即它们没有意义,应该避免,因为如果程序有未定义的行为,程序就会被破坏. (4认同)
  • 只是为了混淆大家,一些这样的例子现在在 C11 中有明确定义,例如 `i = ++i + 1;` 。 (4认同)
  • @PaulManta,如果您看到这一点,编辑答案不是为了向已经接受的答案添加无关信息.这是一个C问题,答案很好,因为它描述了从C90到C11的C标准中的情况.编辑用于修复语法和样式. (3认同)
  • ...通过详细了解哪些角落案例被定义或未被定义,标准的作者认识到实施者应该更好地判断他们期望支持的各种类型的行为需要哪种行为. .超现代主义编译器假装制定某些行动UB意在暗示没有质量计划需要它们,但标准和理由与这种假定的意图不一致. (3认同)
  • 显然,由于u ++周围的括号,编译器决定启动你然后返回它.因为它在C中是未定义的行为,所以这是连续的.不同的编译器或甚至不同的机器和相同的机器可以给出不同的答案.我不知道java,但也许这个行为是明确定义的. (2认同)
  • 阅读标准和公布的理论基础,很明显为什么UB的概念存在.标准从未打算完全描述C实现必须做的所有事情以适合任何特定目的(参见"一个程序"规则的讨论),而是依赖于实现者的判断和产生有用的质量实现的愿望.适用于低级系统编程的质量实现需要定义高端数字运算应用程序中不需要的操作行为.而不是试图使标准复杂化...... (2认同)
  • @jrh:在我意识到超现代主义哲学已经失控之前,我写了这个答案。令我烦恼的是从“我们不需要正式承认这种行为,因为需要它的平台无论如何都可以支持它”到“我们可以在不提供可用替代品的情况下删除这种行为,因为它从未被识别过,因此没有任何代码需要它被打破了”。许多行为早就应该被弃用*支持在各方面都更好的替代品*,但这需要承认它们的合法性。 (2认同)
  • @stillanoob:不,因为包含该表达式的任何代码的_行为_都是未定义的,这意味着它实际上可以做_任何事情_。它的计算结果可能总是为 42,除非周日月亮是凸凸的。它可能会陷入无限循环,而不是对任何东西进行评估。它可能会跳转到代码中的随机位置。它可能会使进程崩溃。它甚至可能会让你的计算机着火并[让恶魔从你的鼻子里飞出来](http://catb.org/jargon/html/N/nasal-demons.html),而 C 标准仍然不在乎。 (2认同)

bad*_*adp 76

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

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

$ 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.
Run Code Online (Sandbox Code Playgroud)

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

  • 为了记录,如果出于某种原因,你想知道给定的构造做了什么 - 并且*尤其是*如果有任何怀疑它可能是未定义的行为 - 这个古老的建议"只需用你的编译器试试看看"可能非常危险.在这个版本的编译器下,你将充分了解它在这种情况下的作用*,在这些情况下,今天*.如果有任何关于它保证做什么的话,你将不会*学到很多东西.通常,"只需使用编译器进行尝试"会导致只能与编译器一起使用的非便携式程序. (47认同)
  • 这个答案并没有真正解决"为什么这些构造未定义的行为?"的问题. (21认同)
  • 顺便说一下,编译到程序集(使用`gcc -S evil.c`)会更容易,这就是这里所需要的.组装然后拆卸它只是一种迂回的方式. (9认同)
  • @ronnieaka`gcc evil.c -c -o evil.bin`和`gdb evil.bin`→`disassemble evil`,或者Windows的等价物:) (5认同)

Chr*_*oph 61

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

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

和6.5.16分配运算符,§4:

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

  • 上述意味着'i = i = 5;'会是未定义的行为吗? (2认同)
  • @supercat 据我所知`i=i=5` 也是未定义的行为 (2认同)
  • @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;`,那么...... (2认同)
  • ...并且编译器没有注意到两次写入都指向同一位置(如果一个或两个左值涉及指针,则可能很难确定),生成的代码可能会死锁。我认为任何现实世界的实现都不会将此类锁定作为其正常行为的一部分,但在标准下这是允许的,并且如果硬件可以廉价地实现此类行为,那么它可能会很有用。在今天的硬件上,这种行为作为默认实现的成本太高,但这并不意味着它总是如此。 (2认同)

hac*_*cks 52

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

顺序:(5.1.2.3)

鉴于任何两个评估AB,如果A之前被测序B,然后执行A应先执行B.

未测序:

如果A在之前或之后没有排序B,那么A并且没有排序B.

评估可以是以下两种方式之一:

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

序列点:

表达式的计算之间的序列点的存在AB意味着每个值计算副作用与相关联的A每之前测序值计算副作用相关联B.

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

int i = 1;
i = i++;
Run Code Online (Sandbox Code Playgroud)

标准说:

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.  
Run Code Online (Sandbox Code Playgroud)

关于Postfix ++运算符的一个重点是:

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

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

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  
Run Code Online (Sandbox Code Playgroud)

要么

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  
Run Code Online (Sandbox Code Playgroud)

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

  • 正在寻找这个答案,它实际上回答了“为什么是 UB”而不是“什么是 UB”。顺便说一句,解释的好方法。非常感谢! (5认同)

Sha*_*our 51

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

所以转向未指明的行为,在草案c99标准6.53节中说(强调我的):

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

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

i = i++ + ++i;
Run Code Online (Sandbox Code Playgroud)

我们不知道是否i++++i将首先评估.这主要是为编译器提供更好的优化选项.

我们也有不确定的行为在这里也因为程序修改变量(i,u,等)比之间的一次序列点.从标准草案6.52节草案(强调我的):

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

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

i = ++i + 1;
a[i++] = i; 
Run Code Online (Sandbox Code Playgroud)

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

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

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^
Run Code Online (Sandbox Code Playgroud)

c99标准草案中的未指定行为在以下部分中3.4.4定义:

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

未定义的行为在以下部分中定义3.4.3:

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

并注意到:

可能的未定义行为包括完全忽略具有不可预测结果的情况,在转换或程序执行期间以环境特征(有或没有发出诊断消息)的特定文档执行,终止翻译或执行(发布时)一条诊断信息).


Ste*_*mit 33

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

第一个片段问i = i++ + ++i我,在我的书中非常疯狂.没有人会在一个真实的程序中编写它,它的作用并不明显,没有一个可以想象的算法,有人可能会尝试编码,这会产生这种特殊的操作序列.因为对你和我来说,它应该做的事情并不明显,如果编译器无法弄清楚它应该做什么,那么在我的书中这很好.

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

i = i + 1
Run Code Online (Sandbox Code Playgroud)

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

i++
Run Code Online (Sandbox Code Playgroud)

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

i = i++
Run Code Online (Sandbox Code Playgroud)

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

实际上,这些疯狂的表达式写入的唯一时间是人们使用它们作为++应该如何工作的人为例子.当然,了解++的工作原理非常重要.但是使用++的一个实际规则是,"如果使用++意味着什么表达不明显,就不要写它."

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

  • @RobertSsupportsMonicaCellio 诚然,它的编写方式有点令人困惑。将其读作“从‘i’获取的值加1,将结果赋回‘i’,并将结果赋回‘i’”。 (2认同)

P.P*_*.P. 25

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

printf("%d %d\n", i, i++);
Run Code Online (Sandbox Code Playgroud)

要么

printf("%d %d\n", ++i, i++);
Run Code Online (Sandbox Code Playgroud)

或类似的变种.

虽然这也是已经说明的未定义行为,但是printf()当与诸如下面的语句进行比较时涉及时会有细微差别:

x = i++ + i++;
Run Code Online (Sandbox Code Playgroud)

在以下声明中:

printf("%d %d\n", ++i, i++);
Run Code Online (Sandbox Code Playgroud)

评价的顺序的论据printf()不确定.这意味着,表达式i++++i以任意顺序可以进行评估.C11标准对此有一些相关的描述:

附件J,未指明的行为

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

3.4.4,未指明的行为

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

示例未指定行为的示例是计算函数参数的顺序.

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

printf("%d %d\n", ++x, y++);
Run Code Online (Sandbox Code Playgroud)

这也有未指定的行为,因为评估的顺序++xy++未指定.但这是完全合法和有效的声明.有没有在这个声明中未定义的行为.因为修改(++xy++)是针对不同的对象完成的.

是什么呈现以下声明

printf("%d %d\n", ++i, i++);
Run Code Online (Sandbox Code Playgroud)

因为未定义的行为是这两个表达式修改同一个对象i而没有插入序列点的事实.


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

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

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
Run Code Online (Sandbox Code Playgroud)

逗号运算符从左到右计算其操作数,并仅生成最后一个操作数的值.因此,在j = (++i, i++);,++i增量i6i++产量的旧值i(6),其被分配给j.然后i变得7由于后递增.

因此,如果逗号在函数调用要成为一个逗号然后操作员

printf("%d %d\n", ++i, i++);
Run Code Online (Sandbox Code Playgroud)

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


对于那些对未定义行为不熟悉的人来说,阅读每个C程序员应该知道的关于未定义行为的内容将有益于 理解C中未定义行为的概念和许多其他变体.

这篇文章:未定义,未指定和实现定义的行为也是相关的.


sup*_*cat 22

虽然任何编译器和处理器都不太可能实际这样做,但在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
Run Code Online (Sandbox Code Playgroud)

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

如果编译器i++如上所述编写(在标准下是合法的)并且在整个表达式的评估(也是合法的)中散布上述指令,并且如果没有注意到其他指令之一发生了为了访问i,编译器可能(并且是合法的)生成将会死锁的指令序列.可以肯定,编译器几乎肯定会发现在同一个变量的情况下,问题i是在这两个地方使用,但如果一个例程接受两个指针引用pq,和用途(*p),并(*q)在上述表达式(而不是使用i两次)的所编译器将无需承认或避免,如果同一个对象的地址是通过两个会发生死锁pq.


Ant*_*ala 15

虽然语法表达的一样a = a++或者a++ + a++是合法的,该行为这些结构是不确定的,因为在C标准不服从.C99 6.5p2:

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

脚注73进一步澄清

  1. 此段落呈现未定义的语句表达式,如

    i = ++i + 1;
    a[i++] = i;
    
    Run Code Online (Sandbox Code Playgroud)

    允许的同时

    i = i + 1;
    a[i] = i;
    
    Run Code Online (Sandbox Code Playgroud)

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

  1. 以下是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语句的每个(可选)表达式(6.8.5.3); return语句中的(可选)表达式(6.8.6.4).
    • 在库函数返回之前(7.1.4).
    • 在与每个格式化的输入/输出函数转换说明符(7.21.6,7.29.2)相关联的操作之后.
    • 紧接在每次调用比较函数之前和之后,以及对比较函数的任何调用和作为参数传递给该调用的对象的任何移动之间(7.22.5).

C11中一段的措辞是:

  1. 如果相对于对同一标量对象的不同副作用或使用相同标量对象的值进行值计算,对标量对象的副作用未被排序,则行为未定义.如果表达式的子表达式有多个允许的排序,则如果在任何排序中发生这种未测序的副作用,则行为是不确定的.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
Run Code Online (Sandbox Code Playgroud)

重要的是要知道序列点是什么 - 什么是序列点,什么不是.例如,逗号运算符是序列点,因此

j = (i ++, ++ i);
Run Code Online (Sandbox Code Playgroud)

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

i += 2;
j = i;
Run Code Online (Sandbox Code Playgroud)

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

int i = 0;
printf("%d %d\n", i++, ++i, i);
Run Code Online (Sandbox Code Playgroud)

未定义的行为,因为有的评估之间没有顺序点i++++i在函数参数,和的值i因此被修改两次,由两个i++++i,先前的和下一个序列点之间.


Nik*_*ani 14

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

i = i++;
i = i++ + ++i;
Run Code Online (Sandbox Code Playgroud)

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

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

while(*src++ = *dst++);
Run Code Online (Sandbox Code Playgroud)

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


Tom*_*ime 11

/sf/ask/2065369631/,有人问起如下声明:

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);
Run Code Online (Sandbox Code Playgroud)

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

++i增量不计算其余前保证所有的完成.实际上,不同的编译器在这里会得到不同的结果.在您所提供的示例中,第一2 ++i执行,则值k[]被读取,然后最后++ik[].

num = k[i+1]+k[i+2] + k[i+3];
i += 3
Run Code Online (Sandbox Code Playgroud)

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


ali*_*oar 6

关于这种计算会发生什么好解释的文件提供n1188该ISO W14网站.

我解释了这些想法.

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

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

表达式中的序列点i=i++是在之前i=和之后i++.

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

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

i=i++
Run Code Online (Sandbox Code Playgroud)

可以解释为

tmp = i
i=i+1
i = tmp
Run Code Online (Sandbox Code Playgroud)

或者作为

tmp = i
i = tmp
i=i+1
Run Code Online (Sandbox Code Playgroud)

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

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

编辑:

解释这种含糊不清的其他好的来源是来自c-faq网站的条目(也作为一本书出版),即此处以及此处此处.


Ste*_*mit 5

您的问题可能不是,“为什么这些构造在C中未定义行为?”。您的问题可能是,“为什么这段代码(使用++)没有给我期望的值?”,有人将您的问题标记为重复,然后将您发送给这里。

这个答案试图回答这个问题:为什么您的代码没有给您期望的答案,以及如何学习识别(并避免)不能按预期工作的表达式。

我想您现在已经听说了C ++--运算符的基本定义,以及前缀形式++x与postfix形式有何不同x++。但是这些运算符很难考虑,因此为确保您理解,也许您编写了一个包含以下内容的小型测试程序:

int x = 5;
printf("%d %d %d\n", x, ++x, x++);
Run Code Online (Sandbox Code Playgroud)

但是,令您惊讶的是,该程序并没有帮助您理解-它打印了一些奇怪的,意想不到的,无法解释的输出,表明也许++做了完全不同的事情,根本没有您想像的那样。

或者,也许您正在看一个难以理解的表达式,例如

int x = 5;
x = x++ + ++x;
printf("%d\n", x);
Run Code Online (Sandbox Code Playgroud)

也许有人把这个代码给您带来困惑。该代码也没有意义,尤其是如果您运行它-并且如果在两个不同的编译器下编译并运行它,则可能会得到两个不同的答案!那是怎么回事?哪个答案是正确的?(答案是他们两个都是,或者两个都不是。)

正如您现在所听到的,所有这些表达式都是undefined,这意味着C语言不能保证它们会做什么。这是一个奇怪且令人惊讶的结果,因为您可能认为只要编写并运行该程序,任何可以编写的程序都会生成唯一的,定义明确的输出。但是对于未定义的行为,事实并非如此。

是什么使表达式不确定?表达式是否涉及++并且--始终未定义?当然不是:它们是有用的运算符,如果正确使用它们,它们的定义就很好。

对于表达式,我们要讨论的是使它们变得不确定的原因是:一次执行的操作过多,不确定何时会发生什么顺序,但是当顺序对结果产生影响时,我们将不确定它们。

让我们回到这个答案中使用的两个例子。当我写

printf("%d %d %d\n", x, ++x, x++);
Run Code Online (Sandbox Code Playgroud)

问题是,在调用之前printf,编译器是否会计算xfirst或x++or或也许的值++x?但是事实证明我们不知道。C中没有规则说函数的参数从左到右或从右到左或以其他顺序求值。所以,我们不能说编译器是否会做x第一个,然后++x,然后x++,或x++++x然后x,或其他一些订单。但是顺序显然很重要,因为根据编译器使用的顺序,我们显然会得到印刷的不同结果printf

那疯狂的表情呢?

x = x++ + ++x;
Run Code Online (Sandbox Code Playgroud)

该表达式的问题在于,它包含三种不同的尝试来修改x的值:(1)该x++部分尝试向x加1,将新值存储在x中x,然后返回x 的旧值x;(2)该++x部分尝试将x加1,将新值存储在中x,然后返回的新值x;(3)该x =部分尝试将其他两个的和分配回x。这三项尝试中的哪一项将“获胜”?这三个值中的哪一个实际上将被分配给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++;
Run Code Online (Sandbox Code Playgroud)

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

x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);
Run Code Online (Sandbox Code Playgroud)

最后一个问题是,如何区分定义正确的表达式和未定义的表达式?

就像我之前说的,未定义的表达式是一次执行太多操作,无法确定事物发生在什么顺序以及在哪里重要的表达式:

  1. 如果有一个变量在两个或多个不同的位置被修改(分配给),您如何知道哪个修改首先发生?
  2. 如果某个变量在一个地方被修改,而其值在另一个地方被使用,您如何知道它使用的是旧值还是新值?

以#1为例,在表达式中

x = x++ + ++x;
Run Code Online (Sandbox Code Playgroud)

有三种修改`x的尝试。

例如,在表达式2中

y = x + x++;
Run Code Online (Sandbox Code Playgroud)

我们都使用的值x,并对其进行修改。

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


归档时间:

查看次数:

70830 次

最近记录:

6 年 前