3. 类型转换
如果有人问C语法规则中最复杂的是哪一部分,我一定会说是类型转换。从上面两节可以看出,有符号、无符号整数和浮点数加起来有那么多种类型,每两种类型之间都要定义一个转换规则,转换规则的数量自然很庞大,更何况由于各种体系结构对于整数和浮点数的实现很不相同,很多类型转换的情况都是C标准未做明确规定的阴暗角落。虽然我们写代码时不会故意去触碰这些阴暗角落,但有时候会不小心犯错,所以了解一些未明确规定的情况还是有必要的,可以在出错时更容易分析错误原因。本节分成几小节,首先介绍哪些情况下会发生类型转换,会把什么类型转成什么类型,然后介绍编译器如何处理这样的类型转换。
3.1. Integer Promotion
在一个表达式中,凡是可以使用int
或unsigned int
类型做右值的地方也都可以使用有符号或无符号的char
型、short
型和Bit-field。如果原始类型的取值范围都能用int
型表示,则其类型被提升为int
,如果原始类型的取值范围用int
型表示不了,则提升为unsigned int
型,这称为Integer Promotion。做Integer Promotion只影响上述几种类型的值,对其它类型无影响。C99规定Integer Promotion适用于以下几种情况:
1、如果一个函数的形参类型未知,例如使用了Old Style C风格的函数声明(详见第 2 节 “自定义函数”),或者函数的参数列表中有…,那么调用函数时要对相应的实参做Integer Promotion,此外,相应的实参如果是float
型的也要被提升为double
型,这条规则称为Default Argument Promotion。我们知道printf
的参数列表中有...
,除了第一个形参之外,其它形参的类型都是未知的,比如有这样的代码:
- char ch = 'A';
- printf("%c", ch);
ch
要被提升为int
型之后再传给printf
。
2、算术运算中的类型转换。有符号或无符号的char
型、short
型和Bit-field在做算术运算之前首先要做Integer Promotion,然后才能参与计算。例如:
- unsigned char c1 = 255, c2 = 2;
- int n = c1 + c2;
计算表达式c1 + c2
的过程其实是先把c1
和c2
提升为int
型然后再相加(unsigned char
的取值范围是0~255,完全可以用int
表示,所以提升为int
就可以了,不需要提升为unsigned int
),整个表达式的值也是int
型,最后的结果是257。假如没有这个提升的过程,c1 + c2
就溢出了,溢出会得到什么结果是Undefined,在大多数平台上会把进位截掉,得到的结果应该是1。
除了+号之外还有哪些运算符在计算之前需要做Integer Promotion呢?我们在下一小节先介绍Usual Arithmetic Conversion规则,然后再解答这个问题。
3.2. Usual Arithmetic Conversion
两个算术类型的操作数做算术运算,比如a + b
,如果两边操作数的类型不同,编译器会自动做类型转换,使两边类型相同之后才做运算,这称为Usual Arithmetic Conversion。转换规则如下:
如果有一边的类型是
long double
,则把另一边也转成long double
。否则,如果有一边的类型是
double
,则把另一边也转成double
。否则,如果有一边的类型是
float
,则把另一边也转成float
。否则,两边应该都是整型,首先按上一小节讲过的规则对
a
和b
做Integer Promotion,然后如果类型仍不相同,则需要继续转换。首先我们规定char
、short
、int
、long
、long long
的转换级别(Integer Conversion Rank)一个比一个高,同一类型的有符号和无符号数具有相同的Rank。转换规则如下:如果两边都是有符号数,或者都是无符号数,那么较低Rank的类型转换成较高Rank的类型。例如
unsigned int
和unsigned long
做算术运算时都转成unsigned long
。否则,如果一边是无符号数另一边是有符号数,无符号数的Rank不低于有符号数的Rank,则把有符号数转成另一边的无符号类型。例如
unsigned long
和int
做算术运算时都转成unsigned long
,unsigned long
和long
做算术运算时也都转成unsigned long
。剩下的情况是:一边有符号另一边无符号,并且无符号数的Rank低于有符号数的Rank。这时又分为两种情况,如果这个有符号数类型能够覆盖这个无符号数类型的取值范围,则把无符号数转成另一边的有符号类型。例如遵循LP64的平台上
unsigned int
和long
在做算术运算时都转成long
。否则,也就是这个有符号数类型不足以覆盖这个无符号数类型的取值范围,则把两边都转成有符号数的Rank对应的无符号类型。例如在遵循ILP32的平台上
unsigned int
和long
在做算术运算时都转成unsigned long
。
可见有符号和无符号整数的转换规则是十分复杂的,虽然这是有明确规定的,不属于阴暗角落,但为了程序的可读性不应该依赖这些规则来写代码。我讲这些规则,不是为了让你用,而是为了让你了解有符号数和无符号数混用会非常麻烦,从而避免触及这些规则,并且在程序出错时记得往这上面找原因。所以这些规则不需要牢记,但要知道有这么回事,以便在用到的时候能找到我书上的这一段。
到目前为止我们学过的+ - * / % > < >= <= == !=运算符都需要做Usual Arithmetic Conversion,因为都要求两边操作数的类型一致,在下一章会介绍几种新的运算符也需要做Usual Arithmetic Conversion。单目运算符+ - ~只有一个操作数,移位运算符<< >>两边的操作数类型不要求一致,这些运算不需要做Usual Arithmetic Conversion,但也需要做Integer Promotion,运算符~ << >>将在下一章介绍。
3.3. 由赋值产生的类型转换
如果赋值或初始化时等号两边的类型不相同,则编译器会把等号右边的类型转换成等号左边的类型再做赋值。例如int c = 3.14;
,编译器会把右边的double
型转成int
型再赋给变量c
。
我们知道,函数调用传参的过程相当于定义形参并且用实参对其做初始化,函数返回的过程相当于定义一个临时变量并且用return
的表达式对其做初始化,所以由赋值产生的类型转换也适用于这两种情况。例如一个函数的原型是int foo(int, int);
,则调用foo(3.1, 4.2)
时会自动把两个double
型的实参转成int
型赋给形参,如果这个函数定义中有返回语句return 1.2;
,则返回值1.2
会自动转成int
型再返回。
在函数调用和返回过程中发生的类型转换往往容易被忽视,因为函数原型和函数调用并没有写在一起。例如char c = getchar();
,看到这一句往往会想当然地认为getchar
的返回值是char
型,而事实上getchar
的返回值是int
型,这样赋值会引起类型转换,可能产生Bug,我们在第 2.5 节 “以字节为单位的I/O函数”详细讨论这个问题。
3.4. 强制类型转换
以上三种情况通称为隐式类型转换(Implicit Conversion,或者叫Coercion),编译器根据它自己的一套规则将一种类型自动转换成另一种类型。除此之外,程序员也可以通过类型转换运算符(Cast Operator)自己规定某个表达式要转换成何种类型,这称为显式类型转换(Explicit Conversion)或强制类型转换(Type Cast)。例如计算表达式(double)3 + i
,首先将整数3强制转换成double
型(值为3.0),然后和整型变量i
相加,这时适用Usual Arithmetic Conversion规则,首先把i
也转成double
型,然后两者相加,最后整个表达式也是double
型的。这里的(double)
就是一个类型转换运算符,这种运算符由一个类型名套()括号组成,属于单目运算符,后面的3是这个运算符的操作数。注意操作数的类型必须是标量类型,转换之后的类型必须是标量类型或者void
型。
3.5. 编译器如何处理类型转换
以上几小节介绍了哪些情况下会发生类型转换,并且明确了每种情况下会把什么类型转成什么类型,本节介绍编译器如何处理任意两种类型之间的转换。现在要把一个M位的类型(值为X)转换成一个N位的类型,所有可能的情况如下表所示。
表 15.3. 如何做类型转换
待转换的类型 | M > N的情况 | M == N的情况 | M < N的情况 |
---|---|---|---|
signed integer to signed integer | 如果X在目标类型的取值范围内则值不变,否则Implementation-defined | 值不变 | 值不变 |
unsigned integer to signed integer | 如果X在目标类型的取值范围内则值不变,否则Implementation-defined | 如果X在目标类型的取值范围内则值不变,否则Implementation-defined | 值不变 |
signed integer to unsigned integer | X % 2N | X % 2N | X % 2N |
unsigned integer to unsigned integer | X % 2N | 值不变 | 值不变 |
floating-point to signed or unsigned integer | Truncate toward Zero,如果X的整数部分超出目标类型的取值范围则Undefined | ||
signed or unsigned integer to floating-point | 如果X在目标类型的取值范围内则值不变,但有可能损失精度,如果X超出目标类型的取值范围则Undefined | ||
floating-point to floating-point | 如果X在目标类型的取值范围内则值不变,但有可能损失精度,如果X超出目标类型的取值范围则Undefined | 值不变 | 值不变 |
注意上表中的“X % 2N”,我想表达的意思是“把X加上或者减去2N的整数倍,使结果落入[0, 2N-1]的范围内”,当X是负数时运算结果也得是正数,即运算结果和除数同号而不是和被除数同号,这不同于C语言%运算的定义。写程序时不要故意用上表中的规则,尤其不要触碰Implementation-defined和Undefined的情况,但程序出错时可以借助上表分析错误原因。
下面举几个例子说明上表的用法。比如把double
型转换成short
型,对应表中的“floating-point to signed or unsigned integer”,如果原值在(-32769.0, 32768.0)之间则截掉小数部分得到转换结果,否则产生溢出,结果是Undefined,例如对于short s = 32768.4;
这个语句gcc
会报警告。
比如把int
型转换成unsigned short
型,对应表中的“signed integer to unsigned integer”,如果原值是正的,则把它除以216取模,其实就是取它的低16位,如果原值是负的,则加上216的整数倍,使结果落在[0, 65535]之间。
比如把int
类型转换成short
类型,对应表中的“signed integer to signed integer”,如果原值在[-32768, 32767]之间则值不变,否则产生溢出,结果是Implementation-defined,例如对于short s = -32769;
这个语句gcc
会报警告。
最后一个例子,把short
型转换成int
型,对应表中的“signed integer to signed integer”,转换之后应该值不变。那怎么维持值不变呢?是不是在高位补16个0就行了呢?如果原值是-1,十六进制表示就是ffff,要转成int
型的-1需要变成ffffffff,因此需要在高位补16个1而不是16个0。换句话说,要维持值不变,在高位补1还是补0取决于原来的符号位,这称为符号扩展(Sign Extension)。