隐式类型提升规则

136

本文旨在作为关于C语言中隐式整型提升的常见问题解答,特别是由于通常算术转换和/或整型提升导致的隐式提升。

示例1)
为什么这个表达式的结果是一个奇怪的大整数而不是255?

unsigned char x = 0;
unsigned char y = 1;
printf("%u\n", x - y); 

例子2)
为什么这会输出“-1比0大”?


为什么这会输出“-1比0大”?

unsigned int a = 1;
signed int b = -2;
if(a + b > 0)
  puts("-1 is larger than 0");

例子3)
为什么将上面的类型更改为short可以解决问题?


为什么将上面的类型更改为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位计算机。)


4
我建议记录示例中的假设,例如,示例3假设“short”比“int”更窄(或者换句话说,它假设“int”可以表示“unsigned short”的所有值)。 - Ian Abbott
6
是的,意图是撰写一篇常见问题解答条目。在 Stack Overflow 上以这种方式分享知识是可以的 - 下次发布问题时,请注意选中“回答你自己的问题”复选框。但当然,该问题仍像其他任何问题一样处理,其他人也可以发布答案。(并且您不会因接受自己的答案而获得任何声望) - Lundin
2
@savram:以这种方式分享知识是完全可以的。请参见此处:自我回答 - Andre Kampling
3
迄今为止,没有任何答案提到 printf("%u\n", x - y); 会导致未定义行为。 - M.M
2
请将 ~((u8)(1 << 7)) 添加到列表中,这是一个不错的例子。 - 0andriy
显示剩余3条评论
5个回答

191
C语言设计的初衷是在表达式中隐式且静默地改变操作数的整数类型。在某些情况下,语言强制编译器要么将操作数改变为更大的类型,要么改变它们的符号。
这样做的原因是为了防止算术运算中的意外溢出,同时也允许具有不同符号的操作数在同一表达式中共存。
不幸的是,隐式类型提升的规则带来的伤害远远超过好处,以至于它们可能是C语言中最大的缺陷之一。这些规则通常甚至不为普通的C程序员所知,因此会导致各种非常微妙的错误。
通常情况下,你会看到程序员说“只需将其转换为类型x,它就能工作”——但他们不知道为什么。或者这些错误表现为罕见的、间歇性的现象,从看似简单和直接的代码中产生。隐式提升在进行位操作的代码中尤其麻烦,因为C语言中的大多数位运算符在给定有符号操作数时具有定义不清的行为。
整数类型和转换等级
在C语言中,整数类型包括char、short、int、long、long long和enum。当涉及到类型提升时,_Bool/bool也被视为整数类型。
所有整数类型都有指定的转换等级。根据C11 6.3.1.1,以下是最重要的部分(重点标注为mine):
每个整数类型都有一个整数转换等级,定义如下: - 即使具有相同的表示形式,两个有符号整数类型的等级也不能相同。 - 有符号整数类型的等级必须大于具有较低精度的任何有符号整数类型的等级。 - long long int 的等级必须大于 long int 的等级,long int 的等级必须大于 int 的等级,int 的等级必须大于 short int 的等级,short int 的等级必须大于 signed char 的等级。 - 任何无符号整数类型的等级必须等于相应的有符号整数类型的等级(如果有的话)。 - 任何标准整数类型的等级必须大于具有相同宽度的任何扩展整数类型的等级。 - char 的等级必须等于 signed char 和 unsigned char 的等级。 - _Bool 的等级必须小于所有其他标准整数类型的等级。 - 任何枚举类型的等级必须等于兼容的整数类型的等级(参见6.7.2.2)。

这里也包括了stdint.h中的类型,它们与给定系统上对应的类型具有相同的等级。例如,在32位系统上,int32_tint具有相同的等级。

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

以下类型可以在表达式中使用,无论何时都可以使用intunsigned int

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

这段有些晦涩的文字在实践中的意思是,_Boolcharshort(以及int8_tuint8_t等)是"小整数类型"。它们以特殊方式处理,并受到隐式提升的影响,如下所述。


整数提升

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

正式地说,规则如下(C11 6.3.1.1):

如果int类型可以表示原始类型的所有值(受位域的宽度限制),则将值转换为int;否则,将其转换为unsigned int。这被称为整数提升

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

