【问题标题】:Is this undefined C behaviour?这是未定义的 C 行为吗?
【发布时间】:2010-08-10 15:25:45
【问题描述】:

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

给你密码:

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

它总是会产生什么输出?

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

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

【问题讨论】:

  • +1 表示一个问得好、看似简单的问题,它会产生有趣的答案和讨论。很高兴看到编程课业中出现的一个问题不是“请帮我做作业”;-)

标签: c undefined-behavior


【解决方案1】:

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

具体来说,标准说:

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

有一个序列点之前评估函数的参数,以及一个序列点之后所有参数都已被评估(但该函数尚未调用)。在这两者之间(即,在评估参数时)not 是一个序列点(除非参数是一个内部包含一个的表达式,例如使用 && || 或 @987654323 @运算符)。

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

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

【讨论】:

  • @Jerry:恭喜你写出正确而清晰的答案!
  • The output is likely to be 2。为 可能 词 +1。 :)
  • 我很遗憾我的英语不是那么好,无法像标准的演讲那样抓住那个律师……但在任何可以定义的实现中,结果总是2,不太可能是2。这是因为“未定义的行为”会影响 x+1,而这根本没有被采用;我无法想象任何暗示。 stdargs,也不是 printf 的实际代码,它们能够在 ++x 上“移动”未定义的行为,而不会在 std 可以定义为已定义行为的情况下破坏整个实现 - 即不会使整个编译器无法使用跨度>
  • @ShinTakezou:你错了;符合要求的实现完全有可能打印“42”。这真的不太可能。我也不相信你的想象力涵盖了所有可能的编译器实现。
  • @Chris:您在哪里看到提到副作用:“此外,先前的值应只读以确定要存储的值”?没有这样的限制,这适用于所有代码。
【解决方案2】:

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

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

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

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

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

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

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

【讨论】:

  • 首先,格式为 "%d" 的 printf 需要一个整数参数,但您已经传递了两个。行为未定义。不它不是。第二个参数仅被评估,但由于这个原因,行为不是未定义的。注意: printf("%d %d",x++); 是未定义的,因为格式说明符的数量大于参数的数量。
  • @ShinTakezou:在这个(假设的)编译器上,+++= 等更新指令恰好被特殊处理。编译器假定如果x 已经加载到r1 中,并且x 在下一个序列点之前更新,那么到那时r1 仍然包含x 的值生成更新指令。
  • @ShinTakezou:它可以很容易地为x+1, x+1 生成不同的代码,因为这些都不会导致x 发生变化。事实上,一个好的编译器可能会将它们识别为常见的子表达式。编译器很可能想要做一些不同的事情来计算包含赋值的函数参数。
  • 这完全是错误的——在第 3 步中,r1 中的值不是x,因此它无法通过将 1 添加到 ++x 来计算它。按照这个逻辑,表达式x = (x + 1) - x 可能会将 x 设置为 0 而不是 1
  • @Chris:编译器可以生成它选择的任何内容,因为代码具有未定义的行为。在x=(x+1)-x 中,上面步骤 3 中使用的规则不适用,因为没有更新操作。如果你接下来想反对x+=x,编译器总是在处理lhs之前计算赋值的rhs,所以它也会生成正确的代码;该推理不适用于printf("%d",++x,x+1),因为++x 部分和x+1 部分之间没有依赖关系。
【解决方案3】:

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

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

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

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

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

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

