C语言非常道
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

4.6.4 认识整型转换阶和整型提升

有些运算符只在操作数原有的类型上操作,例如前缀递增运算符和后缀递增运算符,它们不要求改变操作数的类型。再比如赋值运算符,它不要求改变左操作数的类型,而是要求右操作数必须转换为左操作数的类型。

相比之下,有些运算符的操作数并不是在它原来的类型上操作,尤其是那些需要两个操作数的运算符。不过这也可以理解,如果操作数的类型不同,值的表示方法也不同,这势必要求它们先转换为一致的类型才能运算。下面以一个程序为例来说明这种转换如何进行。

              /**********c0408.c*********/
              int main(void)
              {
                  signed char cx = 1, cy = 2;
                  signed long int sl = 0;
                  unsigned long int ul = 0;

                  sl += cx + cy;
                  sl += cx * 3L;
                  ul += sl <= ul;

                  unsigned char uc = -1;
                    cy = - uc ++;
                }

在这里,表达式sl += cx + cy用于将子表达式cx + cy的值加到左值sl。在子表达式中,左值cx和cy的类型都是signed char,是不是意味着子表达式cx + cy的类型也是signed char?

非常遗憾的是,非也。我们知道,int和unsigned int类型的长度并不是固定的,随不同的计算机系统而异。在C语言的设计者看来,int和unsigned int类型通常应该等于你所用的计算机的自然字长——比如,在16位处理器的计算机上,int类型的长度通常是16个比特,在32位处理器的计算机上,int类型的长度通常是32个比特。

计算机的字长通常等于处理器内部的寄存器宽度,这样就很清楚了:以自然字长来加工和操作数据效率最高。C是追求效率的计算机编程语言,它希望包括二元+在内的很多运算符能够以计算机的自然字长来操作。所以它会想:先看一下这两个操作数的类型,看看有没有比int或者unsigned int短的。如果有,那就先把它加长到int或者unsigned int。加长之后,如果这两个操作数的类型一致,那太好了;如果不一致,再继续以那个较长的为标准来转换那个较短的,使它们最终一致。无论如何,第一步,也是最保守的做法是先将短的类型加宽为int或者unsigned int。

但是,整数类型那么多,到底谁是长的,谁是短的?为此,C语言引入了整型转换阶的概念。整型转换阶用于确定加宽的方向,即,加宽为何种类型。每种整数类型都有自己的整型转换阶,阶的大小主要取决于类型的宽度,图4-8给出了所有标准整数类型的整型转换阶。

图4-8 标准整数类型的转换阶

由图中可知,每个有符号整数类型的阶等于与其相对应的无符号整数类型;_Bool类型的阶最低;long long int和unsigned long long int类型的阶最高。

对于包括二元+在内的很多运算符来说,C语言规定,如果一个操作数相对于int类型来说较窄,但它的值能用int类型来表示,则将其转换为int类型;如果无法表示,则转换为unsigned int类型,这个过程叫作整型提升。

那么,怎样才算是比int类型窄呢?标准之一就是它的整型转换阶小于int和unsigned int类型。显然,_Bool、char、signed char、unsigned char、short int和unsigned short int类型的操作数都必须先做整型提升。

这里有两点需要说明,第一,整型提升是一种特殊的整数类型转换,特指从阶较低的整数类型转换(提升)为int或者unsigned int类型,从int类型转换到long int类型并不是整型提升;第二,并不是所有运算符的操作数都需要做整型提升,例如递增和递减运算符的操作数就不需要,即使它们是整数类型。

让我们继续来看表达式cx + cy,左值cx和cy的类型都是signed char,所以它们都必须进行整型提升,提升为int类型,故表达式cx + cy的类型是int。

原则上,运算符的操作数在整型提升后应具有一致的类型,如果不一致的,还必须做进一步的转换。总的原则是,阶较低的整数类型转换为阶较高的整数类型。

来看表达式sl += cx * 3L的子表达式cx * 3L,常量表达式3L的类型是signed long int,而左值cx的类型是signed char。首先将cx的值从signed char类型提升为int类型。提升后类型仍不一致,故必须将cx的值再次从int类型转换为signed long int类型。换句话说,子表达式cx * 3L的类型是signed long int。

再来看表达式ul += sl<= ul的子表达式sl<= ul,左值sl的类型是signed long int,而左值ul的类型是unsigned long int,这两种整数类型的阶都高于int和unsigned int,所以这里不存在整型提升,但它们仍不是同一种类型。如果提升之后两个操作数的类型不同,一个是有符号整数类型,另一个是无符号整数类型,且无符号整数类型的阶高于或者等于那个有符号整数类型,则将有符号整数类型的操作数转换为那个无符号整数类型。因此,必须将sl的值从signed long int转换为unsigned long int类型。

然而,表达式sl<= ul的类型会是unsigned long int吗?不会的,我们说过,关系表达式的类型始终为int。这里的奥妙在于,操作数sl和ul的值统一在unsigned long int层面上进行比较操作,然后根据比较的结果生成int类型的0或者1。

