C 语言类型转换简介

目录汇总:C 语言零基础入门教程

在执行算术运算时,计算机比 C 语言的限制更多。为了让计算机执行算术运算,通常要求操作数有相同的大小(即位的数量相同),并且要求存储的方式也相同。计算机也许可以直接将两个 16 位整数相加,但是不能直接将 16 位整数和 32 位整数相加,也不能直接将 32 位整数和 32 位浮点数相加。

C 语言则允许在表达式中混合使用基本类型。在单个表达式中可以组合整数、浮点数,甚至是字符。当然,在这种情况下 C 编译器可能需要生成一些指令,将某些操作数转换成不同类型,使得硬件可以对表达式进行计算。例如,如果对 16 位 short 型数和 32 位 int 型数进行加法操作,那么编译器将安排把 16 位 short 型值转换成 32 位值。如果是 int 型数据和 float 型数据进行加法操作,那么编译器将安排把 int 型值转换成为 float 格式。这个转换过程稍微复杂一些,因为 int 型值和 float 型值的存储方式不同。

因为编译器可以自动处理这些转换而无须程序员介入,所以这类转换称为隐式转换(implicit conversion)。C 语言还允许程序员使用强制运算符执行显式转换(explicit conversion)。我们首先讨论隐式转换,显式转换将推迟到本文的最后介绍。遗憾的是,执行隐式转换的规则有些复杂,主要是因为 C 语言有大量不同的算术类型。

当发生下列情况时会进行隐式转换。

  • 当算术表达式或逻辑表达式中操作数的类型不相同时。(C 语言执行所谓的常规算术转换。)
  • 当赋值运算符右侧表达式的类型和左侧变量的类型不匹配时。
  • 当函数调用中的实参类型与其对应的形参类型不匹配时。
  • return 语句中表达式的类型和函数返回值的类型不匹配时。

一、常规算术转换

常规算术转换可用于大多数二元运算符(包括算术运算符、关系运算符和判等运算符)的操作数。例如,假设变量 ffloat 类型,变量 iint 类型。常规算术转换将应用在表达式 f + i 的操作数上,因为两者的类型不同。显然把变量 i 转换成 float 类型(匹配变量 f 的类型)比把变量 f 转换成 int 类型(匹配变量 i 的类型)更安全。整数始终可以转换为 float 类型;可能发生的最糟糕的事是精度会有少量损失。相反,把浮点数转换为 int 类型,将有小数部分的损失;更糟糕的是,如果原始数大于最大可能值或者小于最小可能值,那么将得到一个完全没有意义的结果。

常规算术转换的策略是把操作数转换成可以安全地适用于两个数值的“最狭小的”数据类型。(粗略地说,如果某种类型要求的存储字节比另一种类型少,那么这种类型就比另一种类型更狭小。)为了统一操作数的类型,通常可以将相对较狭小类型的操作数转换成另一个操作数的类型来实现(这就是所谓的提升)。最常用的提升是整值提升(integral promotion),它把字符或短整数转换成 int 类型(或者某些情况下是 unsigned int 类型)。

执行常规算术转换的规则可以划分成两种情况。

  • 任一操作数的类型是浮点类型的情况。按照下图将类型较狭小的操作数进行提升:

    1
    2
    3
    4
    5
    
    long double
        
    double
        
    float
    

    也就是说,如果一个操作数的类型为 long double,那么把另一个操作数的类型转换成 long double 类型。否则,如果一个操作数的类型为 double 类型,那么把另一个操作数转换成 double 类型。否则,如果一个操作数的类型是 float 类型,那么把另一个操作数转换成 float 类型。注意,这些规则涵盖了混合整数和浮点类型的情况。例如,如果一个操作数的类型是 long int 类型,并且另一个操作数的类型是 double 类型,那么把 long int 类型的操作数转换成 double 类型。

  • 两个操作数的类型都不是浮点类型的情况。首先对两个操作数进行整值提升(保证没有一个操作数是字符类型或短整型)。然后按照下图对类型较狭小的操作数进行提升:

    1
    2
    3
    4
    5
    6
    7
    
    unsigned long int
            
        long int
            
    unsigned int
            
        int
    

    有一种特殊情况,只有在 long int 类型和 unsigned int 类型长度相同(比如 32 位)时才会发生。在这类情况下,如果一个操作数的类型是 long int,而另一个操作数的类型是 unsigned int,那么两个操作数都会转换成 unsigned long int 类型。

