3.4 右值的类型转换

所谓的“转换”(Conversions)是一个含义很广的概念。狭义的“转换”是指由某个值得到指定类型的对应值。比如由int类型的3得到double类型的3.。这种转换通常称之为“类型转换”(Type Conversion)。经验表明,“类型转换”这种译法很容易引起初学者的误解,所以在本书把这种转换明确地称之为“值的类型转换”。

无法想象把一个左值这种具有特定类型的表示一块特定存储单元(即对象,Object)的东西转变成另一种类型的左值,这是根本不可能的。所以,“值的类型转换”都指的是“右值的类型转换”。

这种“右值的类型转换”有的是在代码中直接通过运算符直接挑明的,被叫做显式转换(Explicit Conversion);而另一种并不在代码中写明,但无论你是否意识到与否,编译器都会替你完成的值的转换,叫做隐式转换(Implicit Conversion)。由于后者可能发生于你不知不觉的情况下,所以更为复杂、更为危险。一定要清醒地了解这种隐式转换会在何种情况下发生。下面首先介绍相对较为简单的显式转换。

3.4.1 明确写出的显式转换——cast运算

在C语言中,可以通过运算求得与某个值相对应的指定类型的值,这种运算叫转换运算(Cast operate)。

转换运算符(Cast operators)是由一对括号和括号内的类型的名字构成的:

(类型名)值

例如:

3.4 右值的类型转换 - 图1

其含义是求与double类型的值-3.4对应的int类型的值,亦即-3——小数点后面的值被舍弃。

有不少初学者对这个运算常产生一种误解,他们常误以为转换的对象被改变了,例如:

3.4 右值的类型转换 - 图2

往往使他们以为i的类型被改成了double类型,实际上这是不可能发生的事情。

(double)i表示的确切含义是由i的值得到一个对应的double的值5.——在整数后加上一个小数点就可以了。造成这种误解的原因恐怕是因为大家给这个运算的“强制类型转换”这个翻译不是很恰当。实际上cast的原意是“铸造”、“翻造”的意思,也看不出有丝毫“强制”的含义。

所以,cast运算仅仅是从某个值依照某种规则得到一个另外类型的对应值而已。这么说当然不如像说“强制类型转换”那么酷,但这的确是这种运算的全部含义——不多也不少。不要对cast运算有更多想入非非的非分之想,否则倒霉的只能是你自己。

显然,cast运算是一元运算。

3.4.2 cast运算的规则

这种规则估计至少有几十条,没必要记住那么多规则,那样完全违背C的精神。只需要简单的记住并理解若干原则就可以了。

1.浮点类型值→整数类型值

基本原则是把小数部分舍弃,这里没有四舍五入。但具体还可能有如下几种可能。

(1)浮点值舍弃小数部分后,在所转换的整数类型的表示范围之内。这种情况没什么好解释的。

(2)浮点值舍弃小数部分后,不在所转换的整数类型的表示范围之内:未定义行为。

2.浮点类型值→浮点类型值

这种转换同样有若干可能。从短的数据向长的数据,例如把float类型的值转换成double的类型或把double类型的值转换成long double的类型,由于前者的值属于后者的子集,所以会得到类型不同的一个相同的值。

从长的数据向短的数据,可能有下面几种可能。

(1)值不变。恰好两种类型都可以表示这个值。

(2)得到一个近似值。长的数据短类型无法表示,但在短类型表示的数据区间之内,这时得到的是一个最接近原来数值的近似值。但得到是比原来大些的最近值还是比原来小的最近值是编译器决定的。

(3)未定义行为。长的数据不在短类型的表示范围。

3.整数类型值→浮点类型值

这种转换有以下3种可能。

(1)值不变,只是改变了表示方式而已。恰好两种类型都可以表示这个值的情形。

(2)得到一个最接近的近似值。整数类型值在浮点类型的表示范围之内,但浮点类型无法精确表示的情况。

(3)未定义行为。整数类型值不在浮点类型的表示范围的情况。

4.整数类型值→整数类型值

和一般人的初感不同的是,这种情况是最复杂的情况。因为涉及到了signed和unsigned对符号位的解释、负数的表示形式(11)、数据的长短等诸多因素。

