隐式类型促销规则

本文旨在用作关于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 

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

C旨在隐式和静默地更改表达式中使用的操作数的整数类型。 在某些情况下,语言会强制编译器将操作数更改为更大的类型,或更改其签名。

这背后的基本原理是防止算术期间意外溢出,但也允许具有不同签名的操作数共存于同一表达式中。

不幸的是,隐式类型提升的规则造成的危害远大于好处,它们可能是C语言中最大的缺陷之一。 这些规则通常不为普通的C程序员所知,因此会导致各种非常微妙的错误。

通常情况下,您会看到程序员说“只是强制转换为x并且工作正常”的情况 – 但他们不知道为什么。 或者这样的错误表现为罕见的,间歇性的现象,从看似简单和直接的代码中迸发出来。 在执行位操作的代码中,隐式提升尤其麻烦,因为在给定带符号的操作数时,C中的大多数位操作符都具有定义不明确的行为。


整数类型和转换排名

C中的整数类型是charshortintlonglong longenum
在类型提升方面, _Bool / bool也被视为整数类型。

所有整数都具有指定的转换排名 。 C11 6.3.1.1,强调我最重要的部分:

每个整数类型都有一个整数转换等级,定义如下:
– 没有两个有符号整数类型具有相同的等级,即使它们具有相同的表示。
– 有符号整数类型的等级应大于精度较低的任何有符号整数类型的等级。
long long int的等级应大于long long int的等级,该等级应大于int的等级,该等级应大于short int的等级, short int rank应大于signed char的等级。
– 任何无符号整数类型的等级应等于相应的有符号整数类型的等级(如果有)。
– 任何标准整数类型的等级应大于具有相同宽度的任何扩展整数类型的等级。
– char的等级应等于signed char和unsigned char的等级。
– _Bool的等级应小于所有其他标准整数类型的等级。
– 任何枚举类型的等级应等于兼容整数类型的等级(见6.7.2.2)。

stdint.h类型也在这里排序,其排名与它们在给定系统上碰巧对应的类型相同。 例如, int32_t与32位系统上的int具有相同的等级。

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

如果可以使用intunsigned int则可以在表达式中使用以下内容:

– 具有整数类型( intunsigned int除外)的对象或表达式,其整数转换等级小于或等于intunsigned int的等级。

这个有点神秘的文本在实践中意味着什么, _Boolcharshort (以及int8_tuint8_t等)是“小整数类型”。 如下所述,这些是以特殊方式处理并受到隐含促销的约束。


整数促销

只要在表达式中使用小整数类型,它就会隐式转换为始终有符号的int 。 这称为整数提升整数提升规则

在forms上,规则说(C11 6.3.1.1):

如果int可以表示原始类型的所有值(由宽度限制,对于位字段),该值将转换为int ; 否则,它将转换为unsigned int 。 这些被称为整数促销

这个文本经常被误解为:“所有小的有符号整数类型都转换为signed int,所有小的无符号整数类型都转换为unsigned int”。 这是不正确的。 这里的无符号部分仅表示如果我们有一个unsigned short操作数,并且int恰好与给定系统上的short相同,那么unsigned short操作数将转换为unsigned int 。 如同,没有任何真正发生的事情。 但是如果short是比int更小的类型,它总是转换为(signed) int无论short是签名还是未签名

由整数提升引起的严酷现实意味着C中的几乎所有操作都不能在charshort等小类型上执行。 操作始终在int或更大类型上执行。

这可能听起来像废话,但幸运的是编译器允许优化代码。 例如,包含两个unsigned char操作数的表达式将操作数提升为int ,并将操作作为int 。 但是,允许编译器优化表达式以实际执行为8位操作,如预期的那样。 但是,问题出现了:不允许编译器优化由整数提升引起的签名隐式更改。 因为编译器无法判断程序员是否故意依赖隐式促销发生,或者是否是无意的。

这就是问题中的示例1失败的原因。 两个unsigned char操作数都被提升为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给出奇怪结果的原因。 两个操作数都是整数,它们至少是rank int ,因此整数提升不适用。 操作数的类型不同 – aunsigned intbsigned int 。 因此,运算符b暂时转换为unsigned int类型。 在此转换过程中,它会丢失符号信息并最终成为一个较大的值。

在示例3中将类型更改为short的原因解决了问题,因为short是一个小整数类型。 这意味着两个操作数都是整数提升为有符号的int类型。 在整数提升之后,两个操作数具有相同的类型( int ),不需要进一步转换。 然后可以按预期在签名类型上执行操作。

根据之前的post,我想提供有关每个示例的更多信息。

例1)

 int main(){ unsigned char x = 0; unsigned char y = 1; printf("%u\n", x - y); printf("%d\n", x - y); } 

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

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

 4294967295 -1 

怎么解决?

我尝试了之前的post推荐的内容,但它并没有真正起作用。 以下是基于上一篇文章的代码:

将其中一个更改为unsigned int

 int main(){ unsigned int x = 0; unsigned char y = 1; printf("%u\n", x - y); printf("%d\n", x - y); } 

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

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

 4294967295 -1 

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

 int main(){ unsigned char x = 0; unsigned int y = 1; printf("%u\n", x - y); printf("%d\n", x - y); } 

将它们都更改为unsigned int

 int main(){ unsigned int x = 0; unsigned int y = 1; printf("%u\n", x - y); printf("%d\n", x - y); } 

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

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

 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 = xy; printf("%u\n", z); } 

上面代码的输出:

 4294967295 -1 255 

例2)

 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); } 

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

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

 -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); } 

上面代码的输出:

 -1 is smaller than 0 -1 

例3)

 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); } 

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

上面代码的输出:

 -1 is smaller than 0 -1 

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