C 语言给程序员提供了相当大的自由度并允许不同数值类型可以自动转换。由于某些功能
性的原因可以引入显式的强制转换,例如:
1. 用以改变类型使得后续的数值操作可以进行
2. 用以截取数值
3. 出于清晰的角度,用以执行显式的类型转换
为了代码清晰的目的而插入的强制转换通常是有用的,但如果过多使用就会导致程序的
可读性下降。正如下面所描述的,一些隐式转换是可以安全地忽略的,而另一些则不能。
存在三种隐式转换的类别需要加以区分。
1. 整数提升(Integralpromotion)转换:
整数提升描述了一个过程,借此过程数值操作总是在 int 或long (signed 或unsigned )整型操作数上进行。其他整型操作数(char 、short 、bit-field和enum)在数值操作前总是先转化为int 或unsigned int 类型。这些类型称为 small integer 类型。
整数提升的规则命令,在大多数数值操作中,如果int 类型能够代表原来类型的所有值,
那么small integer 类型的操作数要被转化为 int 类型;否则就被转化为 unsigned int 。
注意,整数提升:
1) 仅仅应用在smallinteger 类型上
2) 应用在一元、二元和三元操作数上
3) 不能用在逻辑操作符(&&、|| 、!)的操作数上
4) 用在switch 语句的控制表达式上
整数提升经常和操作数的“平衡”(balancing ,后面提到)发生混淆。事实上,整数提升发生在一元操作的过程中,如果二元操作的两个操作数是同样类型的,那么也可以发生在二元操作之上。由于整数提升,两个类型为unsigned short 的对象相加的结果总是 signed int 或unsigned int类型的;事实上,加法是在后面两种类型上执行的。因此对于这样的操作,就有可能获得一个其值超出了原始操作数类型大小的结果。例如,如果int 类型的大小是32位,那么就能够把两个short(16位)类型的对象相乘并获得一个 32位的结果,而没有溢出的危险。另一方面,如果int类型仅是16位,那么两个16位对象的乘积将只能产生一个 16位的结果,同时必须对操作数的大小给出适当的限制。
2. 赋值转换:
赋值转换发生在:
1) 赋值表达式的类型被转化成赋值对象的类型时
2) 初始化表达式的类型被转化成初始化对象的类型时
3) 函数调用参数的类型被转化成函数原型中声明的形式参数的类型时
4) 返回语句中用到的表达式的类型被转化成函数原型中声明的函数类型时
5) switch-case 标签中的常量表达式的类型被转化成控制表达式的提升类型时。这个转换仅用于比较的目的。
每种情况中,必要时数值表达式的值是无条件转换到其他类型的。
3. 平衡转换(Balancingconversions):
平衡转换的描述是在ISO C[2] 标准中的“UsualArithmetic Conversions”条目下。这套规则提供一个机制,当二元操作符的两个操作数要平衡为一个通用类型时或三元操作符(? : )的第二、第三个操作数要平衡为一个通用类型时,产生一个通用类型。平衡转换总是涉及到两个不同类型的操作数;其中一个、有时是两个需要进行隐式转换。整数提升(上面描述的)的过程使得平衡转换规则变得复杂起来,在整数提升时,small integer 类型的操作数首先要提升到int 或unsigned int 类型。整数提升是常见的数值转换,即使两个操作数的类型一致。
与平衡转换明显相关的操作符是:
1) 乘除 *、/ 、%
2) 加减 +、-
3) 位操作 &、^、|
4) 条件操作符 (… ? … : …)
5) 关系操作符 >、>=、< 、<=
6) 等值操作符 == 、!=
其中大部分操作符产生的结果类型是由平衡过程产生的,除了关系和等值操作符,它们
产生具有int 类型的布尔值。 要注意的是,位移操作符(<<和>>)的操作数不进行平衡,运算结果被提升为第一个操作数的类型;第二个操作数可以是任何有符号或无符号的整型。
类型转换过程中存在大量潜在的危险需要加以避免:
1) 数值的丢失:转化后的类型其数值量级不能被体现
2) 符号的丢失:从有符号类型转换为无符号类型会导致符号的丢失
3) 精度的丢失:从浮点类型转换为整型会导致精度的丢失
对于所有数据和所有可能的兼容性实现来说,唯一可以确保为安全的类型转换是:
1) 整数值进行带符号的转换到更宽类型
2) 浮点类型转换到更宽的浮点类型
当然,在实践中,如果假定了典型类型的大小,也能够把其他类型转换归类为安全的。
普遍来说,采取的原则是,利用显式的转换来辨识潜藏的危险类型转换。
类型转换中还有其他的一些危险需要认清。这些问题产生于C 语言的难度和误解,而不
是由于数据值不能保留。
1. 整数提升中的类型放宽:整数表达式运算的类型依赖于经过整数提升后的操作数的类型。总是能够把两个8 位数据相乘并在有量级需要时访问 16 位的结果。有时而不总是能够把两个16 位数相乘并得到一个 32 位结果。这是 C 语言中比较危险的不一致性,为了避免混淆,安全的做法是不要依赖由整数提升所提供的类型放宽。
考虑如下例子:
INT16U u16a = 40000; /* unsigned short / unsigned int */
INT16U u16b = 30000; /* unsignedshort / unsigned int */
INT32U u32x; /* unsigned int / unsigned long */
u32x = u16a + u16b; /*u32x = 70000 or 4464 ? */
期望的结果是70000,但是赋给 u 的值在实际中依赖于 int 实现的大小。如果 int 实现的大小是32 位,那么加法就会在有符号的 32 位数值上运算并且保存下正确的值。如果 int 实现的大小仅是16 位,那么加法会在无符号的 16 位数值上进行,于是会发生折叠(wraparound )现象并产生值4464(70000%65536 )。无符号数值的折叠(wraparound)是经过良好定义的甚至是有意的;但也会存在潜藏的混淆。
2. 类型计算的混淆:程序员中常见的概念混乱也会产生类似的问题,人们经常会以为参与运算的类型在某种方式上受到被赋值或转换的结果类型的影响。例如,在下面的代码中,两个16 位对象进行 16 位的加法运算(除非被提升为 32 位int),其结果在赋值时被转换为INT32U 类型。
u32x = u16a + u16b;
并非少见的是,程序员会认为此表达式执行的是32 位加法――因为 u32x 的类型。
对这种特性的混淆不只局限于整数运算或隐式转换,下面的例子描述了在某些语句中,
结果是良好定义的但运算并不会按照程序员设想的那样进行。
u32a = (INT32U_t) (u16a * u16b);
f64a = u16a / u16b ;
f32a = (float32_t) (u16a / u16b) ;
f64a = f32a + f32b ;
f64a = (float64_t) (f32a + f32b) ;
3. 数学运算中符号的改变:整数提升经常会导致两个无符号的操作数产生一个(signed )int类型的结果。比如,如果 int 是32位的,那么两个 16 位无符号数的加法将产生一个有符号的32 位结果;而如果 int 是16 位的,那么同样运算会产生一个无符号的 16 位结果。
4. 位运算中符号的改变:当位运算符应用在无符号短整型时,整数提升会有某些特别不利
的反响。比如,在一个unsigned char 类型的操作数上做位补运算通常会产生其值为负的(signed )int 类型结果。在运算之前,操作数被提升为 int 类型,并且多出来的那些高位被补运算置1。那些多余位的个数,若有的话,依赖于int 的大小,而且在补运算后接右移运算是危险的。
为了避免上述问题产生的危险,重要的是要建立一些准则以限制构建表达式的方式。这里首先给出某些概念的定义。
表达式的类型是指其运算结果的类型。当两个 long 类型的值相加时,表达式具有 long 类
型。大多数数值运算符产生其类型依赖于操作数类型的结果。另一方面,某些操作符会给出
具有int 类型的布尔结果而不管其操作数类型如何。所以,举例来说,当两个long 类型的项用系运算符做比较时,该表达式的类型为int。 术语“基本类型”的定义是,在不考虑整数提升的作用下描述由计算表达式而得到的类型。当两个int 类型的操作数相加时,结果是 int 类型,那么表达式可以说具有 int 类型。 当两个unsigned char 类型的数相加时,结果也是 int 类型(通常如此,因为整数提升的原因),但是该表达式基本的类型按照定义则是unsignedchar 。 术语“基本类型”不是C 语言标准或其他C 语言的文本所认知的,但在描述如下规则时它很有用。它描述了一个假想的对C 语言的违背,其中不存在整数提升而且常用的数值转换一致性地应用于所有的整数类型。引进这样的概念是因为整数提升很敏感且有时是危险的。整数提升是C 语言中不可避免的特性,但是这些规则的意图是要使整数提升的作用能够通过不利用发生在smallinteger 操作数上的宽度扩展来中和。
当然,C 标准没有显式地定义在缺乏整数提升时 small integer 类型如何平衡为通用类型
尽管标准确实建立了值保留(value-perserving)原则。
当int 类型的数相加时,程序员必须要确保运算结果不会超出 int 类型所能体现的值。如
果他没有这样做,就可能会发生溢出而结果值是未定义的。这里描述的方法意欲使得在做small integer 类型的加法时使用同样的原则;程序员要确保两个unsigned char 类型的数相加的结果是能够被unsigned char 体现的,即使整数提升会引起更大类型的计算。换句话说,对表达式基本类型的遵守要多于其真实类型。
整数常量表达式的基本类型
C 语言的一个不利方面是,它不能定义一个 char 或short 类型的整型常量。比如,值“5 ”
可以通过附加的一个合适的后缀来表示为int、unsigned int 、long或unsigned long 类型的常量;但没有合适的后缀用来创建不同的char 或short 类型的常量形式。这为维护表达式中的类型一致性提出了困难。如果需要为一个unsigned char 类型的对象赋值,那么或者要承受对一个整数类型的隐式转换,或者要实行强制转换。很多人会辩称在这种情况下使用强制转换只能导致可读性下降。在初始化、函数参数或数值表达式中需要常量时也会遇到同样的问题。然而只要遵守强类型(strong typing)原则,这个问题就会比较乐观。
解决该问题的一个方法是,想象整型常量、枚举常量、字符常量或者整型常量表达式具
有适合其量级的类型。这个目标可以通过以下方法达到,即延伸基本类型的概念到整型常量
上,并想象在可能的情况下数值常量已经通过提升想象中的具有较小基本类型的常量而获得。
这样,整型常量表达式的基本类型就可以如下定义:
1. 如果表达式的真实类型是(signed )int,其基本类型就是能够体现其值的最小的有符
号整型。
2. 如果表达式的真实类型是unsigned int ,其基本类型就是能够体现其值的最小的无符
号整型。
3. 在所有其他情况下,表达式的基本类型与其真实类型相同。
在常规的体系结构中,整型常量表达式的基本类型可以根据其量级和符号确定如下:
无符号值
0U |
to |
255U |
8 bit unsigned |
256U |
to |
65535U |
16 bit unsigned |
65536U |
to |
4294967295U |
32 bit unsigned |
有符号值
-2147483648 |
to |
-32769 |
32 bit signed |
-32768 |
to |
-129 |
16 bit signed |
-128 |
to |
127 |
8 bit signed |
128 |
to |
32767 |
16 bit signed |
32768 |
to |
2147483647 |
32 bit signed |
注意,基本类型是人造的概念,它不会以任何方式影响实际运算的类型。这个概念的提
出只是为了定义一个在其中可以构建数值表达式的安全框架。
后面章节中描述的类型转换规则在某些地方是针对“复杂表达式”的概念。术语“复杂
表达式”意味着任何不是如下类型的表达式:
1) 非常量表达式
2) lvalue (即一个对象)
3) 函数的返回值
应用在复杂表达式的转换也要加以限制以避免上面总结出的危险。特别地,表达式中的
数值运算序列需要以相同类型进行。
以下是复杂表达式:
s8a + s8b
~u16a
u16a >> 2
foo (2) + u8a
*ppc + 1
++u8a
以下不是复杂表达式,尽管某些包含了复杂的子表达式:
pc[u8a]
foo (u8a + u8b)
++ppuc
** (ppc + 1)
pcbuf[s16a * 2]
1) 有符号和无符号之间没有隐式转换
2) 整型和浮点类型之间没有隐式转换
3) 没有从宽类型向窄类型的隐式转换
4) 函数参数没有隐式转换
5) 函数的返回表达式没有隐式转换
6) 复杂表达式没有隐式转换
限制复杂表达式的隐式转换的目的,是为了要求在一个表达式里的数值运算序列中,所有的运算应该准确地以相同的数值类型进行。注意这并不是说表达式中的所有操作数必须具备相同的类型。