C Programming Language

Basics #05: Type Conversion

# Basic Tutorial,  # Programming Language,  # C Programming Language

Last Updated: 2020-07-01


前置知识

在阅读本文档前,请确保你已经掌握函数的基础知识,详见 Basics #02: Function

在阅读本文档前,请确保你已经掌握类型的基础知识,详见 Basics #03: Types


前言

类型转换 (type conversion) 指的是将变量或字面量从一种类型转换为另一种类型的过程,可以分为隐式转换和显式转换两大类。

类型转换在 C 语言中是非常重要的概念,许多初学者对类型转换的认知都是含糊不清的,并因此在编程时屡遭挫折。本文将对类型转换的相关概念进行详细的介绍。

为便于描述,本文中将变量和字面量等统称为变量,因为在二者都可用的情况下,它们的类型转换规则是一致的。

考虑到初学者很少会需要使用复数,本文对复数类型的类型转换规则大多予以略去。


隐式类型转换

基本概念

隐式类型转换 (implicit type conversion) 是由编译器自动完成的类型转换过程,不需要使用任何专门的语法,因而有时也称为 自动类型转换 (automatic type conversion)

隐式类型转换广泛存在于表达式执行中,即便很多时候你可能并没有意识到它发生了。例如下面的代码就发生了隐式类型转换:

int x = 0.5;

因为 xx 是 int 类型的变量,而 0.5 是浮点型的字面量,将 0.5 直接赋给 xx 是无法做到的。

再例如:

int a = 1;
double b = 1.5;
int c = a + b;

当求解算术表达式 a+ba + b 时,变量 aa 的值会被隐式转换为 double 类型,再与 bb 的值相加。而结果在被赋给 cc 之前,又被隐式转换为 int 类型。

注意,被转换的变量本身仍然不会受到任何影响,仅是其副本在参与各类表达式的执行。例如上面的例子中,对 cc 赋值后,变量 bb 的值仍为 1.5

转换发生时机

隐式类型转换可能会在下列情况中发生:

  • 执行赋值表达式时,如果左值和右值的类型不同;

  • 计算表达式时,如果表达式中存在多个数据类型;

  • 调用函数时,如果对应的一组实参和形参的类型不同;

  • 函数返回时,返回值类型与函数声明的类型不同;

表达式相关的隐式类型转换。

对于执行表达式时发生的隐式类型转换,上文中已经给出了一些此种情况的例子,此处将给出更为详细的解释。

在执行赋值表达式时,如果左值和右值的类型不同,则会发生隐式类型转换,右值将被转换为左值的类型,然后赋给左值。

以最简单的赋值表达式为例,因为 xx 是 int 类型的变量,而 0.5 是浮点型的字面量,所以在赋值时需要进行类型转换:

int x = 0.5;

即便是变量也是一样:

double x = 1.8;
int y = x;

在计算表达式时,如果表达式中存在多个数据类型,则会对其中一些操作数进行隐式类型转换,将其类型转换为所有操作数中“排名”最高的类型,然后进行运算。

注:这里“排名”的说法事实上是不太准确的,后文会对这个所谓的“排名”进行更为详细的介绍,现在你就姑且先认为 double 类型的“排名”比 int 类型更高,而 int 类型比 short 类型更高即可。

例如下面的代码:

int x = 1;
double y = 1.5;
double z = x + y;

我们考虑第 33 行,首先计算赋值表达式右侧的 x+yx + y 的值,由于 xxyy 的类型不同,且 yy 的类型“排名”更高,xx 的值会先被隐式转换为 double 类型再参与运算。而 x+yx + y 的运算结果为 double 类型,可以直接赋给 zz

再例如:

double x = 70536.0;
int y = 65536;
short z = 5000;
if (x == y + z) {
    printf("OK\n");
}

在计算 y+zy + z 时,zz 的值会先被隐式转换为 int 类型再参与运算。随后执行判断相等的逻辑表达式,y+zy + z 的运算结果是 int 类型,因此会先被隐式转换为 double 类型再参与运算。

函数相关的隐式类型转换。

对于调用函数时发生的隐式类型转换,如果对应的一组实参和形参的类型不同,则实参会被隐式转换为形参的类型。例如:

