隐式类型提升规则


72

这篇文章旨在用作有关C语言中隐式整数提升的FAQ,尤其是由通常的算术转换和/或整数提升引起的隐式提升。

示例1)
为什么给出一个奇怪的大整数而不是255?

示例2)
为什么给出“ -1大于0”?

示例3)
为什么在上面的示例中更改类型以short解决问题?

(这些示例适用于16位短的32位或64位计算机。)


3
我建议记录这些示例的假设,例如,示例3假设short比窄int(或换句话说,假设int可以代表的所有值unsigned short)。
伊恩·雅培

稍等片刻,OP是回答问题的同一个人?它说伦丁问,最好的回答是伦丁公司以及洛尔
savram

3
@savram是的,目的是编写一个FAQ条目。这样共享知识非常适合-下次您发布问题时,请注意复选框“回答您自己的问题”。但是,当然,该问题仍会像其他任何问题一样对待,其他人也可以发布答案。(而且您不会因接受自己的回答而赢得任何回报)
Lundin

1
到目前为止,没有一个答案提到printf("%u\n", x - y);导致不确定行为的事实
MM

1
很好的例子就是~((u8)(1 << 7))清单。
0andriy

Answers:


100

C被设计为隐式和无声地更改表达式中使用的操作数的整数类型。在几种情况下,语言会迫使编译器将操作数更改为更大的类型,或者更改其符号。

其基本原理是为了防止算术期间意外溢出,而且还允许具有不同符号的操作数在同一表达式中共存。

不幸的是,隐式类型提升的规则弊大于利,以至于它们可能是C语言中最大的缺陷之一。这些规则通常对于普通C程序员来说甚至是未知的,因此会引起各种非常细微的错误。

通常情况下,您会看到程序员说“只需强制转换为x即可使用它”的情况-但他们不知道为什么。或者,这些bug表现为看似简单而直接的代码中罕见的间歇性现象。隐式提升在进行位操作的代码中特别麻烦,因为在给定有符号操作数的情况下,C中大多数位运算符的行为都定义不明确。


整数类型和转换等级

在C中的整数类型是charshortintlonglong longenum
_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具有与int32位系统相同的等级。

此外,C11 6.3.1.1指定哪些类型被视为小整数类型(不是形式术语):

在可以使用int或的表达式中unsigned int可以使用以下代码:

-一个整型(比其他的物体或表达intunsigned int),其整数转换秩小于或等于的秩intunsigned int

