隐式类型促销规则

时间:2017-09-06 10:50:25

标签: c type-conversion implicit-conversion

这篇文章旨在用作关于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位或短16位的32位或64位计算机。)

2 个答案:

答案 0 :(得分:36)

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

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

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

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

整数类型和转化次数

C中的整数类型包括charshortintlonglong longenum
类型促销时,_Bool / bool也被视为整数类型。

所有整数都有指定的转化次数。 C11 6.3.1.1,强调我最重要的部分:

  

每个整数类型都有一个整数转换等级,定义如下:
   - 没有两个有符号整数类型具有相同的等级,即使它们具有相同的表示    - 有符号整数类型的等级应大于精度较低的任何有符号整数类型的等级    - long long int的等级应大于long int的等级,该等级应大于int的等级,该等级应大于{{1}的等级},大于short int的等级    - 任何无符号整数类型的等级应等于相应有符号整数类型的等级(如果有)。
   - 任何标准整数类型的等级应大于具有相同宽度的任何扩展整数类型的等级    - char的等级应等于signed char和unsigned char的等级    - _Bool的等级应小于所有其他标准整数类型的等级    - 任何枚举类型的等级应等于兼容整数类型的等级(见6.7.2.2)。

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

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

  

以下内容可用于intint的任何地方   使用:

     

- 具有整数类型(unsigned intint除外)的对象或表达式,其整数转换等级小于或等于unsigned int和{{1}的等级}。

这个有点神秘的文字在实践中意味着什么,intunsigned int_Bool(以及charshort等)是& #34;小整数类型"。如下所述,这些是以特殊方式处理并受到隐含促销的。

整数促销

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

正式规则(C11 6.3.1.1):

  

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

这个文本经常被误解为:"所有小的有符号整数类型都转换为signed int,所有小的无符号整数类型都转换为unsigned int"。这是不正确的。此处的未签名部分仅表示如果我们有一个int操作数,并且unsigned int恰好与给定系统上的unsigned short具有相同的大小,那么int操作数转换为short。就像在,没有任何真正发生的事情。但是,如果unsigned short的类型小于unsigned int,则会始终将其转换为(已签名)short,无论短信是签名还是未签名

由整数提升引起的严酷现实意味着在intint等小类型中几乎不能执行C操作。操作始终在char或更大类型上执行。

这可能听起来像废话,但幸运的是,编译器可以优化代码。例如,包含两个short操作数的表达式将操作数提升为int,操作执行为unsigned char。但是,允许编译器优化表达式以实际执行为8位操作,如预期的那样。但是,问题出现了:编译器允许优化由整数提升引起的签名的隐式更改。因为编译器无法判断程序员是否故意依赖隐式促销,或者是无意的。

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

除了printf("%uunsigned char运算符等少数特殊情况之外,无论是否使用一元,二元(或三元)运算符,整数提升都适用于C中的几乎所有运算。

通常的算术转换

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

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

  

6.3.1.8通常的算术转换

     

许多期望算术类型操作数的运算符会导致转换并产生结果   以类似的方式输入类型。目的是确定操作数的通用实数类型   和结果。对于指定的操作数,将转换每个操作数,而不更改类型   域,对应的实类型是常见的实类型。除非   另外明确说明,常见的真实类型也是相应的实际类型   结果,其类型域是操作数的类型域,如果它们是相同的,   否则复杂。此模式称为通常的算术转换

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

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

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

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

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

答案 1 :(得分:3)

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

示例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。 / p>

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

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,因此不需要整数提升。通过通常的算术收敛(具有相同的类型),(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

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