void f(int x);

如果调用 f(1.5)f(1.5) ,则实际上 xx 的初值为 1

对于函数返回时发生的隐式类型转换,如果返回值类型与函数声明的类型不同,则返回值会被转换为函数声明中规定的返回值类型。例如:

int f(double x) {
    return x;
}

如果调用 f(1.5)f(1.5) ,则实际上返回值为 1 。即便用 double 类型的变量来接收返回值也只会得到 1.0 ,因为 xx 在函数返回时会先被转换为 int 类型,已经丢失了浮点数的信息,即便再转换为 double 类型也无法复原为 1.5

double y = f(1.5);

因为这些和赋值表达式中发生的转换的规则非常类似,所以我们就不再对此进行更加详细的解释了。

隐藏的风险

隐式类型转换可能会导致信息丢失,这在前文中已经出现过多次,例如:

int x = 1;
int y = x + 1.5;

还可能会导致溢出,例如:

int y = 10000000;
short y = x;

然而不幸的是,编译器对此既不会报错也不会给出警告,因为这个问题只能在运行期检查出来,在编译期是无法发现的。因此要小心隐式类型转换的隐藏风险,尤其是有符号整数溢出会导致未定义行为,运行结果将是未知的。


显式类型转换

基本概念

显式类型转换 (explicit type conversion) 又称为 强制类型转换 (cast) ,使用专门的语法将变量值的类型转换为指定类型。

强制类型转换的语法如下所示,空格可有可无:

(type) expression

例如下面的代码,第 22xx 的值 2.5 会先被强制转换为 int 类型,其值变为 2 ,然后赋给 yy ,因此最终 yy 的值为 2

double x = 2.5;
double y = (int)x;

下面的代码中给出了一个较为复杂的例子:

double x = 1.5;
int y = 1;
int z = 4;
double a = (int)((double)((int)x + y) / z);

首先,表达式 (int)x + y 的值为 2 ,将其转换为 double 类型后除以 zz 得到 0.5 ,然后将这个商转换为 int 类型再赋给 aa ,最终 aa 的值为 0

当然,即便你使用显式类型转换,被转换的变量本身仍然不会受到任何影响,仅是其副本在参与各类表达式的执行。例如上面的例子中,对 aa 赋值后,虽然其中执行了 (int)x ,但变量 xx 的值仍为 1.5

妥善运用

隐式类型转换存在两个问题,前文都曾提到过:

  • 其一,由于隐式类型转换是由编译器悄悄完成的,有时太过隐蔽,有时容易被看错,需要程序员谨慎地对待;

  • 其二,隐式类型转换是遵循由 C 语言标准预设的转换顺序进行转换的,这个顺序是不可变更的。

而强制类型转换则破除了这两个弊端。

当我们认为一些地方的隐式类型转换太过隐蔽或容易被看错,需要人为地强调时,只需直接加上显式转换即可;

当我们需要以违背隐式转换顺序的方式进行类型转换时,使用显式转化即可,例如:

double x = 1;
double y = x / 2;

如果我们希望表达式 x / 2 取整,用强制类型转换可以很方便地解决:

double x = 1;
double y = (int)x / 2; // or (int)(x / 2);

但相对地,在表达式中大量使用强制类型转换会让代码可读性变差,就像上文中那个复杂的例子一样,我想你读起来一定不好受。为此我们应当考虑将复杂的表达式分解,以上例为例,我们可以将计算 aa 的语句改写为:

double x = 1.5;
int y = 1;
int z = 4;
// double a = (int)((double)((int)x + y) / z);
double t = (int)x + y;
double a = (int)(t / z);

用一个临时变量接下中间的子表达式,这可以极大地提高代码的可读性,且对代码的运行结果和运行效率没有任何影响。


类型转换规则

前文已经对两种类型转换进行了初步的介绍,下面我们将详细介绍这两种类型转换所遵循的具体转换规则。

C 语言标准中的类型转换规则较为晦涩难懂,为此我详细阅读了 C99 标准的原文,并对规则进行总结,但其中的部分内容对初学者而言仍然较为困难。

整数转换排名

