C 语言中的浮点数类型
2.1.4 处理的是整数在 C 里的转换规则,本节是它的浮点版本。两者的结构完全平行:同样有”宽度”和”符号性”之外的第三个维度——编码族(整数族与浮点族),而跨族的转换是本节全部陷阱的来源。
一条贯穿全节的判据:转换会不会丢东西,只看目标类型的有效位数够不够装下源类型的全部信息。
机制
类型与常量
| 类型 | 典型实现 | 有效位 | 十进制有效数字 |
|---|---|---|---|
float | IEEE 754 binary32 | 24 | 约 7 |
double | IEEE 754 binary64 | 53 | 约 15~16 |
long double | 平台相关(x86 上常为 80 位扩展精度) | 64 | 约 18~19 |
C 标准并不强制浮点必须是 IEEE 754(只有定义了 __STDC_IEC_559__ 的实现才作此承诺),但408 与所有主流平台都按 IEEE 754 处理。
浮点常量默认是 double:
float f = 3.14; /* 3.14 是 double,赋值时被转成 float,精度丢失 */
float g = 3.14f; /* 后缀 f 才是 float 常量 */
double d = 1.0 / 3; /* 3 先转 double,除法在 double 域完成 */1.0/3 与 1.0f/3 结果不同,这是常量后缀被忽略造成的典型偏差。
表达式中的转换
浮点的通常算术转换比整数简单,没有”无符号传染”这种反直觉规则,只是向精度高的一方看齐:int + float 的结果是 float;float + double 的结果是 double。整型一律先转成浮点再运算。
一个后果:int a=1, b=3; float x = a/b; 得到 0.0,不是 0.333——除法在整型域就已完成并截断,转成 float 是之后的事。要正确必须写 (float)a / b。
整数与浮点互转:四条路径
这是本节的核心,四条路径的行为各不相同:
| 转换 | 会不会丢精度 | 会不会溢出 | 原因 |
|---|---|---|---|
int → float | 会 | 不会 | int 需 31 位有效位,float 只有 24 位 |
int → double | 不会 | 不会 | double 有 53 位 |
float → int | 会(截断小数) | 会(超范围是 UB) | 向零截断 |
double → float | 会 | 会(可能变 | 53 位压到 24 位;范围也变窄 |
int → float 会丢精度这一条最反直觉,因为两者都是 32 位。但 float 的 32 位里有 8 位给了阶码,有效位只剩 24 位,所以
int i = 16777217; /* 2²⁴ + 1 */
float f = i; /* 变成 16777216.0f,末位丢失 */
printf("%d\n", (int)f == i); /* 0 */由此得到两条必须记住的恒等式判断:
float → int 的截断方向是”向零”,与 double → int 相同,也与整数除法一致:(int)3.9 == 3,(int)(-3.9) == -3。注意它与算术右移的向负无穷取整仍然不同。
转换不是位型重解释
float f = 1.0f;
int a = (int)f; /* a == 1:值转换 */
int b = *(int*)&f; /* b == 1065353216:位型重解释,0x3F800000 */(int)f 让编译器生成一条真正的转换指令(x86 上是 cvttss2si),*(int*)&f 一条指令都不生成,只是换了个类型标签去读同一块内存。
这是 2.1.4 同名边界在浮点上的体现,也是位型题与数值题的分界线:题目问”把这个 float 赋给 int 得到什么”是值转换;问”把这段内存按 int 读出来是什么”是位型重解释。
浮点比较
== 对浮点不可靠,因为参与比较的两个值可能各自带着不同的舍入误差:
if (0.1 + 0.2 == 0.3) { /* 不成立 */ }三个数各自在编译期就被舍入过一次(2.1.1),0.1+0.2 又在运算时舍入一次,最终位串与 0.3 的位串相差一个最低位。
正确写法是比较差的绝对值:
if (fabs(a - b) < 1e-9) { /* 认为相等 */ }但这个 double 的相邻可表示值就相差约 1e-9 判等等于用 ==。严谨的做法是用相对误差:
两个例外,浮点比较是可靠的:
- 精确表示的值之间:
、 、小于 的整数在 float里都是精确的,if (f == 1.0f)在f由精确运算得来时可靠。 - 与 0 比较符号:
if (f > 0)没有精度问题。
NaN 的比较行为
NaN 与任何值(包括自己)的所有比较都返回假,!= 除外。 因此判断 NaN 的标准写法就是 x != x(或 isnan(x))。
这条还导致一个连带后果:if (a < b) ... else ... 与 if (a >= b) ... else ... 在 a 或 b 为 NaN 时走的分支不同,两者不是简单的取反关系。
默认实参提升
传给 printf 这类可变参数函数时,float 会被自动提升为 double:
float f = 1.5f;
printf("%f\n", f); /* f 被提升为 double,%f 对应 double,正确 */所以 printf 没有专门的 float 格式符——%f 一直就是 double 的格式符。这也是 scanf 必须区分 %f(float*)和 %lf(double*)而 printf 不必区分的原因:指针不会被提升。
边界
哪些恒等式成立
这是最典型的选择题形式,逐条给出结论(设 i 为 int,f 为 float,d 为 double,均非 NaN/无穷):
| 表达式 | 成立? | 理由 |
|---|---|---|
(float)(double)f == f | 成立 | float → double 无损,转回来一模一样 |
(double)(float)d == d | 不成立 | 53 位压到 24 位,一般有损 |
(int)(float)i == i | 不成立 | |
(int)(double)i == i | 成立 | 53 位足以装下 32 位 int |
f + d 的类型 | double | 向高精度看齐 |
d * d >= 0 | 成立 | 平方非负,浮点乘法保号 |
d1 + d2 == d2 + d1 | 成立 | 交换律成立 |
(d1+d2)+d3 == d1+(d2+d3) | 不成立 | 结合律不成立 |
d1 + d2 - d2 == d1 | 不成立 | 大数吃小数 |
d / 0.0 | 不报错 | 得 |
d/0.0 与 i/0 的差别值得单独记:浮点除以零由 IEEE 754 定义为 #DE 异常。
精度损失发生在赋值还是运算
float a = 1.0f/3.0f; /* 运算在 float 域,误差较大 */
float b = 1.0/3.0; /* 运算在 double 域(常量是 double),赋值时才压成 float */b 的误差更小,因为它多在 double 里精算了一步。这解释了为什么中间变量用 double、最终结果再转 float 是好做法。
x87 的历史遗留还带来一个更隐蔽的现象:早期 x86 的浮点寄存器是 80 位,中间结果精度高于 double,同一段代码开不开优化可能得到不同结果。现代编译器用 SSE 后这个问题基本消失,但它说明”浮点结果依赖实现”不是空话。
C 标准并不承诺 IEEE 754
严格地说,C 标准只要求浮点类型能表示某个范围、有某个精度,没有强制 IEEE 754 的位布局、舍入模式和特殊值行为。
做题时的判据:题目说”按 IEEE 754”就套 IEEE 754;题目问”C 标准是否保证”,答案往往是”不保证”。这与整数那边”C 不强制补码(C23 起才强制)“是同一类问题。
对照速查
| 转换 | 精度 | 范围 |
|---|---|---|
int → float | 可能丢 | 安全 |
int → double | 精确 | 安全 |
float → double | 精确 | 安全 |
double → float | 可能丢 | 可能溢出 |
float/double → int | 截断小数(向零) | 可能 UB |
| 记忆点 | |
|---|---|
| 浮点常量默认类型 | double |
printf 的 %f | 对应 double(float 被提升) |
| 判断 NaN | x != x |
| 浮点除以 0 | 得 |
| 唯一成立的定律 | 交换律 |
考点
int → double精确,int → float可能丢精度(float有效位只有 24)(int)(double)i == i恒成立;(int)(float)i == i不成立(float)(double)f == f成立;(double)(float)d == d不成立- 浮点常量默认是
double,3.14f才是float int除法先截断再转浮点:(float)(1/3) == 0.0==不可用于浮点判等;应取相对误差 x != x当且仅当x是 NaN- 浮点除以零得
,整数除以零触发异常 - 浮点只有交换律,没有结合律和分配律
float传入可变参数会被提升为double
链接
- 🏠 返回总览:计算机组成原理第 2 章:数据的表示和运算总览
- ⬅️ 上一节:2.3.2 浮点数的加减运算
- ➡️ 下一节:2.3.4 数据的大小端和对齐存储
- 📖 名词库:第 2 章名词库