2.7 类型转换
当一个操作符有几个不同类型的操作数时,会根据少量规则将几个操作数转换为一个公共的类型。
通常来说,仅有的自动转换,是在不丢失信息的情况下将“窄”的操作数转换为“宽”的类型,例如在 表达式 f + i 中将整数转换成浮点数。无意义的表达式,如用 float 做数组下标,是不允许的。可能丢失信息的表达式,如将一个较长的整数赋给较短的,或把浮点类型赋值给整数,可能会引起警告,但并非不合法。
char 就是小整数,因此 char 可以自由地用在算术表达式中。这为某些类型的字符转换带来相当可观的灵活性。例如下面这个简单实现的函数 atoi, 将数字字符串转换成对应的数值:
/* atoi: 把s转换成整数 */
int atoi(char s[])
{
int i,n;
n = 0;
for (i = 0; s[i] >= '0' && s[i] <= '9'; ++i)
n = 10 * n + (s[i] - '0');
return n;
}
如我们在第一章讨论的,表达式
s[i] - '0'
会得到s[i]中字符对应的数值,因为'0','1',...到 '9'的值是递增的。
另一个 char 到 int 转换的例子是函数 lower,能把ASCII字符集的单个字符转换成小写。如果该字符不是大写的,lower 将其原样返回。
/* lower: 把c转换成小写;仅用于ASCII码 */
int lower(int c)
{
if (c >= 'A' && c <= 'Z')
return c + 'a' - 'A';
else
return c;
}
这对ASCII编码是有效的,因为对应的大写和小写字母的数值之间有一个固定的差值,而且大小写字母表都是连续的——在A和Z之间只有字母。然而,第二个规则对EBCDIC字符集无效,因此如果用到EBCDIC字符集上,这个代码不只会转换字母。
标准库头文件 <ctype.h>(具体描述见附录B)中,定义了一系列不依赖于字符集的测试和转换函数。例如,函数 tolower 是可移植的,可替代上面写的 lower 函数。类似的,
c >= '0' && c <= '9'
上面这个测试可以替换为:
isdigit(c)
从现在开始我们都将用 <ctpye.h> 中的函数。
字符到整数的转换有个微妙之处。语言没有指定 char 类型的变量是有符号还是无符号值。当 char 被转换为 int 时,可能会得到负整数吗?答案随机器的不同而不同,反映了架构之间的差异。在某些机器上,最左端 bit 位为1的char 会被转换为负数(即“符号位扩展”)。而在另外的机器上,char 提升为 int 是通过在左端加 0 来做的,这样就总是正数。
C的定义保证,机器的标准打印字符集中的所有字符永不为负,因此这些字符在表达式中也总是为正值。但在字符变量中储存的任意位模式,可能在某些机器上看起来是负的,而在其他机器上是正的。为了可移植性,如果要在char 变量中保存非字符数据,总是指定 signed 或 unsigned。
关系表达式如 i > j,以及通过 && 和 || 连接起来的逻辑表达式,如果为真则值为1,为假则值为0。这样的话,下面的赋值:
d = c >= '0' && c <= '9'
如果c为数字则d的值被设为1,否则为0。然而,如 isdigt 这样的函数可能返回任意非0值来表示真。在 if、while 和 for 等的测试部分中,“真”即意味着“非0”,所以不会有区别。
隐式的算术转换,也大多如程序员的预期。通常来说,对有两个操作数的操作符(二元操作符)如 + 和 *,如果操作数的类型不一样,则在操作之前,“低”的类型会被提升为“高”的类型。结果得到高的类型。附录A的第6节精确描述了这些规则。然而,如果没有 unsigned 类型的话,下面这几条不太正式的规则就够用了:
若其中一个操作数为 long double,则把另一个也转换为long double。
否则,若其中一个操作数为 double,则把另一个也转换为 double。
否则,若其中一个操作数为 float,则把另一个也转换为 float。
否则,把 char 和 short 转换为 int。
然后,若其中一个操作数为 long,则把另一个也转换为 long。
注意,表达式中的 float 不会自动转换成double;这里对语言最初的定义做了修改。通常来说,像 <math.h> 中的算术函数会使用双精度。使用 float 的主要原因,是为了节省大数组的存储空间,稍微次要的原因,是为了在双精度计算特别“昂贵”的机器上节省时间。
当涉及 unsigned 操作数时转换规则更加复杂。问题在于有符号和无符号值的比较是依赖于机器的,因为它们依赖于不同整数类型的长度。例如,假设 int 是 16 位而 long 是 32 位。则 -1L < 1U,因为 1U 是 int ,提升为 signed long。但 -1L > -1UL,因为 -1L被提升为 unsigned long,看起来像是个非常大的正数。
转换也伴随着赋值发生,右侧的值被转换成左侧值的类型,即是赋值表达式的结果类型。
字符被转换为整数,可能是符号扩展也可能不是,如前所述。
通过丢弃高位,较长整数被转换成较短的整数或char。因此经过如下赋值
int i;
char c;
i = c;
c = i;
c 的值保持不变。不管是否涉及符号扩展,都是如此。然而,调换赋值顺序可能会丢失信息。
如果 x 是 float 而 i 是 int,则 x = i 和 i = x 都会引起转换;float 到 int 会截断所有小数部分。当 double 转换为 float, 值是四舍五入还是截断,依赖于实现。
由于函数调用的参数也是表达式,当参数传给函数时也会发生转换。在缺少函数原型时,char 和 short 成为 int,float 成为 double。这就解释了为什么我们在声明函数实参时用 int 和 double,即使调用函数时传入的值是 char 和 float。
最后一提,可以使用一元操作符(称为 cast),在任意表达式中进行强制的类型转换。如下结构
(类型名) 表达式
上面的表达式被转换为括号中的类型(通过前述的转换规则)。cast 的精确含义,就像是将表达式赋值给指定类型的变量,然后把这个变量代替整个结构来使用。例如,库函数 sqrt 期望接受一个 double 参数,如果不小心传入了其他类型则会产生垃圾结果。(sqrt 在 <math.h> 中声明)因此,如果 n 是整数,我们要使用
sqrt((double) n)
在将其传入sqrt之前,把 n 的值转换成 double。注意,cast 转换生成的是 n 值的合适类型,n本身不会被改变。cast 操作符和其他一元操作符有相同的优先级,参见本章末尾总结的操作符优先级表。
如果函数原型有声明参数(正常来说都应当如此),则当函数调用时,该声明可以造成所有参数的强制类型转换。这样,给出 sqrt 的函数原型:
double sqrt(double);
对它的调用
root2 = sqrt(2);
会强制将整数 2 转换为 double 值 2.0,不需要进行任何 cast 操作。
标准库包含了一个可移植的伪随机数生成器和一个初始化种子的函数;前者演示了cast的使用:
unsigned long int next = 1;
/* rand: 返回 0到32767之间的伪随机数 */
int rand(void)
{
next = next * 1103515245 + 12345;
return (unsigned int)(next/65536) % 32768;
}
/* srand: 为rand设置种子 */
void srand(unsigned int seed)
{
next = seed;
}
练习 2-3,写个函数 htoi(s) 将十六进制字符串(包括可选的0x或0X),转换成对应的整数值。十六进制允许的数位是 0-9,a-z,A-Z。