注意

当有符号操作数和无符号操作数组合起来时,有符号操作数会被“转换”为无符号的值。转换过程中需要加上或者减去 n+1 的倍数,其中 n 是无符号类型能表示的最大值。这条规则可能会导致某些隐蔽的编程错误。

假设 int 类型的变量 i 值为 -10,而 unsigned int 类型的变量 u 值为 10。如果用 < 运算符比较变量 i 和变量 u,那么期望的结果应该是 1(真)。但是,在比较前,变量 i 转换为 unsigned int 类型。因为负数不能被表示成无符号整数,所以转换后的值将不再为 -10,而是加上 4 294 967 296 的结果(假定 4 294 967 295 是最大的无符号整数),即 4 294 967 286。因而 i < u 比较的结果将为 0。有些编译器会在程序试图比较有符号数与无符号数时给出一条类似“comparison between signed and unsigned”的警告消息。

因为此类陷阱的存在,所以最好尽量避免使用无符号整数,特别是不要把它和有符号整数混合使用。

下面的例子显示了常规算术转换的实际执行情况:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
char c;
short int s;
int i;
unsigned int u;
long int l;
unsigned long int ul;
float f;
double d;
long double ld;

i = i + c;       /* c is converted to int               */
i = i + s;       /* s is converted to int               */
u = u + i;       /* i is converted to unsigned int      */
l = l + u;       /* u is converted to long int          */
ul = ul + l;     /* l is converted to unsigned long int */
f = f + ul;      /* ul is converted to float            */
d = d + f;       /* f is converted to double            */
ld = ld + d;     /* d is converted to long double       */

二、赋值过程中的转换

常规算术转换不适用于赋值运算。C 语言会遵循另一条简单的转换规则,那就是把赋值运算右边的表达式转换成左边变量的类型。如果变量的类型至少和表达式类型一样“宽”,那么这种转换将没有任何障碍。例如:

1
2
3
4
5
6
7
8
char c;
int i;
float f;
double d;

i = c;   /* c is converted to int    */
f = i;   /* i is converted to float  */
d = f;   /* f is converted to double */

其他情况下是有问题的。把浮点数赋值给整型变量会丢掉该数的小数部分:

1
2
3
4
int i;

i = 842.97;   /* i is now 842  */
i = -842.97;  /* i is now –842 */

此外,把某种类型的值赋给类型更狭小的变量时,如果该值在变量类型范围之外,那么将得到无意义的结果(甚至更糟)。

1
2
3
c = 10000;    /*** WRONG ***/
i = 1.0e20;   /*** WRONG ***/
f = 1.0e100;  /*** WRONG ***/

这类赋值可能会导致编译器或 lint 之类的工具发出警告。

如果浮点常量被赋值给 float 型变量,那么建议在浮点常量尾部加上后缀 f

1
f = 3.14159f;

如果没有后缀,常量 3.14159 将是 double 类型,可能会触发警告消息。

三、C99 中的隐式转换

C99 中的隐式转换和 C89 中的隐式转换略有不同,这主要是因为 C99 增加了一些类型(_Boollong long 类型、扩展的整数类型和复数类型)。

为了定义转换规则,C99 允许每个整数类型具有“整数转换等级”。下面按从最高级到最低级的顺序排列:

(1) long long intunsigned long long int

(2) long intunsigned long int

(3) intunsigned int

(4) short intunsigned short int

(5) charsigned charunsigned char

(6) _Bool

简单起见,这里忽略了扩展的整数类型和枚举类型。

C99 用整数提升(integer promotion)取代了 C89 中的整值提升(integral promotion),可以将任何等级低于 intunsigned int 的类型转换为 int(只要该类型的所有值都可以用 int 类型表示)或 unsigned int

