2.7 类型转换
当一个运算符的几个操作数类型不同时,就需要通过一些规则把它们转换为某种共同的类型
一般来说,自动转换是指把 “ 比较窄的 ” 操作数转换为 “ 比较宽的 ” 操作数,并且不丢失信息的转换
例如,在计算表达式 f + i
时,将整型变量 i
的值自动转换为浮点型(这里的变量 f
为浮点型)
不允许使用无意义的表达式,例如,不允许把 float
类型的表达式作为下标
针对可能导致信息丢失的表达式,编译器可能会给出警告信息
比如把较长的整型值赋给较短的整型变量,把浮点型值赋值给整型变量等等,但这些表达式并不是非法的
由于 char
类型就是较小的整型,因此在算术表达式中可以自由使用 char
类型的变量
这就为实现某些字符转换提供了很大的灵活性
比如下面的函数 atoi
就是一个例子
它将一个数字字符串转换为相应的数值(如:'423''
转换为 423
):
/* atoi: convert s to integer */
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;
}
我们在第 1 章讲过,表达式 s[i] - '0'
能够计算出 s[i]
中存储的字符所对应的数字值
这是因为 '0'
、'1'
等在字符集中对应的数值是一个连续的递增序列
函数 lower
是将 char
类型转换为 int
类型的另一个例子
它将 ASCII 字符集中的字符映射到对应的小写字母
如果待转换的字符不是大写字母,lower
函数将返回字符本身
/* lower: convert c to lower case; ASCII only */
int lower(int c)
{
if (c >= 'A' && c <= 'Z')
return c + 'a' - 'A';
else
return c;
}
上述这个函数是为 ASCII 字符集设计的
在 ASCII 字符集中,大写字母与对应的小写字母作为数字值来说具有固定的间隔
并且每个字母表都是连续的,也就是说,在 A ~ Z 之间只有字母
但是,后面一点对 EBCDIC 字符集是不成立的,因此这一函数作用在 EBCDIC 字符集中就不仅限于转换字母的大小写
附录 B 介绍的标准头文件 <ctype.h>
定义了一组与字符集无关的测试和转换函数
例如 tolower(c)
函数将 c
转换为小写形式(如果 c
为大写形式的话)
可以使用 tolower
替代上述 lower
函数
类似地,测试语句 c >= '0' && c <= '9'
可以用该标准库中的函数 isdigit(c)
替代
在本书的后续内容中,我们将使用 <ctype.h>
中定义的函数
将字符类型转换为整型时,我们需要注意一点,C 语言没有指定 char
类型的变量是无符号变量(signed
)还是带符号变量(unsigned
)
当把一个 char
类型的值转换为 int
类型的值时,其结果有没有可能为负整数?
对于不同的机器,其结果也不同,这反映了不同机器结构之间的区别
在某些机器中,如果 char
类型值的最左一位为 1,则转换为负整数(进行 “ 符号扩展 ”)
而在另一些机器中,把 char
类型值转换为 int
类型时,在 char
类型值的左边添加 0,这样导致的转换结果值总是正值
C 语言的定义保证了机器的标准打印字符集中的字符不会是负值,因此,在表达式中这些字符总是正值
但是,存储在字符变量中的位模式在某些机器中可能是负的,而在另一些机器上可能是正的
为了保证程序的可移植性,如果要在 char
类型的变量中存储非字符数据,最好指定 signed
或 unsigned
限定符
当关系表达式(如 i > j
)以及由 &&
、||
连接的逻辑表达式的判定结果为真时,表达式的值为 1
,当判定结果为假时,表达式的值为 0
因此,对于赋值语句 d = c >= '0' && c <= '9'
来说,当 c
为数字时,d
的值为 1
,否则 d
的值为 0
但是,某些函数(比如 isdigit
)在结果为真时可能返回任意的非 0 值
在 if
、while
、for
等语句的测试部分中,“ 真 ” 就意味着 “非 0
”,这二者之间没有区别
C 语言中,很多情况下会进行隐式的算术类型转换
一般来说,如果二元运算符(具有两个操作数的运算符称为二元运算符,比如 +
或 *
)的两个操作数具有不同的类型
那么在进行运算之前先要把 “ 较低 ” 的类型提升为 “ 较高 ” 的类型,运算的结果为较高的类型
附录 A.6 节详细地列出了这些转换规则
但是,如果没有 unsigned
类型的操作数,则只要使用下面这些非正式的规则就可以了:
- 如果其中一个操作数的类型为
long double
,则将另一个操作数转换为1ong double
类型 - 如果其中一个操作数的类型为
double
,则将另一个操作数转换为double
类型 - 如果其中一个操作数的类型为
float
,则将另一个操作数转换为float
类型 - 将
char
与short
类型的操作数转换为int
类型 - 如果其中一个操作数的类型为
long
,则将另一个操作数也转换为long
类型
注意,表达式中 float
类型的操作数不会自动转换为 double
类型,这一点与最初的定义有所不同
一般来说,数学函数(如标准头文件 <math.h>
中定义的函数)使用双精度类型的变量
使用 float
类型主要是为了在使用较大的数组时节省存储空间,有时也为了节省机器执行时间(双精度算术运算特别费时)
当表达式中包含 unsigned
类型的操作数时,转换规则要复杂一些
主要原因在于,带符号值与无符号值之间的比较运算是与机器相关的,因为它们取决于机器中不同整数类型的大小
例如,假定 int
类型占 16 位,long
类型占 32 位,那么 -1L < 1U
这是因为 unsighed int
类型的 1U
将被提升为 signed long
类型
但 -1L > 1UL
,这是因为 -1L
将被提升为 unslgned 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
类型
因此,即使调用函数的参数为 char
或 float
类型,我们也把函数参数声明为 int
或 double
类型
最后,在任何表达式中都可以使用一个称为强制类型转换的一元运算符强制进行显式类型转换
在语句 (类型名) 表达式
中,表达式
将按照上述转换规则被转换为类型名指定的类型
我们可以这样来理解强制类型转换的准确含义:
在上述语句中,表达式首先被赋值给类型名指定的类型的某个变量,然后再用该变量替换上述整条语句
例如,库函数 sqrt
的参数为 double
类型,如果处理不当,结果可能会无意义(sqrt
在 <math.h>
中声明)
因此,如果 n
是整数,可以使用 sqrt((double) n)
,在把 n
传递给函数 sqrt
之前先将其转换为 double
类型
注意,强制类型转换只是生成一个指定类型的 n
的值,n
本身的值并没有改变
强制类型转换运算符与其它一元运算符具有相同的优先级,表 2-1(2.12 节)对运算符优先级进行了总结
在通常情况下,参数是通过函数原型声明的,这样,当函数被调用时,声明将对参数进行自动强制转换
例如,对于 sqrt
的函数原型 double sqrt(double);
下列函数调用:
root2 = sqrt(2);
不需要使用强制类型转换运算符就可以自动将整数 2
强制转换为 double
类型的值 2.0
标准库中包含一个可移植的实现伪随机数发生器的函数 rand
以及一个初始化种子数的函数 srand
前一个函数 rand
使用了强制类型转换
unsigned long int next = 1;
/* rand: return pseudo-random integer on 0..32767 */
int rand(void)
{
next = next * 1103515245 + 12345;
return (unsigned int)(next / 65536) % 32768;
}
/* srand: set seed for rand() */
void srand(unsigned int seed)
{
next = seed;
}