隐式类型提升规则
本文旨在用作关于C中隐式整数提升的常见问题解答,特别是由常规算术转换和/或整数提升引起的隐式提升。
例1)
为什么这会给出一个奇怪的大整数而不是255?
unsigned char x = 0;
unsigned char y = 1;
printf("%un", 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位短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的等级应与已签名的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
。 这些被称为整数促销。
这段文字经常被误解为:“所有小的有符号整数类型都被转换为带符号整数,而所有小的无符号整数类型都被转换为无符号整数”。 这是不正确的。 这里的无符号部分只意味着如果我们有例如一个unsigned short
操作数,并且int
在给定系统上恰好与short
相同,那么unsigned short
操作数被转换为unsigned int
。 如同,没有什么值得注意的事情发生。 但是,如果short
是比int
更小的类型,则它总是转换为(signed) int
,而不管它是否有符号或无符号!
整数升级造成的严酷现实意味着几乎不可以在像char
或short
这样的小型类型上进行C中的操作。 操作始终以int
或更大的类型进行。
这听起来像是无稽之谈,但幸运的是编译器可以优化代码。 例如,包含两个unsigned char
操作数的表达式会将操作数提升为int
,并将操作作为int
。 但是,正如预期的那样,编译器允许优化表达式以实际执行8位操作。 但是,问题在于:编译器不允许优化整数提升引起的隐式签名变化。 因为编译器无法判断程序员是故意依赖隐式升级发生还是无意识。
这就是为什么问题中的示例1失败。 两个无符号字符操作数都被提升为int
类型,操作在int
类型上执行,而x - y
的结果类型为int
。 这意味着我们得到-1
而不是255
,这可能是预期的。 编译器可以生成用8位指令而不是int
来执行代码的机器码,但它可能不会优化签名的变化。 这意味着我们最终得到一个负面结果,当printf("%u
被调用时,反过来会导致一个奇怪的数字。例1可以通过投入一个或两个操作数来输入unsigned int
。
除了像++
和sizeof
运算符这样的一些特殊情况外,无论是否使用一元,二元(或三元)运算符,整数提升都适用于C中的几乎所有运算。
通常的算术转换
无论何时在C中完成二元操作(具有2个操作数的操作),操作符的两个操作数必须是相同的类型。 因此,如果操作数的类型不同,C强制将一个操作数隐式转换为另一个操作数的类型。 这种做法的规则被称为通常的艺术化转换(有时非正式地称为“平衡”)。 这些在C11 6.3.18中规定:
(把这个规则想象成一个长的,嵌套的if-else if
语句,它可能更容易阅读:))
6.3.1.8通常的算术转换
许多期望算术类型操作数的操作符会以相似的方式导致转换并生成结果类型。 目的是为操作数和结果确定一个通用的实际类型。 对于指定的操作数,每个操作数在不更改类型域的情况下转换为对应的实型为常用实型的类型。 除非另有明确说明,否则公共实型也是结果的相应实型,如果它们相同,则其类型域是操作数的类型域,否则是复数。 这种模式被称为通常的算术转换:
long double
,则另一个操作数将在不更改类型域的情况下转换为对应实型为long double
精度型的类型。 double
精度型,则另一个操作数将转换为相应实型为double
精度的类型,而不会更改类型域。 float
,则另一个操作数会在不更改类型域的情况下转换为对应的实型为float的类型。 否则,整数升级在两个操作数上执行。 然后将下列规则应用于提升的操作数:
这里值得注意的是通常的算术转换适用于浮点和整数变量。 在整数情况下,我们还可以注意到整数提升是在通常的算术转换中调用的。 在此之后,当两个操作数至少具有int
的等级时,操作符被平衡为相同类型,具有相同的符号。
这就是为什么例2中的a + b
给出了一个奇怪的结果。 两个操作数都是整数,它们至少是int
的整数,所以整数提升不适用。 操作数不是同一类型 - a
是unsigned int
, b
是signed int
。 因此运算符b
暂时转换为unsigned int
类型。 在此转换过程中,它丢失了标志信息,并以一个很大的值结束。
在例子3中改变类型为short
的原因是修复问题,是因为short
是一个小整数类型。 这意味着两个操作数都是整数提升为int
类型的,并且被签名。 整数提升后,两个操作数的类型都是相同的( int
),不需要进一步转换。 然后可以按照预期的方式对签名类型执行操作。