这是 C 中的未定义行为吗?如果不能逻辑地预测输出
Is this undefined behaviour in C ? If not predict the output logically
代码 1
#include <stdio.h>
int f(int *a, int b)
{
b = b - 1;
if(b == 0) return 1;
else {
*a = *a+1;
return *a + f(a, b);
}
}
int main() {
int X = 5;
printf("%d\n",f(&X, X));
}
考虑这个 C 代码。这里的问题是预测输出。从逻辑上讲,我得到 31 作为输出。 (Output on machine)
当我将 return 语句更改为
return f(a, b) + *a;
我逻辑上得到 37。(Output on machine)
我的一个朋友说,在计算
中的 return 语句时
return *a + f(a, b);
我们计算树的 while going 深度的值,即 *a 首先计算然后调用 f(a, b)
,而在
return f(a,b) + *a;
它在 return返回时解决,即先计算 f(a, b)
然后调用 *a
。
通过这种方法,我尝试自己预测以下代码的输出:
代码2
#include <stdio.h>
int foo(int n)
{
static int r;
if(n <= 1)
return 1;
r = n + r;
return r + foo(n - 2);
}
int main () {
printf("value : %d",foo(5));
}
对于return(r+foo(n-2));
我得到 14 作为逻辑输出 (Output on machine)
对于return(foo(n-2)+r);
我得到 17 作为输出。 (Output on machine)
然而,当我 运行 我的系统上的代码时,我在两种情况下都得到 17。
我的问题:
- 朋友给的方法对吗?
- 如果是这样,为什么我在 Code 2 中得到与 运行 相同的输出?
- 如果不是,那么解释 代码 1 和 代码 2 的正确方法是什么?
- 是否因为 C 不支持引用传递而导致未定义的行为?由于它在 Code 1 中使用,所以它可以使用指针来实现吗?
简而言之,我只是想知道在上述 4 种情况下预测输出的正确方法。
C 标准规定
6.5.2.2/10 函数调用:
There is a sequence point after the evaluations of the function designator and the actual arguments but before the actual call. Every evaluation in the calling function (including other function calls) that is not otherwise specifically sequenced before or after the execution of the body of the called function is indeterminately sequenced1 with respect to the execution of the called function.94)
脚注 86(第 6.5/3 节)说:
In an expression that is evaluated more than once during the execution of a program, unsequenced and indeterminately sequenced evaluations of its subexpressions need not be performed consistently in different evaluations.
在表达式 return f(a,b) + *a;
和 return *a + f(a,b);
中,子表达式 *a
的计算顺序不确定。在这种情况下,对于同一个程序可以看到不同的结果。
请注意,a
的副作用在上述表达式中按顺序排列,但未指定顺序。
1. 当 A 排序在 B 之前或之后时,评估 A 和 B 的排序不确定,但未指定是哪个。 (C11- 5.1.2.3/3)
代码 1
对于代码1,因为return *a + f(a, b);
(和return f(a, b) + *a;
)中的项的求值顺序未由标准指定,并且函数修改 a
指向的值,您的代码具有未指定的行为,并且可能有各种答案。
从评论中的狂热可以看出,术语 'undefined behaviour'、'unspecified behaviour' 等在 C 标准中具有技术含义,而该答案的早期版本被误用 'undefined behaviour' 它应该使用 'unspecified'.
题目是"Is this undefined behaviour in C?",答案是"No; it is unspecified behaviour, not undefined behaviour"。
代码 2 — 修订后
对于固定的代码2,函数也有未指定的行为:静态变量r
的值被递归调用改变,所以改变到评估顺序可能会改变结果。
代码 2 — pre-revision
对于 代码 2,如 int f(static int n) { … }
最初所示,代码不(或至少不应该)编译。函数参数定义中唯一允许的存储 class 是 register
,因此 static
的存在应该会给您带来编译错误。
ISO/IEC 9899:2011 §6.7.6.3 Function declarators (including prototypes)
¶2 The only storage-class specifier that shall occur in a parameter declaration is register
.
在 macOS Sierra 10.12.2 上使用 GCC 6.3.0 编译,像这样(注意,没有要求额外的警告):
$ gcc -O ub17.c -o ub17
ub17.c:3:27: error: storage class specified for parameter ‘n’
int foo(static int n)
^
否;如图所示,它根本无法编译 — 至少,对于使用现代版 GCC 的我来说是这样。
然而,假设它是固定的,函数也有 undefined 未指定的行为:静态变量 r
的值被递归调用改变,所以改变评估顺序可能会改变结果。
我将重点放在第一个例子的定义上。
第一个示例定义了未指定的行为。这意味着有多种可能的结果,但行为并非未定义。
(如果代码可以处理这些结果,则行为已定义。)
未指定行为的一个简单示例是:
int a = 0;
int c = a + a;
未指定是先评估左 a 还是先评估右 a,因为它们 未排序。 +
运算符不指定任何序列点 1。有两种可能的顺序,要么先评估左 a,然后评估右 a,要么 vice-versa。由于两边都没有被修改2,所以定义了行为。
如果左 a 或右 a 在没有序列点的情况下被修改,即 unsequenced,行为将是未定义的2:
int a = 0;
int c = ++a + a;
如果左 a 或右 a 之间有一个序列点被修改,那么左侧和右侧将 不确定 排序 3。这意味着它们已排序,但未指定先评估哪个。行为将被定义。注意逗号运算符引入了一个序列点4:
int a = 0;
int c = a + ((void)0,++a,0);
有两种可能的顺序。
如果先计算左侧,则 a 的计算结果为 0。然后计算右侧。首先评估 (void)0,然后评估序列点。然后 a 递增,然后是序列点。然后 0 被评估为 0 并添加到左侧。结果为 0。
如果首先计算右侧,则计算 (void)0,然后是序列点。然后 a 递增,然后是序列点。然后0求值为0。然后求左边,a求值为1。结果为1。
您的示例属于后一类,因为操作数 不确定 排序。该函数调用与上例中的逗号运算符具有相同的目的5。你的例子很复杂,所以我会用我的,这也适用于你的。唯一的区别是你的例子比我的例子有更多可能的结果,但推理是一样的:
void Function( int* a)
{
++(*a);
return 0;
}
int a = 0;
int c = a + Function( &a );
assert( c == 0 || c == 1 );
有两种可能的顺序。
如果先求左边,a求值为0,再求右边,有序列点,调用函数。然后 a 递增,然后是完整表达式末尾引入的另一个序列点 6,其末尾由分号指示。然后返回0加0,结果为0。
如果右边先求值,就有序列点,调用函数。然后 a 递增,然后是完整表达式末尾引入的另一个序列点。然后返回 0。然后左边求值,a求值为1加0,结果为1。
(引自:ISO/IEC 9899:201x)
1(6.5 表达式 3)
除非另有说明
之后,子表达式的副作用和值计算是无序的。
2(6.5 表达式 2)
如果一个标量对象的副作用相对于另一个不同的副作用是无序的
在同一标量对象上或使用同一标量的值进行值计算
对象,行为未定义。
3 (5.1.2.3 程序执行)
当 A 排序时,评估 A 和 B 的排序不确定
在B之前或之后,但未指定是哪个。
4(6.5.17 逗号运算符 2)
逗号运算符的左操作数被评估为 void 表达式;有一个
其评估与右操作数评估之间的序列点。
5 (6.5.2.2 函数调用 10)
在功能指示符和实际的评估之后有一个序列点
参数但在实际调用之前。
6 (6.8 语句和块 4)
在完整表达式的求值和
计算下一个要计算的完整表达式。
代码 1
#include <stdio.h>
int f(int *a, int b)
{
b = b - 1;
if(b == 0) return 1;
else {
*a = *a+1;
return *a + f(a, b);
}
}
int main() {
int X = 5;
printf("%d\n",f(&X, X));
}
考虑这个 C 代码。这里的问题是预测输出。从逻辑上讲,我得到 31 作为输出。 (Output on machine)
当我将 return 语句更改为
return f(a, b) + *a;
我逻辑上得到 37。(Output on machine)
我的一个朋友说,在计算
中的 return 语句时return *a + f(a, b);
我们计算树的 while going 深度的值,即 *a 首先计算然后调用 f(a, b)
,而在
return f(a,b) + *a;
它在 return返回时解决,即先计算 f(a, b)
然后调用 *a
。
通过这种方法,我尝试自己预测以下代码的输出:
代码2
#include <stdio.h>
int foo(int n)
{
static int r;
if(n <= 1)
return 1;
r = n + r;
return r + foo(n - 2);
}
int main () {
printf("value : %d",foo(5));
}
对于return(r+foo(n-2));
我得到 14 作为逻辑输出 (Output on machine)
对于return(foo(n-2)+r);
我得到 17 作为输出。 (Output on machine)
然而,当我 运行 我的系统上的代码时,我在两种情况下都得到 17。
我的问题:
- 朋友给的方法对吗?
- 如果是这样,为什么我在 Code 2 中得到与 运行 相同的输出?
- 如果不是,那么解释 代码 1 和 代码 2 的正确方法是什么?
- 是否因为 C 不支持引用传递而导致未定义的行为?由于它在 Code 1 中使用,所以它可以使用指针来实现吗?
简而言之,我只是想知道在上述 4 种情况下预测输出的正确方法。
C 标准规定
6.5.2.2/10 函数调用:
There is a sequence point after the evaluations of the function designator and the actual arguments but before the actual call. Every evaluation in the calling function (including other function calls) that is not otherwise specifically sequenced before or after the execution of the body of the called function is indeterminately sequenced1 with respect to the execution of the called function.94)
脚注 86(第 6.5/3 节)说:
In an expression that is evaluated more than once during the execution of a program, unsequenced and indeterminately sequenced evaluations of its subexpressions need not be performed consistently in different evaluations.
在表达式 return f(a,b) + *a;
和 return *a + f(a,b);
中,子表达式 *a
的计算顺序不确定。在这种情况下,对于同一个程序可以看到不同的结果。
请注意,a
的副作用在上述表达式中按顺序排列,但未指定顺序。
1. 当 A 排序在 B 之前或之后时,评估 A 和 B 的排序不确定,但未指定是哪个。 (C11- 5.1.2.3/3)
代码 1
对于代码1,因为return *a + f(a, b);
(和return f(a, b) + *a;
)中的项的求值顺序未由标准指定,并且函数修改 a
指向的值,您的代码具有未指定的行为,并且可能有各种答案。
从评论中的狂热可以看出,术语 'undefined behaviour'、'unspecified behaviour' 等在 C 标准中具有技术含义,而该答案的早期版本被误用 'undefined behaviour' 它应该使用 'unspecified'.
题目是"Is this undefined behaviour in C?",答案是"No; it is unspecified behaviour, not undefined behaviour"。
代码 2 — 修订后
对于固定的代码2,函数也有未指定的行为:静态变量r
的值被递归调用改变,所以改变到评估顺序可能会改变结果。
代码 2 — pre-revision
对于 代码 2,如 int f(static int n) { … }
最初所示,代码不(或至少不应该)编译。函数参数定义中唯一允许的存储 class 是 register
,因此 static
的存在应该会给您带来编译错误。
ISO/IEC 9899:2011 §6.7.6.3 Function declarators (including prototypes) ¶2 The only storage-class specifier that shall occur in a parameter declaration is
register
.
在 macOS Sierra 10.12.2 上使用 GCC 6.3.0 编译,像这样(注意,没有要求额外的警告):
$ gcc -O ub17.c -o ub17
ub17.c:3:27: error: storage class specified for parameter ‘n’
int foo(static int n)
^
否;如图所示,它根本无法编译 — 至少,对于使用现代版 GCC 的我来说是这样。
然而,假设它是固定的,函数也有 undefined 未指定的行为:静态变量 r
的值被递归调用改变,所以改变评估顺序可能会改变结果。
我将重点放在第一个例子的定义上。
第一个示例定义了未指定的行为。这意味着有多种可能的结果,但行为并非未定义。 (如果代码可以处理这些结果,则行为已定义。)
未指定行为的一个简单示例是:
int a = 0;
int c = a + a;
未指定是先评估左 a 还是先评估右 a,因为它们 未排序。 +
运算符不指定任何序列点 1。有两种可能的顺序,要么先评估左 a,然后评估右 a,要么 vice-versa。由于两边都没有被修改2,所以定义了行为。
如果左 a 或右 a 在没有序列点的情况下被修改,即 unsequenced,行为将是未定义的2:
int a = 0;
int c = ++a + a;
如果左 a 或右 a 之间有一个序列点被修改,那么左侧和右侧将 不确定 排序 3。这意味着它们已排序,但未指定先评估哪个。行为将被定义。注意逗号运算符引入了一个序列点4:
int a = 0;
int c = a + ((void)0,++a,0);
有两种可能的顺序。
如果先计算左侧,则 a 的计算结果为 0。然后计算右侧。首先评估 (void)0,然后评估序列点。然后 a 递增,然后是序列点。然后 0 被评估为 0 并添加到左侧。结果为 0。
如果首先计算右侧,则计算 (void)0,然后是序列点。然后 a 递增,然后是序列点。然后0求值为0。然后求左边,a求值为1。结果为1。
您的示例属于后一类,因为操作数 不确定 排序。该函数调用与上例中的逗号运算符具有相同的目的5。你的例子很复杂,所以我会用我的,这也适用于你的。唯一的区别是你的例子比我的例子有更多可能的结果,但推理是一样的:
void Function( int* a)
{
++(*a);
return 0;
}
int a = 0;
int c = a + Function( &a );
assert( c == 0 || c == 1 );
有两种可能的顺序。
如果先求左边,a求值为0,再求右边,有序列点,调用函数。然后 a 递增,然后是完整表达式末尾引入的另一个序列点 6,其末尾由分号指示。然后返回0加0,结果为0。
如果右边先求值,就有序列点,调用函数。然后 a 递增,然后是完整表达式末尾引入的另一个序列点。然后返回 0。然后左边求值,a求值为1加0,结果为1。
(引自:ISO/IEC 9899:201x)
1(6.5 表达式 3)
除非另有说明
之后,子表达式的副作用和值计算是无序的。
2(6.5 表达式 2)
如果一个标量对象的副作用相对于另一个不同的副作用是无序的
在同一标量对象上或使用同一标量的值进行值计算
对象,行为未定义。
3 (5.1.2.3 程序执行)
当 A 排序时,评估 A 和 B 的排序不确定
在B之前或之后,但未指定是哪个。
4(6.5.17 逗号运算符 2)
逗号运算符的左操作数被评估为 void 表达式;有一个
其评估与右操作数评估之间的序列点。
5 (6.5.2.2 函数调用 10)
在功能指示符和实际的评估之后有一个序列点
参数但在实际调用之前。
6 (6.8 语句和块 4)
在完整表达式的求值和
计算下一个要计算的完整表达式。