C语言对这种情况处理的一般原则是转换之后的值应当与源值相等。

但是许多情况下这是做不到的。在这种情况下,如果是转换成无符号整数类型,C语言规定了运算结果;如果是转换成有符号的整数类型,结果取决于编译器。下面以补码存储格式并假定long long类型为8字节长,int类型为4字节长进行说明。

(1)(long long)1234

由于1234是int类型,且long long的表示范围涵盖了int类型的表示范围,这是结果值与源值相等的情况。在机器内部是由:

3.4 右值的类型转换 - 图3

得到了:

3.4 右值的类型转换 - 图4

(2)(int)-1234LL

这是long long数据转换成int类型的例子,但由于-1234也在int类型表示的范围之内,所以转换成立,在机器内部由:

3.4 右值的类型转换 - 图5

得到了一个:

3.4 右值的类型转换 - 图6

(3)(unsigned) 69259509840

这个例子中69259509840是long long类型的,属于较长的数据转换为较短的数据,但由于最终的结果是无符号整数类型,转换依然可以得到结果,这个结果可以简单地把高位截去得到。从:

0000 00000 00000 0000 0000 0000 00001 0000 0010 0000 0011 0000 0100 0000 0101 0000得到了:

3.4 右值的类型转换 - 图7

这实际上就是69259509840对232求余的结果。但是下面的例子可能让人有些沮丧。

(4)(int) 69259509840

尽管这也是从8个字节的整数类型转换成4个字节的整数类型,但C语言没有规定结果应该是什么,因为存在溢出。但这并非是说这个表达式存在语法问题,只是结果究竟是什么却的确是个问题。站在C语言的角度来说,结果是不确定的,因为各种不同的编译器可能会给出不同的结果或反应。这叫做“结果是实现定义的”(Result is implementation-defined)——C语言放权了。这种情况多发生于没有统一的数学运算定义的情况下(比如把十进制-123截断成两位究竟应该得到-3、-23还是23呢?)或者各个处理器处理方法无法统一的情况。但是,简单的截取目标所需要的低位字节数目是多数编译器在这种情况下的处理手段,因此(int) 69259509840得到的也是0010 0000 0011 0000 0100 0000 0101 0000,至少在DEV C++这个编译器中是这样处理的。

所以,假如是采用补码的情况,整数类型之间转换的原则可以概括为如下。

■ 短→长:填充符号

■ 长→短:削足适履

■ 同→同:内容一致

后面两种情况目标值可能与原值不同,当目标值类型为有符号整数类型时,如果源值不在目标值的范围内,得到的是一个不具备可移植性的编译器结果。

练习

1.(double)30的值是什么?(int)-4.5呢?

2.(unsigned)-1的值是什么?

3.4.3 赋值中的转换

用另一种类型数据进行赋值,发生类型转换是必然的。C语言规定,赋值号右边的表达式的值必须转换成赋值左边的数据类型。比如:

3.4 右值的类型转换 - 图8

由于C语言崇尚简洁,所以通常i = (int)1.2这样的表达式也可以写成i =1.2。后者尽管没有明显地写出转换的运算,但这个转换依然会由编译器自动完成,这是隐式转换发生的情形之一。在C语言中,这是一条很普遍的潜规则,除了=运算,其他一些运算以及在另外一些场合下都存在这种表面上并不明说的转换。当然,如果你喜欢明明白白地说出来也未尝不可,这纯粹是个风格问题。

在赋值运算中,“=”右边的值最终要转变为“=”左边的变量或表达式的类型,这可能会导致精度的降失(demotion)。当发生降失的转换时,比如浮点量赋值给一个整形量,浮点量小数点后面的值将被直接舍弃而不是四舍五入。也就是说表达式(假定i为int类型的变量):

3.4 右值的类型转换 - 图9

求值完成后,i的值为5。

3.4.4 1+1.0=?

这个在一般人看起来毫无问题的问题,在计算机世界里其实是很成问题的。