【讨论】:

  • (我又开始与盲目的性病作斗争了,抱歉)。 但是 OP Q 是:输出是什么?并且输出是 always 2。一般来说,xyz(f, ++x, x+1) 是 undef 行为,因为我们不能说首先评估哪个,并且一个有副作用(修改 x)。但在这种情况下,我们知道 printf 只能得到 ++x,而 x+1 不能修改 x,否则我们将有一个损坏的 C impl。所以,输出是可预测的,它总是 2
  • @ShinTakezou:不正确。首先,当行为未定义时,任何事情都可能发生。所以,你关于什么可以发生和什么不能发生的断言根本站不住脚。其次,即使定义了行为x + 1,只要效果满足规范,x 绝对可以做任何事情。是的,x + 1可以修改x,只要它之后将其恢复为原始值。 Gilles 的回答有一个这样的评估示例。
  • @ShinTakezou:第三,你说的是“undef 行为,因为我们不能说首先评估哪个”。这表明您不了解 undefinedunspecified 行为之间的区别。您将 undefined 行为误认为是 unspecified 行为,并且由于该错误而得出毫无意义的结论。在这种情况下,未定义的行为与首先评估的内容完全无关
  • @ShinTakezou:为了 C 语言的所有用户的利益,该标准对符合标准的程序和符合标准的实现进行了限制。如果您拒绝对 C 语言中的核心概念使用与其他所有人相同的定义,那么您与大多数其他人在行为是否未定义或输出是否始终为 2 上存在分歧也就不足为奇了,但没有任何意义那个论点。其他人是从你不使用的定义开始的。
  • @ShinTakezou:至于你拒绝理解标准条款……嗯,这正是你无法提出(或接受)这个问题的正确答案的原因。
【解决方案4】:

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

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

【讨论】:

  • 但顺序无关紧要,因为只有一个参数被打印出来
  • 这无关紧要;有一个 ++xx+1 没有中间序列点的事实是它未定义的原因。
  • 那些标准定义合规性让我总是(m/s)广告。由于 printf 将使用由“%d”驱动的,只有第一个参数通过,顺便说一句,这是唯一一个修改 x 导致副作用的参数。由于 x+1 不会改变 x,所以可以在之前,或之后,或从不求值,++x 的结果不会改变;并且由于 ++x 是唯一采用的,因此结果/行为不是未定义、未指定或任何其他标准字。总是这样,无论实施如何; printf 是 (fmt, ...) 虽然编译器可以检查 fmt 是否与额外的参数匹配,但这不是强制性的,也不是总是需要的
  • @ShinTakezou:如果你不喜欢关注标准,那是一回事,但如果你不这样做,你真的不应该参与这样的讨论。此外,如果您将这种态度与使用不合格代码的意愿结合起来,有时您会被您所做的假设所困扰,编译器编写者也不会同情。
  • @ShinTakezou:我是说这是未定义的行为,根据标准的字母,没有熟悉标准的人会坚持告诉你其他任何事情。关于各种标准有合理的争论,这不是其中之一。就标准而言, printf() 将仅使用一个值这一事实在这里无关紧要。一件事情实际上是未定义的,即使它没有被使用,这意味着整个事情是未定义的。我会非常惊讶地发现一个没有输出 2 的实现,但它在它的权限范围内。
【解决方案5】:

它总是会产生什么输出?

它会在我能想到的所有环境中产生 2。然而,对 C99 标准的严格解释导致行为未定义,因为对 x 的访问不满足序列点之间存在的要求。

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

我现在将解决我理解的第二个问题“为什么我班的大多数学生都说显示的代码构成未定义的行为?”我认为到目前为止还没有其他发帖人回答。一部分学生会记住表达式的未定义值示例,例如

f(++i,i)

您提供的代码符合这种模式,但学生错误地认为无论如何都定义了该行为,因为 printf 忽略了最后一个参数。这种细微差别让许多学生感到困惑。另一部分学生将与 David Thornley 一样精通标准,并出于上述正确原因说出“未定义的行为”。

