这是未定义的C行为?
我们class的C程序devise教授问了这个问题:
你得到的代码是:
int x=1; printf("%d",++x,x+1);
它会一直产生什么输出?
大多math生说未定义的行为。 谁能帮我理解为什么是这样吗?
感谢编辑和答案,但我仍然困惑。
在每一个合理的情况下,产出可能是2。 实际上,你所拥有的是未定义的行为。
具体来说,标准说:
在前一个序列点和下一个序列点之间,一个对象应该通过评估一个expression式来最多修改其存储值一次。 此外,先前的值应该是只读的,以确定要存储的值。
在评估一个函数的参数之前有一个序列点,并且在所有参数已经被评估之后(但是该函数尚未被调用)的序列点。 在这两者之间(即在参数被评估的时候), 没有序列点(除非参数是一个expression式在内部包含一个参数,比如使用&&
||
或运算符)。
这意味着对printf
的调用正在读取先前的值,以确定所存储的值(即++x
) 并确定第二个参数(即x+1
)的值。 这显然违反了上面引用的要求,导致未定义的行为。
事实上,你提供了一个额外的参数没有给出转换说明符不会导致未定义的行为。 如果提供的转换说明符的参数较less , 或者参数的(提升的)types与转换说明符的types不一致,则会得到未定义的行为 – 但传递一个额外的参数不会。
任何时候程序的行为都是不确定的,任何事情都可能发生 – 古典的说法是“恶魔可能会飞出你的鼻子” – 尽pipe大多数实现并没有走得太远。
一个函数的参数在概念上是并行的(技术术语是在它们的评估之间没有序列点)。 这意味着expression式++x
和x+1
可以按照这个顺序,相反的顺序, 或者以某种交错的方式被评估。 当你修改一个variables并尝试并行访问它的值时,行为是不确定的。
在许多实现中,参数按顺序评估(尽pipe不总是从左到右)。 所以你不可能在现实世界中看到任何东西,除了2。
但是,编译器可以生成这样的代码:
- 将x载入寄存器
r1
。 - 通过给
r1
加1来计算x+1
。 - 通过将
r1
加1来计算++x
。 没关系,因为x
已经加载到r1
。 鉴于编译器是如何devise的,第2步不能修改r1
,因为只有在读取x
以及写入两个序列点之间的情况下才会发生这种情况。 这是C标准禁止的。 - 将
r1
存储到x
。
在这个(假设的,但是正确的)编译器上,程序会打印3。
( 编辑:向printf
传递额外的参数是正确的(N1256中的§7.19.6.1-2 ;感谢Prasoon Saurav )指出了这一点。
正确的答案是:代码产生未定义的行为。
行为未定义的原因是,两个expression式++x
和x + 1
正在修改x
和读取一个不相关(修改)原因的x
,而这两个操作不是由一个顺序点分隔的。 这导致C(和C ++)中的未定义行为。 要求在C语言标准的6.5 / 2中给出。
请注意,在这种情况下未定义的行为与printf
函数仅给出一个格式说明符和两个实际参数完全无关。 为了给printf
提供更多的参数,而不是格式化string中的格式说明符,在C中是完全合法的。问题的根源在于违反了C语言的expression式求值要求。
还要注意的是,这个讨论的一些参与者没有把握未定义行为的概念,并坚持将其与未指定行为的概念混合。 为了更好地说明差异,我们考虑下面的简单例子
int inc_x(int *x) { return ++*x; } int x_plus_1(int x) { return x + 1; } int x = 1; printf("%d", inc_x(&x), x_plus_1(x));
上面的代码和原来的代码是“等价的”,除了涉及到我们x
的操作被包装到函数中。 这个最新的例子会发生什么?
这段代码中没有未定义的行为。 但是由于printf
参数的评估顺序是未指定的 ,所以这个代码会产生未指定的行为 ,也就是说printf
可能被称为printf("%d", 2, 2)
或printf("%d", 2, 3)
。 在这两种情况下,输出确实是2
。 然而,这个变体的重要区别在于对x
所有访问都被包装成每个函数开始和结尾的序列点,所以这个变体不会产生未定义的行为。
这正是一些其他海报试图强迫原来的例子的原因。 但是这是不可能的。 原来的例子产生了未定义的行为,这是一个完全不同的野兽。 他们显然试图坚持在实践中,未定义的行为总是等同于未指定的行为。 这是一个完全虚假的说法,只表明缺乏专业知识的人。 原始代码产生未定义的行为,句点。
为了继续这个例子,我们来修改前面的代码示例
printf("%d %d", inc_x(&x), x_plus_1(x));
代码的输出将变得通常不可预测。 它可以打印2 2
或打印2 3
。 但是请注意,即使行为是不可预知的,它仍然不会产生未定义的行为 。 行为是未指定的 ,有点不确定 。 未指定的行为仅限于两种可能性: 2 2
或2 3
。 未定义的行为不限于任何事物。 它可以格式化你的硬盘,而不是打印一些东西。 感到不同。
大多math生说未定义的行为。 谁能帮我理解为什么是这样吗?
因为没有指定函数参数的计算顺序。
它会一直产生什么输出?
它会在我能想到的所有环境中产生2个。 但是,对C99标准的严格解释使得行为未定义,因为对x的访问不符合序列点之间存在的要求。
大多math生说未定义的行为。 谁能帮我理解为什么是这样吗?
我现在将解答第二个问题,我理解为:“为什么我class的大部分学生都说所示的代码构成未定义的行为? 而且我认为目前还没有其他的海报可以回答。 学生的一部分将会记住例如未定义的expression式的例子
f(++i,i)
你给出的代码适合这种模式,但学生错误地认为行为是无论如何定义的,因为printf忽略了最后一个参数。 这种细微差别让许多学生感到困惑。 学生的另一部分将像David Thornley一样精通标准,并为了上面解释的正确理由而说出“未定义的行为”。
关于未定义的行为的观点是正确的,但还有一个额外的皱纹:printf可能会失败。 它正在做文件IO; 有很多原因可能会失败,如果不知道完整的程序和执行的上下文,就不可能消除它们。
回答codaddict的答案是2。
printf将被调用参数2,它将打印它。
如果这个代码被放在如下的上下文中:
void do_something() { int x=1; printf("%d",++x,x+1); }
那么这个函数的行为是完全无误的定义的。 当然,我并不认为这是好的或正确的,或者说x的价值是可以确定的。
输出将始终(对于最重要的符合stadard的编译器和系统的99.98%)2。
按照这个标准,这个定义看起来似乎是“未定义的行为”,这个定义/答案是自我辩护的,而且没有说明究竟会发生什么事情,尤其是为什么 。
实用夹板 (这不是一个标准的合规性检查工具)和夹板程序员认为这是“未指定的行为”。 这意味着,基本上, (x+1)
的评估可以给出1 + 1或2 + 1,这取决于x
的更新何时实际完成。 由于expression式被丢弃(printf格式读取1个参数),所以输出不受影响,我们仍然可以说它是2。
undefined.c:7:20:参数2修改参数3使用的x(实际参数的评估顺序未定义):printf(“%d \ n”,++ x,x + 1)代码具有未指定的行为。 函数参数或子expression式的评估顺序没有定义,所以如果一个值在不是由序列点约束评估顺序隔开的不同位置被使用和修改,那么expression式的结果是不确定的。
如前所述,未指定的行为仅影响(x+1)
的评价,而不影响整个陈述或其他expression。 所以在“不明确的行为”的情况下,我们可以说输出是2,没有人可以反对。
但这不是没有说明的行为,它似乎是“未定义的行为”。 而“未定义的行为”似乎必须是影响整个陈述而不是单一expression的东西。 这是由于“未定义行为”实际发生的地方(即究竟是什么影响)所引起的。
如果将“未定义的行为” 附加到(x+1)
expression式的动机中,就像在“未指定的行为”情况下一样,那么我们仍然可以说输出总是(100%)。2.附加“未定义的行为“ (x+1)
意味着我们不能说是1 + 1还是2 + 1; 这只是“ 什么 ”。 但是,由于printf,这个“任何东西”都被放弃了,这意味着答案将是“始终(100%)2”。
相反,由于神秘的不对称性,“未定义的行为”不能仅仅附加到x+1
,而是至less要影响++x
(顺便说一下,这是未定义的行为的责任),如果不是全部的说法。 如果它只感染++x
expression式,那么输出是一个“未定义的值”,即任何整数,例如-5847834或9032.如果它感染了整个语句,那么你可以在控制台输出中看到gargabe,可能你可以用ctrl-c停止程序,可能在它开始窒息你的cpu之前。
根据一个都市传说,“未定义的行为”不仅会感染整个程序,还会感染你的计算机和物理定律,以便通过你的程序创build神秘的生物,飞走或吃掉你。
没有答案解释有关这个话题胜任的事情。 他们只是一个“哦,看标准说这个”(这只是一个解释,像往常一样!)。 因此,至less你已经了解到“标准存在”,它们使教育问题干涸(当然,不要忘记你的代码是错误的 ,不pipe未定义/未指定的行为和其他标准事实),无用的逻辑论点和漫无目的的深入调查和了解。