中间算术表达式中的整数溢出
Integer overflow in intermediate arithmetic expression
这可能是一个非常基础的编程问题,但这是我想了解一段时间的问题。
考虑这个简单的例子:
int main(void)
{
unsigned char a = 5;
unsigned char b = 20;
unsigned char m = 0xFF;
unsigned char s1 = m + a - b;
unsigned char s2 = m - b + a;
printf("s1 %d s2 %d", s1, s2);
return 0;
}
鉴于C中的算术运算符是从左到右计算的,这里的第一个计算应该在m + a处溢出。但是,运行 该程序 return 对 s1 和 s2 的答案相同。
我的问题是:第一个表达式是否会因为溢出而导致未定义的行为?
第二个表达式应该避免溢出,但我想明白为什么两个表达式return答案相同。
(已更正) 在对整数类型进行算术运算时,所有小于 int 的类型在计算过程中都会提升为 int,如果结果类型更小,则会被截断。
参见:
https://wiki.sei.cmu.edu/confluence/display/c/INT02-C.+Understand+integer+conversion+rules
根据ISO C specification §6.2.5.9
A computation involving unsigned operands can never overflow,
because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.
这意味着在你的加法和减法中似乎分别发生的正 和负 溢出实际上是按符号 int
执行的,所以它们是两者都定义明确。计算表达式后,结果将被截断回 unsigned char
,因为这是左侧结果类型。
由于 C 的整数提升,s1 计算有效执行为:
unsigned char s1 = (unsigned char)( (int)m + (int)a - (int)b );
并且没有临时溢出。
对小于 int
的类型的操作是通过将结果转换为 int
,进行计算,然后将结果转换回原始类型来执行的。对于小型无符号类型,假设计算结果符合类型 int
,这将导致结果的高位被静默忽略。该标准已发布的基本原理表明,作者预计在将值存储到不大于 int
的无符号类型时,非古老的实现会忽略高位,而不考虑计算是否适合类型int
,但 "modern" 编译器不再流行 可靠地 以这种方式运行。例如在16位short和32位int
的系统上,函数
unsigned mulMod65536(unsigned short x, unsigned short y)
{ return (x*y) & 0xFFFFu; }
通常的行为方式等同于:
unsigned mulMod65536(unsigned short x, unsigned short y)
{ return (1u*x*y) & 0xFFFFu; }
但在某些情况下,gcc 会根据以下事实进行 "clever" 优化
如果 x*y
超过 2147483647,它允许以任意方式运行,即使高位没有理由影响结果。
涉及小型有符号类型的操作类似于使用无符号类型的操作,不同之处在于允许实现将超出较小类型范围的值映射到实现定义的方式中的那些类型的值,或者引发实现定义的如果尝试转换超出范围的值,则发出信号。实际上,即使在这种情况下,几乎所有实现都使用二进制补码截断。虽然某些其他行为在某些情况下可能成本更低,但标准要求实现以一致的记录方式运行。
这可能是一个非常基础的编程问题,但这是我想了解一段时间的问题。
考虑这个简单的例子:
int main(void)
{
unsigned char a = 5;
unsigned char b = 20;
unsigned char m = 0xFF;
unsigned char s1 = m + a - b;
unsigned char s2 = m - b + a;
printf("s1 %d s2 %d", s1, s2);
return 0;
}
鉴于C中的算术运算符是从左到右计算的,这里的第一个计算应该在m + a处溢出。但是,运行 该程序 return 对 s1 和 s2 的答案相同。 我的问题是:第一个表达式是否会因为溢出而导致未定义的行为? 第二个表达式应该避免溢出,但我想明白为什么两个表达式return答案相同。
(已更正) 在对整数类型进行算术运算时,所有小于 int 的类型在计算过程中都会提升为 int,如果结果类型更小,则会被截断。
参见:
https://wiki.sei.cmu.edu/confluence/display/c/INT02-C.+Understand+integer+conversion+rules
根据ISO C specification §6.2.5.9
A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.
这意味着在你的加法和减法中似乎分别发生的正 和负 溢出实际上是按符号 int
执行的,所以它们是两者都定义明确。计算表达式后,结果将被截断回 unsigned char
,因为这是左侧结果类型。
由于 C 的整数提升,s1 计算有效执行为:
unsigned char s1 = (unsigned char)( (int)m + (int)a - (int)b );
并且没有临时溢出。
对小于 int
的类型的操作是通过将结果转换为 int
,进行计算,然后将结果转换回原始类型来执行的。对于小型无符号类型,假设计算结果符合类型 int
,这将导致结果的高位被静默忽略。该标准已发布的基本原理表明,作者预计在将值存储到不大于 int
的无符号类型时,非古老的实现会忽略高位,而不考虑计算是否适合类型int
,但 "modern" 编译器不再流行 可靠地 以这种方式运行。例如在16位short和32位int
的系统上,函数
unsigned mulMod65536(unsigned short x, unsigned short y)
{ return (x*y) & 0xFFFFu; }
通常的行为方式等同于:
unsigned mulMod65536(unsigned short x, unsigned short y)
{ return (1u*x*y) & 0xFFFFu; }
但在某些情况下,gcc 会根据以下事实进行 "clever" 优化
如果 x*y
超过 2147483647,它允许以任意方式运行,即使高位没有理由影响结果。
涉及小型有符号类型的操作类似于使用无符号类型的操作,不同之处在于允许实现将超出较小类型范围的值映射到实现定义的方式中的那些类型的值,或者引发实现定义的如果尝试转换超出范围的值,则发出信号。实际上,即使在这种情况下,几乎所有实现都使用二进制补码截断。虽然某些其他行为在某些情况下可能成本更低,但标准要求实现以一致的记录方式运行。