每个整数类型都拥有其 整数转换排名 (integer conversion rank) 。在发生整数类型之间的类型转换时,会根据该排名的大小来决定具体的转换方式。

为便于描述,下文中我们用 rank(T)rank(T) 表示整数类型 TT 的整数转换排名。

整数转换排名的大小关系是可传递的,也就是说,如果 rank(T1)>rank(T2)rank(T_1) > rank(T_2) , rank(T2)>rank(T3)rank(T_2) > rank(T_3) ,那么 rank(T1)>rank(T3)rank(T_1) > rank(T_3)

该排名的详细定义翻译自 C99 标准原文,这对初学者而言可能有些晦涩难懂,且不是必要的内容,读者可以选择跳到后文直接看结论。

整数转换排名的详细定义如下:

  • 任意两个有符号整数类型的排名都是不同的,即便它们的长度和表示形式都相同;

  • 对于两个有符号整数类型 T1T_1T2T_2 ,如果 T1T_1 的长度比 T2T_2 大,那么 rank(T1)>rank(T2)rank(T_1) > rank(T_2) ,例如 rank(rank( long int )) 类型的排名比 rank(rank( short int )) 类型高 ;

  • 无论 int 类型在当前编译环境下的长度是多少,有符号整数类型的排名大小都满足 rank(rank( long long int )>rank() > rank( long int )>rank() > rank( int )>rank() > rank( short int )>rank() > rank( signed char ))

  • 任意无符号整数类型的排名都和与其对应的有符号整数类型相同,例如 rank(rank( int )=rank() = rank( unsigned int ))

  • 对于标准整数类型 T1T_1 和扩展整数类型 T2T_2 ,如果 T1T_1T2T_2 的长度相同,那么 rank(T1)>rank(T2)rank(T_1) > rank(T_2)

  • rank(rank( char )=rank() = rank( signed char )=rank() = rank( unsigned char ))

  • 布尔类型的排名比其它所有标准整数类型的排名都要小;

  • 任何枚举类型的排名应和与其兼容的整数类型的排名相同。

结论:由此可以归纳标准整数类型的整数转换排名从低到高如下,同一栏内的类型排名相同:

  • _Bool

  • char , signed char , unsigned char

  • short int , unsigned short int

  • int , unsigned int

  • long int , unsigned long int

  • long long int , unsigned long long int

注意 unsigned T 类型的排名和 signed T 类型相同,而不是广为流传的 unsigned T 类型的排名比 signed T 类型高。下面是 C99 标准中对此的原文解释:

6.3.1.1 Boolean, characters, and integers 1 Every integer type has an integer conversion rank defined as follows:

  • ...
  • The rank of any unsigned integer type shall equal the rank of the corresponding signed integer type, if any.
  • ...

基本规则

本节给出了各个类型之间发生类型转换时所遵循的基本规则,适用于任何情况下发生的类型转换。

当任何标量类型(包括所有整数类型、浮点类型和指针类型)的值被转换为布尔类型时:

  • 如果原值等于 0 ,则新值为 0

  • 否则,新值为 1

当一个整数类型 T1T_1 的值被转换为除布尔类型外的任意整数类型 T2T_2 时:

  • 如果原值在 T2T_2 可表示的数据范围内,则新值等于原值;

  • 如果原值超出 T2T_2 的表示范围,且 T2T_2 是一个无符号整数类型,则对原值不断加上或减去 max{T2}+1\max\{T_2\} + 1 ,直到数值处于 T2T_2 可表示的数据范围内为止,即得到新值;

  • 如果原值超出 T2T_2 的表示范围,且 T2T_2 是一个有符号整数类型,则在不同的编译环境下会得到不同的新值,也可能会报告一个溢出信息。事实上这属于实现定义行为。

第二条规则可能有些难以理解,以下面的代码为例:

int a = 75536;
unsigned short b = a;
int c = -20000;
unsigned short d = c;
int e = 655360;
unsigned short f = e;

因为 unsigned short 类型的表示范围为 [0, 65535][0,\ 65535] ,所以 bb 的值为 10000dd 的值为 45536ff 的值为 0

