隐式类型升级规则

Implicit type promotion rules

提问人:Lundin 提问时间:9/6/2017 最后编辑:Lundin 更新时间:9/15/2023 访问量:43091

问:

这篇文章旨在用作有关 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

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

C 类型转换 隐式转换

评论

4赞 Ian Abbott 9/6/2017
我建议记录示例的假设,例如示例 3 假设它比(或者换句话说,它假设可以表示 )的所有值。shortintintunsigned short
6赞 Lundin 9/7/2017
@savram 是的,目的是写一个FAQ条目。以这种方式分享知识对 SO 来说很好 - 下次您发布问题时,请注意“回答您自己的问题”复选框。但是,当然,该问题仍然像任何其他问题一样对待,其他人也可以发布答案。(而且你不会因为接受自己的答案而获得任何代表)
2赞 Andre Kampling 9/7/2017
@savram:以这种方式分享知识是绝对可以的。请看这里:自我回答
3赞 M.M 8/21/2019
到目前为止,这两个答案都没有提到导致未定义行为的事实printf("%u\n", x - y);
2赞 0andriy 2/25/2020
很好的例子是列表。~((u8)(1 << 7))

答:

193赞 Lundin 9/6/2017 #1

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

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

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

通常,你会看到程序员说“只需转换为 x 类型就可以工作了”的场景——但他们不知道为什么。或者,这些错误表现为罕见的间歇性现象,这些现象从看似简单明了的代码中出现。隐式提升在执行位操作的代码中特别麻烦,因为 C 语言中的大多数位运算符在给定有符号操作数时都带有定义不明确的行为。


整数类型和转换排名

C 中的整数类型有 、 、 、 和 。
在类型升级时,/ 也被视为整数类型。
charshortintlonglong longenum_Boolbool

所有整数都有一个指定的转换秩。C11 6.3.1.1,强调我最重要的部分:

每个整数类型都有一个整数转换秩,定义如下:
— 没有两个有符号整数类型具有相同的秩,即使它们具有相同的表示形式。
— 有符号整数类型的秩应大于任何有符号整数类型的秩,但精度较低。
— long long int 的秩应大于 long int 的秩,long int 的秩应大于 int 的秩,int 的秩应大于 short int 的秩,short int 的秩应大于有符号字符秩。
— 任何无符号整数类型的秩应等于相应有符号整数类型的秩, 如果有的话。

— 任何标准整数类型的秩应大于具有相同宽度的任何扩展整数类型的秩。
— char 的秩应等于有符号 char 和无符号 char 的秩。 — _Bool 的秩应小于所有其他标准整数类型的秩。

— 任何枚举类型的秩应等于兼容整数类型的秩(见 6.7.2.2)。

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

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

以下内容可以在表达式中使用,只要 或 用于:intunsigned int

— 具有整数类型(除 或 外)且整数转换秩小于或等于 和 的列的对象或表达式。intunsigned intintunsigned int

这个有点隐晦的文本在实践中的意思是,和(以及 等)是“小整数类型”。这些以特殊方式处理,并受到隐性促销的影响,如下所述。_Boolcharshortint8_tuint8_t


整数促销

每当在表达式中使用小整数类型时,都会将其隐式转换为始终带符号的整数类型。这称为整数升级或整数升级规则int

从形式上讲,该规则说(C11 6.3.1.1):

如果 可以表示原始类型的所有值(对于位域,受宽度限制),则该值将转换为 ;否则,它将转换为 .这些称为整数升级intintunsigned int

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

此文本经常被误解为:“所有小的有符号整数类型都转换为有符号整数类型,所有小的无符号整数类型都转换为无符号整数”。这是不正确的。这里的无符号部分仅意味着,如果我们有一个操作数,并且恰好与给定系统上的大小相同,那么操作数将转换为 。就像这样,没有什么值得注意的事情真正发生。但是,如果类型小于 ,它总是转换为 (signed) ,无论短号是有符号还是未签名unsigned shortintshortunsigned shortunsigned intshortintint

