本文旨在用作关于C中隐式整数提升的常见问题解答,特别是由通常的算术转换和/或整数提升引起的隐式提升。
例1) 为什么这会给出一个奇怪的大整数而不是255呢?
unsigned char x = 0;
unsigned char y = 1;
printf("%u\n", x - y);
例2) 为什么这会给“-1大于0”?
unsigned int a = 1;
signed int b = -2;
if(a + b > 0)
puts("-1 is larger than 0");
例3)
为什么将上面示例中的类型更改为short
可以解决问题?
unsigned short a = 1;
signed short b = -2;
if(a + b > 0)
puts("-1 is larger than 0"); // will not print
(这些示例适用于16位或短16位的32位或64位计算机。)
C被设计为隐式和静默地更改表达式中使用的操作数的整数类型。在某些情况下,语言会强制编译器将操作数更改为更大的类型,或更改其签名。
这背后的基本原理是防止算术期间意外溢出,但也允许具有不同签名的操作数共存于同一表达式中。
不幸的是,隐式类型提升的规则造成的危害大于好处,它们可能是C语言中最大的缺陷之一。这些规则通常不为普通的C程序员所知,因此会导致各种非常微妙的错误。
通常情况下,您会看到程序员说“只是强制转换为x并且工作正常”的情况 - 但他们不知道原因。或者这样的错误表现为罕见的,间歇性的现象,从看似简单和直接的代码中迸发出来。在进行位操作的代码中,隐式提升尤其麻烦,因为在给定带符号的操作数时,C中的大多数位操作符都具有定义不明确的行为。
C中的整数类型是char
,short
,int
,long
,long long
和enum
。
在类型促销方面,_Bool
/ bool
也被视为整数类型。
所有整数都具有指定的转换排名。 C11 6.3.1.1,强调我最重要的部分:
每个整数类型都有一个整数转换等级,定义如下: - 没有两个有符号整数类型具有相同的等级,即使它们具有相同的表示。 - 有符号整数类型的等级应大于精度较低的任何有符号整数类型的等级。 -
long long int
的等级应大于long int
的等级,该等级应大于int
的等级,其应大于short int
的等级,其应大于signed char
的等级。 - 任何无符号整数类型的等级应等于相应的有符号整数类型的等级(如果有的话)。 - 任何标准整数类型的等级应大于具有相同宽度的任何扩展整数类型的等级。 - char的等级应等于signed char和unsigned char的等级。 - _Bool的等级应小于所有其他标准整数类型的等级。 - 任何枚举类型的等级应等于兼容整数类型的等级(见6.7.2.2)。
来自stdint.h
的类型也在这里排序,具有与它们在给定系统上碰巧对应的任何类型相同的等级。例如,int32_t
与32位系统上的int
具有相同的等级。
此外,C11 6.3.1.1规定了哪些类型被视为小整数类型(不是正式术语):
如果可以使用
int
或unsigned int
,可以在以下表达式中使用以下内容:- 具有整数类型(
int
或unsigned int
除外)的对象或表达式,其整数转换等级小于或等于int
和unsigned int
的等级。
这个有点神秘的文字在实践中意味着什么,_Bool
,char
和short
(以及int8_t
,uint8_t
等)是“小整数类型”。如下所述,这些以特殊方式处理并受到隐含促销。
只要在表达式中使用小整数类型,它就会隐式转换为始终有符号的int
。这称为整数提升或整数提升规则。
在形式上,规则说(C11 6.3.1.1):
如果
int
可以表示原始类型的所有值(受宽度限制,对于位字段),该值将转换为int
;否则,它将转换为unsigned int
。这些被称为整数促销。
这意味着所有小整数类型,无论是有符号性,在大多数表达式中使用时都会隐式转换为(signed)int
。
这个文本经常被误解为:“所有小的有符号整数类型都转换为signed int,所有小的无符号整数类型都转换为unsigned int”。这是不正确的。这里的无符号部分仅意味着如果我们有例如unsigned short
操作数,并且int
恰好与给定系统上的short
具有相同的大小,则unsigned short
操作数被转换为unsigned int
。就像在,没有任何真正发生的事情。但是,如果short
是一个比int
更小的类型,它总是转换为(签名)int
,无论它是签名还是未签名!
由整数提升引起的残酷现实意味着在Qazxswpoi或char
等小类型中几乎不能执行C操作。操作总是在short
或更大类型上进行。
这可能听起来像废话,但幸运的是,编译器可以优化代码。例如,包含两个int
操作数的表达式将把操作数提升为unsigned char
,并将操作作为int
执行。但是,允许编译器优化表达式以实际执行为8位操作,如预期的那样。但是,问题出现了:不允许编译器优化由整数提升引起的签名隐式更改。因为编译器无法判断程序员是否故意依赖隐式促销发生,或者它是无意的。
这就是问题中的示例1失败的原因。两个unsigned char操作数都被提升为int
类型,操作在int
类型上执行,int
的结果是x - y
类型。这意味着我们得到int
而不是-1
,这可能是预期的。编译器可以生成使用8位指令而不是255
执行代码的机器代码,但它可能不会优化签名的更改。这意味着我们最终会得到一个负面结果,这会在调用int
时产生一个奇怪的数字。可以通过将操作结果转换回类型printf("%u
来修复示例1。
除了一些特殊情况,如unsigned char
和++
运算符,整数提升适用于C中的几乎所有运算,无论是否使用一元,二元(或三元)运算符。
每当在C中完成二进制操作(具有2个操作数的操作)时,操作符的两个操作数必须是相同的类型。因此,在操作数具有不同类型的情况下,C强制将一个操作数隐式转换为另一个操作数的类型。如何做到这一点的规则被称为通常的艺术转换(有时非正式地称为“平衡”)。这些在C11 6.3.18中规定:
(将此规则视为一个长的,嵌套的sizeof
语句,它可能更容易阅读:))
6.3.1.8通常的算术转换
许多期望算术类型操作数的运算符会以类似的方式导致转换并产生结果类型。目的是确定操作数和结果的通用实数类型。对于指定的操作数,每个操作数在不更改类型域的情况下转换为其对应的实类型是公共实类型的类型。除非另有明确说明,否则公共实类型也是结果的对应实数类型,如果它们相同则其类型域是操作数的类型域,否则是复数。这种模式称为通常的算术转换:
- 首先,如果任一操作数的相应实数类型是
if-else if
,则另一个操作数在不改变类型域的情况下被转换为其对应的实类型为long double
的类型。- 否则,如果任一操作数的对应实数类型是
long double
,则另一个操作数在不更改类型域的情况下转换为其对应的实类型为double
的类型。- 否则,如果任一操作数的相应实数类型是
double
,则另一个操作数在不更改类型域的情况下转换为其对应的实数类型为float的类型。- 否则,将对两个操作数执行整数提升。然后将以下规则应用于提升的操作数: 如果两个操作数具有相同的类型,则不需要进一步转换。 否则,如果两个操作数都具有有符号整数类型或两者都具有无符号整数类型,则具有较小整数转换等级类型的操作数将转换为具有更高等级的操作数的类型。 否则,如果具有无符号整数类型的操作数的秩大于或等于另一个操作数的类型的秩,则具有有符号整数类型的操作数将转换为具有无符号整数类型的操作数的类型。 否则,如果带有符号整数类型的操作数的类型可以表示具有无符号整数类型的操作数类型的所有值,则具有无符号整数类型的操作数将转换为带有符号整数类型的操作数的类型。 否则,两个操作数都转换为无符号整数类型,对应于带有符号整数类型的操作数的类型。
值得注意的是,通常的算术转换适用于浮点和整数变量。对于整数,我们还可以注意到整数提升是在通常的算术转换中调用的。之后,当两个操作数至少具有float
的等级时,运算符被平衡为相同类型,具有相同的符号。
这就是为什么示例2中的int
给出奇怪结果的原因。两个操作数都是整数,它们至少是a + b
等级,因此整数提升不适用。操作数不是同一类型 - int
是a
而unsigned int
是b
。因此,运营商signed int
暂时转换为b
类型。在此转换过程中,它会丢失符号信息并最终成为一个较大的值。
在示例3中将类型更改为unsigned int
的原因解决了问题,因为short
是一个小整数类型。这意味着两个操作数都是整数提升为类型为short
的签名。在整数提升之后,两个操作数具有相同的类型(int
),不需要进一步转换。然后可以按预期在签名类型上执行操作。
根据上一篇文章,我想提供有关每个示例的更多信息。
例1)
int
由于unsigned char小于int,我们对它们应用整数提升,然后我们有(int)x-(int)y =(int)( - 1)和unsigned int(-1)= 4294967295。
上面代码的输出:(与我们预期的相同)
int main(){
unsigned char x = 0;
unsigned char y = 1;
printf("%u\n", x - y);
printf("%d\n", x - y);
}
怎么解决?
我试过上一篇文章推荐的内容,但它并没有真正起作用。以下是基于上一篇文章的代码:
将其中一个更改为unsigned int
4294967295
-1
由于x已经是无符号整数,因此我们只将整数提升应用于y。然后我们得到(unsigned int)x-(int)y。由于它们仍然没有相同的类型,我们应用通常的算术收敛,我们得到(unsigned int)x-(unsigned int)y = 4294967295。
上面代码的输出:(与我们预期的相同):
int main(){
unsigned int x = 0;
unsigned char y = 1;
printf("%u\n", x - y);
printf("%d\n", x - y);
}
同样,以下代码获得相同的结果:
4294967295
-1
将它们都更改为unsigned int
int main(){
unsigned char x = 0;
unsigned int y = 1;
printf("%u\n", x - y);
printf("%d\n", x - y);
}
由于它们都是unsigned int,因此不需要整数提升。通过通常的算术收敛(具有相同的类型),(unsigned int)x-(unsigned int)y = 4294967295。
上面代码的输出:(与我们预期的相同):
int main(){
unsigned int x = 0;
unsigned int y = 1;
printf("%u\n", x - y);
printf("%d\n", x - y);
}
修复代码的一种可能方法:(最后添加一个类型转换)
4294967295
-1
上面代码的输出:
int main(){
unsigned char x = 0;
unsigned char y = 1;
printf("%u\n", x - y);
printf("%d\n", x - y);
unsigned char z = x-y;
printf("%u\n", z);
}
例2)
4294967295
-1
255
由于它们都是整数,因此不需要整数提升。通过通常的算术转换,我们得到(unsigned int)a +(unsigned int)b = 1 + 4294967294 = 4294967295。
上面代码的输出:(与我们预期的相同)
int main(){
unsigned int a = 1;
signed int b = -2;
if(a + b > 0)
puts("-1 is larger than 0");
printf("%u\n", a+b);
}
怎么解决?
-1 is larger than 0
4294967295
上面代码的输出:
int main(){
unsigned int a = 1;
signed int b = -2;
signed int c = a+b;
if(c < 0)
puts("-1 is smaller than 0");
printf("%d\n", c);
}
例3)
-1 is smaller than 0
-1
最后一个示例解决了问题,因为由于整数提升,a和b都转换为int。
上面代码的输出:
int main(){
unsigned short a = 1;
signed short b = -2;
if(a + b < 0)
puts("-1 is smaller than 0");
printf("%d\n", a+b);
}
如果我混淆了一些概念,请告诉我。谢谢〜