隐式类型促销规则

Lun*_*din 50 c type-conversion implicit-conversion

本文旨在用作关于C中隐式整数提升的常见问题解答,特别是由通常的算术转换和/或整数提升引起的隐式提升.

示例1)
为什么这会给出一个奇怪的大整数而不是255?

unsigned char x = 0;
unsigned char y = 1;
printf("%u\n", x - y); 
Run Code Online (Sandbox Code Playgroud)

例2)
为什么这会给"-1大于0"?

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

示例3)
为什么更改上例中的类型来short解决问题?

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

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

Lun*_*din 61

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

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

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

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


整数类型和转换排名

在C中的整数类型是char,short,int,long,long longenum.
_Bool/ bool类型促销时,/ 也被视为整数类型.

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

每个整数类型都有一个整数转换等级,定义如下:
- 没有两个有符号整数类型具有相同的等级,即使它们具有相同的表示.
- 有符号整数类型的等级应大于精度较低的任何有符号整数类型的等级.
- 等级long long int应大于等级long int,等级应大于等级int,等级应大于等级short int,等级应大于等级signed char.
- 任何无符号整数类型的等级应等于相应的有符号整数类型的等级(如果有的话).

- 任何标准整数类型的等级应大于具有相同宽度的任何扩展整数类型的等级.
- char的等级应等于signed char和unsigned char的等级.
- _Bool的等级应小于所有其他标准整数类型的等级.
- 任何枚举类型的等级应等于兼容整数类型的等级(见6.7.2.2).

这里的类型也可以stdint.h排序,与它们在给定系统上碰巧对应的任何类型具有相同的等级.例如,int32_t具有与int32位系统相同的等级.

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

以下内容可用于任何地方intunsigned int可能使用的表达式:

-一个对象或表达一个整型(除intunsigned int),其整数转换秩小于或等于的秩intunsigned int.

什么这个晦涩的文字在实践中意味着,是_Bool,charshort(也int8_t,uint8_t等等)是"小整数类型".如下所述,这些以特殊方式处理并受到隐含促销.


整数促销

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

在形式上,规则说(C11 6.3.1.1):

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

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

引起的整数优惠严酷的现实意味着,在C几乎没有操作可在小的类型等来进行intchar.操作总是在short更大的类型上进行.

这可能听起来像废话,但幸运的是,编译器可以优化代码.例如,包含两个int操作数的表达式将获得提升的操作数,unsigned char并将操作执行为int.但是,允许编译器优化表达式以实际执行为8位操作,如预期的那样.然而,这里来了问题:编译器容许优化出引起整数推广符号性的隐含变化.因为编译器无法判断程序员是否故意依赖隐式促销发生,或者它是无意的.

这就是问题中的示例1失败的原因.两个unsigned char操作数都被提升为类型int,操作在类型上执行int,结果int是类型x - y.这意味着我们得到的int不是-1可能的预期.编译器可以生成使用8位指令而不是执行代码的机器代码255,但是它可能不会优化签名的更改.这意味着我们最终会得到一个否定结果,这反过来会在int调用时产生一个奇怪的数字.可以通过将操作结果转换回类型来修复示例1 printf("%u.

除了少数特殊情况下,如异常unsigned char++运营商来说,整数促销应用在C几乎所有的操作,如果使用不管一元,二元(或三元)运算符.


通常的算术转换

每当在C中完成二进制操作(具有2个操作数的操作)时,操作符的两个操作数必须是相同的类型.因此,在操作数具有不同类型的情况下,C强制将一个操作数隐式转换为另一个操作数的类型.如何做到这一点的规则被称为通常的艺术转换(有时非正式地称为"平衡").这些在C11 6.3.18中规定:

(将此规则视为一个冗长的嵌套sizeof语句,它可能更容易阅读:))

6.3.1.8通常的算术转换

许多期望算术类型操作数的运算符会以类似的方式导致转换并产生结果类型.目的是确定操作数和结果的通用实数类型.对于指定的操作数,每个操作数在不更改类型域的情况下转换为其对应的实类型是公共实类型的类型.除非另有明确说明,否则公共实类型也是结果的对应实数类型,如果它们相同则其类型域是操作数的类型域,否则是复数.这种模式称为通常的算术转换:

  • 首先,如果任一操作数的相应实数类型是if-else if,则另一个操作数在不改变类型域的情况下被转换为其对应的实数类型的类型long double.
  • 否则,如果任一操作数的相应实数类型是long double,则另一个操作数在不改变类型域的情况下被转换为其对应的实数类型的类型double.
  • 否则,如果任一操作数的相应实数类型是double,则另一个操作数在不更改类型域的情况下转换为对应的实类型为float的类型.
  • 否则,将对两个操作数执行整数提升.然后将以下规则应用于提升的操作数:

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

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

这就是为什么int在例子2中给出一个奇怪的结果的原因.两个操作数都是整数,它们至少是排名a + b,因此整数提升不适用.操作数不是同一类型 - intaunsigned intb.因此,操作员signed int暂时转换为类型b.在此转换过程中,它会丢失符号信息并最终成为一个较大的值.

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

  • @jfs“否则,...”(如果两个操作数都不是浮点类型)“...对两个操作数都执行整数提升。” 。然后“如果两个操作数具有相同的类型,则不需要进一步转换。” (2认同)
  • “示例 1 可以通过将一个或两个操作数转换为 unsigned int 类型来修复。” 建议的演员阵容不会像OP预期的那样产生255。正确的解决方法是将减法的结果转换回操作数开始的“(unsigned char)”,如“(unsigned char) (xy)”:这将为OP提供预期的255。人们经常失败然而,为了欣赏转换为较小的大小,这是完成截断的正确方法(随后将隐式/自动签名或零扩展到 ~int 大小)。 (2认同)

Lus*_* Li 6

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

示例 1)

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

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

上面代码的输出:(和我们预期的一样)

4294967295
-1
Run Code Online (Sandbox Code Playgroud)

如何解决?

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

将其中之一更改为 unsigned int

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

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

上面代码的输出:(和我们预期的一样):

4294967295
-1
Run Code Online (Sandbox Code Playgroud)

同样,下面的代码得到相同的结果:

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

将它们都更改为 unsigned int

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

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

上面代码的输出:(和我们预期的一样):

4294967295
-1
Run Code Online (Sandbox Code Playgroud)

修复代码的可能方法之一:(最后添加类型转换)

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);
}
Run Code Online (Sandbox Code Playgroud)

上述代码的输出:

4294967295
-1
255
Run Code Online (Sandbox Code Playgroud)

例 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);
}
Run Code Online (Sandbox Code Playgroud)

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

上面代码的输出:(和我们预期的一样)

-1 is larger than 0
4294967295
Run Code Online (Sandbox Code Playgroud)

如何解决?

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);
}
Run Code Online (Sandbox Code Playgroud)

上述代码的输出:

-1 is smaller than 0
-1
Run Code Online (Sandbox Code Playgroud)

例 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);
}
Run Code Online (Sandbox Code Playgroud)

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

上述代码的输出:

-1 is smaller than 0
-1
Run Code Online (Sandbox Code Playgroud)

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

  • 您对上述示例 2 `signed int c = a+b;` 的修复调用了 UB。a+b 的结果类型是无符号的,并且计算值超出了有符号整数的范围。 (2认同)
  • @Cheshar 超出范围的赋值不是 UB (2认同)
  • 这个答案中的许多例子通过使用错误的格式说明符而导致 UB,并且它还对“int”的大小做出了无根据的假设 (2认同)