这段文字经常被误解为:“所有小的有符号整数类型都被转换为有符号整数,所有小的无符号整数类型都被转换为无符号整数”。这是不正确的。这里的无符号部分只是意味着,如果我们有一个例如unsigned short的操作数,并且int恰好在给定系统上与short具有相同的大小,那么unsigned short操作数将被转换为unsigned int。也就是说,没有什么特别的事情发生。但是,如果short是比int更小的类型,它总是被转换为(有符号的)int,而不管short是有符号还是无符号!
整数提升带来的严酷现实意味着,在C语言中几乎不能对像charshort这样的小类型进行任何操作。操作总是在int或更大的类型上进行。
这可能听起来像胡言乱语,但幸运的是编译器可以对代码进行优化。例如,包含两个unsigned char操作数的表达式会将操作数提升为int,并进行int操作。但编译器可以优化表达式,使其实际上作为一个8位操作执行,这是可以预期的。然而,问题来了:编译器不允许优化由整数提升引起的隐式符号更改,因为编译器无法判断程序员是有意依赖隐式提升,还是无意之间发生了这种情况。
这就是为什么问题中的示例1失败的原因。两个无符号字符操作数都被提升为int类型,操作是在int类型上进行的,x - y的结果是int类型。这意味着我们得到的是-1而不是可能期望的255。编译器可能会生成使用8位指令执行代码的机器码,而不是int,但它可能不会优化掉符号的改变。这意味着我们最终得到一个负数的结果,当调用printf("%u时,会得到一个奇怪的数字。通过将操作的结果强制转换回unsigned char类型,可以修复示例1。
除了一些特殊情况,如++sizeof运算符,整数提升适用于C中几乎所有的操作,无论是一元、二元(或三元)运算符。

通常的算术转换

在C语言中,每当进行二元操作(即涉及两个操作数的操作)时,操作符的两个操作数必须是相同的类型。因此,如果操作数的类型不同,C语言会强制将其中一个操作数隐式转换为另一个操作数的类型。这种转换的规则被称为通常的算术转换(有时非正式地称为"平衡")。这些规则在C11 6.3.18中进行了详细说明:

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

6.3.1.8 通常的算术转换

许多期望算术类型操作数的运算符会进行转换,并以类似的方式产生结果类型。其目的是确定操作数和结果的共同实际类型。对于指定的操作数,每个操作数都会被转换为一个类型,其对应的实际类型是共同的实际类型,而不改变类型域。除非另有明确说明,否则共同的实际类型也是结果的对应实际类型,其类型域与操作数相同,如果它们相同,则为复数。这种模式被称为“通常的算术转换”。
首先,如果任一操作数的对应实际类型为long double,则另一个操作数会被转换为一个类型,其对应的实际类型也是long double,而不改变类型域。
否则,如果任一操作数的相应实际类型是double,则另一个操作数将被转换为相应实际类型为double的类型,而不改变类型域。
否则,如果任一操作数的相应实际类型是float,则另一个操作数将被转换为相应实际类型为float的类型,而不改变类型域。
否则,对两个操作数执行整数提升。然后,对提升后的操作数应用以下规则:
  • 如果两个操作数具有相同的类型,则不需要进一步转换。
否则,如果两个操作数都是有符号整数类型或都是无符号整数类型,则具有较低整数转换等级的操作数将转换为具有较高等级的操作数的类型。
否则,如果具有无符号整数类型的操作数的等级大于或等于其他操作数类型的等级,则具有有符号整数类型的操作数将转换为具有无符号整数类型的操作数的类型。
否则,如果具有有符号整数类型的操作数的类型能够表示无符号整数类型的所有值,则具有无符号整数类型的操作数将转换为具有有符号整数类型的操作数的类型。
否则,两个操作数将转换为与具有有符号整数类型的操作数的类型相对应的无符号整数类型。
值得注意的是,通常的算术转换适用于浮点数和整数变量。在整数的情况下,我们还可以注意到整数提升是从通常的算术转换中调用的。在此之后,当两个操作数至少具有int的等级时,运算符将平衡到相同的类型,并具有相同的符号。
这就是为什么在示例2中,a + b会得到一个奇怪的结果的原因。两个操作数都是整数,并且它们至少是int级别,因此不会进行整数提升。这两个操作数的类型不同 - aunsigned int,而bsigned int。因此,操作数b会被临时转换为unsigned int类型。在这个转换过程中,它失去了符号信息,最终变成了一个很大的值。
在示例3中,将类型更改为short可以解决这个问题的原因是short是一个小的整数类型。这意味着两个操作数都会被整数提升为int类型,而int是有符号的。在整数提升之后,两个操作数具有相同的类型(int),不需要进一步的转换。然后,操作可以按预期在有符号类型上执行。
值得注意的是,C++应用的规则几乎完全相同。

2
@jfs “否则,…”(如果两个操作数均不是浮点类型)“…会对两个操作数执行整数提升。” 然后接着说:“如果两个操作数具有相同的类型,则无需进一步转换。” - Lundin
1
示例1可以通过将一个或两个操作数强制转换为无符号整数类型来修复。建议的强制转换不会产生OP期望的255。正确的修复方法是将减法的结果强制转换回操作数最初的(unsigned char),如(unsigned char)(x-y):这将给OP期望的255。然而,人们经常忽视向较小的大小进行强制转换,但这是实现截断的正确方法(随后将跟随隐式/自动有符号或零扩展到~int大小)。 - Erik Eidt
2
@学生 现在我明白了,解释的期望确实与提出的解决方案不符。已更新,谢谢。 - Lundin
1
@Lundin stdint.h 定义的类型可能是扩展整数类型。请参阅 C17/C18 脚注 265、C11 脚注 261 或 C99 脚注 218:“这些类型中的一些可能表示实现定义的扩展整数类型。”任何此类类型都将比相同宽度的标准整数类型具有较低的等级。(一个跳入脑海的例子-如果标准的有符号类型是一补数,但有一个特殊的二补数关键字来定义 int32_t 等)。 - Ian Abbott
1
@JasonS 在每个运算符适用的规范文本中都有说明,例如6.5.7“对每个操作数执行整数提升”或6.5.5“对操作数执行通常的算术转换”。 - Lundin
显示剩余20条评论

10
根据之前的帖子,我想提供更多有关每个示例的信息。 示例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);
}

