类型转换
数据有不同的类型,不同类型数据之间进行混合运算时必然涉及到类型的转换问题。
在执行算术运算时,计算机比 C 语言的限制更多。计算机要求操作数具有相同的位数,并且操作数的存储方式也要相同。计算机可能可以直接将两个 16 位的整数相加,但是不能直接将 16 位的整数和 32 位的整数相加;也不能直接将 32 位的整数和 32 位的浮点数相加。
C 语言则允许在表达式中混合使用基本类型,表达式中可以组合整数、浮点数甚至是字符。在这种情况下,C 编译器需要生成一些指令将某些操作数转换成其他类型的数据,使得硬件可以对表达式进行求值。
有时候编译器可以自动处理这些转换,无需程序员介入,所以这类转换称为隐式类型转换。C 语言还允许程序员使用强制转换运算符()进行显式转换。我们首先讨论隐式转换,然后再讨论显式转换。
- :遵循一定的规则,由编译系统自动完成。
- :把表达式的运算结果强制转换成所需的数据类型。
类型转换的原则:占用内存字节数少(值域⼩)的类型,向占用内存字节数多(值域⼤)的类型转 换,以保证精度不降低。
隐式转换
当发生下列情况时,C 语言会进行隐式转换。
- 当算术表达式或逻辑表达式中操作数的类型不相同时。
- 当赋值运算符右侧表达式的类型和左侧变量的类型不匹配时。
- 当函数调用中的实参类型和其对应的行参类型不匹配时。
- 当 return 语句中表达式的类型和函数返回值的类型不匹配时。
总而言之,我们可以这么理解:当给定的类型与需要的类型不同时,就可能发生隐式转换。
#include <stdio.h>
#include <stdlib.h>
int main() {
int num = 5;
printf("s1=%d\n", num / 2);
printf("s2=%lf\n", num / 2.0);
return 0;
}
程序输出:
s1=2
s2=2.500000
常用算术转换
为了定义转换规则,给每个整数类型都定义了"整数转换等级",从高到低依次为:
(1) long double
(2) double
(3) float
(4) long long int, unsigned long long int
(5) long int, unsigned long int
(6) int, unsigned int
(7) short int, unsigned short int
(8) char, signed char, unsigned char
C 语言常用算术转换规则很复杂,我们只需要记住下面三个规则就好:
- 如果操作数中有任何低于 int 和 unsigned int 的类型,会首先将该操作数转换为 int 类型或者 unsigned int 类型,这个过程我们称之为整数提升。(why?)
int -> long -> long long -> float -> double -> long double
- 同一转换等级的有符号整数和无符号整数一起参与运算时:有符号整数会转换成对应的无符号整数。
signed -> unsigned
TIP
把有符号操作数转换为无符号类型时,可能会导致某些隐蔽的编程问题:
int i = -10;
unsigned int u = 10;
if(i < u)
printf("i is less than u\n");
赋值过程中的转换
赋值运算会遵循一条更为简单的规则:把赋值运算右边的表达式转换为左边变量的类型。
如果变量的类型至少和表达式类型一样"宽",那么这样的转换是没有任何问题的。如:
char c;
int i;
float f;
double d;
i = c;
f = i;
d = f;
其他情况下是有问题的。把浮点数赋值给整型变量会丢失小数部分:
int i;
i = 842.97; /* i is now 842 */
i = -842.97; /* i is now -842 */
如果该值在变量类型的表示范围之外,那么将会得到无意义的结果 (甚至更糟)。
c = 10000; /* WRONG */
i = 1.0e20; /* WRONG */
f = 1.0e100; /* WRONG */
将浮点常量赋值给 float 类型变量时,一个好的习惯是在常量后面加字符 f,如:
f = 3.14f
。如果没有后缀 f,那么常量 3.14 是 double 类型,会引发隐式转换。
注意事项(重要)
在讲上述表达式相关的隐式类型转换时,我们刻意避开了无符号的整数类型。
实际上C语言有以下明确的语法规定:
也就是说:
unsigned + int = unsigned
unsigned long + long = unsigned long
...
但,我们来看两个例子:
无符号整数会带来转换等级的复杂性
你认为:
unsigned + long 结果是什么类型啊?
你可能觉得long类型等级更高,结果应该是long类型。但实际上在VS的MSVC编译器下这个结果类型是unsigned long。
这是因为在VS的MSVC编译器平台下,int和long类型的大小是一致的,都是4个字节。
因此unsigned long被视为转换等级的最高级别,所以结果就是unsigned long。
这说明由于无符号整数的参与,转换等级变得需要考虑平台和编译器等额外因素了,这无疑增加了编程的复杂性。
当然,这个例子最多导致一些类型的误判,不算太坑。但下面的例子就是一个纯粹的"陷阱"了。
无符号数参与的类型转换将导致直觉上的逻辑错误
请看以下代码:
int a = -10;
unsigned b = 100;
if (b > a){
printf("b > a is true.\n");
}else{
printf("b > a is false.\n");
}
printf("%u\n", (a + b));
请问程序的两行输出都是什么呢?
答:
- b > a is false.
- 90
这是因为:
当表达式中同时存在有符号数和无符号数时,有符号数会被隐式转换为无符号数。在这个例子中,
a
是一个有符号整数值为 -10,这个数的补码形式是11111111 11111111 11111111 11110110
。当将这个补码解释为无符号整数时,它代表的是一个非常大的正数,具体来说,是N - 10( 即2^32 - 10,实际上这个数是最大值 - 9)。注:N - 10是怎么来的呢?
N是模且系统中没有负数,于是-10就可以转换成模相关的一个加法从而去掉负号,-10在这个系统中就等价于 +(N - 10),于是-10在此系统中变成了一个正数。
明白上面代码的原因后,那么a + b = 90,应该也是非常容易理解的了:
- a转换成无符号数结果是N - 10,那么再加上100,结果就是N - 10 + 100 = N + 90
- 但N是系统的模,任何达到或超出模的数值,都会从0开始计算。
- 所以N + 90,结果就是90
总之,由于无符号数的类型转换,给程序员带来了极大的麻烦,这是C语言的一个常见"设计陷阱"。
针对无符号数的类型转换,我们给出以下建议:
- 在一般的应用级C程序开发中,无符号整数是用不到的,往往只有在底层开发中才会使用它。
- 假如真的要使用无符号整数,也建议不要混合使用无符号整数和有符号整数。或者要更小心谨慎的思考它们之间的转换。
强制转换
虽然 C 语言的隐式转换使用起来非常方便,但是有时候我们需要更大程度上控制类型 转换。因此,C 语言提供了强制类型转换。
强制类型转换指的是使用强制类型转换运算符,将一个变量或表达式转化成所需的类型,其基本语法格式如下所示:
(类型说明符) (表达式)
C/C++ 把类型说明符看作是一元运算符,一元运算符的优先级是高于二元运算符的。
#include <stdio.h>
#include <stdlib.h>
int main() {
float x = 0;
int i = 0;
x = 3.6f;
i = x; //x为实型, i为整型,直接赋值会有警告
i = (int) x;//使用强制类型转换
printf("x=%f, i=%d\n", x, i);
return 0;
}
程序输出:
x=3.600000, i=3
Q:为什么需要显式类型转换?
A:
我们可以使用强制类型转换计算浮点数的小数部分,如:
cppfloat f, frace_part; frace_part = f - (int) f;
使用强制类型转换显示表明肯定会发生的转换。
cppi = (int) f; /* f is converted to int */
使用强制类型转换进行我们需要的类型转换。
cppfloat quotient; int dividend, divisor; /* What's the difference between next two expressions? */ quotient = dividend / divisor; quotient = (float) dividend / dividor;
有时候,我们还可以使用强制类型转换来避免溢出。
cpplong i; int j = 1000; i = j * j; /* overflow may occur */ i = (long) (j * j); /* overflow may occur */ i = (long) j * j;
注意事项
C语言的强制类型转换语法非常灵活,理论上允许程序员将变量从一种类型转换为任意的另一种类型。但这种灵活的语法应当谨慎使用,多多思考,避免无意义或不安全的强转导致数据错误或者未定义行为。