初学者请注意,此规则并不需要死记硬背,只要你学习过二进制的相关知识,就很容易能理解此规则。

当一个实浮点类型 T1T_1 的有限值被转换为除布尔类型外的任意整数类型 T2T_2 时:

  • 如果原值的整数部分在 T2T_2 可表示的数据范围内,则新值等于原值的整数部分,小数部分将被舍弃(即向靠近 0 的方向取整);

  • 如果原值的整数部分超出 T2T_2 的表示范围,则将触发未定义行为,结果将是不确定的。

当一个整数类型 T1T_1 的值被转换为任意实浮点类型 T2T_2 时:

  • 如果原值在 T2T_2 可表示的数据范围内,则新值等于原值;

  • 如果原值不能被 T2T_2 精确表示,但是并未超出其表示范围的最大最小值,则新值等于比原值大或小的最靠近原值的数值,具体是大或小在不同的编译环境下有所不同。事实上这属于实现定义行为;

  • 如果原值超出 T2T_2 的表示范围,则将触发未定义行为,结果将是不确定的。

当一个实浮点类型 T1T_1 的值被转换为实浮点类型 T2T_2 时:

  • 如果 T2T_2 的表示范围比 T1T_1 更大,则新值等于原值;

  • 如果 T2T_2 的表示范围比 T1T_1 更小:

    • 如果原值在 T2T_2 可表示的数据范围内,则新值等于原值;

    • 如果原值不能被 T2T_2 精确表示,但是并未超出其表示范围的最大最小值,则新值等于比原值大或小的最靠近原值的数值,具体是大或小在不同的编译环境下有所不同。事实上这属于实现定义行为;

    • 如果原值超出 T2T_2 的表示范围,则将触发未定义行为,结果将是不确定的。

复数类型相关的类型转换规则在此不予赘述。

整数提升

C 语言中有一个特殊的隐式类型转换的规则:当我们执行算术表达式时,如果表达式中含有整数转换排名小于等于 int 类型和 unsigned int 类型的类型(例如 char 类型和 short int 类型等),将会触发 整数提升 (integer promotion) 。这些类型将会在参与运算前被转换为 int 类型或 unsigned int 类型,例如:

char a = 30, b = 40, c = 5;
int d = (a * b) / c;

你认为 dd 的值会是多少?根据你已经掌握的知识,计算 a * b 的结果首先会超出 char 类型的表示范围,因而结果会溢出。然而事实上 dd 的值将是 240 ,一切正常,没有发生任何溢出!

这其中的秘密就是,在计算 a * b 之前,aabb 的值的类型先执行了整数提升,因而计算结果不会溢出。

对于下面的另一个例子也是一样,计算结果不会溢出:

short a = 10000, b = 10000;
int c = a * b;

整数提升的具体规则为:所有操作数的值都在 int 类型的表示范围内,则将所有操作数转换为 int 类型,否则转换为 unsigned int 类型。

整数提升不会改变操作数中的值,转换后的新值一定等于原值。

整数转换排名大于 int 类型和 unsigned int 类型的所有类型均不会受到整数提升的影响。

常规算术转换

在上文中我们提到过,在计算表达式时,如果表达式中存在多个数据类型,则会对其中一些操作数进行隐式类型转换。此时发生的转换的正式名称为 常规算术转换 (usual arithmetic conversions)

事实上,该规则是针对单个二元运算符的,也就是针对形如 a opt b 的二元表达式的,其中 opt 可能是算术运算符,也可能是比较运算符。复杂的表达式将根据运算符优先级与结合性被拆分为若干个子表达式来看待。

该规则的详细定义翻译自 C99 标准原文,这对初学者而言可能有些晦涩难懂,且不是必要的内容,读者可以选择跳到后文直接看结论。