与 C89 一样,C99 中执行常规算术转换的规则可以分为两种情况。

  • 任一操作数的类型是浮点类型的情况。只要两个操作数都不是复数型,规则与前面一样。

  • 两个操作数的类型都不是浮点类型的情况。首先对两个操作数进行整数提升。如果这时两个操作数的类型相同,过程结束。否则,依次尝试下面的规则,一旦遇到可应用的规则就不再考虑别的规则。

    • 如果两个操作数都是有符号型或者都是无符号型,将整数转换等级较低的操作数转换为等级较高的操作数的类型。
    • 如果无符号操作数的等级高于或等于有符号操作数的等级,将有符号操作数转换为无符号操作数的类型。
    • 如果有符号操作数类型可以表示无符号操作数类型的所有值,将无符号操作数转换为有符号操作数的类型。
    • 否则,将两个操作数都转换为与有符号操作数的类型相对应的无符号类型。

另外,所有算术类型都可以转换为 _Bool 类型。如果原始值为 0 则转换结果为 0,否则结果为 1。

四、强制类型转换

虽然 C 语言的隐式转换使用起来非常方便,但我们有些时候还需要从更大程度上控制类型转换。基于这种原因,C 语言提供了强制类型转换。强制类型转换表达式如下所示:

1
[强制转换表达式]  (类型名) 表达式

这里的类型名表示的是表达式应该转换成的类型。

下面的例子显示了使用强制类型转换表达式计算 float 类型值小数部分的方法:

1
2
3
float f, frac_part;

frac_part = f  (int) f;

强制类型转换表达式 (int)f 表示把 f 的值转换成 int 类型后的结果。C 语言的常规算术转换则要求在进行减法运算前把 (int)f 转换回 float 类型。f(int)f 的不同之处就在于 f 的小数部分,这部分在强制类型转换时被丢掉了。

强制类型转换表达式可以用于显示那些肯定会发生的类型转换:

1
i = (int) f;       /* f is converted to int */

它也可以用来控制编译器并且强制它进行我们需要的转换。思考下面的例子:

1
2
3
4
float quotient;
int dividend, divisor;

quotient = dividend / divisor;

正如现在写的那样,除法的结果是一个整数,在把结果存储在 quotient 变量中之前,要把结果转换成 float 格式。但是,为了得到更精确的结果,可能需要在除法执行之前dividenddivisor 的类型转换成 float 格式的。强制类型转换表达式可以完成这一点:

1
quotient = (float) dividend / divisor;

变量 divisor 不需要强制类型转换,因为把变量 dividend 强制转换成 float 类型会迫使编译器把 divisor 也转换成 float 类型。

顺便提一下,C 语言把(类型名)视为一元运算符。一元运算符的优先级高于二元运算符,因此编译器会把表达式

1
(float) dividend / divisor

解释为

1
((float) dividend) / divisor

如果感觉有点困惑,还有其他方法可以实现同样的效果:

1
quotient = dividend / (float) divisor;

或者

1
quotient = (float) dividend / (float) divisor;

有些时候,需要使用强制类型转换来避免溢出。思考下面这个例子:

1
2
3
4
long i;
int j = 1000;

i = j * j;   /* overflow may occur */

乍看之下,这条语句没有问题。表达式 j * j 的值是 1 000 000,并且变量 ilong int 类型的,所以 i 应该能很容易地存储这种大小的值,不是吗?问题是,当两个 int 类型值相乘时,结果也应该是 int 类型的,但是 j * j 的结果太大,以致在某些机器上无法表示为 int 型,从而导致溢出。幸运的是,可以使用强制类型转换避免这种问题发生:

1
i = (long) j * j;

因为强制运算符的优先级高于 *,所以第一个变量 j 会被转换成 long int 类型,同时也迫使第二个 j 进行转换。注意,语句

1
i = (long) (j * j);   /*** WRONG ***/

是不对的,因为溢出在强制类型转换之前就已经发生了。

请参阅

(完)

comments powered by Disqus