C 浮点类型
最后修改日期:2025 年 4 月 1 日
C 语言提供了几种具有不同精度和存储特性的浮点类型。本教程涵盖 IEEE 754 表示法、精度限制、硬件注意事项和实际用法模式。
我们将检查浮点的二进制表示,解释舍入模式、非规格化数,并通过实际示例演示常见陷阱。理解这些概念对于系统编程和性能敏感型应用至关重要。
C 浮点类型
C 语言提供三种主要的浮点类型,精度递增
float_types.c
#include <stdio.h> #include <float.h> int main() { float f = 3.1415926535f; // Single precision (32-bit) double d = 3.141592653589793; // Double precision (64-bit) long double ld = 3.14159265358979323846L; // Extended precision printf("float: %.15f\n", f); printf("double: %.15lf\n", d); printf("long double: %.21Lf\n", ld); printf("\nPrecision:\n"); printf("float mantissa bits: %d\n", FLT_MANT_DIG); printf("double mantissa bits: %d\n", DBL_MANT_DIG); printf("long double mantissa bits: %d\n", LDBL_MANT_DIG); return 0; }
标准 C 浮点类型遵循 IEEE 754 规范(在支持的情况下)
- float:32 位单精度(24 位尾数)
- double:64 位双精度(53 位尾数)
- long double:实现定义(x86 上为 80 位)
float.h
头文件定义了 FLT_MANT_DIG
等常量,用于揭示实现细节。请注意,long double 的精度因架构而异。
IEEE 754 二进制表示
浮点数使用符号-指数-尾数格式
float_representation.c
#include <stdio.h> #include <stdint.h> void print_float_bits(float f) { uint32_t* p = (uint32_t*)&f; uint32_t bits = *p; uint32_t sign = bits >> 31; uint32_t exponent = (bits >> 23) & 0xFF; uint32_t mantissa = bits & 0x7FFFFF; printf("Float: %f\n", f); printf("Sign: %d\n", sign); printf("Exponent: 0x%X (%d biased, %d actual)\n", exponent, exponent, exponent - 127); printf("Mantissa: 0x%X\n", mantissa); printf("Binary: "); for (int i = 31; i >= 0; i--) { printf("%d", (bits >> i) & 1); if (i == 31 || i == 23) printf(" "); } printf("\n\n"); } int main() { print_float_bits(1.0f); print_float_bits(0.1f); print_float_bits(-3.5f); return 0; }
IEEE 754 单精度格式包括
- 1 位符号位:0 为正,1 为负
- 8 位指数位:存储时带有 127 的偏差
- 23 位尾数位:隐含前导 1(规格化数)
值为 (-1)sign × 2exponent-127 × 1.mantissa2
特殊浮点值
IEEE 754 定义了特殊的位模式
special_values.c
#include <stdio.h> #include <math.h> int main() { float inf = INFINITY; float nan = NAN; float zero = 0.0f; float neg_zero = -0.0f; printf("Positive infinity: %f\n", inf); printf("NaN: %f\n", nan); printf("Zero: %f\n", zero); printf("Negative zero: %f\n", neg_zero); printf("\nSpecial comparisons:\n"); printf("inf == inf: %d\n", inf == inf); // 1 printf("nan == nan: %d\n", nan == nan); // 0 printf("zero == neg_zero: %d\n", zero == neg_zero); // 1 printf("\nClassification:\n"); printf("isinf(inf): %d\n", isinf(inf)); printf("isnan(nan): %d\n", isnan(nan)); printf("isnormal(1.0f): %d\n", isnormal(1.0f)); printf("fpclassify(denormal): %d\n", fpclassify(1e-45f)); return 0; }
特殊的浮点值包括
- 无穷大:0x7F800000(正无穷大),0xFF800000(负无穷大)
- NaN:指数为 255 且尾数不为 0 的任何值
- 零:指数为 0,尾数为 0(符号区分 ±0)
- 非规格化数:指数为 0,尾数不为 0
math.h
头文件提供了分类宏(isnan、isinf 等)以便正确处理。
精度与舍入
浮点运算涉及舍入
rounding.c
#include <stdio.h> #include <fenv.h> void show_rounding_mode() { switch (fegetround()) { case FE_TONEAREST: printf("FE_TONEAREST\n"); break; case FE_DOWNWARD: printf("FE_DOWNWARD\n"); break; case FE_UPWARD: printf("FE_UPWARD\n"); break; case FE_TOWARDZERO: printf("FE_TOWARDZERO\n"); break; default: printf("Unknown\n"); } } int main() { printf("Default rounding: "); show_rounding_mode(); // Demonstrate rounding effects float a = 1.0f / 3.0f; printf("1/3 as float: %.20f\n", a); // Change rounding mode fesetround(FE_UPWARD); printf("Current rounding: "); show_rounding_mode(); float b = 1.0f / 3.0f; printf("1/3 with FE_UPWARD: %.20f\n", b); return 0; }
关键精度概念
- 机器 epsilon:FLT_EPSILON(float 为 2-23)
- 舍入模式:就近舍入(默认)、向上舍入、向下舍入、向零舍入
- 保护位:计算过程中使用的额外精度
fenv.h
头文件提供了对舍入模式和浮点环境的控制。
非规格化数
非常小的数使用非规格化表示
denormals.c
#include <stdio.h> #include <float.h> int main() { float normal = FLT_MIN; // Smallest normal number float denormal = normal / 2.0f; // Becomes denormal printf("FLT_MIN: %e\n", normal); printf("FLT_MIN/2: %e\n", denormal); printf("\nProperties:\n"); printf("isnormal(normal): %d\n", isnormal(normal)); printf("isnormal(denormal): %d\n", isnormal(denormal)); printf("fpclassify(denormal): %d\n", fpclassify(denormal)); // Performance impact volatile float sum = 0.0f; for (int i = 0; i < 1000000; i++) { sum += denormal; // Much slower than normal floats } return 0; }
非规格化数
- 指数为 0,尾数不为 0
- 表示比 FLT_MIN 小的值
- 当它们接近零时会丢失精度
- 通常会导致显著的性能下降
某些系统为了性能会将非规格化数刷(flush)为零 (FTZ)。
误差累积
浮点误差在计算中会累加
error_accumulation.c
#include <stdio.h> #include <math.h> int main() { // Classic precision problem float sum = 0.0f; for (int i = 0; i < 10000; i++) { sum += 0.01f; } printf("Sum of 0.01 10000 times: %.10f\n", sum); // Kahan summation algorithm float kahan_sum = 0.0f; float c = 0.0f; // Compensation for (int i = 0; i < 10000; i++) { float y = 0.01f - c; float t = kahan_sum + y; c = (t - kahan_sum) - y; kahan_sum = t; } printf("Kahan sum: %.10f\n", kahan_sum); // Catastrophic cancellation float x = 1e8f; float y = x + 1.0f; printf("(1e8 + 1) - 1e8 = %.1f\n", y - x); return 0; }
常见误差源
- 舍入误差:每次运算都会引入微小误差
- 灾难性抵消:两个近似相等数字的减法
- 吸收:将小数字加到大数字上
Kahan 求和算法演示了如何减少累积误差。
浮点异常
浮点运算可能引发异常
exceptions.c
#include <stdio.h> #include <fenv.h> #include <math.h> #pragma STDC FENV_ACCESS ON void show_exceptions() { printf("Raised exceptions: "); if (fetestexcept(FE_DIVBYZERO)) printf("FE_DIVBYZERO "); if (fetestexcept(FE_INVALID)) printf("FE_INVALID "); if (fetestexcept(FE_OVERFLOW)) printf("FE_OVERFLOW "); if (fetestexcept(FE_UNDERFLOW)) printf("FE_UNDERFLOW "); if (fetestexcept(FE_INEXACT)) printf("FE_INEXACT "); printf("\n"); } int main() { feclearexcept(FE_ALL_EXCEPT); float x = 1.0f / 0.0f; // Division by zero show_exceptions(); feclearexcept(FE_ALL_EXCEPT); float y = sqrt(-1.0f); // Invalid operation show_exceptions(); feclearexcept(FE_ALL_EXCEPT); float z = FLT_MAX * 2.0f; // Overflow show_exceptions(); return 0; }
标准浮点异常
- FE_DIVBYZERO:除以零
- FE_INVALID:无效操作(sqrt(-1))
- FE_OVERFLOW:结果过大无法表示
- FE_UNDERFLOW:结果过小无法表示
- FE_INEXACT:不精确结果(发生舍入)
异常处理需要仔细管理浮点环境。
硬件注意事项
浮点性能因架构而异
hardware.c
#include <stdio.h> void print_fpu_control() { #if defined(__x86_64__) || defined(__i386__) unsigned short cw; __asm__ __volatile__ ("fstcw %0" : "=m" (cw)); printf("FPU control word: 0x%04X\n", cw); #endif } int main() { printf("FPU features:\n"); #ifdef __SSE2__ printf("SSE2 available\n"); #endif #ifdef __AVX__ printf("AVX available\n"); #endif print_fpu_control(); // SIMD example float a[4] = {1.0f, 2.0f, 3.0f, 4.0f}; float b[4] = {5.0f, 6.0f, 7.0f, 8.0f}; float c[4]; #ifdef __SSE__ __asm__ ( "movups %1, %%xmm0\n" "movups %2, %%xmm1\n" "addps %%xmm1, %%xmm0\n" "movups %%xmm0, %0" : "=m" (c) : "m" (a), "m" (b) ); printf("SIMD add: %.1f, %.1f, %.1f, %.1f\n", c[0], c[1], c[2], c[3]); #endif return 0; }
关键硬件方面
- x87 FPU:传统浮点堆栈架构
- SSE/AVX:现代 SIMD 浮点指令
- 控制寄存器:管理舍入、精度、异常
- 性能:非规格化数、精度混合会影响速度
现代编译器会根据目标架构生成优化代码。
最佳实践
- 选择合适的精度:内存受限时使用 float,大多数计算使用 double
- 避免相等性比较:改用相对误差检查
- 最小化操作:减少误差累积
- 注意硬件影响:非规格化数、SIMD 对齐
- 使用编译器标志:-ffast-math(谨慎使用)、-mfpmath
- 考虑替代方案:某些应用使用定点数
资料来源
作者
我叫 Jan Bodnar,我是一名热情的程序员,拥有丰富的编程经验。我自 2007 年以来一直撰写编程文章。至今,我已撰写了 1,400 多篇文章和 8 本电子书。我在编程教学方面拥有十多年的经验。