C语言 隐式类型提升规则

vbopmzt1  于 2022-12-17  发布在  其他
关注(0)|答案(4)|浏览(179)
  • 这篇文章是关于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位计算机,其中16位不足。)

3qpi33ja

3qpi33ja1#

C语言被设计成隐式地、无声地改变表达式中使用的操作数的整数类型。在一些情况下,语言强迫编译器要么将操作数改变为更大的类型,要么改变它们的符号。
这样做的基本原理是为了防止算术过程中意外溢出,但也允许不同符号的操作数共存于同一表达式中。
不幸的是,隐式类型提升规则弊大于利,以至于它们可能成为C语言中最大的缺陷之一。这些规则通常甚至不为普通C程序员所知,因此会导致各种非常微妙的bug。
通常你会看到程序员说“只要转换成x类型就行了”--但他们不知道为什么。或者这样的错误表现为罕见的、间歇性的现象,从看似简单明了的代码中突然出现。隐式提升在进行位操作的代码中尤其麻烦,因为C中的大多数位操作符在给定有符号操作数时都有定义不良的行为。

整数类型和转换等级

C语言中的整数类型有charshortintlonglong longenum
在类型升级时,_Bool/bool也被视为整数类型。
所有整数都有一个指定的 * 转换秩 *. C11 6.3.1.1,重点放在最重要的部分:
每个整数类型都有一个整数转换秩,定义如下:

  • 两个有符号整数类型不应具有相同的秩,即使它们具有相同的表示形式。
  • 有符号整数类型的秩应大于精度较低的任何有符号整数类型的秩。

**-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的类型也在这里排序,与它们在给定系统上对应的任何类型具有相同的等级,例如,int32_t在32位系统上与int具有相同的等级。
    此外,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
不管短整型是有符号的还是无符号的 *!
整数提升带来的残酷现实意味着,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 intbsigned int。因此,运算符b被临时转换为类型unsigned int,在此转换过程中,它丢失了符号信息,最终成为一个大值。
在示例3中将类型更改为short可以解决问题,原因是short是一个小整数类型。这意味着两个操作数都是整数,并升级为带符号的int类型。整数升级后,两个操作数具有相同的类型(int),则不需要进一步的转换,然后可以如预期的那样对有符号类型进行操作。

ego6inou

ego6inou2#

根据之前的帖子,我想给予每个例子的更多信息。

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

如何修复

我尝试了前一篇文章推荐的方法,但效果不佳。以下是基于前一篇文章的代码:

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

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

将它们都更改为无符号整型

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

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

fae0ux8s

fae0ux8s3#

C和C++中的整数和浮点秩及提升规则

我想试着总结一下规则,以便快速参考。我已经充分研究了这个问题和其他两个答案,包括the main one by @Lundin。如果你想了解更多的例子,请参考我下面的“规则”和“晋升流程”总结,同时详细研究这个答案。
我也写了我自己的例子和演示代码:integer_promotion_overflow_underflow_undefined_behavior.c .
尽管我自己通常非常冗长,但我将尽量保持这是一个简短的总结,因为其他两个答案加上我的测试代码已经通过它们必要的冗长而有了足够的细节。

整数和变量促销快速参考指南和摘要

3条简单规则

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

示例

示例:使用此代码:

uint8_t x = 0;
uint8_t y = 1;

......如果执行x - y,它们首先会隐式提升为int(在我的64位系统上是int32_t),最后得到以下结果:(int)x - (int)y,这将生成值为-1int类型,而不是值为255uint8_t类型。要获得所需的255结果,请手动将结果强制转换回uint8_t,方法是:(uint8_t)(x - y) .

促销流程

升级规则如下:从 * 最小到最大 * 类型的升级规则如下:

将“-->“读作“升级为”。

方括号中的类型(例如:[int8_t])是典型64位Unix(Linux或Mac)体系结构上给定标准类型的典型"fixed-width integer types"

  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
    1.* * 更好的是,在您的计算机上运行我的代码,亲自测试它!:stdint_sizes.c**来自我的eRCaGuy_hello_world存储库。
1.对于整数类型

注:“小型” = 1米18英寸(1米19英寸)、1米20英寸、1米21英寸、1米22英寸、1米23英寸。

小号:x个1米24英寸1 x个(x个1米25英寸1 x个)、x个1米26英寸1 x个、x个1米27英寸1 x个、x个1米28英寸1 x个、x个1米29英寸1 x个
--〉一米三十寸一寸
--〉一米三一米一x
--〉一米三十二分一秒
--〉x一米33英寸1x
--〉一米三四英寸一英寸
--〉一米三十五英寸一英寸

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

2.对于浮点类型

一米三十九纳一x--〉一米四十纳一x--〉一米四十纳一x

s4chpxco

s4chpxco4#

我想对@Lundin在其他方面的出色回答补充两点说明,关于示例1,其中有两个相同整数类型的操作数,但都是需要整数提升的“小类型”。
我使用的是N1256 draft,因为我没有C标准的付费副本。

第一:(规范性)

6.3.1.1对整数提升的定义并不是实际 * 执行 * 整数提升的触发子句,实际上它是6.3.1.8常用的算术转换。
大多数情况下,当操作数是 * 不同 * 类型时,“通常的算术转换”适用,在这种情况下,至少有一个操作数必须升级。但问题是,对于整数类型,在所有情况下都需要整数升级。
[浮点类型的子句在前]
否则,对两个操作数都执行整数提升。然后,对提升的操作数应用以下规则:

  • 如果两个操作数具有相同的类型,则不需要进一步转换。
  • 否则,如果两个操作数都是有符号整数类型,或者都是无符号整数类型,则将具有较小整数转换秩类型的操作数转换为具有较大秩类型的操作数。
  • 否则,如果具有无符号整数类型的操作数的秩大于或等于另一个操作数的类型的秩,则将具有无符号整数类型的操作数转换为具有无符号整数类型的操作数的类型。
  • 否则,如果具有有符号整数类型的操作数的类型可以表示具有无符号整数类型的操作数的类型的所有值,则将具有无符号整数类型的操作数转换为具有有符号整数类型的操作数的类型。
  • 否则,两个操作数都转换为与具有有符号整数类型的操作数的类型对应的无符号整数类型。
    第二次:(非规范性)

标准引用了一个明确的例子来说明这一点:
示例2在执行片段时

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

“整数提升”要求抽象机将每个变量的值提升到int大小,然后将两个int相加并截断和。假设两个char的相加可以在没有溢出的情况下完成,或者在溢出静默地回绕的情况下完成以产生正确的结果,则实际执行仅需要产生相同的结果,可能省略提升。

相关问题