整数提升造成的严酷现实意味着,在 C 语言中,几乎无法对像 或 这样的小类型进行任何操作。操作始终在或更大的类型上执行。charshortint

这听起来像是无稽之谈,但幸运的是,编译器可以优化代码。例如,包含两个操作数的表达式将把操作数提升为 ,并将操作作为 执行。但是编译器可以优化表达式,以实际作为 8 位操作执行,正如预期的那样。然而,问题来了:编译器不允许优化整数提升导致的隐式符号更改,因为编译器无法判断程序员是故意依赖隐式提升发生的,还是无意的。unsigned charintint

这就是问题中的示例 1 失败的原因。两个无符号的字符操作数都被提升为类型,操作是在类型上执行的,并且 的结果为 类型。这意味着我们得到了,而不是预期的。编译器可能会生成使用 8 位指令而不是 执行代码的机器代码,但它可能不会优化符号性的变化。这意味着我们最终会得到一个负结果,这反过来又会导致调用时出现一个奇怪的数字。示例 1 可以通过将操作结果转换回类型来修复。intintx - yint-1255intprintf("%uunsigned char

除了少数特殊情况(如 and 运算符)外,整数提升适用于 C 语言中的几乎所有运算,无论是否使用一元、二进制(或三元)运算符。++sizeof


通常的算术转换

每当在 C 语言中完成二进制运算(具有 2 个操作数的运算)时,运算符的两个操作数必须属于同一类型。因此,如果操作数是不同类型的,C 强制将一个操作数隐式转换为另一个操作数的类型。如何做到这一点的规则被命名为通常的算术转换(有时非正式地称为“平衡”)。C11 6.3.18 中指定了这些内容:

(将此规则视为一个长而嵌套的语句,:)可能更容易阅读)if-else if

6.3.1.8 常用算术转换

许多期望算术类型操作数的运算符会导致转换和产生结果 以类似的方式键入。目的是确定操作数的通用实数类型 和结果。对于指定的操作数,将转换每个操作数,而不更改类型 domain,设置为其对应的实数类型为普通实数类型的类型。除非 另有明确说明,常见的实数类型也是对应的实数类型 结果,其类型域是操作数的类型域(如果它们相同), 否则就很复杂。这种模式称为通常的算术转换

  • 首先,如果任一操作数的对应实数类型为 ,则在不更改类型域的情况下,将另一个操作数转换为对应实数类型为 的类型。long doublelong double
  • 否则,如果任一操作数的对应实数类型为 ,则在不更改类型域的情况下,将另一个操作数转换为其对应实数类型为 的类型。doubledouble
  • 否则,如果任一操作数的对应实数类型为 ,则在不更改类型域的情况下,将另一个操作数转换为其对应实数类型为 float 的类型。float
  • 否则,将对两个操作数执行整数升级。然后 以下规则应用于提升的操作数:
  • 如果两个操作数具有相同的类型,则不需要进一步转换。
  • 否则,如果两个操作数都具有有符号整数类型,或者两个操作数都具有无符号 整数类型,则整数转换等级类型的操作数为 转换为具有更高等级的操作数类型。
  • 否则,如果具有无符号整数类型的操作数的秩大于 或 等于另一个操作数类型的秩,则操作数与 有符号整数类型转换为操作数的类型,无符号 整数类型。
  • 否则,如果具有符号整数类型的操作数的类型可以表示 具有无符号整数类型的操作数类型的所有值,则 具有无符号整数类型的操作数将转换为 具有有符号整数类型的操作数。
  • 否则,两个操作数都将转换为无符号整数类型 对应于具有 signed Integer 类型的操作数的类型。

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

这就是为什么在示例 2 中给出奇怪结果的原因。两个操作数都是整数,并且它们至少是秩,因此整数提升不适用。操作数不是同一类型 - 是 和 是 。因此,运算符暂时转换为类型 。在此转换过程中,它会丢失符号信息并最终成为大值。a + bintaunsigned intbsigned intbunsigned int

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

