35

C 编程教授向我们的班级提出了这个问题:

您将获得以下代码:

int x=1;
printf("%d",++x,x+1);

它总是会产生什么输出?

大多数学生表示不确定的行为。谁能帮我理解为什么会这样?

感谢您的编辑和答案,但我仍然感到困惑。

4

8 回答 8

40

在每个合理的情况下,输出都可能是 2。实际上,您所拥有的是未定义的行为。

具体来说,该标准说:

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

在评估函数的参数之前有一个序列点,并且在评估所有参数之后有一个序列点(但尚未调用该函数)。在这两者之间(即,在评估参数时)没有序列点(除非参数是一个表达式,在内部包含一个,例如使用&& ||or,运算符)。

这意味着调用printf正在读取先前值确定存储的值(即++x确定第二个参数的值(即x+1)。这显然违反了上面引用的要求,导致未定义的行为。

您提供了一个没有给出转换说明符的额外参数这一事实不会导致未定义的行为。如果您提供的参数少于转换说明符,或者如果参数的(提升)类型与转换说明符的类型不一致,您会得到未定义的行为——但传递额外的参数不会

于 2010-08-10T15:59:56.073 回答
14

任何时候程序的行为是未定义的,任何事情都可能发生——经典的短语是“恶魔可能从你的鼻子里飞出来”——尽管大多数实现并没有走那么远。

函数的参数在概念上是并行评估的(技术术语是在它们的评估之间没有序列点)。这意味着表达式++x,并且x+1可以按此顺序、相反顺序或某种交错方式进行计算。当您修改变量并尝试并行访问其值时,行为未定义。

在许多实现中,参数是按顺序计算的(尽管并不总是从左到右)。因此,在现实世界中,除了 2 之外,您几乎看不到任何东西。

但是,编译器可以生成如下代码:

  1. 将 x 加载到寄存器r1中。
  2. x+1通过将 1 加到 来计算r1
  3. ++x通过将 1 加到 来计算r1。没关系,因为x已加载到r1. 鉴于编译器的设计方式,步骤 2 不能修改r1,因为只有x在两个序列点之间读取和写入时才会发生这种情况。这是C标准禁止的。
  4. 存入. r1_x

在这个(假设的,但正确的)编译器上,程序将打印 3。

编辑:传递一个额外的参数printf是正确的(N1256 中的第 7.19.6.1-2 节;感谢Prasoon Saurav)指出了这一点。另外:添加了一个例子。)

于 2010-08-10T15:38:43.133 回答
12

正确答案是:代码会产生未定义的行为。

行为未定义的原因是这两个表达式++xx + 1正在修改x和读取x一个不相关的(与修改)原因,并且这两个动作没有被序列点分隔。这会导致 C(和 C++)中的未定义行为。在 C 语言标准的 6.5/2 中给出了要求。

printf请注意,在这种情况下未定义的行为与仅给函数一个格式说明符和两个实际参数这一事实完全无关 。在格式字符串中提供比格式说明符更多的参数printf在 C 中是完全合法的。同样,问题的根源在于违反 C 语言的表达式求值要求。

另请注意,本次讨论的一些参与者未能掌握未定义行为的概念,并坚持将其与未指定行为的概念混为一谈。为了更好地说明差异,让我们考虑以下简单示例

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会被称为 asprintf("%d", 2, 2)或 as printf("%d", 2, 3)。在这两种情况下,输出确实是2. 但是,此变体的重要区别在于,所有访问x都被包装到每个函数开头和结尾的序列点中,因此此变体不会产生未定义的行为。

这正是其他一些海报试图强加于原始示例的原因。但这是不可能的。原始示例产生未定义的行为,这是一个完全不同的野兽。他们显然试图坚持在实践中未定义的行为总是等同于未指定的行为。这是一个完全虚假的说法,仅表明制造它的人缺乏专业知识。原始代码产生未定义的行为,句号。

为了继续这个例子,让我们将之前的代码示例修改为

printf("%d %d", inc_x(&x), x_plus_1(x));

代码的输出通常会变得不可预测。它可以打印2 2,也可以打印2 3。但是请注意,即使行为是不可预测的,它仍然不会产生未定义的行为。行为是未指定的,位不是未定义的。未指定的行为仅限于两种可能性:要么2 2要么2 3。未定义的行为不限于任何事情。它可以格式化你的硬盘驱动器而不是打印一些东西。感到不同。

于 2010-08-10T16:46:51.483 回答
2

