隐式类型提升规则

这篇文章的目的是作为一个常见问题,关于隐式整数提升 C,特别是隐式提升所造成的通常算术转换和/或整数提升。

例一)
为什么它给出的是一个奇怪的大整数而不是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");

例三)
为什么将上面例子中的类型改为 short可以解决这个问题?

unsigned short a = 1;
signed short b = -2;
if(a + b > 0)
puts("-1 is larger than 0"); // will not print

(These examples were intended for a 32 or 64 bit computer with 16 bit short.)

33514 次浏览

C 被设计用于隐式和无声地更改表达式中使用的操作数的整数类型。有几种情况下,语言强制编译器要么将操作数更改为更大的类型,要么更改它们的有符号性。

这样做的基本原理是为了防止在算术过程中意外溢出,同时也是为了允许具有不同正负号的操作数在同一个表达式中共存。

不幸的是,隐式类型提升规则的弊大于利,以至于它们可能是 C 语言中最大的缺陷之一。这些规则通常连普通的 C 程序员都不知道,因此会导致各种各样非常微妙的 bug。

通常你会看到这样的场景: 程序员说“只要转换成 x 类型就可以了”——但是他们不知道为什么。或者,这样的错误表现为罕见的、间歇性的现象,从看似简单和直接的代码中突然显现出来。在进行位操作的代码中,隐式提升尤其麻烦,因为 C 中的大多数位操作符在给定有符号操作数时都具有定义不清的行为。


整数类型和转换等级

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

All integers have a specified conversion rank. C11 6.3.1.1, emphasis mine on the most important parts:

每个整数类型都有一个整数转换等级,定义如下:
ー任何两个有符号整数类型都不应具有相同的秩,即使它们具有相同的表示形式。
ー有符号整数类型的秩应大于任何精度较低的有符号整数类型的秩。
--long long int的排名应大于 long int的排名,long int的排名应大于 int的排名,int的排名应大于 short int的排名,short int的排名应大于 signed char的排名。
ー任何无符号整数类型的秩应等于相应的有符号整数类型(如果有的话)的秩。
< br/> ー任何标准整数类型的秩应大于任何具有相同宽度的扩展整数类型的秩。
字符的等级等于有符号字符和无符号字符的等级。
- _ Bool 的秩应小于所有其他标准整数类型的秩。
ー任何枚举类型的秩应等于兼容整数类型的秩(见6.7.2.2)。

来自 stdint.h的类型在这里也排序,与它们碰巧对应于给定系统的任何类型具有相同的等级。例如,在32位系统上,int32_tint具有相同的等级。

此外,C116.3.1.1指定哪些类型被视为 小整数类型(非正式术语) :

无论 intunsigned int在何处,以下内容都可用于表达式中 使用:

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

What this somewhat cryptic text means in practice, is that _Bool, char and short (and also int8_t, uint8_t etc) are the "small integer types". These are treated in special ways and subject to implicit promotion, as explained below.


The integer promotions

无论何时在表达式中使用小整数类型,都会将其隐式转换为始终有符号的 int。这被称为 整数提升整数提升规则

形式上,规则说(C116.3.1.1) :

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

这意味着在大多数表达式中使用时,所有小整数类型(无论是否有符号)都会隐式转换为(有符号的) int

这篇文章经常被误解为: “所有小的有符号整数类型都转换为有符号整数,所有小的无符号整数类型都转换为无符号整数”。这是错误的。这里的无符号部分仅意味着,如果我们有一个例如 unsigned short操作数,并且 int碰巧与给定系统上的 short具有相同的大小,那么 unsigned short操作数将被转换为 unsigned int。也就是说,没有什么值得注意的事情发生。但是,如果 short是一个比 int小的类型,它总是转换为(签名) int不管是签了还是没签

整数促销造成的严酷现实意味着几乎没有 C 语言的操作可以在像 charshort这样的小型语言上进行。操作总是在 int或更大的类型上进行。

这听起来可能像是无稽之谈,但幸运的是,编译器可以优化代码。例如,包含两个 unsigned char操作数的表达式将获得提升为 int的操作数以及作为 int执行的操作。但是允许编译器优化表达式,以实际执行8位操作,正如所期望的那样。然而,问题来了: 编译器被 没有允许优化整数提升引起的隐式有符号性变化,因为编译器没有办法判断程序员是有意依赖隐式提升发生,还是无意的。

