隐式类型提升规则

Implicit type promotion rules

此 post 旨在用作有关 C 中隐式整数提升的常见问题解答,特别是由通常的算术转换引起的隐式提升 and/or 整数提升。

示例 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 被设计为隐式地和静默地改变表达式中使用的操作数的整数类型。存在几种情况,其中语言强制编译器将操作数更改为更大的类型,或者更改它们的符号。

这背后的基本原理是为了防止在算术运算过程中意外溢出,同时也允许具有不同符号的操作数共存于同一表达式中。

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

通常你会看到程序员说 "just cast to type x and it works" 的场景 - 但他们不知道为什么。或者,此类错误表现为罕见的、间歇性的现象,从看似简单直接的代码中产生。隐式提升在执行位操作的代码中特别麻烦,因为 C 中的大多数位运算符在给定有符号操作数时都会出现定义不明确的行为。


整数类型和转换等级

C中的整数类型有charshortintlonglong longenum
_Bool/bool 在类型提升时也被视为整数类型。

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

Every integer type has an integer conversion rank defined as follows:
— No two signed integer types shall have the same rank, even if they have the same representation.
— The rank of a signed integer type shall be greater than the rank of any signed integer type with less precision.
— The rank of long long int shall be greater than the rank of long int, which shall be greater than the rank of int, which shall be greater than the rank of short int, which shall be greater than the rank of signed char.
— The rank of any unsigned integer type shall equal the rank of the corresponding signed integer type, if any.

— The rank of any standard integer type shall be greater than the rank of any extended integer type with the same width.
— The rank of char shall equal the rank of signed char and unsigned char.
— The rank of _Bool shall be less than the rank of all other standard integer types.
— The rank of any enumerated type shall equal the rank of the compatible integer type (see 6.7.2.2).

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

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

The following may be used in an expression wherever an int or unsigned int may be used:

— An object or expression with an integer type (other than int or unsigned int) whose integer conversion rank is less than or equal to the rank of int and unsigned int.

这个有点神秘的文字在实践中的意思是,_Boolcharshort(以及 int8_tuint8_t 等)是"small integer types"。这些以特殊方式处理并受到隐式提升,如下所述。


整数促销

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

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

If an int can represent all values of the original type (as restricted by the width, for a bit-field), the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions.

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

这段文字经常被误解为:"all small, signed integer types are converted to signed int and all small, unsigned integer types are converted to unsigned int"。这是不正确的。这里的无符号部分仅意味着如果我们有一个 unsigned short 操作数,并且 int 恰好与给定系统上的 short 具有相同的大小,那么 unsigned short操作数转换为 unsigned int。就像,没有什么值得注意的事情真的发生了。但是如果 short 是比 int 更小的类型,它总是被转换为(有符号的)int 不管它是有符号的还是无符号的!

整数提升导致的严酷现实意味着 C 中几乎没有任何操作可以在像 charshort 这样的小类型上执行。操作总是在 int 或更大的类型上执行。

这听起来像是胡说八道,但幸运的是允许编译器优化代码。例如,一个包含两个 unsigned char 操作数的表达式会将操作数提升为 int,并将操作执行为 int。但是编译器被允许优化表达式以实际作为 8 位操作执行,正如预期的那样。然而,问题来了:编译器不允许优化掉由整数提升引起的符号的隐式变化。因为编译器无法判断程序员是有意依赖隐式提升,还是无意。

这就是问题中示例1失败的原因。两个unsigned char操作数都提升为int类型,对int类型进行运算,x - y的结果为int类型。这意味着我们得到 -1 而不是预期的 255 。编译器可能会生成使用 8 位指令而不是 int 执行代码的机器代码,但它可能无法优化符号性的变化。这意味着我们最终得到一个否定的结果,当 printf("%u 被调用时,这又会导致一个奇怪的数字。可以通过将操作结果转换回类型 unsigned char.

来修复示例 1

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


常用算术转换

每当在 C 中进行二元运算(具有 2 个操作数的运算)时,运算符的两个操作数必须属于同一类型。因此,如果操作数的类型不同,C 会强制将一个操作数隐式转换为另一个操作数的类型。完成此操作的规则被命名为 通常的算术转换(有时非正式地称为 "balancing")。这些在 C11 6.3.18 中指定:

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

6.3.1.8 Usual arithmetic conversions

Many operators that expect operands of arithmetic type cause conversions and yield result types in a similar way. The purpose is to determine a common real type for the operands and result. For the specified operands, each operand is converted, without change of type domain, to a type whose corresponding real type is the common real type. Unless explicitly stated otherwise, the common real type is also the corresponding real type of the result, whose type domain is the type domain of the operands if they are the same, and complex otherwise. This pattern is called the usual arithmetic conversions:

  • First, if the corresponding real type of either operand is long double, the other operand is converted, without change of type domain, to a type whose corresponding real type is long double.
  • Otherwise, if the corresponding real type of either operand is double, the other operand is converted, without change of type domain, to a type whose corresponding real type is double.
  • Otherwise, if the corresponding real type of either operand is float, the other operand is converted, without change of type domain, to a type whose corresponding real type is float.
  • Otherwise, the integer promotions are performed on both operands. Then the following rules are applied to the promoted operands:

    • If both operands have the same type, then no further conversion is needed.
    • Otherwise, if both operands have signed integer types or both have unsigned integer types, the operand with the type of lesser integer conversion rank is converted to the type of the operand with greater rank.
    • Otherwise, if the operand that has unsigned integer type has rank greater or equal to the rank of the type of the other operand, then the operand with signed integer type is converted to the type of the operand with unsigned integer type.
    • Otherwise, if the type of the operand with signed integer type can represent all of the values of the type of the operand with unsigned integer type, then the operand with unsigned integer type is converted to the type of the operand with signed integer type.
    • Otherwise, both operands are converted to the unsigned integer type corresponding to the type of the operand with signed integer type.

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

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

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

根据前面的post,我想给每个例子更多的信息。

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

如何解决?

我尝试了之前post推荐的方法,但它并没有真正起作用。 这里的代码是基于之前的post:

把其中一个改成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

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