大多数学生表示不确定的行为。谁能帮我理解为什么会这样?

因为未指定计算函数参数的顺序。

于 2010-08-10T15:28:59.060 回答
2

What output will it always produce ?

It will produce 2 in all environments I can think of. Strict interpretation of the C99 standard however renders the behaviour undefined because the accesses to x do not meet the requirements that exist between sequence points.

Most students said undefined behavior. Can anyone help me understand why it is so?

I will now address the second question which I understand as "Why do most of the students of my class say that the shown code constitutes undefined behaviour?" and I think no other poster has answered so far. One part of the students will have remembered examples of undefined value of expressions like

f(++i,i)

The code you give fits this pattern but the students erroneously think that the behaviour is defined anyway because printf ignores the last parameter. This nuance confuses many students. Another part of the student will be as well versed in standard as David Thornley and say "undefined behaviour" for the correct reasons explained above.

于 2010-08-10T15:33:53.430 回答
1

关于未定义行为的观点是正确的,但还有一个额外的问题: printf 可能会失败。它在做文件 IO;它可能失败的原因有很多,如果不知道完整的程序和执行它的上下文,就不可能消除它们。

于 2010-08-10T17:37:14.530 回答
0

回应 codaddict 的答案是 2。

printf 将使用参数 2 调用并打印它。

如果将此代码放在如下上下文中:

void do_something()
{
    int x=1;
    printf("%d",++x,x+1);
}

然后该函数的行为被完全且明确地定义。我当然不是说这是好的或正确的,或者 x 的值是可以确定的。

于 2010-08-11T00:28:51.223 回答
-1

输出将始终为(对于 99.98% 的最重要的符合标准的编译器和系统)2。

根据标准,根据定义,这似乎是“未定义的行为”,这是一个自我证明的定义/答案,并且没有说明实际可能发生的事情,尤其是为什么

实用夹板(它不是标准合规性检查工具)以及夹板的程序员,将其视为“未指定的行为”。这意味着,基本上,评估(x+1)可以给出 1+1 或 2+1,具体取决于x实际完成更新的时间。然而,由于表达式被丢弃(printf 格式读取 1 个参数),输出不受影响,我们仍然可以说它是 2。

undefined.c:7:20:参数 2 修改 x,由参数 3 使用(实际参数的评估顺序未定义): printf("%d\n", ++x, x + 1) 代码具有未指定的行为。未定义函数参数或子表达式的求值顺序,因此如果在不同的地方使用和修改一个值,而不用序列点约束求值顺序分隔,则表达式的结果是未指定的。

如前所述,未指定的行为仅影响 的评估(x+1),而不影响整个语句或它的其他表达式。因此,在“未指定行为”的情况下,我们可以说输出为 2,没有人可以反对。

但这不是未指定的行为,它似乎是“未定义的行为”。而且“未定义的行为”似乎必须是影响整个语句而不是单个表达式的东西。这是由于“未定义的行为”实际发生的位置(即究竟是什么影响)的谜团。

如果有动机“未定义的行为”附加到(x+1)表达式上,就像在“未指定的行为”的情况下一样,那么我们仍然可以说输出总是(100%)2. 将“未定义的行为”附加到(x+1)意味着我们不能说它是1+1还是2+1;它只是“任何东西”。但同样,由于 printf,“任何东西”都被丢弃了,这意味着答案将是“总是 (100%) 2”。

相反,由于神秘的不对称性,“未定义的行为”不能仅附加x+1,但实际上它必须至少影响++x(顺便说一句,它负责未定义的行为),如果不是整个语句。如果它只感染++x表达式,则输出是“未定义值”,即任何整数,例如 -5847834 或 9032。如果它感染整个语句,那么您可能会在控制台输出中看到 gargabe,您可能不得不停止使用 ctrl-c 编程,可能在它开始阻塞你的 CPU 之前。

根据一个都市传说,“未定义的行为”不仅会感染整个程序,还会感染您的计算机和物理定律,因此您的程序可以创造出神秘的生物并飞走或吃掉您。

没有答案可以有效地解释有关该主题的任何内容。它们只是“哦,看标准是这样说的”(和往常一样,这只是一种解释!)。因此,至少您已经了解到“标准存在”,并且它们消除了教育问题(因为当然,不要忘记您的代码是错误的,无论未定义/未指定的行为主义和其他标准事实),无用的逻辑论点和漫无目的的深入调查和了解。

于 2010-08-10T19:32:40.363 回答