#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 回答
C具有未定义行为的概念,即某些语言结构在语法上有效,但您无法预测代码运行时的行为 .
据我所知,该标准没有明确说明为什么存在未定义行为的概念 . 在我看来,这仅仅是因为语言设计者希望在语义上有一些余地,而不是要求所有实现以完全相同的方式处理整数溢出,这很可能会带来严重的性能成本,他们只是留下了行为未定义,以便如果您编写导致整数溢出的代码,任何事情都可能发生 .
那么,考虑到这一点,为什么这些"issues"?该语言明确指出某些事情会导致undefined behavior . 没有问题,没有"should"参与 . 如果未声明的行为在其中一个涉及的变量被声明为
volatile
时发生更改,则不会证明或更改任何内容 . 它是未定义的;你无法推理这种行为 .你看起来最有趣的例子
是未定义行为的教科书示例(请参阅维基百科在sequence points上的条目) .
只需编译和反汇编您的代码行,如果您倾向于知道它是如何得到您正在获得的 .
这就是我在我的机器上得到的,以及我的想法:
(我......假设0x00000014指令是某种编译器优化?)
我认为C99标准的相关部分是6.5表达式,§2
和6.5.16分配运算符,§4:
行为无法真正解释,因为它同时调用了unspecified behavior和undefined behavior,所以我们无法对此代码进行任何一般性预测,尽管如果您阅读Olve Maudal的工作,例如Deep C和Unspecified and Undefined,有时您可以在非常具体的情况下做出正确的猜测使用特定的编译器和环境,但请不要在 生产环境 附近的任何地方这样做 .
所以转向未指明的行为,在draft c99 standard部分
6.5
第3段说(强调我的):所以,当我们有这样一条线:
我们不知道是先评估
i++
还是++i
. 这主要是给编译器better options for optimization .我们在这里也有未定义的行为,因为程序在sequence points之间多次修改变量(
i
,u
等等) . 从草案标准部分6.5
第2段(强调我的):它引用以下代码示例为未定义:
在所有这些示例中,代码尝试在同一序列点中多次修改对象,这将在每种情况下以
;
结束:draft c99 standard部分draft c99 standard中定义了未指定的行为:
和未定义的行为在
3.4.3
节中定义为:并注意到:
这里引用的大多数答案来自C标准,强调这些结构的行为是不确定的 . 要理解 why the behavior of these constructs are undefined ,让我们首先根据C11标准理解这些术语:
Sequenced: (5.1.2.3)
Unsequenced:
评估可以是两件事之一:
value computations ,它解决了表达式的结果;和
side effects ,它们是对象的修改 .
Sequence Point:
现在提出问题,对于像这样的表达式
标准说:
6.5表达式:
因此,上面的表达式调用UB,因为对同一对象
i
的两个副作用相对于彼此是无序的 . 这意味着没有对i
的副作用是否在++
的副作用之前或之后进行排序 .根据赋值是在增量之前还是之后发生,将产生不同的结果,这是 undefined behavior 的情况之一 .
让我们重命名赋值左边的
i
是il
并且在赋值的右边(在表达式i++
中)是ir
,那么表达式就像An important point关于Postfix
++
运算符是:这意味着表达式
il = ir++
可以被评估为要么
导致两个不同的结果
1
和2
,这取决于通过赋值和++
的副作用的顺序,因此调用UB .另一种回答这个问题的方法,不仅仅是陷入关于序列点和未定义行为的神秘细节,而是简单地问,它们应该是什么意思?程序员试图做什么?
第一个片段询问,
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的最基本方法在几乎所有编程语言中都是相同的:当然,C有一个方便的捷径:
这意味着,“向i添加1,并将结果返回给i” . 因此,如果我们通过写作构建两者的大杂烩
我们真正要说的是“向i添加1,并将结果返回给i,并将结果返回给i” . 我们很困惑,所以如果编译器也感到困惑,它也不会让我感到困扰 .
实际上,这些疯狂的表达式写作的唯一时间是人们使用它们作为应该如何工作的人为例子 . 当然,理解工作原理很重要 . 但是使用的一个实际规则是,“如果使用表达式表达不明显,请不要写它 . ”
我们曾经花了不少时间在comp.lang.c上讨论像这样的表达式以及为什么它们未定义 . 我的两个较长的答案,试图真正解释原因,在网上存档:
Why doesn't the Standard define what these do?
Doesn't operator precedence determine the order of evaluation?
虽然任何编译器和处理器都不太可能实际这样做,但在C标准下,编译器使用序列实现“i”是合法的:
虽然我不认为任何处理器支持硬件以允许有效地完成这样的事情,但是可以很容易地想象这样的行为会使多线程代码更容易的情况(例如,如果两个线程试图执行上述操作,它将保证同时序列,
i
将增加2)并且一些未来的处理器可能提供类似的功能并不是完全不可思议的 .如果编译器如上所述编写
i++
(在标准下是合法的)并且在整个表达式的评估期间(也是合法的)散布上述指令,并且如果没有注意到其他指令之一碰巧访问i
,编译器生成一系列会死锁的指令是可能的(也是合法的) . 可以肯定的是,如果在两个地方使用相同的变量i
,但是如果例程接受对两个指针的引用,则编译器几乎肯定会检测到该问题 .p
和q
,并在上面的表达式中使用(*p)
和(*q)
(而不是使用i
两次),如果为p
和q
传递了相同的对象地址,则不需要编译器识别或避免发生的死锁 .通常这个问题被链接为与代码相关的问题的副本
要么
或类似的变种 .
虽然这也是undefined behaviour已经如上所述,但在与以下语句进行比较时涉及
printf()
时会有细微差别:在以下声明中:
printf()
中order of evaluation的参数是unspecified . 这意味着,可以按任何顺序评估表达式i++
和++i
. C11 standard对此有一些相关的描述:Annex J, unspecified behaviours
3.4.4, unspecified behavior
未指定的行为本身不是问题 . 考虑这个例子:
这也有未指定的行为,因为未指定
++x
和y++
的评估顺序 . 但它's perfectly legal and valid statement. There'在此声明中没有未定义的行为 . 因为修改(++x
和y++
)是针对不同的对象完成的 .是什么呈现以下声明
因为未定义的行为是这两个表达式修改同一个对象
i
而没有干预sequence point的事实 .另一个细节是printf()调用中涉及的逗号是分隔符,而不是comma operator .
这是一个重要的区别,因为逗号运算符确实在其操作数的评估之间引入了一个序列点,这使得以下内容合法:
逗号运算符从左到右计算其操作数,并仅生成最后一个操作数的值 . 所以在
j = (++i, i++);
中,++i
增量i
到6
和i++
产生i
(6
)的旧值,该值被分配给j
. 由于后增量,i
变为7
.因此,如果函数调用中的逗号是逗号运算符,那么
不会有问题 . 但是它会调用未定义的行为,因为这里的逗号是一个分隔符 .
对于那些对未定义行为不熟悉的人来说,阅读What Every C Programmer Should Know About Undefined Behavior可以从中理解C中未定义行为的概念和许多其他变体 .
这篇文章:Undefined, unspecified and implementation-defined behavior也是相关的 .
C标准规定变量最多只能在两个序列点之间分配一次 . 例如,分号是序列点 .
所以表格的每一个陈述:
等违反了这条规则 . 该标准还表示行为未定义且未指定 . 有些编译器会检测到这些并产生一些结果,但这不符合标准 .
但是,两个不同的变量可以在两个序列点之间递增 .
以上是复制/分析字符串时的常见编码习惯 .
虽然像
a = a++
或a++ + a++
这样的表达式的 syntax 是合法的,但这些结构的 behaviour 是 undefined ,因为不符合C标准中的 shall . C99 6.5p2:随着footnote 73进一步澄清
各种序列点列于C11(和C99)的附件C中:
同一个paragraph in C11的措辞是:
您可以通过例如使用最新版本的GCC与
-Wall
和-Werror
来检测程序中的此类错误,然后GCC将完全拒绝编译您的程序 . 以下是gcc(Ubuntu 6.2.0-5ubuntu12)6.2.0 20161005的输出:重要的是要知道what a sequence point is -- and what is a sequence point and what isn't . 例如,逗号运算符是序列点,所以
是明确的,并将
i
增加一个,产生旧值,丢弃该值;然后在逗号操作员,解决副作用;然后将i
递增1,结果值成为表达式的值 - 也就是说这只是一种人为的写j = (i += 2)
的方式,这又是一种"clever"写的方式但是,
,
in函数参数列表不是逗号运算符,并且在不同参数的计算之间没有序列点;相反,他们的评价对彼此没有考虑;所以函数调用具有未定义的行为,因为 there is no sequence point between the evaluations of i++ and ++i in function arguments ,因此
i
的值在前一个和下一个序列点之间由i++
和++i
两次修改 .在https://stackoverflow.com/questions/29505280/incrementing-array-index-in-c,有人问起如下声明:
打印7 ... OP预计它打印6 .
在其余计算之前,不保证
++i
增量全部完成 . 实际上,不同的编译器在这里会得到不同的结果 . 在您提供的示例中,执行了前两个++i
,然后读取了k[]
的值,然后是最后的++i
然后是k[]
.现代编译器将很好地优化它 . 事实上,可能比你最初编写的代码更好(假设它按照你希望的方式工作) .
关于这种计算中发生的事情的一个很好的解释在n1188的文件n1188中提供 .
我解释了这些想法 .
在这种情况下适用的标准ISO 9899的主要规则是6.5p2 .
像
i=i++
这样的表达式中的序列点位于i=
之前和i++
之后 .在我上面引用的论文中,解释了你可以把程序想象成由小盒子组成,每个盒子包含2个连续序列点之间的指令 . 序列点在标准的附录C中定义,在
i=i++
的情况下,有2个序列点来界定完整表达 . 这样的表达式在语法上与语法的Backus-Naur形式的expression-statement
条目相同(语法在附录A中提供) .因此,框内的指令顺序没有明确的顺序 .
可以解释为
或者作为
因为所有这些形式来解释代码
i=i++
都是有效的,并且因为两者都生成不同的答案,所以行为是未定义的 .因此,组成程序的每个框的开头和结尾都可以看到序列点[框中是C中的原子单元],并且在框内,所有情况下都没有定义指令的顺序 . 更改该订单有时可以更改结果 .
编辑:
解释这种含糊不清的其他好的来源是来自c-faq网站(也发布as a book)的条目,即here和here以及here .
原因是程序正在运行未定义的行为 . 问题在于评估顺序,因为根据C 98标准没有所需的序列点(根据C 11术语,没有操作在其他操作之前或之后排序) .
但是,如果你坚持使用一个编译器,你会发现行为是持久的,只要你不添加函数调用或指针,这会使行为更加混乱 .
}
海湾合作委员会如何运作?它右侧(RHS)按从左到右的顺序计算子表达式,然后将值分配给左侧(LHS) . 这正是Java和C#的行为和定义标准的方式 . (是的,Java和C#中的等效软件已经定义了行为) . 它按照从左到右的顺序在RHS声明中逐个评估每个子表达式;对于每个子表达式:首先评估c(预增量),然后将值c用于操作,然后是后增量c) .
根据GCC C++: Operators
GCC理解的定义行为C中的等效代码:
然后我们去Visual Studio . Visual Studio 2015,您将获得:
visual studio如何工作,它采用另一种方法,它在第一遍中评估所有预增量表达式,然后在第二遍中使用操作中的变量值,在第三遍中从RHS分配到LHS,然后在最后一遍中它评估所有的一次传递后增量表达式 .
因此,作为Visual C的定义行为C中的等价物理解:
正如Visual Studio文档在Precedence and Order of Evaluation处所述:
你的问题可能不是,"Why are these constructs undefined behavior in C?" . 你的问题可能是,“为什么这段代码(使用
++
)没有给我预期的 Value ?”,有人将你的问题标记为重复,并将你发送到这里 .这个答案试图回答这个问题:为什么你的代码没有给你你想要的答案,你怎么能学会识别(并避免)不能按预期工作的表达式 .
我现在假设您've heard the basic definition of C' s
++
和--
运算符,以及++x
的前缀与后缀形式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
之前,编译器是先计算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的值:(1)
x++
部分尝试将1添加到x,将新值存储在x
中,并返回旧值x
; (2)++x
部分尝试将1添加到x,将新值存储在x
中,并返回x
的新值; (3)x =
部分尝试将其他两个的总和分配回x . 这三个尝试作业中的哪一个将"win"?这三个值中的哪一个实际上会分配给x
?再一次,也许令人惊讶的是,C中没有任何规则可以告诉我们 .您可能会想到优先级或关联性或从左到右的评估会告诉您事情发生的顺序,但事实并非如此 . 你可能不相信我,但请接受我的话,我会再说一遍:优先级和结合性不会决定C中表达式的评估顺序的每个方面 . 特别是,如果在一个表达式中有多个我们尝试的不同地点为
x
,优先级和关联性等事物分配新值不会告诉我们哪些尝试首先发生,或最后发生,或任何事情发生 .因此,如果您想确保所有程序都定义明确,可以编写哪些表达式,哪些表达式可以编写?
这些表达式都很好:
这些表达式都是未定义的:
最后一个问题是,如何判断哪些表达式定义明确,哪些表达式未定义?
正如我之前所说的那样,未定义的表达式是那些一次性过多的表达式,在那里你无法确定发生了什么顺序,以及顺序的重要性:
如果's one variable that'在两个或多个不同的地方被修改(分配给),你怎么知道哪个修改首先发生?
如果's a variable that'在一个地方被修改,并且在另一个地方使用了它的值,你怎么知道它是使用旧值还是新值?
作为#1的一个例子,在表达式中
有三次尝试修改`x .
作为#2的一个例子,在表达式中
我们都使用
x
的值,并修改它 .这就是答案:确保在您编写的任何表达式中,每个变量最多被修改一次,如果修改了变量,您也不会尝试在其他地方使用该变量的值 .