C被设计为隐式和无声地更改表达式中使用的操作数的整数类型。在几种情况下,语言会迫使编译器将操作数更改为更大的类型,或者更改其符号。
其基本原理是为了防止算术期间意外溢出,而且还允许具有不同符号的操作数在同一表达式中共存。
不幸的是,隐式类型提升的规则弊大于利,以至于它们可能是C语言中最大的缺陷之一。这些规则通常对于普通C程序员来说甚至是未知的,因此会引起各种非常细微的错误。
通常情况下,您会看到程序员说“只需强制转换为x即可使用它”的情况-但他们不知道为什么。或者,这些bug表现为看似简单而直接的代码中罕见的间歇性现象。隐式提升在进行位操作的代码中特别麻烦,因为在给定有符号操作数的情况下,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
。
—任何无符号整数类型的等级应等于相应的有符号整数类型的等级(如果有)。
—字符的等级应等于有符号字符和无符号字符的等级。
—任何标准整数类型的等级应大于宽度相同的任何扩展整数类型的等级。
— _Bool的等级应小于所有其他标准整数类型的等级。
—任何枚举类型的等级应等于兼容整数类型的等级(见6.7.2.2)。
来自stdint.h
这里的排序类型也与它们在给定系统上对应的任何类型具有相同的等级。例如,int32_t
具有与int
32位系统相同的等级。
此外,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):
如果anint
可以代表原始类型的所有值(受位字段的宽度限制),则该值将转换为an int
。否则,它将转换为unsigned int
。这些称为整数促销。
这意味着,所有带符号的小整数类型,int
在大多数表达式中使用时,都会隐式转换为(带符号)。
该文本经常被误解为:“所有小的带符号整数类型都转换为带符号int,所有小的带符号整数类型都转换为无符号int”。这是不正确的。这里的无符号部分仅意味着,如果我们有一个unsigned short
操作数,并且int
碰巧short
与给定系统上的大小相同,则该unsigned short
操作数将转换为unsigned int
。在这种情况下,什么都没有真正发生。但是,如果short
类型小于int
,则始终将其转换为(signed)int
,而不管short是带符号的还是无符号的!
整数提升导致的严酷现实意味着,几乎无法在Cchar
或C等小型类型上执行C中的运算short
。操作始终在int
较大的类型上执行。
这听起来像是胡说八道,但是幸运的是编译器被允许优化代码。例如,包含两个unsigned char
操作数的表达式会将操作数提升为int
,并将操作执行为int
。但是,可以预期,编译器可以优化表达式以实际以8位运算的形式执行。然而,这里来了问题:编译器不容许优化出引起整数推广符号性的隐含变化。因为编译器无法判断程序员是否故意依赖隐式升级,还是非故意的。
这就是为什么问题中的示例1失败的原因。这两个无符号char操作数都被提升为type int
,对type进行运算int
,并且结果x - y
为type int
。意味着我们得到的-1
不是255
预期的。编译器可能会生成使用8位指令(而不是8位指令)执行代码的机器代码int
,但可能无法优化签名的更改。这意味着我们最终得到一个否定的结果,这反过来导致在printf("%u
调用时产生一个怪异的数字。可以通过将运算结果转换回type来固定示例1 unsigned char
。
除++
和sizeof
运算符之类的一些特殊情况外,整数提升适用于C中几乎所有的运算,无论是否使用一元,二进制(或三元)运算符。
通常的算术转换
每当在C中执行二进制运算(带有2个操作数的运算)时,运算符的两个操作数都必须具有相同的类型。因此,在操作数为不同类型的情况下,C强制将一个操作数隐式转换为另一操作数的类型。如何完成此操作的规则称为通常的人工转换(有时非正式地称为“平衡”)。这些在C11 6.3.18中指定:
(将此规则视为长的嵌套if-else if
语句,可能更容易阅读:))
6.3.1.8常规算术转换
许多期望算术类型的操作数的运算符都以类似的方式引起转换并产生结果类型。目的是确定操作数和结果的通用实型。对于指定的操作数,每个操作数在不更改类型域的情况下被转换为其对应的实型为普通实型的类型。除非另有明确说明,否则普通实型也是结果的对应实型,如果操作数相同,则其类型域是操作数的类型域,否则为复杂。这种模式称为通常的算术转换:
这里值得注意的是,通常的算术转换适用于浮点数和整数变量。对于整数,我们还可以注意到,整数提升是从常规算术转换中调用的。之后,当两个操作数的秩至少int
为时,运算符将被平衡为具有相同符号的相同类型。
这就是a + b
示例2中给出奇怪结果的原因。这两个操作数都是整数,并且至少为rank int
,因此整数提升不适用。操作数是不一样的类型-a
是unsigned int
和b
是signed int
。因此,运算符b
将临时转换为type unsigned int
。在此转换过程中,它会丢失符号信息,并最终变成较大的值。
之所以将short
示例3中的类型更改为固定的问题,是因为short
是一个小的整数类型。这意味着两个操作数都是整数,提升int
为带符号的类型。整数提升后,两个操作数具有相同的类型(int
),无需进一步转换。然后可以按预期对带符号的类型执行该操作。
short
比窄int
(或换句话说,假设int
可以代表的所有值unsigned short
)。