这是UB;在ISO C ++术语中,对于最终触发UB的执行,整个程序的行为完全未指定。经典示例是根据C ++标准,它可以让恶魔从你的鼻子里飞出来。(我建议不要使用可能导致鼻部恶魔出现的实现)。有关详细信息,请参见其他答案。
编译器可以在编译时为它们能够看到的导致编译时可见UB的执行路径“引起麻烦”,例如假设那些基本块永远不会被执行。
另请参见
What Every C Programmer Should Know About Undefined Behavior(LLVM博客)。如此解释,有符号溢出UB使编译器能够证明
for(... i <= n ...)
循环不是无限循环,即使
n
是未知的。它还允许它们将int循环计数器提升为指针宽度,而不是重新进行符号扩展。(因此,在这种情况下,UB的后果可能是访问数组的低64k或4G元素之外,如果您期望对
i
进行符号包装,则可能会出现这种情况。)
在某些情况下,编译器会为一个可证明会导致未定义行为的代码块发出类似于x86
ud2
的非法指令。(请注意,函数可能永远不会被调用,因此编译器通常不会失控并破坏其他函数,甚至是不触发未定义行为的函数可能路径。也就是说,它编译成的机器码必须对所有不导致UB的输入都有效。)
最有效的解决方案可能是手动剥离最后一次迭代,以避免不必要的factor *= 10
。
int result = 0;
int factor = 1;
for (... i < n-1) {
result = ...
factor *= 10;
}
result = ...
return result;
如果循环体较大,考虑仅使用无符号类型的
factor
。然后,您可以让无符号乘法溢出,并且它将执行定义良好的包装到2的某个幂(无符号类型中的值位数)。
即使您将其与有符号类型一起使用,也可以使用此方法,特别是如果您的无符号->有符号转换永远不会溢出。
在无符号和2的补码有符号之间进行转换是免费的(所有值的位模式相同);C++标准指定的int->unsigned的模数包装简化为仅使用相同的位模式,而不像1的补码或符号/大小那样。
而 unsigned->signed 类似于微不足道,但对于大于 INT_MAX 的值,它是实现定义的。如果您没有使用上一次迭代中的巨大无符号结果,则无需担心。但是,如果您正在使用,请参见
转换从无符号到有符号是否未定义?。value-doesn't-fit 情况是实现定义的,这意味着实现必须选择某些行为。理智的做法是仅在必要时将无符号位模式截断并将其用作有符号位,因为对于范围内的值,工作方式相同且无需额外工作。这绝对不是 UB。因此,大的无符号值可以变为负的有符号整数。例如,在
int x = u;
之后,
gcc 和 clang 不会优化掉 x>=0
,即使没有
-fwrapv
,因为它们定义了行为。