This is why example 1 in the question fails. Both unsigned char operands are promoted to type int, the operation is carried out on type int, and the result of x - y is of type int. Meaning that we get -1 instead of 255 which might have been expected. The compiler may generate machine code that executes the code with 8 bit instructions instead of int, but it may not optimize out the change of signedness. Meaning that we end up with a negative result, that in turn results in a weird number when printf("%u is invoked. Example 1 could be fixed by casting the result of the operation back to type unsigned char.

除了像 ++sizeof操作符这样的特殊情况之外,整数提升几乎适用于 C 中的所有操作,不管是否使用了一元、二元(或三元)操作符。


通常的算术转换

无论何时在 C 语言中执行一个二元运算操作(一个有两个操作数的操作) ,操作符的两个操作数都必须是同一类型的。因此,在操作数具有不同类型的情况下,C 强制将一个操作数隐式转换为另一个操作数的类型。如何做到这一点的规则被命名为 the usual artihmetic conversions(有时非正式地称为“平衡”)。这些都在 C116.3.18中指明:

(把这条规则想象成一个长的嵌套的 if-else if语句,它可能更容易理解:)

6.3.1.8常用的算术转换

许多期望使用算术类型的操作数的运算符会导致转换并产生结果 其目的是为操作数确定一个通用的实数类型 and result. For the specified operands, each operand is converted, without change of type domain, to a type whose corresponding real type is the common real type. Unless 否则,公共实数类型也是相应的实数类型 结果,其类型域是操作数的类型域(如果它们相同) , 这种模式被称为 通常的算术转换:

  • 首先,如果其中一个操作数的对应实数类型是 long double,那么另一个操作数将在不改变类型域的情况下转换为对应实数类型为 long double的类型。
  • 否则,如果其中一个操作数的对应实数类型为 double,则另一个操作数将在不改变类型域的情况下转换为其对应实数类型为 double的类型。
  • 否则,如果其中一个操作数的对应实数类型为 float,则另一个操作数将在不改变类型域的情况下转换为对应实数类型为 float 的类型。
  • 否则,整数提升将在两个操作数上执行 following rules are applied to the promoted operands:
  • 如果两个操作数具有相同的类型,则不需要进一步转换。
  • 否则,如果两个操作数都具有有符号整数类型或两者都具有无符号整数类型 整数类型时,转换秩较小的整数类型的操作数为 转换为具有更大秩的操作数的类型。
  • Otherwise, if the operand that has unsigned integer type has rank greater or 等于另一个操作数类型的秩,然后使用 有符号整数类型转换为具有无符号 整数类型。
  • 否则,如果带有有符号整数类型的操作数的类型可以表示 所有具有无符号整数类型的操作数类型的值,则 具有无符号整数类型的操作数转换为 有符号整数类型的操作数。
  • 否则,两个操作数都将转换为无符号整数类型 对应于具有有符号整数类型的操作数的类型。

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

这就是为什么例2中的 a + b给出了一个奇怪的结果。两个操作数都是整数,它们至少是等级 int,因此整数提升不适用。操作数的类型不同-aunsigned intbsigned int。因此,操作符 b被临时转换为 unsigned int类型。在此转换过程中,它将丢失符号信息,并最终成为一个大值。

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

根据前面的帖子,我想给出更多关于每个例子的信息。

例一)

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

Since unsigned char is smaller than int, we apply the integer promotion on them, then we have (int)x-(int)y = (int)(-1) and unsigned int (-1) = 4294967295.

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

4294967295
-1

How to fix it?

我尝试了上一篇文章推荐的方法,但是并不奏效。 下面是基于上一篇文章的代码:

将其中一个改为无符号整型

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

Since x is already an unsigned integer, we only apply the integer promotion to y. Then we get (unsigned int)x-(int)y. Since they still don't have the same type, we apply the usual arithmetic converions, we get (unsigned int)x-(unsigned int)y = 4294967295.

The output from the above code:(same as what we expected):

4294967295
-1

Similarly, the following code gets the same result:

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

change both of them to unsigned int

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

因为它们都是无符号整型,所以不需要整型提升。按照通常的算术转换(具有相同的类型) ,(无符号整数) x-(无符号整数) y = 4294967295。

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

4294967295
-1

One of possible ways to fix the code:(add a type cast in the end)

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

以上代码的输出:

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

因为它们都是整数,所以不需要进行整数提升。通过常规的算术转换,我们得到了(无符号整数) a + (无符号整数) b = 1 + 4294967294 = 4294967295。

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

-1 is larger than 0
4294967295

How to fix it?

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

The output from the above code:

-1 is smaller than 0
-1

例三)

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

如果我有一些概念混淆,请让我知道。谢谢 ~

C 和 C + + 中的整数和浮点排名及升级规则

我想尝试一下总结一下这些规则,这样我就可以快速地引用它们。我已经充分研究了这个问题和其他两个答案,包括 作者:@Lundin。如果你想在下面的例子之外有更多的例子,去研究那个答案的细节,同时参考我的“规则”和“促销流程”摘要如下。