首先,计算机里并没有4个字节的int类型量与8个字节的double类型量相加这样的指令,据我所知,至少到目前为止还没有。甚至两个int类型量的相加与两个double类型量的相加——尽管同样是加法但翻译成的计算机指令是不同的,本质上它们是两种不同性质的CPU动作。如果思考一下345+456与1.2×103+3.4×10-5这两个算术运算的区别,这个道理并不难理解。

其次,计算结果应该等于2还是2.0呢?也就是说结果应该是4个字节的int类型量还是8个字节的double类型量呢?

运算符的运算规则告诉我们,只有两个相同类型的整数类型量或浮点类型量才可以相加,这是规则。所以仔细推敲起来,标题中的算式的确是有些不伦不类的。

然而C语言也并不严守着刻板不变的法则。在意义明确的前提下,C语言相当灵活而变通。事实上,标题那样的表达式在C语言是容许的,它的真实意义是:

3.4 右值的类型转换 - 图10

尽管没有明说,但潜台词的确如此。C语言允许在一定的潜规则后面写出一些看似不通的表达式代码。

实际上,混合类型之间的运算是代码中应该努力避免的东西。因为这里面存在着一些可能你并不清楚了解的、隐含的类型转换。如果无法避免,那么最好使用cast运算进行显式的转换(Explicit Conversion),因为这样使代码更明确。C语言允许混合运算的表达式体现的是给予程序员自由的精神(12),只有在确保不滥用这种自由的前提下,你才有资格享受这种自由。

3.4.5 算术转换:早已废弃的规则和依然有效的规则

1.整数提升

C语言规定,在表达式中可以使用int或unsigned类型数据的场合皆可以使用char、short(包括signed和unsigned)类型数据。如果后一种类型所有的值都在int类型的表示范围之内,那么后者的值被转换成一个int类型的值,否则被转换成一个unsigned int类型的值。这叫整数提升(Integer Promotions)(13)

(一元)“+”、“-”运算和算术运算中存在这种情况。例如,假设:

3.4 右值的类型转换 - 图11

那么表达式+si实际上是一个int类型的值,甚至si表达式值本身就是一个int类型的值。再比如在函数调用时:

3.4 右值的类型转换 - 图12

其输出结果显然是:

3.4 右值的类型转换 - 图13

原因很简单,这里的c和97编译后都是同样的东西,都是int类型的值。而不是因为“C语言使字符型数据和整数之间可以通用”,C语言从来没说过那样的话。

2.“float类型一律转换为double型”吗?

当算术运算(+、-、*、\、%)符的两个操作数的类型不同时,编译器一般首先把两个操作数转换成一个公共类型。这个公共类型在多数情况下是两个操作数中的较高级别的类型(14),这种级别的高低大体上是依据数据类型表示范围的大小来划分的,依次为:

(1)long double

(2)double

(3)float

(4)整数类型

当算术运算符的两个操作数中有实浮点类型时,编译器按照如图3-1所示的决策方法进行类型转换。

3.4 右值的类型转换 - 图14

图3-1 有实浮点数时算术运算转换规则

这套规则在C89和C99中是一致的。

特别要注意的是,C语言标准并没有要求“float类型需一律转换为double型”。实际上float类型一律转换为double在刚刚发明C语言的时候是成立的,因为那时刚刚出现了处理double类型数据的协处理器。但在C89之后这个说法就不再成立了。而在C99中尤其增加了专门处理float类型的库函数。

但是在有时,float类型值还是转变成double类型的,比如:

3.4 右值的类型转换 - 图15

在这里的float类型的f被替换成了一个对应的double类型值。这发生在float类型作为函数参数(实参)的情形,但也不是“一律”。在后面的相应的章节将继续探讨这个问题。现在只要知道在printf()函数调用中的float类型的参数(实参)本质上是一个double类型的对应值就可以了。否则无法解释在printf()函数调用中double类型和float类型都可以用%f这个转换说明的原因。类似地,出现在printf()输出参数中的char类型值,也一律被编译器转换成it类型的值。

此外要说明的是,这种隐式的算术转换不仅仅发生在+、-、*、\、%运算中,关系运算(<、<=、>、>=),判等运算(==、!=),位运算(&、|、^)和条件运算(?:)中也同样存在,这些将在后面相关章节中进一步讨论。

3.整数转换阶(C99)

