有人可以向我解释为什么这段代码会打印 14 吗?我只是被另一个学生问了,我想不通。
int i = 5;
i = ++i + ++i;
cout<<i;
有人可以向我解释为什么这段代码会打印 14 吗?我只是被另一个学生问了,我想不通。
int i = 5;
i = ++i + ++i;
cout<<i;
C++ 中未定义副作用的顺序。此外,在单个表达式中修改变量两次没有定义的行为(参见C++ 标准,§5.0.4,物理页 87/逻辑页 73)。
解决方法:在复杂的表达式中不要使用副作用,在简单的表达式中不要使用多个。启用编译器可以给你的所有警告也没有什么坏处:在命令行中添加-Wall
(gcc) 或/Wall /W4
(Visual C++) 会产生合适的警告:
test-so-side-effects.c: In function 'main':
test-so-side-effects.c:5: warning: operation on 'i' may be undefined
test-so-side-effects.c:5: warning: operation on 'i' may be undefined
显然,代码编译为:
i = i + 1;
i = i + 1;
i = i + i;
这是未定义的行为,结果会因您使用的编译器而异。例如,参见C++ FAQ Lite。
在一些答案/评论中,讨论了“未定义行为”的含义以及这是否会使程序无效。所以我发布了这个相当长的答案,详细说明了标准所说的一些注释。我希望它不会太无聊...
该标准的引用位来自当前的 C++ 标准 (ISO/IEC 14882:2003)。C标准中有类似的措辞。
根据 C++ 标准,在一组序列点内多次修改一个值会导致未定义的行为(第 5 节第 4 段):
除非另有说明,单个运算符的操作数和单个表达式的子表达式的求值顺序以及副作用发生的顺序是未指定的。53) 在前一个和下一个序列点之间,一个标量对象应修改其存储值最多一次通过表达式的评估。此外,只能访问先验值以确定要存储的值。对于完整表达式的子表达式的每个允许排序,都应满足本段的要求;否则行为未定义。[例子:
i = v[i++]; // the behavior is unspecified i = 7, i++, i++; // i becomes 9 i = ++i + 1; // the behavior is unspecified i = i + 1; // the value of i is incremented
——结束示例]
请注意,第二个示例“ i = 7, i++, i++;
”是定义的,因为逗号运算符是一个序列点。
以下是 C++ 标准所说的“未定义行为”的含义:
1.3.12 未定义的行为 [defns.undefined]
行为,例如在使用错误程序结构或错误数据时可能出现的行为,本国际标准对此没有要求。当本国际标准省略对任何明确的行为定义的描述时,也可能会出现未定义的行为。[注意:允许的未定义行为范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或没有发出诊断消息),到终止翻译或执行(发出诊断消息)。许多错误的程序结构不会产生未定义的行为;他们需要被诊断出来。]
换句话说,编译器可以为所欲为,包括
第二项涵盖了大多数编译器具有的语言扩展,但标准中当然没有定义。
所以我想严格来说,表现出未定义行为的东西并不是“非法的”,但根据我的经验,只要 C/C++ 程序中有东西表现出“未定义行为”(除非它是扩展)——它就是一个错误。我认为将这样的构造称为非法并不令人困惑、误导或误导。
另外,我认为试图解释编译器为达到值 14 所做的工作并不是特别有帮助,因为这没有抓住重点。编译器几乎可以做任何事情;事实上,当编译器使用不同的优化选项运行时,编译器可能会得到不同的结果(或者可能会产生崩溃的代码——谁知道呢?)。
对于那些想要一些额外参考或呼吁权威的人,这里有一些提示:
从 1995 年开始,Steve Summit(comp.lang.c 常见问题的维护者)对这个主题的长篇回答:
以下是 Bjarne Stroustrup 对此事的看法:
脚注:C++ 标准只使用了一次“非法”这个词——在描述 C++ 和标准 C 之间关于类型声明的使用static
或extern
类型声明的差异时。
简单...您的编译器在执行求和之前评估两个增量,而不缓存中间结果。这意味着当您添加 i 两次时,它现在的值是 7。
如果你这样做
int j=++i;
int k=++i;
i = j+k;
你会按预期看到 13。
在您的特定编译器上,它选择先执行两个 ++ 操作,然后是加法。它将代码解释为:
int i = 5;
++i;
++i;
i = i + i;
cout << i;
这是完全有效的。
一个更好的问题是,它会一直如此14
吗?
int i = 5;
i = ++i + ++i;
cout<<i;
i = ++i + ++i ;
i = ++(5) + ++(5) ;
i = 6 + 6 ;
i = 12;
i = ++i + ++i ;
i = ++i + ++(5) ;
i = ++i + (6) ;
i = ++(6) + 6 ;
i = (7) + 6 ;
i = 13;
i = ++i + ++i ;
i = ++i + ++(5) ;
i = ++(6) + (6) ;
i = (7) + (7) ;
i = 14;
很可能它会是14
,因为它稍微更有意义。
我认为,从语法树的角度来看问题,问题的答案就更清晰了:
我
|
=
|
+
|
一元表达式 - 一元表达式
一元表达式:一元运算符表达式
在我们的例子中,表达式归结为变量 i。
现在发生的情况是两个一元表达式都修改了相同的操作数,因此在添加两个一元表达式的结果之前,代码在计算一元表达式时会执行两次 ++i 。
所以代码所做的确实是
++i;
++i;
我 = 我 + 我;
对于 i = 5,这意味着
i = i + 1;//i <- 6
i = i + 1; //i <- 7
i = i + i; //i <- 14
因为前缀增量具有优先权:
int i = 5;
i = i+1; // First ++i, i is now 6
i = i+1; // Second ++i, i is now 7
i = i + i // i = 7 + 7
cout << i // i = 14
i = i++ + i; //11
i = i++ + i++; //12
i = i++ + ++i; //13
i = ++i + i++; //13
i = ++i + ++i; //14