代码1
#includeint 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作为输出.(机器输出)
当我将return语句更改为
return f(a, b) + *a;
我逻辑上得到37.(机器输出)
我的一个朋友说,在计算返回语句时
return *a + f(a, b);
我们计算树的深度值,即*先f(a, b)
调用然后调用,而在
return f(a,b) + *a;
它在返回时被解析,即先f(a, b)
被计算然后*a
被调用.
通过这种方法,我尝试自己预测以下代码的输出:
代码2
#includeint 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(机器输出)
对于 return(foo(n-2)+r);
我得到17作为输出.(机器输出)
但是,当我在我的系统上运行代码时,我在两种情况下都得到17.
我的问题:
我的朋友给出的方法是正确的吗?
如果是这样,为什么我在机器上运行时在代码2中获得相同的输出?
如果没有,解释代码1和代码2的正确方法是什么?
是否有任何未定义的行为,因为C不支持通过引用传递?由于它在代码1中使用很难,它可以使用指针实现吗?
简而言之,我只是想知道在上述4种情况下预测输出的正确方法.
对于代码1,由于标准未指定return *a + f(a, b);
(和in return f(a, b) + *a;
)中的术语的评估顺序,并且函数修改了a
指向的值,因此您的代码具有未指定的行为,并且可能有各种答案.
正如您在评论中从愤怒中可以看出的那样,术语"未定义的行为","未指定的行为"等在C标准中具有技术含义,并且此答案的早期版本滥用了"未定义的行为",它应该使用'未指定".
问题的标题是"这是C中未定义的行为吗?",答案是"否;它是未指定的行为,而不是未定义的行为".
对于固定的代码2,该函数还具有未指定的行为:静态变量的值r
由递归调用更改,因此对评估顺序的更改可能会更改结果.
对于代码2,如最初所示int f(static int n) { … }
,代码不会(或者,至少不应该)编译.函数参数定义中允许的唯一存储类是register
,所以存在static
应该给你编译错误.
ISO/IEC 9899:2011§6.7.6.3 函数声明符(包括原型) 2参数声明中唯一的存储类说明符是
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.
但是,假设已修复,该函数还具有未定义的未指定行为:静态变量的值r
由递归调用更改,因此对评估顺序的更改可能会更改结果.
C标准规定
在评估函数指示符和实际参数之后但在实际调用之前有一个序列点.调用函数(包括其他函数调用)中的每个评估(在执行被调用函数的主体之前或之后未特别排序)在被调用函数的执行方面不确定地被排序1.94)
脚注86(第6.5/3节)说:
在程序执行期间不止一次评估的表达式中,不需要在不同的评估中一致地执行其子表达式的未序列 和不确定顺序的评估.
在表达式return f(a,b) + *a;
和return *a + f(a,b);
子表达式的评价*a
是不定测序.在这种情况下,可以看到同一程序的不同结果.
请注意,副作用a
在上面的表达式中排序,但未指定顺序.
1.当A在B之前或之后进行测序时,评估A和B是不确定的,但未指明哪一个.(C11-5.1.2.3/3)
我将重点关注第一个例子的定义.
第一个示例使用未指定的行为进行定义.这意味着有多种可能的结果,但行为未定义.(如果代码可以处理这些结果,则定义行为.)
一个未指明的行为的简单例子是:
int a = 0; int c = a + a;
未指定是否首先评估左a或右a,因为它们未被排序.该+
运营商不指定任何顺序指出1.有两种可能的排序,左边a先评估然后右边a,反之亦然.由于两侧都没有被修改2,因此定义了行为.
留下了一个或右边被修改,没有顺序点,即未测序,行为将是不确定的2:
int a = 0; int c = ++a + a;
如果左侧或右侧的序列点被修改过,那么左侧和右侧将被不确定地排序3.这意味着它们是按顺序排序的,但是未指定哪个是先评估的.行为将被定义.请注意逗号运算符引入序列点4:
int a = 0; int c = a + ((void)0,++a,0);
有两种可能的排序.
如果首先评估左侧,则评估为0.然后评估右侧.评估First(void)0,然后是序列点.然后a增加,然后是序列点.然后将0评估为0并将其添加到左侧.结果是0.
如果首先评估右侧,则评估(void)0,然后是序列点.然后a增加,然后是序列点.然后将0评估为0.然后评估左侧,评估为1.结果为1.
您的示例属于后一类,因为操作数是不确定的顺序.函数调用与上例中的逗号运算符具有相同的用途5.你的例子很复杂,所以我会使用我的,这也适用于你的.唯一的区别是你的例子中有更多可能的结果,但我的推理是相同的:
void Function( int* a) { ++(*a); return 0; } int a = 0; int c = a + Function( &a ); assert( c == 0 || c == 1 );
有两种可能的排序.
如果首先评估左侧,则评估为0.然后评估右侧,有一个序列点并调用该函数.然后a递增,接着是由完整表达式6的末尾引入的另一个序列点,其结尾由分号表示.然后返回0并添加到0.结果为0.
如果首先评估右侧,则存在序列点并调用该函数.然后a递增,接着是完整表达式结束引入的另一个序列点.然后返回0.然后评估左侧,计算结果为1并添加到0.结果为1.
(引用自:ISO/IEC 9899:201x)
1(6.5表达式3)
除非后面指出,否则子表达的副作用和值计算未被排序.
2(6.5表达式2)
如果相对于同一标量对象的不同副作用或使用相同标量对象的值计算值对标量对象的副作用未被排序,则行为未定义.
3(5.1.2.3程序执行)
当A在B之前或之后进行测序时,评估A和B是不确定的,但未指定哪一个.
4(6.5.17逗号运算符2)逗号运算符
的左操作数被计算为void表达式; 它的评估与右操作数之间存在一个序列点.
5(6.5.2.2函数调用10)
在评估函数指示符和实际参数之后但在实际调用之前有一个序列点.
6(6.8语句和块4)
在完整表达式的评估和要评估的下一个完整表达式的评估之间存在一个序列点.