C语言数据类型转换详解
2014-02-16 20:41
627 查看
转自 宋劲彬
http://learn.akae.cn/media/ch15s03.html
3. 类型转换
如果有人问C语法规则中最复杂的是哪一部分,我一定会说是类型转换。从上面两节可以看出,有符号、无符号整数和浮点数加起来有那么多种类型,每两种类型之间都要定义一个转换规则,转换规则的数量自然很庞大,更何况由于各种体系结构对于整数和浮点数的实现很不相同,很多类型转换的情况都是C标准未做明确规定的阴暗角落。虽然我们写代码时不会故意去触碰这些阴暗角落,但有时候会不小心犯错,所以了解一些未明确规定的情况还是有必要的,可以在出错时更容易分析错误原因。本节分成几小节,首先介绍哪些情况下会发生类型转换,会把什么类型转成什么类型,然后介绍编译器如何处理这样的类型转换。
3.1. Integer Promotion
在一个表达式中,凡是可以使用
1、如果一个函数的形参类型未知,例如使用了Old Style C风格的函数声明或者函数的参数列表中有...,那么调用函数时要对相应的实参做Integer Promotion,此外,相应的实参如果是
Argument Promotion。我们知道
2、算术运算中的类型转换。有符号或无符号的
计算表达式
除了+号之外还有哪些运算符在计算之前需要做Integer Promotion呢?我们在下一小节先介绍Usual Arithmetic Conversion规则,然后再解答这个问题。
3.2. Usual Arithmetic Conversion
两个算术类型的操作数做算术运算,比如
如果有一边的类型是
否则,如果有一边的类型是
否则,如果有一边的类型是
否则,两边应该都是整型,首先按上一小节讲过的规则对
如果两边都是有符号数,或者都是无符号数,那么较低Rank的类型转换成较高Rank的类型。例如
否则,如果一边是无符号数另一边是有符号数,无符号数的Rank不低于有符号数的Rank,则把有符号数转成另一边的无符号类型。例如
剩下的情况是:一边有符号另一边无符号,并且无符号数的Rank低于有符号数的Rank。这时又分为两种情况,如果这个有符号数类型能够覆盖这个无符号数类型的取值范围,则把无符号数转成另一边的有符号类型。例如遵循LP64的平台上
否则,也就是这个有符号数类型不足以覆盖这个无符号数类型的取值范围,则把两边都转成有符号数的Rank对应的无符号类型。例如在遵循ILP32的平台上
可见有符号和无符号整数的转换规则是十分复杂的,虽然这是有明确规定的,不属于阴暗角落,但为了程序的可读性不应该依赖这些规则来写代码。我讲这些规则,不是为了让你用,而是为了让你了解有符号数和无符号数混用会非常麻烦,从而避免触及这些规则,并且在程序出错时记得往这上面找原因。所以这些规则不需要牢记,但要知道有这么回事,以便在用到的时候能找到我书上的这一段。
到目前为止我们学过的+ - * / % > < >= <= == !=运算符都需要做Usual Arithmetic Conversion,因为都要求两边操作数的类型一致,在下一章会介绍几种新的运算符也需要做Usual Arithmetic Conversion。单目运算符+ - ~只有一个操作数,移位运算符<< >>两边的操作数类型不要求一致,这些运算不需要做Usual Arithmetic Conversion,但也需要做Integer Promotion,运算符~
<< >>将在下一章介绍。
3.3. 由赋值产生的类型转换
如果赋值或初始化时等号两边的类型不相同,则编译器会把等号右边的类型转换成等号左边的类型再做赋值。例如
我们知道,函数调用传参的过程相当于定义形参并且用实参对其做初始化,函数返回的过程相当于定义一个临时变量并且用
在函数调用和返回过程中发生的类型转换往往容易被忽视,因为函数原型和函数调用并没有写在一起。例如
“以字节为单位的I/O函数”详细讨论这个问题。
3.4. 强制类型转换
以上三种情况通称为隐式类型转换(Implicit Conversion,或者叫Coercion),编译器根据它自己的一套规则将一种类型自动转换成另一种类型。除此之外,程序员也可以通过类型转换运算符(Cast
Operator)自己规定某个表达式要转换成何种类型,这称为显式类型转换(Explicit Conversion)或强制类型转换(Type Cast)。例如计算表达式
3.5. 编译器如何处理类型转换
以上几小节介绍了哪些情况下会发生类型转换,并且明确了每种情况下会把什么类型转成什么类型,本节介绍编译器如何处理任意两种类型之间的转换。现在要把一个M位的类型(值为X)转换成一个N位的类型,所有可能的情况如下表所示。
表 15.3. 如何做类型转换
注意上表中的“X % 2N”,我想表达的意思是“把X加上或者减去2N的整数倍,使结果落入[0, 2N-1]的范围内”,当X是负数时运算结果也得是正数,即运算结果和除数同号而不是和被除数同号,这不同于C语言%运算的定义。写程序时不要故意用上表中的规则,尤其不要触碰Implementation-defined和Undefined的情况,但程序出错时可以借助上表分析错误原因。
下面举几个例子说明上表的用法。比如把
比如把
65535]之间。
比如把
最后一个例子,把
Extension)。
http://learn.akae.cn/media/ch15s03.html
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风格的函数声明或者函数的参数列表中有...,那么调用函数时要对相应的实参做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),编译器根据它自己的一套规则将一种类型自动转换成另一种类型。除此之外,程序员也可以通过类型转换运算符(CastOperator)自己规定某个表达式要转换成何种类型,这称为显式类型转换(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 | 值不变 | 值不变 |
下面举几个例子说明上表的用法。比如把
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)。
相关文章推荐
- C语言中的数据类型及其转换详解
- C语言中的数据类型及其转换详解
- 详解C语言中的char数据类型及其与int类型的转换
- C语言数据类型转换详解
- 详解C语言中的char数据类型及其与int类型的转换
- C语言中的数据类型及其转换详解
- C语言数据类型转换详解
- C语言中数据类型转换详解
- C语言数据类型转换详解
- VC常用数据类型使用转换详解
- VC常用数据类型使用转换详解
- Java数据类型转换详解
- c语言中数据类型的自动转换原则
- VC常用数据类型使用转换详解
- C语言 数据类型转换 自动转换 vs 强制转换
- 常用数据类型使用转换详解
- DBus glib 各数据类型接收与发送详解—C语言(1)
- 常用数据类型使用转换详解
- C语言基本数据类型:整型(int)用法详解
- 基于C++浮点数(float、double)类型数据比较与转换的详解