【讨论】:

  • 行为未定义,因为 x 被分配给并在不同的上下文中引用,在相同的序列点内。输出几乎总是 2。C 标准中没有要求这样做。
  • @David 我检查了标准,从技术上讲你是 100% 正确的。我现在将更正我的答案。
  • @Peter G.:“……但无论如何定义了行为……”。它如何成为“无论如何定义”?在这种情况下,未定义的行为不会以任何方式“附加”到printf 的最后一个参数。仅仅因为printf 忽略它并不会突然定义行为。
  • @Andrey 谢谢。在修订后的答案中,我的意思是说学生们无论如何都会相信它是被定义的(就像我不久前所做的那样......)。我现在在修订后的^2 答案中更正了这一点。
  • 我需要强调我的 +1 是为了“您提供的代码符合这种模式,但无论如何定义了行为,因为 printf 忽略了最后一个参数”。即使对于 std-lawyer,您应该强调您使用的是常识中的“已定义行为”这一事实,而不是对于非未定义行为的标准定义。一个程序,给定一个输入,总是发出相同的输出,因此是可预测的,不应该被定义为“未定义的行为”,即使它可以正式地定义。
【解决方案6】:

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

【讨论】:

  • 讽刺+1(或者我发现了一些不存在的东西?!:D)
【解决方案7】:

回应 codaddict,答案是 2。

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

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

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

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

【讨论】:

  • 我同意你和 codaddict 的说法,输出将始终为 2;但是你可以读到很多关于它的东西,有些人认为这是未定义的行为,因此我们不能说它总是 2 ......我试图了解他们的说法是否合理;目前,我看不到很好的解释来说明它们是对的而我们是错的(即我们不能有论据来说它总是 2);如果您有时间、耐心并且愿意,可以尝试为您的答案提供更可靠的论据。
【解决方案8】:

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

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

实用程序 splint(它不是标准合规性检查工具),因此 splint 的程序员将其视为“未指定行为”。这意味着,基本上,(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,但实际上它必须至少影响 @ 987654327@ (顺便说一句,它负责未定义的行为),如果不是整个语句。如果它只感染++x 表达式,则输出是“未定义值”,即任何整数,例如-5847834 或 9032。如果它感染了整个语句,那么您可能会在控制台输出中看到 gargabe,可能您必须使用 ctrl-c 停止程序,可能在它开始阻塞您的 CPU 之前。

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

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

【讨论】:

  • @ShinTakezou:标准编写者决定不定义f(++x,x+1) 行为的原因是他们选择遵循一个简单的原则:在一段并行执行期间(即,在两个序列点之间) ),每个变量可能有一个写入或任意数量的读取,但不能同时具有。这是并发或并行系统中的常见设计原则。
  • @ShinTakezou:顺便问一下,您希望printf("%d %d", ++x, x+1)printf("%d %d", x+1, ++x) 有什么行为?
  • @Gilles 我在写下关于不存在 expl 的评论后读到了这篇文章。供选择;但不是很清楚;我会说 USB 为“func1(&x), func2(x)”,除非“++x, x+1”可以并行化,而“func1(&x), func2(x)” 则不能(为什么?);并行化中的问题不在于副作用,它们都存在于 ++x, x+1 和 func1(&x), func2(x) 中?即使考虑到并行化,我也看不出有任何理由让它成为 UDB 而不是 USB,就像 caf 对 func1(&x)、func2(x) 所说的那样;非虚假并行化的例子,证明它而不是 USB 是值得赞赏的
  • 简短的回答是否定的,因为这样做编译器可能会破坏其正确编译某些“合法”代码的能力......如果编译器发现两个相同的引用函数调用的参数列表中的int 变量,其中一个在变量上使用++,它可以发出代码以打印有关未定义行为的错误消息,然后调用abort。它将完全符合标准,它仍然可以正确编译所有符合标准的程序,但它不会从 OP 给出的程序中输出2。是不是更清楚了?
  • @ShinTakezou - 一旦“生成”代码,运行它总是会得到 2(这是我的主张) - 完全有可能符合标准的 C 实现为该表达式生成不会导致第二个参数为“2”的代码。无论您是否知道这样做的人都无关紧要,问题是这是否可能,而且确实如此。
猜你喜欢
  • 1970-01-01
  • 2020-06-15
  • 1970-01-01
  • 1970-01-01
  • 2018-05-03
  • 1970-01-01
  • 1970-01-01
  • 2011-08-23
相关资源
最近更新 更多