值得注意的是,C++应用了几乎相同的规则。

评论

1赞 jfs 11/17/2017
“每当在表达式中使用小整数类型时,它都会隐式转换为始终带符号的 int。”您能否指出标准中应该发生的确切位置?C11 6.3.1.1 引用说它是如何发生的(如果它发生),但它并没有说它必须发生,例如,为什么在问题中表现得像而不是 goo.gl/nCvJy5。标准在哪里说如果 is char 那么 is (或 unsigned)?在 c++ 中,它是 §5.3.1.7 goo.gl/FkEakXx - y(unsigned)(int)((int)x - (int)y)(unsigned)(int)((Uchar)((Uchar)x - (Uchar)y))x+xint
2赞 Lundin 11/17/2017
@jfs “否则,......”(如果两个操作数都不是浮点型)"...整数提升在两个操作数上执行。然后,“如果两个操作数具有相同的类型,则不需要进一步转换。
1赞 Erik Eidt 1/5/2019
“示例 1 可以通过将一个或两个操作数转换为键入 unsigned int 来修复。”建议的施法不会像 OP 预期的那样产生 255 个。正确的解决方法是将减法的结果转换回操作数开始的结果,如: 这将给 OP 预期的 255。人们通常无法欣赏转换为较小的尺寸,但是,这是完成截断的正确方法(随后将隐式/自动有符号或零扩展到 ~int 大小)。(unsigned char)(unsigned char) (x-y)
1赞 Ian Abbott 9/2/2021
@Lundin stdint.h 定义的类型可以是扩展整数类型。见C17/C18脚注265、C11脚注261或C99脚注218:“其中一些类型可能表示实现定义的扩展整数类型。任何此类类型的秩都低于相同宽度的标准整数类型。(我想到的一个例子 - 如果标准有符号类型是 one-complement,但有一个特殊的 twos-complement 关键字要定义,等等)int32_t
1赞 Lundin 10/31/2022
@JasonS 在适用的每个运算符的规范文本中都表示,例如 6.5.7 “对每个操作数执行整数提升”或 6.5.5 “对操作数执行通常的算术转换”。
10赞 Lusha Li 6/28/2018 #2

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

示例 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

如何解决?

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

将其中一个更改为 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);
}

由于它们都是无符号整数,因此不需要整数升级。通过通常的算术 converison(具有相同的类型),(无符号 int)x-(无符号 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 = 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);
}

