这是未定义的C行为吗?

时间:2010-08-10 15:25:45

标签: c undefined-behavior

我们班的C编程教授问了这个问题:

您将获得代码:

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

它总会产生什么输出?

大多数学生说未定义的行为。任何人都可以帮助我理解为什么会这样吗?

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

8 个答案:

答案 0 :(得分:40)

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

具体来说,标准说:

  

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

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

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

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

答案 1 :(得分:14)

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

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

对于许多实现,参数是按顺序计算的(尽管并不总是从左到右)。所以你不太可能在现实世界中看到任何东西。

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

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

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

答案 2 :(得分:11)

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

行为未定义的原因是两个表达式++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。未定义的行为不限于任何内容。它可以格式化硬盘而不是打印东西。感受不同。

答案 3 :(得分:2)

  

大多数学生说未定义的行为。任何人都可以帮助我理解为什么会这样吗?

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

答案 4 :(得分:2)

  

它总会产生什么输出?

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

  

大多数学生说未定义的行为。   任何人都可以帮助我理解它为什么   是这样吗?

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

f(++i,i)

你给出的代码符合这种模式,但学生们错误地认为行为是定义的,因为printf忽略了最后一个参数。这种细微差别使许多学生感到困惑。学生的另一部分将像大卫·索恩利一样精通标准,并且出于上述正确理由说出“未定义的行为”。

答案 5 :(得分:1)

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

答案 6 :(得分:0)

回应codaddict答案是2。

将使用参数2调用printf,它将打印出来。

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

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

然后完全无误地定义该函数的行为。我当然不是认为这是好的或正确的,或者之后x的值是可以确定的。

答案 7 :(得分:-1)

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

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

实用程序夹板(这不是std合规性检查工具),以及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,但实际上必须至少影响 ++x(顺便说一下,它是负责未定义的行为),如果不是整个语句。如果它只感染++x表达式,则输出是“未定义的值”,即任何整数,例如如果它感染整个语句,那么你可以在控制台输出中看到gargabe,可能你可能必须用ctrl-c停止程序,可能在它开始扼杀你的cpu之前。

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

没有答案可以解释有关该主题的任何内容。它们只是一个“哦看标准说这个”(这只是一个解释,像往常一样!)。所以至少你已经学会了“标准存在”,并且他们提出了教育问题(当然,不要忘记你的代码错误,无论未定义/未指明的行为主义和其他标准事实),无用的逻辑论证和漫无目的的深入调查和理解。