类型转换
C语言中,类型转换可以说是非常复杂的存在了。有符号、无符号,整数、浮点数等等,C语言中定义了这么多类型。而很多类型之间的转换,C标准中并没有做很详细的规定。如果不对这些转换有一个基本的认识的话,那么难免会留下隐患。本文将从四个方面,对C原因中的一些隐式的类型转换做一些总结。
整型提升
一个C表达式中,方式可以用int、unsigned int类型做右值的地方都可以使用有符号或者无符号的char型、short型和位域。如果原来类型的取值范围能够用int表示,则其类型被提升为int,如果原始类型的值超出int可以表示的范围,则提升为unsigned int类型。以上就称为整型提升。整型提升只对上面提到的几种类型有影响,对其他类型无影响。
总结一下,上面提到的类型有:char 、short、位域
-
函数形参
如果一个函数的形参类型未知,或形参列表中有上面提到的三种类型,那么调用函数时要对应相应的实参做整型提升。另外相应的实参如果是float型的也要被提升为double型。这条规则又叫默认参数提升。比如,我们常用printf()函数,它是一个可变参数函数,参数列表中含‘...’。除了第一个形参外,其他形参的类型是未知的。比如:
char ch = '; printf("%c",ch); |
ch要被提升为int型后再传给printf。
-
算术运算中类型转换
算术运算表达式中含有char、short、位域在做算术运算之前要先做整型提升,然后才能参与计算。
算术转换
两个算术类型的操作数做算术运算,比如a+b,如果两边操作数的类型不同,编译器会自动做类型转换,使两边类型相同之后才做运算,这就是算术运算转换规则。其实上面已经提到了一部分,下面列出算术转换规则:
-
如果有一边的类型是long double,则另一边也转换成long double。
-
如果有一边的类型是double,则另一边也转换成double。
-
如果有一边的类型是float,则另一边也转换成float。
-
否则,两边应该都是整型。
-
如果两边都是有符号数或者无符号数,那么较低Rank的类型转换成较高Rank的类型。例如unsigned int和unsigned long做算术运算时都转换成unsigned long。
-
否则,如果一边是无符号数另一边是有符号数,无符号数的等级不低于有符号数的等级,则把有符号数转换成另一边的无符号数。例如unsigned long和int做算术运算时都转成unsigned long ,unsigned long和long做算术运算时也都转换成unsigned long。
-
还有一种情况是:一边是有符号数另一边是无符号数,并且无符号数的等级低于有符号数的等级。此时分两种情况,如果这个有符号数类型能够覆盖这个无符号数类型的取值范围,则把无符号数转换成另一边的有符号类型。例如遵循64位机器的平台上unsigned int和long在做算术运算时都转换成long。
-
否则,也就是这个有符号数类型不足以覆盖这个无符号数类型的取值范围,则把两边都转换成有符号数的等级对应的无符号类型。
-
我们常用的运算符:+ - * / % > < >= <= == != 都遵循上面提到的这些规则。其实我们可以发现,这些转换时有一个规律的:转换是从低等级向高等级转换的,即会向范围较大的类型转换。
赋值转换
由赋值产生的类型转换,如果赋值或者初始化时等号两边的类型不相同,则编译器会把等号右边的类型转换成等号左边的类型再做赋值。比如int c = 3.14,那么编译器会把右边double类型的转换成int类型再赋值给变量c。
我们知道,函数调用传参的过程相当于定义形参对其做初始化,函数返回的过程相当于定义一个临时变量并且用return的表达式对其做初始化,所以由赋值产生的类型转换也适用于这两种情况。例如一个函数的原型是int foo(int,int),如果我们调用该函数时传入foo(3.1, 2.3)时会自动把这两个double型的实参转换为int型赋给形参,如果这个函数定义中有返回语句return 1.2,那么1.2会转换成int类型再返回。
在函数调用和返回过程中发生的类型转换往往容易被忽视,因为函数原型和函数调用并没有写在一起。例如char c = getchar();看到这一句往往会认为getchar()返回值是char型,而事实上getchar的返回值是int型,这样赋值会引起类型转换,可能会引起Bug。
强制类型转换
上面提到的三种情况都属于隐式类型转换,即编译器根据他自己的一套规则将一种类型自动转换成另一种类型。除此之外,程序员也可以通过类型转换运算符自己规定某个表达式要转换成何种类型,这称为显式类型转换或强制类型转换。
比如:(double)3 + i ,会先将3强制转换成3.0,然后在和整形变量i相加,这是使用前面提到的算术运算类型转换的规则,首先会把i转换成double型变量,这里double就是一个类型转换运算符,这种预算符由一个类型名套()括号组成,属于单目运算符,后面的3是这个运算符的操作数。注意操作数的类型必须是标量类型,转换之后的类型必须是标量类型或者void。
编译器做类型转换的几种形式:
带转换的类型 | M > N的情况 | M == N的情况 | M < N 的情况 |
signed int -> signed int | 如果X在目标类型的取值范围内则值不变,否则由运行环境决定 | 值不变 | 值不变 |
unsigned int -> signed int | 如果X的目标值的取值范围不变,否则有环境决定 | 如果X的目标值的取值范围不变,否则有环境决定 | 值不变 |
signed int -> unsigned int | X%2^N | X%2^N | X%2^N |
unsigned int -> unsigned int | X%2^N | 值不变 | 值不变 |
float ->unsigned int or unsigned int | 小数部分取0,如果整数部分超出取值范围则未定义 | 小数部分取0,如果整数部分超出取值范围则未定义 | 小数部分取0,如果整数部分超出取值范围则未定义 |
float -> float | 如果X在目标类型的取值范围内则值不变,但有可能损失精度,如果X超出目标类型的取值范围则Undefined | 值不变 | 值不变 |
注:表中“X%2N”,表示把X加上或者减去2N的整数倍,是结果落入[0,2N-1]范围内,当X是负数时运算结果也得是正数,即运算结果和除数同号而不是和被除数同号,这不同于C语言%运算的定义。