我还在这里编写了自己的示例和演示代码: 整数 _ 促进 _ 溢出 _ 下流 _ 未定义 _ 行为

尽管我自己通常都非常冗长,但我还是尽量保持一个简短的总结,因为其他两个答案加上我的测试代码已经通过它们必要的冗长获得了足够的细节。

整数和变量推广快速参考指南和总结

三条简单的规则

  1. 对于涉及多个操作数(输入变量)的任何操作(例如: 数学操作、比较或三元操作) ,根据需要的变量类型 之前的要求,这些变量是 升职了
    1. 因此,如果不希望隐式地为您选择所需的类型,则必须手动显式地将 投影输出设置为所需的任何类型。请看下面的例子。
  2. 所有小于 int(在我的64位 Linux 系统上是 int32_t)的类型都是“小型类型”。它们不能在任何操作中使用。因此,如果所有输入变量都是“小类型”,那么在执行操作之前,它们首先都被提升到 int(在我的64位 Linux 系统上是 int32_t)。
  3. 否则,如果至少一个输入类型是 int或更大的,那么另一个较小的输入类型将被提升为这个最大输入类型的类型。

例子

示例: 使用此代码:

uint8_t x = 0;
uint8_t y = 1;

... 如果你做 x - y,他们首先得到隐式提升到 int(这是 int32_t在我的64位 System) ,结果是: (int)x - (int)y,这将导致具有值的 int类型 -1, rather than a uint8_t type of value 255. To get the desired 255 result, 手动操作 通过这样做,将结果抛回到 uint8_t: (uint8_t)(x - y)

晋升流程

推广规则如下。从 smallest to largest类型推广如下。
把“ -->”读作“升职”。

方括号中的类型(例如: [int8_t])是典型的64位 Unix (Linux 或 Mac)架构上给定标准类型的典型 “固定宽度整数类型”。比如说:

  1. Https://www.cs.yale.edu/homes/aspnes/pinewiki/c(2f)integertypes.html
  2. Https://www.ibm.com/docs/en/ibm-mq/7.5?topic=platforms-standard-data-types
  3. 更好的是,自己在机器上测试一下通过运行我的代码在这里! : stdint_sizes.c从我的 你好,世界回购。

1. 对于整数类型

注: “小型” = bool(_Bool) ,char [int8_t]unsigned char [uint8_t]short [int16_t]unsigned short [uint16_t]

小型类型: bool(_Bool) ,char [int8_t]unsigned char [uint8_t]short [int16_t]unsigned short [uint16_t]
- > int [int32_t]
- > unsigned int [uint32_t]
- > long int [int64_t]
- > unsigned long int [uint64_t]
- > long long int [int64_t]
--> unsigned long long int [uint64_t]

指针(例如: void*)和 size_t都是64位的,所以我想它们应该属于上面的 uint64_t类别。

2. For floating point types

float [32-bits]-> double [64-bits]-> long double [128-bits]

我想对@Lundin 另一个出色的回答添加两点澄清,关于例子1,其中有两个操作数是相同的整数类型,但是是需要整数提升的“小类型”。

我使用的是 N1256汇票,因为我没有 C 标准的付费拷贝。

First: (normative)

6.3.1.1对整数提升的定义不是实际 做什么整数提升的触发子句。实际上是6.3.1.8常规的算术转换。

大多数情况下,当操作数为 与众不同类型时,应用“通常的算术转换”,在这种情况下,必须提升至少一个操作数。但问题是,对于整数类型,在所有情况下都需要进行整数提升。

[浮点类型的子句优先]

否则,整数提升将在两个操作数上执行 适用于升级操作数的规则如下:

  • If both operands have the same type, then no further conversion is needed.
  • 否则,如果两个操作数都具有有符号整数类型或两者都具有无符号整数类型 整数类型时,转换秩较小的整数类型的操作数为 转换为具有更大秩的操作数的类型。
  • 否则,如果具有无符号整数类型的操作数的秩大于或 等于另一个操作数类型的秩,然后使用 有符号整数类型转换为具有无符号 整数类型。
  • 否则,如果带有有符号整数类型的操作数的类型可以表示 所有具有无符号整数类型的操作数类型的值,则 the operand with unsigned integer type is converted to the type of the 有符号整数类型的操作数。
  • 否则,两个操作数都将转换为无符号整数类型 对应于具有有符号整数类型的操作数的类型。

第二 : (非规范)

该标准列举了一个明确的例子来证明这一点:

在执行片段时

char c1, c2;
/* ... */
c1 = c1 + c2;

“整数提升”要求抽象机器将每个变量的值提升到 int大小 然后加上两个 int并截断总和。假设加上两个 char不需要 溢出,或者通过以静默方式包装溢出来产生正确的结果,实际执行只需要 产生相同的结果,可能省略了促销。

enter image description here