由于它们都是整数,因此不需要整数提升。通过通常的算术转换,我们得到 (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

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

评论

2赞 Cheshar 6/6/2019
对上述示例 2 的修复调用了 UB。结果类型 a+b 为无符号,并且计算值超出有符号整数的范围。signed int c = a+b;
2赞 M.M 8/21/2019
@Cheshar范围外分配不是 UB
2赞 M.M 8/21/2019
本答案中的许多示例都因使用错误的格式说明符而导致 UB,并且它对int
1赞 Cheshar 8/23/2019
@M.M 我的坏!同意,它应该是“实现定义的或实现定义的信号”。不过,签名溢出是 UB。更容易失去对UB / IB的跟踪。
1赞 supercat 9/8/2019
@Cheshar:与一些编译器维护者散布的神话相反,该标准的术语是“未定义的行为”,这些行为应该被 99.9% 的实现以相同的方式处理,但在不切实际的情况下不需要由实现进行有意义的处理。术语 IDB 仅用于所有实现都应该有意义地处理的操作。
4赞 Gabriel Staples 6/17/2022 #3

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

我想尝试一下总结一下规则,以便我可以快速参考它们。我已经完全研究了这个问题和这里的另外两个答案,包括@Lundin的主要答案。如果您想要以下以外的更多示例,请继续详细研究该答案,同时参考下面的“规则”和“晋升流程”摘要。

我还在这里编写了自己的示例和演示代码:integer_promotion_overflow_underflow_undefined_behavior.c

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

整数和变量升级快速参考指南和摘要

3 条简单规则

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

示例:使用以下代码:

uint8_t x = 0;
uint8_t y = 1;

...如果你这样做,他们首先被隐式提升到(这是在我的 64 位上 system),你最终会得到这个:,这会产生一个带有 value 的类型,而不是一个 value 的类型。要获得所需的结果,请通过执行以下操作手动将结果转换回 : 。x - yintint32_t(int)x - (int)yint-1uint8_t255255uint8_t(uint8_t)(x - y)

晋升流程

促销规则如下。从最小类型到最大类型的晋升如下。
将“-->”读作“晋升为”。

方括号中的类型(例如:)是典型的 64 位 Unix(Linux 或 Mac)体系结构上给定标准类型的典型“固定宽度整数类型”。例如,请参阅:[int8_t]

  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. 更好的是,通过运行我的代码 here!: stdint_sizes.c 从我的 eRCaGuy_hello_world 存储库中在您的机器上亲自测试它

1. 适用于 64 位 x86-64 架构 CPU 中的整数类型

注意:“小类型”=()、、、、。bool_Boolchar [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]

指针(例如:)都是 64 位,所以我想它们属于上述类别。void*size_tuint64_t

2. 对于浮点类型

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

另请参阅

  1. https://cppinsights.io/ - 一个非常有用的工具,它可以将C++代码扩展为编译器所看到的内容,包括在编译器中应用所有自动隐式类型提升规则之后。
    1. 例如:请在此处 CPPInsights.io 我的答案中查看我的代码:https://cppinsights.io/s/bfc425f6 -->然后单击播放按钮将其转换并扩展为编译器看到的内容,包括在应用所有自动隐式类型升级规则之后。

我使用这些规则的地方

  1. 如何安全有效地找到 abs((int)num1 - (int)num2)
1赞 Jason S 10/30/2022 #4

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

我正在使用 N1256 草案,因为我无法访问 C 标准的付费副本。

第一:(规范性)

6.3.1.1 对整数提升的定义并不是实际进行整数提升的触发子句。实际上,它是 6.3.1.8 通常的算术转换。

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

[浮点类型的子句在前]

否则,将对两个操作数执行整数升级。然后 以下规则应用于提升的操作数:

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

第二:(非规范性)

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

示例 2:在执行片段时

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

“整数提升”要求抽象机器将每个变量的值提升为大小 然后将两个 s 相加并截断总和。前提是可以不用 溢出,或者用溢出静默包装来产生正确的结果,实际执行只需要 产生相同的结果,可能会省略促销。intintchar

enter image description here

1赞 Stand with Gaza 7/19/2023 #5

在这个答案中,我将讨论可用于跟踪的编译器标志 关闭与隐式类型提升相关的错误,因为我刚刚遇到了 这个“功能”。在下面的错误代码片段中是 类型:expuint32_t

for (int32_t i = 22; i >= MAX(22 - exp + 1, 0); i--) {
    ...
}

如果 < 23 代码工作正常,则 if = 23 循环将永远运行,如果 > 23 循环永远不会运行。解决方法是将第一个参数更改为 。为了更容易发现此类错误,我 建议打开警告。它包含在 中,对于日常使用来说可能有点重。expexpexpMAX22 - (int32_t)exp + 1-Wsign-compare-Wextra

另一个示例中的错误;

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

被捕获,也包含在 中。在我的 自己的代码库:这个标志会产生大约 40 个警告,所有这些警告都是 完全良性,不值得费心去修复。-Wsign-conversion-Wextra

不幸的是,gcc 和 clang 都没有标记警告 “可疑”类型的促销,但留下安全的促销(例如)。for (int i = 0; i < strlen(s); i++)

你可能想阅读好友不要让好友使用 “-W”代表(知情)意见 何时以及何时不使用编译器的警告标志。