C语言学习指南:从规范编程到专业级开发
上QQ阅读APP看书,第一时间看更新

5.4.1 隐式类型转换

如果某条表达式里面的两个操作数(operand)不是同一个类型,那么程序会如何处理呢?比方说,把int型的值与float型的值相乘,或把short型的值与double型的值相减。

为了回答这个问题,我们重新看看第3章的sizes_ranges2.c程序。那个程序当时演示了各种数据类型所占据的字节数,有的占一个字节,有的占两个字节,有的占四个字节,还有很多类型占八个字节。

如果C语言发现表达式里面有不同类型的数据,那么首先会对(字节数)较小的那种数据做隐式转换(implicit conversion),把它转成(字节数)最大的那种数据所属的类型。这种转换是想把该数据从取值范围比较窄的类型转化成取值范围比较宽的类型。

考虑下面这段代码中的算式:

在这个算式中,feet变量与字面量3都是整数值,因此,这个除法表达式的结果也是整数。程序把这个整数赋给yards变量时,会将其隐式地(也就是自动地)转化成double类型,这样double的值就是3.0,这显然跟我们想要的结果不符。这个算式有两种修改办法,一种是把feet手工转换成double型,另一种是把字面量3改为3.0:

第一条语句先把feet变量的值手工转换成double类型,然后再做除法,这样计算出来的结果也是double类型。第二条语句采用3.0作除数,这是一个double型的字面量,因此feet变量与它相除的结果也是double类型。无论你采用哪一条语句来写程序,它都会把计算结果直接赋给yards变量,而不像刚才那样,必须先把结果从int型转为double型,然后再赋值。现在的计算结果是3.666667,这与我们预想的相符。

你在调用函数时所传的实参值,如果跟函数定义中所写的参数类型不符,那么程序也会试着执行隐式类型转换。

像这样把值从某个较小的类型转换成较大的类型是很简单的,例如把整数值从某个表示范围比较窄的类型,转换成某个表示范围比较宽的类型,或者把小数从单精度浮点数类型转换成双精度浮点数类型。

我们考虑下面这段函数声明与调用代码:

add()函数有两个参数,它们的类型都是long int,占8个字节。我们在调用add()函数时传入的那两个变量(也就是值为254的b1变量与值为253的b2变量)其类型只占1个字节。因此,程序会把这两个变量的值从unsigned char隐式地(也就是自动地)转换成long int,然后将转换后的值分别复制到add()函数的i1参数与i2参数里面。调用完函数之后,得到的结果是507,这个结果是正确的。

许多整数值都可以顺利转换成单精度型(float)或双精度型(double)的浮点数值。如果你把int型的整数(这种整数占4个字节)与float型的浮点数(这种浮点数也占4个字节)相乘,那么程序就会执行隐式转换,它会把int值转换成float值。这种乘法表达式的结果默认也是float型。

如果你把short型的整数值(这种值占2个字节)与double型的浮点数值(这种值占8个字节)相减,那么程序会对short值做两次转换,首先把它转换成long类型的整数值(这种值占8个字节),然后再将这个long类型的整数值转换成double型的浮点数值。这样一条减法表达式的结果默认是double类型。如果这条减法表达式是某个复合表达式中的一部分,那么程序可能还会对减法表达式的结果继续做出转换。具体怎么转,要看复合表达式里的下一项运算所涉及的操作数有没有明确指定类型。如果明确指定了,那么程序可能把结果转换成那个操作数所属的类型;如果没有明确指定,那么程序会把结果转换成各操作数里面取值范围最宽的那个类型。

然而,如果你把表达式的结果(无论这种结果的类型是你明确指定的,还是程序根据C语言的默认规则所确定的)赋给某个取值范围比较小的变量,那么就有可能导致数据不准确(这也叫作精度丢失)。对于整数来说,这样做会导致权重较高的那些二进制位丢失。例如将32000000(二进制形式为00000001 11101000 01001000 00000000)这样的int值(这种值占4个字节)转换成char类型的值(这种值占1个字节),结果肯定是0。因为权重较高的那24个二进制位,或者说,权重较高的那3个字节,会在转换之后丢失。对于实数来说,这种转换会造成截取(truncation)误差或舍入(rounding)误差。把double型的浮点数转化成float型的浮点数会引发舍入或截取,具体情况要看编译器的实现方式。把float型的浮点数转化成int型的整数会导致小数部分丢失,因为这一部分在转换时被截断了。

考虑下面这段代码:

这段代码跟早前那段相比只有一个区别,就是r1变量的类型不同。它现在成了一个单字节的unsigned char型变量。程序执行这段代码时会先把b1与b2的值拓宽成long int型,然后传入add()函数并得到一个long int型的结果,但由于这次接收该结果的变量r1是一个单字节的类型,因此程序必须将long int型的结果截取为一个字节,以便赋给r1。这样计算出来的结果是252,这个结果是错误的。

如果你要编写的表达式比较复杂,而你又要求计算结果必须相当精确,那么在计算的过程中,最好把操作数转化成其中最宽的那种数据类型,等到有了最终的结果,再将其转换成比较窄的类型。

我们写一个简单的程序来测试一下。我们在这个truncRounding.c程序里面定义两个函数,一个接受double值作参数,并将其打印出来,另一个接受long int值作参数,并将其打印出来。通过这个程序,我们可以看到C语言会如何将调用函数时所传入的参数值转换成函数定义里面所要求的那种类型:

我们还没有讲解怎样用printf()函数打印各种形式的值,大家现在只需要知道:%.2f这个格式说明符的意思是把double值显示到小数点后面第2位;%ld这个格式说明符的意思是显示一个长整数(long int)。详细的含义我们会在第19章讲解。

请大家录入这段代码,然后保存文件,最后编译并运行程序。你应该会看到类似下面这样的输出信息。

请注意,程序在调用longintFunc(floatValue)时需要把58.73转换成long int型的整数。这时它并没有根据小数点后第一位做四舍五入,而是直接丢弃小数部分。这种处理方式称为截取(truncation),也叫作截断或截尾。程序在调用doubleFunc(intValue)时需要把58这样的short int值转换成double值,这个转换是没有问题的,另外,程序在执行doubleFunc(floatValue)时要把float值转换成double值,这个转换也没有问题。

另外还要注意,虽然把float转为long int会丢失精度,但编译器在编译trunc-Rounding.c这个源代码文件的时候并没有报错[1],而且我们在运行编译之后的程序时也不会看到警示信息。

[1] 如果使用gcc来编译这份文件,并加上-Wconversion选项,那么编译器就会对有可能导致值发生变化的隐式转换现象给出警告。——译者注