在Go(我最熟悉的语言)中,数学运算的结果总是与操作数相同的数据类型,这意味着如果运算溢出,结果将不正确。例如:
func main() {
var a byte = 100
var b byte = 9
var r byte = (a << b) >> b
fmt.Println(r)
}
这将打印 0,因为在初始<< 9
操作期间,所有位都移出byte
的范围,然后在>> 9
操作期间将零移回。
但是,在 C 中并非如此:
int main() {
unsigned char a = 100;
unsigned char b = 9;
unsigned char r = (a << b) >> b;
printf("%dn", r);
return 0;
}
此代码打印 100。虽然这会产生"正确"的结果,但这对我来说是出乎意料的,因为我只期望在其中一个操作数大于一个字节时进行提升,但在这种情况下,所有操作数都是字节。就好像保存<< 9
运算结果的临时变量大于结果变量,并且仅在评估完整的 RHS 后向下转换为字节,因此在>> 9
操作恢复位之后
。显然,如果在继续之前将>> 9
的结果显式存储到一个字节中,你会得到与 Go 相同的结果:
int main() {
unsigned char a = 100;
unsigned char b = 9;
unsigned char c = a << b;
unsigned char r = c >> b;
printf("%dn", r);
return 0;
}
这不仅仅是按位运算符的情况。我也用乘法/除法进行了测试,它表现出相同的行为。
我的问题是:C的这种行为是定义的吗?如果是,在哪里?它实际上是否对复杂表达式的临时值使用特定的数据类型?或者这实际上是未定义的行为,就像在保存回内存之前在 32/64 位 CPU 寄存器中执行操作的偶然结果?
C 2018 6.5.7 讨论了移位运算符。第3段说:
整数提升在每个操作数上执行...
6.3.1.1 2 指定整数提升:
。如果
int
可以表示原始类型的所有值(受位字段的宽度限制),则该值将转换为int
;否则,它将转换为unsigned int
。这些称为整数促销。整数促销将更改所有其他类型。
因此在a << b
a
和b
unsigned char
,a
被提升为int
,至少是16位。(C 实现可以将unsigned char
定义为超过 8 位。它可以与int
宽度相同。在这种情况下,整数升级不会转换a
或b
.)
请注意,如果未应用整数提升,则 C 标准不会定义计算b
等于 9 的a << b
的行为,因为移位运算符的行为不是为大于或等于左运算符宽度的移位量定义的。
6.5.5 指定乘法运算符。第3段说:
通常的算术转换是在操作数上执行的。
6.3.1.8 指定了通常的算术转换:
。首先,如果其中一个操作数的相应实数类型
long double
,则另一个操作数在不更改类型域 [复数或实数] 的情况下转换为其相应实数类型为long double
的类型。否则,如果其中一个操作数的相应实数类型
double
,则另一个操作数在不更改类型域的情况下转换为其相应实数类型为double
的类型。否则,如果其中一个操作数的相应实数类型
float
,则另一个操作数在不更改类型域的情况下转换为其相应实数类型为float.
否则,将对两个操作数执行整数提升。然后,以下规则将应用于升级的操作数:
- 否则,如果两个操作数都具有有符号整数类型
如果两个操作数具有相同的类型,则无需进一步转换。
- 否则,如果具有无符号整数类型的操作数
或都具有无符号整数类型,则整数转换秩较小的操作数将转换为秩较大的操作数的类型。
- 否则,如果具有有符号整数类型的操作数的类型可以表示具有无符号整数类型的操作数类型的所有值,则具有无符号整数类型的操作数
的秩大于或等于其他操作数类型的秩,则具有有符号整数类型的操作数将转换为具有无符号整数类型的操作数的类型。
将转换为具有有符号整数类型的操作数的类型。
否则,两个操作数都将转换为与具有有符号整数类型的操作数类型相对应的无符号整数类型。
Rank 有一个技术定义,很大程度上对应于宽度(整数类型的位数)。
因此,在a
和b
unsigned char
a * b
,它们都被提升为int
(上面关于宽unsigned char
的警告),不需要进一步的转换。 如果一个操作数比int
宽,比如long long int
,而另一个unsigned char
则两个操作数都将转换为该更宽的类型。
欢迎来到整数促销!C 语言的一种行为(我会补充一点,这是一个经常受到批评的行为)是,在对它们进行任何算术运算之前,char
和short
等类型被提升为int
,结果也是int
。这是什么意思?
unsigned char foo(unsigned char x) {
return (x << 4) >> 4;
}
int main(void) {
if (foo(0xFF) == 0x0F) {
printf("Yay!n");
}
else {
printf("... hey, wait a minute!n");
}
return 0;
}
不用说,上面的代码打印... hey, wait a minute!
.让我们找出原因:
// this line of code:
return (x << 4) >> 4;
// is converted to this (because of integer promotion):
return ((int) x << 4) >> 4;
因此,这是发生的事情:
x
unsigned char
(8 位),其值为0xFF
,x << 4
需要执行,但首先x
转换为int
(32 位),x << 4
变得0x000000FF << 4
,结果0x00000FF0
也是int
,0x00000FF0 >> 4
被执行,产生0x000000FF
,- 最后,
0x000000FF
转换为unsigned char
(因为这是foo()
的返回值),所以它变成了0xFF
, - 这就是为什么
foo(0xFF)
产生0xFF
而不是0x0F
.
如何预防?简单:将x << 4
的结果转换为unsigned char
。在前面的示例中,0x00000FF0
将变为0xF0
。
unsigned char foo(unsigned char x) {
return ((unsigned char) (x << 4)) >> 4;
}
foo(0xFF) == 0x0F
注意:在前面的示例中,假设unsigned char
是 8 位,int
是 32 位,但这些示例基本上适用于CHAR_BIT == 8
的任何情况(因为 C17 要求sizeof(int) * CHAR_BIT >= 16
)。
PS:当然,这个答案并不像C官方标准文档那么详尽。但是,您可以在ISO/IEC 9899:2018标准(又名C17/C18)的最新草案中找到C的所有(有效和定义的)行为。