C99中的整数类型更多,为了便于描述整数类型的转换规则,C99提出了整数转换阶(Integer Conversion Rank)的概念。下面是转换阶由高到低的次序,同一行的类型转换阶相同。对于C89,只要不考虑long long和unsigned long long类型,规则是一样的。

3.4 右值的类型转换 - 图16

C语言规定两个整数类型进行算术运算时,首先进行整数提升(消灭比int类型低的类型),然后一律转换成转换阶较高的类型。例外的情况是转换阶较高的是有符号类型,另一个转换阶较低的是无符号类型(或者两者阶数相等),且前者的类型无法表示后者类型中的所有值,此时两个操作数都转换前者对应的无符号类型。

C99的这种描述给人的感觉似乎是更复杂了,然而对于广义的情形(更多的整数类型,包括扩展的整数类型的情形),这种描述是比较简洁的。

在Dev C++这种具体的环境下(long long: 8B, long: 4B, int: 4B, short: 2B, char: 1B)可以归纳为下面3条。

■ 首先,char,short→int(整数提升)。

■ 其次,按照int→unsigned→long→unsigned long→int long long→unsigned long long由低向高转换。

■ 例外,unsigned与long运算时,两者皆转换为unsigned long。

这种转换的潜规则在不同的书籍中可能有不同的称呼,如自动转换、隐式转换、提升或延展等,规则有些琐碎难记。运用这种规则书写程序唯一的好处是代码简洁,但可能带来的是代码难以理解、可读性差、可移植性差,一旦由于疏忽产生错误,这类错误往往是难于查找和定位的。因此依靠编译器进行进行隐式类型转换对于初学者来说可能经常是一种冒险。建议初学者如果需要进行类型转换,最好自己明确地写出转换表达式,而且赋值应该只在同一类型间进行。举例来说:

把3+5.0写成(double)3+5.0

同样如果需要把5.6赋值给一个int类型的变量i,那么最好写成:

3.4 右值的类型转换 - 图17

而不是:

3.4 右值的类型转换 - 图18

在逐渐熟悉转换规则之后,再逐步地过渡到隐式转换。

练习

求表达式3.5F+ -8%3*-4.2F/-5.F的值

4.对10+'a'+i*f-d/e表达式的分析

假设有:

3.4 右值的类型转换 - 图19

上面这个表达式10+'a'+i*f-d/e应该如何分析呢?

首先,要考察在这个表达式中出现的运算符都有哪些。在这个表达式中有“+”、“+”、“*”、“-”、“/”5个运算符。不难看出,在这个表达式中它们都是二元运算。这样就确定了这些运算的含义分别为“加”、“加”、“乘”、“减”、“除”。

由于乘、除运算的优先级相同且高于加、减运算,而乘、除运算的结合性是从左到右,所以应该首先确定*的运算对象。表达式等价于:

3.4 右值的类型转换 - 图20

同理,表达式亦等价于:

3.4 右值的类型转换 - 图21

然后再确定+、+、-这三个运算的运算对象。由于+、-运算的优先级相同,结合性为从左到右,所以上式最终等价于:

3.4 右值的类型转换 - 图22

无法真正确定这个表达式的运算次序,因为这是编译器的自由选择。C语言标准给予了编译器这种自由。但是该表达式的值是唯一确定的(否则就是一种错误的代码)。

由于该表达式中的运算对象有多种类型,因而存在隐式的值的类型转换。

第一个“+”的运算对象为10和'a',这是两个int类型的值,在运算时不存在类型转换。

由于i为int类型,f为float类型,所以根据前面提到的规则,if的真实含义是(float)if。同理,d/e实际上是d/(double)e。现在对分析结果稍微整理一下,可以得到:

3.4 右值的类型转换 - 图23

由于第二个“+”的运算对象分别为int和float类型,所以int类型的运算对象需要转换成float类型,得到一个float类型的值。又由于(d/(double)e)为double类型,所以(10 + 'a')+((float) i*f)所得到的float类型的值需要转换为double类型。这样最终可以看出表达式的确切含义是:

3.4 右值的类型转换 - 图24

这就是表达式10 + 'a' + i * f-d/e的真正含义。