练习4.8

在上面的程序中,表达式sl += cx * 3L和ul += sl<= ul的类型各是什么,为什么?

4.6.4.1 负号运算符

细心的同学可能已经发现了,整型常量里没有负数。我们在编程时不可避免地会用到负数,例如-67,但是在C语言里,67是整型常量表达式,前面的负号是运算符,称为负号运算符。换句话说,在C语言里,负数是通过“运算”得到的,它将一个整型常量表达式的值转换为一个负的内部表示。

负号运算符需要一个右操作数,如果这个操作数是整数类型,必须先整型提升,负号运算符的结果是提升后的负值,结果的类型是提升后的类型;如果操作数不是整数类型,则负号运算符的结果是其操作数的负值,结果的类型与其操作数的类型相同。

在上面的程序里,末尾部分有一个声明:

              unsigned char uc = -1;

在这里,负号运算符的操作数1是整型常量表达式,按要求必须先做整型提升,但其类型已经是int,名义上要做但是没做。最终,表达式-1的类型也是int。

表达式-1的值用于初始化变量uc,变量uc的类型是unsigned char,这就要把表达式-1的值从原先的int类型转换为unsigned char类型。在我的机器上,signed char类型的最大值是255,所以转换的方法是256 +(-1)= 255(参见前面的整数-整数转换)。这就是说,将表达式-1的值赋给unsigned char类型的变量,将使该变量的值为unsigned char类型所能表示的最大值。

推而广之,将表达式-1的值转换为任何一种无符号整数类型,其结果是得到这种无符号整数类型的最大值。

最后来看表达式cy = - uc ++,在这里,后缀递增运算符的优先级最高,负号运算符的优先级次之,赋值运算符的优先级最低,故它等价于cy = -(uc ++)。递增运算符不改变其操作数的类型,而且,递增表达式的类型也是其操作数的类型。因为左值uc的类型是unsigned char,故表达式uc ++的类型也是unsigned char。

作为负号运算符的操作数,表达式uc ++的值要从原先的unsigned char类型提升为int类型,而且这也是表达式-uc ++的类型。提升的过程也是类型转换的过程,由于unsigned char类型的值总能用int类型来表示,故转换(提升)后的值不变。

因为赋值运算符左操作数cy的类型是signed char,所以表达式-uc ++的值还要从int类型转换为signed char类型。

我们已经讲过整数—整数转换,如果表达式-uc ++的值能够被signed char类型表示,则转换后的值不变;如果不能,因目标类型是有符号整数类型,故转换后的结果不能确定。在我的机器上,转换前,表达式-uc ++的值是255,但不能被signed char类型表示,故转换后的值无法预知。

不同的运算符需要不同的操作数,需要做不同的转换。每种运算符需要什么类型的操作数,如果操作数的类型不同该如何转换,都将在本书的后面逐一介绍。

练习4.9

1.如果知道unsigned char、unsigned int、unsigned long int和unsigned long long int类型所能表示的最大值(在你的计算机上)?编写一个程序,然后在gdb中观察一下到底是多少。

2.表达式-3u的结果是多少?结果的类型是什么?

4.6.4.2 转型运算符

一般来说,整数之间的转换是自动进行的。当然,如果你不嫌麻烦的话,也可以手工进行转换,手工转换的方法是使用转型表达式。转型表达式由转型运算符组成,其形式为

类型名表达式

在C语言里,有三种运算符非常相似,都使用了一对圆括号,它们是函数调用运算符、转型运算符和基本表达式。不过,它们之间的区别也相当明显:函数调用运算符的操作数在左边和圆括号内;转型运算符的操作数在右边;基本表达式的操作数在圆括号内。

转型表达式的作用是将“表达式”的值从它原先的类型转换为“类型名”指定的那种类型。举个例子来说,在以下声明中,是将整型常量0x33从它原先的int类型转换为long long int类型后,再用于初始化变量ll:

              long long ll =(long long)0x33;

注意,被转型的表达式里可能含有别的运算符,如果它们的优先级低于转型运算符,则被转型的表达式应当用圆括号括起来以形成基本表达式,例如:

              signed char cx, cy;
              cx =(signed char)0x33;
              cy =(signed char)(cx + 0x30);

以上,转型运算符的优先级高于赋值运算符,表达式cx =(signed char)0x33是将整型常量0x33从它原来的int类型转换为signed char类型,然后赋给左值cx。

在表达式cy =(signed char)(cx + 0x30)里,是将表达式cx的值从它原先的signed char类型提升为int类型,再与int类型的0x30相加,得到一个int类型的结果。这个结果再转型为signed char类型,赋给变量cy。

转型运算符的优先级高于加性运算符,所以表达式cx + 0x30必须用圆括号括住。如果没有这个圆括号,意思就完全不同了:

              cy =(signed char)cx + 0x30;

这是将表达式cx的值转换为signed char类型后,再与int类型的0x30相加。顺便说一句,在相加之前,表达式(signed char)cx的值还要作整型提升。