常规算术转换的具体规则如下,其中省略了复数类型相关的规则:

  • 首先,如果任意一个操作数为 long double 类型,则将另一个操作数转换为 long double 类型;

  • 否则,如果任意一个操作数为 double 类型,则将另一个操作数转换为 double 类型;

  • 否则,如果任意一个操作数为 float 类型,则将另一个操作数转换为 float 类型;

  • 否则,先对两个操作数执行 整数提升 ,设提升后两个操作数的类型分别为 T1T_1T2T_2 ,然后:

    • 如果两个操作数的类型相同,则转换完成;

    • 否则,如果 T1T_1T2T_2 都是有符号整数类型或者都是无符号整数类型,若 rank(T1)<rank(T2)rank(T_1) < rank(T_2) ,则将类型为 T1T_1 的操作数转换为 T2T_2 类型,反之同理;

    • 否则,设 T1T_1 是有符号整数类型,T2T_2 是无符号整数类型,然后:

      • rank(T1)rank(T2)rank(T_1) \le rank(T_2) ,则将类型为 T1T_1 的操作数转换为 T2T_2 类型;

      • 否则,若 T2T_2 的表示范围是 T1T_1 的子集,则将类型为 T2T_2 的操作数转换为 T1T_1 类型;

      • 否则,将两个操作数都转换为与 T1T_1 对应的无符号整数类型。

  • 此外,浮点类型的操作数的值和浮点表达式的结果可以用精度和表示范围更大的类型来表示,不会因此发生类型转换。

结论:由此可以将常规算数转换的规则归纳总结为:

对于二元表达式 a opt b ,根据如下从低到高的“排名”,“排名”低的操作数的类型转换为“排名”高的类型:

  • _Bool , char , signed char , unsigned char , short int , unsigned short int 都将被执行整数提升,不在考虑范围之内;

  • int

  • unsigned int

  • long int

  • unsigned long int

  • long long int

  • unsigned long long int

  • float

  • double

  • long double

这里的“排名”即是前文曾提到过的“不太准确的”“排名”,用于在执行算术表达式时触发的所有隐式类型转换中。

下面给出几个例子帮助理解:

  • a, ba,\ b 分别为 float 类型和 int 类型,则 bb 被转换为 float 类型, a opt bfloat 类型;

  • a, ba,\ b 分别为 int 类型和 unsigned int 类型,则 aa 被转换为 unsigned int 类型, a opt bunsigned int 类型;

  • a, ba,\ b 分别为 long int 类型和 int 类型,则 bb 被转换为 long int 类型, a opt blong int 类型。


太过隐蔽的隐式转换

前文曾经提到过,有时隐式类型转换太过隐蔽。在学完类型转换规则后,我们终于可以来看这个例子了:

int a = -2;
unsigned int b = 1;
if (a + b > 0) {
    printf("a + b > 0\n");
}
printf("a + b = %d\n", a + b);

上述代码的运行结果为:

a + b > 0
a + b = -1

你可能会对此感到迷惑。根据类型转换规则, a + b 的结果应该是 unsigned int 类型的,为什么输出结果会是 -1 ?而且前面还输出 a + b > 0 ,这也太奇怪了吧?

事实上问题出在 printf 身上。 a + b 的结果的确是 unsigned int 类型的,但由于你使用了 %d 符号,该结果在函数传参时又被隐式转换为 int 类型,这才造成了如此结果。

另外,如果我们将代码修改为:

int a = 1;
unsigned int b = 1;
if (a + b > -1) {
    printf("a + b > -1\n");
}
printf("a + b = %d\n", a + b);

上述代码的运行结果为:

a + b = 2

因为在执行表达式时 -1 被转换为 unsigned int 类型,当 int 类型的长度为 3232 位时其值为 4294967295 ,显然比 2 大。若想让比较运算的结果符合预期,我们应该写成:

if ((int)(a + b) > -1) {
    printf("a + b > -1\n");
}

兼容类型

(这段内容不属于这篇文章,只是先放在这,将来会移走)

(这段内容不是面向初学者的)

C 语言类型系统定义了 兼容类型 (compatible type) 的概念,明确规定了哪些类型之间是兼容类型。与之相对的概念是 不兼容类型 (imcompatible type)

对于相同的变量或函数在不同的翻译单元中的多个声明,不必使用相同的类型,它们只需要使用兼容类型即可。同样地,在函数调用时,形参类型必须与实参类型相兼容;在访问左值时,左值表达式的类型必须与其所访问的对象的类型相兼容。

将操作数值转换为兼容类型不会改变值或表示形式。

(unfinished)