由于它们都是无符号整数,因此不需要整数提升。根据通常的算术转换(具有相同类型),(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 = 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
你对上面例子2的修复signed int c = a+b;引发了未定义行为。a+b的结果类型是无符号的,计算出的值超出了有符号整数的范围。 - Cheshar
2
@Cheshar 超出范围的赋值不是未定义行为。 - M.M
2
这个答案中的许多示例都使用了错误的格式说明符,从而导致了未定义行为,并且还对int类型的大小做出了不必要的假设。 - M.M
1
@M.M 对不起!同意,应该是“实现定义或引发实现定义的信号”。有符号溢出是未定义行为。但是很容易失去对UB / IB的追踪。 - Cheshar
1
@Cheshar: 与某些编译器维护者散布的神话相反,标准术语中指的是由99.9%实现应该以相同方式处理但不需要在实现中有意义处理的操作为“未定义行为”。术语IDB仅用于所有实现都应该有意义处理的操作。 - supercat
I tried what the previous post recommended, but it doesn't really work. Here is the code based on the previous post: change one of them to unsigned int. No, the main answer says: Example 1 could be fixed by casting the result of the operation back to type unsigned char. So, they mean to do this, which does work to provide the expected result of 255!: printf("%u\n", (unsigned char)(x - y)); // Output: 255 - Gabriel Staples

4
整数和浮点数在C和C++中的等级和提升规则

我想试着概括一下这些规则,以便我可以快速参考它们。我已经完全研究了问题和其他两个答案,包括 @Lundin 的主要答案。如果你想要更多例子,超出以下的例子,请也详细研究那个答案,并参考我的"规则"和"提升流程"概述。

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

尽管我通常非常啰嗦,但我会尽量保持这篇简短摘要,因为其他两个答案加上我的测试代码已经有足够详细的内容。

整数和变量提升快速参考指南和概述

3个简单规则

对于涉及多个操作数(输入变量)的任何操作(例如:数学运算、比较或三元运算),在执行操作之前,变量会根据需要自动隐式提升为所需的变量类型。
因此,如果您不希望输出类型被隐式选择,您必须手动、明确地将其转换为所需的类型。请参考下面的示例。
所有小于int(在我的64位Linux系统上为int32_t)的类型都被称为“小型类型”。它们不能用于任何操作。因此,如果所有输入变量都是“小型类型”,在执行操作之前,它们都会首先自动隐式提升为int(在我的64位Linux系统上为int32_t)。
否则,如果至少一个输入类型是int或更大,则其他较小的输入类型会自动隐式提升为最大输入类型的类型。
示例:
代码示例:
uint8_t x = 0;
uint8_t y = 1;

如果你执行`x - y`,它们首先会被隐式提升为`int`类型(在我的64位系统上是`int32_t`),结果就变成了`(int)x - (int)y`,这将得到一个值为`-1`的`int`类型,而不是一个值为`255`的`uint8_t`类型。要获得期望的`255`结果,需要手动将结果强制转换回`uint8_t`,可以这样做:`(uint8_t)(x - y)`。

提升流程

提升规则如下。从最小到最大类型的提升如下所示。
将"-->"解读为"被提升为"

方括号中的类型(例如:[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,它来自我的eRCaGuy_hello_world仓库。

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

注意:"small types" = 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. 对于浮点类型 float [32位] --> double [64位] --> long double [128位] 另请参阅
  1. https://cppinsights.io/ - 一个非常有用的工具,可以将你的C++代码展开成编译器实际看到的样子,包括应用编译器中所有自动隐式类型提升规则之后的结果。
    1. 例如:在CPPInsights.io上查看我在这里的代码from my answer herehttps://cppinsights.io/s/bfc425f6 --> 然后点击播放按钮,将其转换并展开为编译器实际看到的样子,包括应用所有自动隐式类型提升规则之后的结果。

我使用这些规则的地方

  1. 如何安全高效地找到 abs((int)num1 - (int)num2)

1
在这个回答中,我将讨论你可以使用的编译器标志来追踪与隐式类型提升相关的错误,因为我刚刚遇到了这个"特性"。在下面的有bug的代码片段中,exp的类型是uint32_t
for (int32_t i = 22; i >= MAX(22 - exp + 1, 0); i--) {
    ...
}

如果 exp < 23,则代码正常工作;如果 exp = 23,则循环将永远运行;如果 exp > 23,则循环永远不会运行。修复方法是将第一个参数更改为 MAX,即 22 - (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

-Wsign-conversion捕获,也包含在-Wextra中。在我的代码库中,这个标志产生了大约40个警告,所有这些警告都是完全无害的,不值得费力去修复。

不幸的是,无论是gcc还是clang都没有警告来标记“可疑”的类型提升,但会保留安全的提升(例如for (int i = 0; i < strlen(s); i++))。

您可能想阅读朋友们不要使用“-W”以获取关于何时使用和何时不使用编译器警告标志的(知情的)意见。


1

我想对@Lundin的回答中的示例1进行两点澄清,其中有两个相同整数类型的操作数,但是它们是需要整数提升的“小类型”。

我正在使用N1256 draft,因为我没有付费版的C标准。

首先:(规范性)

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

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

[浮点类型的子句首先处理]
否则,整数提升将在两个操作数上执行。然后将以下规则应用于提升的操作数:
- 如果两个操作数具有相同的类型,则不需要进一步转换。 - 否则,如果两个操作数都具有带符号整数类型或都具有无符号整数类型,则具有较小整数转换等级的类型的操作数将转换为具有更高等级的操作数的类型。 - 否则,如果具有无符号整数类型的操作数的等级大于或等于其他操作数类型的等级,则具有带符号整数类型的操作数将转换为具有无符号整数类型的操作数的类型。 - 否则,如果具有带符号整数类型的操作数的类型可以表示无符号整数类型的所有值,则具有无符号整数类型的操作数将转换为具有带符号整数类型的操作数的类型。 - 否则,两个操作数都将转换为与具有带符号整数类型的操作数的类型相对应的无符号整数类型。
第二点:(非规范性)
标准引用了一个明确的示例来证明这一点。

EXAMPLE 2 In executing the fragment

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

the "integer promotions" require that the abstract machine promote the value of each variable to int size and then add the two ints and truncate the sum. Provided the addition of two chars can be done without overflow, or with overflow wrapping silently to produce the correct result, the actual execution need only produce the same result, possibly omitting the promotions.

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接