Menu

C 语言类型转换:隐式转换与强制类型转换

C 语言无时无刻不在做类型转换:有时是因为你用强制转换要求了,更多时候是它自己完成的。本页介绍类型提升规则、强制转换的语法,以及那些会悄悄丢失数据的转换。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

C 无时无刻不在类型之间转换值。其中一些转换是你自己用强制转换写出来的;大部分则是编译器按照不由你选择的规则悄悄完成的。分清这两者,就是“为什么我的平均值总是 3?”和代码言行一致之间的区别。

隐式转换

只要某个类型的值出现在需要另一种类型的地方,C 就会转换它:

不会丢失信息的转换(int 转 double、char 转 int、short 转 long)是拓宽转换,始终安全。反方向的转换是收窄转换,可能丢失数据:上面的 3.9 变成了 3,除非你用 -Wconversion 要求警告,否则不会有任何提示。

常规算术转换

当二元运算符的操作数类型不同时,C 会先把它们转换为同一种类型再进行运算。这个阶梯自下而上是:

  1. 所有比 int 小的类型(char、short、_Bool)都会提升为 int。这叫整数提升,它总是最先发生。
  2. 如果任意一边是 long double,另一边就变成 long double。
  3. 否则,如果任意一边是 double,另一边就变成 double。
  4. 否则,如果任意一边是 float,另一边就变成 float。
  5. 否则,在整数类型之间,等级高的一方胜出;如果等级相同,无符号的一方胜出。

最后这条规则才是真正引发 bug 的那条,其余的都很直观。

整数提升解释了为什么 char 运算不会像你预想的那样溢出,也解释了为什么把结果存回 char 时会溢出。

显式强制转换

强制转换就是放在括号里的目标类型:

(double)x
(int)3.9
(char)65
(unsigned int)n

它作用于紧跟在它后面的表达式,而且结合得非常紧,比 *、/ 或 + 都紧。

第一行先按整数相除(得到 3),再把 3 转换成 3.0,已经太晚了。第二行在除法之前就把 total 转换成了 double,所以 / 运算符看到的是一个 double 和一个 int,它会提升那个 int,然后进行浮点除法。

转换一个操作数就够了,常规算术转换会处理另一个。

解决整数除法问题

这是在 C 中写强制转换最常见的原因:

百分比那一行很有启发性:passed / n 是 3 / 5,按整数计算是 0,而 0 * 100 还是 0。先乘后除(100 * passed / n)即使不用强制转换也能修好,因为 300 / 5 能整除,但这只在数字恰好配合时才有效。强制转换才是可靠的修复方法。

是截断,不是四舍五入

把浮点值强制转换成整数会丢弃小数部分。它向零截断,而不是四舍五入:

如果在你自己的机器上编译,记住在 Linux 上 math.h 需要在链接时加 -lm。

还有一个隐患:转换一个超出整数类型范围的浮点值是未定义行为,而不是回绕。(int)1e20 可能产生任何结果。当值不受你控制时,转换前先检查范围。

char 与 int

C 中的 char 本身就是一个存放字符编码的小整数。两者之间的转换是日常操作:

digit - '0' 是把数字字符转换成数值的标准写法,之所以可行,是因为十个数字字符保证是连续的。对于字母,优先使用 ctype.h 中的 toupper() 和 tolower(),而不是 + 32 运算:这个偏移量是 ASCII 的事实,而不是 C 的保证。

相关的陷阱:ctype.h 中的函数,比如 isdigit 和 toupper,接收的 int 必须是 EOF 或者能用 unsigned char 表示的值。传入一个为负数的普通 char(这是可能的,因为普通 char 可能是有符号的)属于未定义行为。要先转换:isdigit((unsigned char)c)。

有符号与无符号的陷阱

转换阶梯的第 5 步,即等级相同时无符号胜出,造就了 C 中最出人意料的比较:

-1 被转换为 unsigned int,它的位模式被重新解释为 4,294,967,295。这比 1 大,所以比较结果为假。

同样的转换会让循环永远不结束:

/* BUG: i is unsigned, so i >= 0 is always true. When i is 0, i-- wraps. */
for (size_t i = n - 1; i >= 0; i--) { ... }

也会让长度检查失效:

/* BUG: strlen returns size_t (unsigned). If the string is shorter
   than 5, len - 5 wraps to a huge number and the test passes. */
if (strlen(s) - 5 > 0) { ... }

改写成 if (strlen(s) > 5),减法就不会发生了。

防御方法:让计数和下标始终保持同一种符号性,用 -Wsign-compare 编译(包含在 -Wextra 中),必须混用时,先确认值不可能为负,再显式转换。

指针的强制转换

强制转换也可以在指针类型之间转换,这时它带有真正的风险,因为它改变的是内存的解读方式,而不是字节本身。

在小端机器上,这会打印 01 00 00 00。通过 unsigned char * 查看对象的表示形式,是标准明确认可的少数几种指针转换之一。

其他大多数指针转换都没有得到认可。通过 float * 读取 int 违反了严格别名规则,即使能编译也是未定义行为;要重新解释字节,请改用 memcpy。

有两个值得了解的惯例。在 C 中,void * 可以不加强制转换地与任何对象指针类型互相转换,所以你不应该对 malloc 的结果做强制转换:

int *arr = malloc(n * sizeof *arr);        /* correct C */
int *arr = (int *)malloc(n * sizeof *arr); /* needless; hides a missing header */

C++ 中必须加这个强制转换,所以很多代码里都有它。在 C 中,它可能会掩盖忘记包含 <stdlib.h> 的错误。

另外,printf("%p", ...) 需要 void *,所以在这里指针参数确实需要强制转换:printf("%p", (void *)p)。

强制转换不是正确答案的时候

强制转换会让编译器闭嘴。但有时候编译器是对的。

long big = 5000000000L;
int  small = (int)big;    /* the cast hides real data loss */

如果值确实放得下,强制转换表明你检查过了。如果可能放不下,强制转换就把一条警告变成了一个悄无声息的错误结果。写强制转换之前,先问问自己,正确的修复是不是改变变量的类型:用 double 代替 int,用 size_t 代替 int,用 long long 代替 long。强制转换主要适用于两种都正确的类型必须在一次运算中相遇的情况,比如 (double)sum / count。

常见问题

C 语言中如何进行强制类型转换?

把目标类型放在值前面的括号里:(double)x、(int)3.9、(char)65。强制转换作用于紧跟在它后面的表达式,所以 (double)a / b 先转换 a 再做除法,而 (double)(a / b) 先按整数相除,再转换结果。

C 语言中如何把 int 转换成 float?

赋值时会隐式完成:double d = 5; 存的是 5.0。在表达式内部通常需要显式转换:(double)total / count 强制进行浮点除法,而不是整数除法。

C 语言中把 float 强制转换成 int 会发生什么?

小数部分会被丢弃:向零截断,从不四舍五入。(int)3.9 是 3,(int)-3.9 是 -3。要四舍五入,对正数可以在转换前加 0.5,或者使用 math.h 中的 round()、floor() 或 ceil()。

为什么比较有符号和无符号整数会得到错误的结果?

C 的常规算术转换会把有符号值转换成无符号值,所以 -1 < 1u 为假:-1 变成了一个巨大的正数。让计数和大小保持同一种符号性,或者在确认值不可能为负之后再显式转换。

Coddy 编程语言插图

和 Coddy 一起学编程

开始