什么在实践中,这种略带神秘文字的手段,是_Boolcharshort(也int8_tuint8_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常规算术转换

许多期望算术类型的操作数的运算符都以类似的方式引起转换并产生结果类型。目的是确定操作数和结果的通用实型。对于指定的操作数,每个操作数在不更改类型域的情况下被转换为其对应的实型为普通实型的类型。除非另有明确说明,否则普通实型也是结果的对应实型,如果操作数相同,则其类型域是操作数的类型域,否则为复杂。这种模式称为通常的算术转换

  • 首先,如果一个操作数的对应实型为long double,则另一个操作数在不改变类型域的情况下转换为其对应实型为的类型long double
  • 否则,如果一个操作数的对应实型为double,则另一个操作数将被转换为对应实型为的类型,而不会改变类型域double
  • 否则,如果任一操作数的对应实型为float,则另一个操作数将在不更改类型域的情况下转换为其对应实型为float的类型。
  • 否则,将对两个操作数执行整数提升。然后,将以下规则应用于提升后的操作数:

    • 如果两个操作数具有相同的类型,则无需进一步转换。
    • 否则,如果两个操作数都具有符号整数类型或都具有无符号整数类型,则将具有较小整数转换等级的操作数转换为具有较大等级的操作数的类型。
    • 否则,如果具有无符号整数类型的操作数的秩大于或等于另一个操作数的类型的秩,则将带符号整数类型的操作数转换为无符号整数类型的操作数的类型。
    • 否则,如果带符号整数类型的操作数的类型可以表示带无符号整数类型的操作数的所有值,则带无符号整数类型的操作数将转换为带符号整数类型的操作数的类型。
    • 否则,两个操作数都将转换为与带符号整数类型的操作数类型相对应的无符号整数类型。

这里值得注意的是,通常的算术转换适用于浮点数和整数变量。对于整数,我们还可以注意到,整数提升是从常规算术转换中调用的。之后,当两个操作数的秩至少int为时,运算符将被平衡为具有相同符号的相同类型。

这就是a + b示例2中给出奇怪结果的原因。这两个操作数都是整数,并且至少为rank int,因此整数提升不适用。操作数是不一样的类型-aunsigned intbsigned int。因此,运算符b将临时转换为type unsigned int。在此转换过程中,它会丢失符号信息,并最终变成较大的值。

之所以将short示例3中的类型更改为固定的问题,是因为short是一个小的整数类型。这意味着两个操作数都是整数,提升int为带符号的类型。整数提升后,两个操作数具有相同的类型(int),无需进一步转换。然后可以按预期对带符号的类型执行该操作。


“只要在表达式中使用小整数类型,它就会隐式转换为始终带符号的int。” 您能否指出标准中应该发生的确切位置?该C11 6.3.1.1报价说如何它发生(如果发生),但它并没有说,它一定会发生比如,为什么x - y在这个问题表现为(unsigned)(int)((int)x - (int)y),而不是(unsigned)(int)((Uchar)((Uchar)x - (Uchar)y))goo.gl/nCvJy5。标准在哪里说如果xchar+x是那么int(或未签名)?在c ++中,它是§5.3.1.7goo.gl/FkEakX
jfs

@jfs对于适用于常规算术转换的二进制运算符,上面所述的那些规则中包括了“应该”部分。对于其他操作员,如果特定操作员(第6.5节)需要,则将其提升。例如,一元运算-符6.5.3.3:“一元运算符的结果是其(提升的)操作数的负数。对运算数执行整数提升,并且结果具有提升的类型。” 另一个示例(特殊情况)是prefix ++,它没有提到整数提升,因此不会提升其操作数。
伦丁

@Lundin “这些规则中包括“应该”部分”,我看不到。您能否从标准中提供一个特定的报价,该报价说在执行二进制运算(例如)时会提升相同的小整数类型的操作数x-y?(换句话说,没有一个(char)-(char)运算符是唯一的(int)-(int)或没有运算符的。据我所知,这是事实。c11 6.5.6 / 4指向6.3.1.8/1,其中指出“目的是确定通用实型”,但是如果操作数已经是同一类型,则不明确执行任何对话。
jfs

1
“可以通过强制转换一个或两个操作数以键入unsigned int来固定示例1。” 建议的转换不会产生OP预期的255。正确的解决方法是将减法的结果强制转换回(unsigned char)操作数的起始位置,例如(unsigned char) (x-y):这将使OP达到预期的255。完成截断(后面是隐式/自动签名或零扩展到〜int大小)。
Erik Eidt

1
@Student Ah现在我明白了,解释的期望确实与提出的解决方案不符。更新,谢谢。
伦丁

4

根据上一篇文章,我想提供有关每个示例的更多信息。

示例1)

由于unsigned char小于int,因此我们对它们应用整数提升,因此我们有(int)x-(int)y =(int)(-1)和unsigned int(-1)= 4294967295。

上面代码的输出:(与我们的预期相同)

如何解决?

我尝试了上一篇文章的建议,但实际上没有用。这是基于前一篇文章的代码:

将其中之一更改为unsigned int

由于x已经是无符号整数,因此我们仅将整数提升应用于y。然后我们得到(unsigned int)x-(int)y。由于它们仍然没有相同的类型,因此我们应用通常的算术转换,得到(unsigned int)x-(unsigned int)y = 4294967295。

上面代码的输出:(与我们的预期相同):

同样,以下代码获得相同的结果:

将它们都更改为unsigned int

由于它们都是unsigned int,因此不需要整数提升。通过通常的算术收敛(具有相同的类型),(unsigned int)x-(unsigned int)y = 4294967295。

上面代码的输出:(与我们的预期相同):

修复代码的一种可能方法:(最后添加类型强制转换)

上面代码的输出:

示例2)

由于它们都是整数,因此不需要整数提升。通过通常的算术转换,我们得到(unsigned int)a +(unsigned int)b = 1 + 4294967294 = 4294967295。

上面代码的输出:(与我们的预期相同)

如何解决?

上面代码的输出:

示例3)

最后一个示例解决了该问题,因为由于整数提升,a和b都转换为int。

上面代码的输出:

如果我混淆了一些概念,请告诉我。谢谢〜


1
您对signed int c = a+b;上面的示例2的修复调用了UB。a + b的结果类型是无符号的,并且计算的值超出了有符号整数的范围。
Cheshar

1
@Cheshar超出范围的任务不是UB
MM

1
此答案中的许多示例均使用错误的格式说明符导致了UB,并且也对int
MM

@MM我不好!同意,它应该是“实现定义的或引发实现定义的信号”。但是签名溢出是UB。忘记UB / IB比较容易。
Cheshar

@Cheshar:与一些编译器维护者所散布的神话相反,该标准的动作术语应由“未定义行为”来定义,这些动作应由99.9%的实现方式进行相同处理,而在不切实际的实现中不需要进行有意义的处理。IDB术语仅用于所有实现都应该有意义地处理的动作。
超级猫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.