4-8 浮点数

整数非常适合计数数字,但有时我们需要存储非常大的(正数或负数)数字,或者带有小数部分的数字。浮点floating point类型变量是一种能够存储带有小数部分数字的变量,例如4320.0、-3.33或0.01226。浮点名称中的“浮点”指小数点位置可“浮动”——即小数点前后可支持不同数量的有效位。浮点数据类型始终带符号(可存储正负值)。

提示
在代码中书写浮点数时,小数点必须使用十进制点符号。若您来自使用十进制逗号的国家,则需习惯改用十进制点符号。


C++浮点类型

C++包含三种基础浮点数据类型:单精度浮点数float、双精度浮点数double以及扩展精度浮点数long double。与整数类型类似,C++并未明确定义这些类型的实际大小。

Category C++ Type Typical Size
floating point float 4 bytes
double 8 bytes
long double 8, 12, or 16 bytes

在现代架构中,浮点类型通常采用IEEE 754标准(参见https://en.wikipedia.org/wiki/IEEE_754 )定义的浮点格式之一实现。因此,float类型几乎总是占用4字节,double类型几乎总是占用8字节。

另一方面,long double类型较为特殊。在不同平台上,其大小可能在8字节至16字节之间浮动,且未必采用符合IEEE 754标准的格式。我们建议避免使用long double类型。

提示

本教程系列假设您的编译器采用 IEEE 754 兼容格式处理浮点数(float 和 double)。

可通过以下代码验证浮点类型是否兼容 IEEE 754 标准:

#include <iostream>
#include <limits>

int main()
{
    std::cout << std::boolalpha; // print bool as true or false rather than 1 or 0
    std::cout << "float: " << std::numeric_limits<float>::is_iec559 << '\n';
    std::cout << "double: " << std::numeric_limits<double>::is_iec559 << '\n';
    std::cout << "long double: " << std::numeric_limits<long double>::is_iec559 << '\n';
}

image

对于高级读者
float 类型几乎总是采用 4 字节 IEEE 754 单精度格式实现。
double 类型几乎总是采用 8 字节 IEEE 754 双精度格式实现。
然而,long double 的实现格式因平台而异。常见选项包括:

  • 8 字节 IEEE 754 双精度格式(与 double 相同)。
  • 80位(常补足至12或16字节)x87扩展精度格式(兼容IEEE 754)。
  • 16字节IEEE 754四倍精度格式。
  • 16字节双倍精度格式(不兼容IEEE 754)。

浮点变量与常量

以下是浮点变量的一些定义:

float f;
double d;
long double ld;

使用浮点数字面量时,请始终包含至少一位小数点(即使小数点后为0)。这有助于编译器识别该数值为浮点数而非整数。

int a { 5 };      // 5 means integer
double b { 5.0 }; // 5.0 is a floating point literal (no suffix means double type by default)
float c { 5.0f }; // 5.0 is a floating point literal, f suffix means float type

int d { 0 };      // 0 is an integer
double e { 0.0 }; // 0.0 is a double

请注意,默认情况下浮点数字面量默认为 double 类型。使用 f 后缀表示 float 类型的字面量。

最佳实践
始终确保字面量的类型与被赋值或初始化的变量类型匹配。否则将导致不必要的转换,可能造成精度损失。


打印浮点数

现在考虑这个简单的程序:

#include <iostream>

int main()
{
	std::cout << 5.0 << '\n';
	std::cout << 6.7f << '\n';
	std::cout << 9876543.21 << '\n';

	return 0;
}

这个看似简单的程序的结果可能会让你大吃一惊:

image

在第一种情况下,尽管我们输入的是5.0,但std::cout打印出了5。默认情况下,如果小数部分为0,std::cout不会打印该部分。

在第二种情况下,数字按预期打印出来。

在第三种情况下,它以科学记数法打印了该数值(若需复习科学记数法,请参阅第4.7课——科学记数法介绍)。


浮点数范围

Format Range Precision
IEEE 754 single-precision (4 bytes) ±1.18 x 10-38 to ±3.4 x 1038 and 0.0 6-9 significant digits, typically 7
IEEE 754 double-precision (8 bytes) ±2.23 x 10-308 to ±1.80 x 10308 and 0.0 15-18 significant digits, typically 16
x87 extended-precision (80 bits) ±3.36 x 10-4932 to ±1.18 x 104932 and 0.0 18-21 significant digits
IEEE 754 quadruple-precision (16 bytes) ±3.36 x 10-4932 to ±1.18 x 104932 and 0.0 33-36 significant digits

对于高级读者

80位x87扩展精度浮点类型在某种程度上是个历史遗留现象。在现代处理器上,此类对象通常会被填充至12或16字节(这是处理器更自然的处理尺寸)。这意味着这些对象包含80位浮点数据,剩余内存为填充字节。

80位浮点类型与16字节浮点类型具有相同数值范围看似有些奇怪。这是因为它们分配给指数部分的位数相同——但16字节数值能存储更多有效位。


浮点精度

考虑分数 1/3。该数的十进制表示为 0.33333333333333…,其中 3 的位数无限延伸。若在纸上书写此数,手臂终将疲惫而停笔。此时所写数字虽接近0.3333333333…(无限延伸的3),却无法完全等同。

在计算机中,无限精度数需要无限存储空间,而我们通常每个值仅分配4或8字节。这种存储限制意味着浮点数只能保存有限位有效数字——超出部分要么丢失,要么被不精确地表示。实际存储的数值可能接近目标数值,但并非完全精确。下一节将展示具体示例。

浮点类型的精度precision定义了其在无信息丢失情况下可表示的有意义位数。

浮点类型的精度位数取决于其大小(float精度低于double)和具体存储值(某些值可被更精确地表示)。

例如,float精度为6至9位。这意味着float能精确表示任何具有最多6位有效数字的数值。具有7到9位有效数字的数值可能精确表示也可能无法精确表示,具体取决于数值本身。而超过9位有效数字的数值则绝对无法精确表示。

double类型的精度范围在15到18位之间,其中大多数double值至少具有16位有效数字。long double的最小精度取决于其占用的字节数,分别为15位、18位或33位有效数字。

关键洞察
浮点类型只能精确表示特定数量的有效数字。使用有效数字超过最小值的数值可能会导致存储不精确。


输出浮点数值

输出浮点数时,std::cout 的默认精度为 6——即它默认所有浮点变量仅保留 6 位有效数字(float 类型的最小精度),因此会截断小数点后的数字。

以下程序展示了 std::cout 截断truncate至 6 位的效果:

#include <iostream>

int main()
{
    std::cout << 9.87654321f << '\n';
    std::cout << 987.654321f << '\n';
    std::cout << 987654.321f << '\n';
    std::cout << 9876543.21f << '\n';
    std::cout << 0.0000987654321f << '\n';

    return 0;
}

该程序输出:

image

请注意,这些数值各自仅有6位有效数字。

另需注意,在某些情况下std::cout会切换为科学记数法输出数字。根据编译器不同,指数部分通常会补零至最小位数。无需担心,9.87654e+006与9.87654e6完全等价,仅存在补零差异。指数位数的最小显示位数因编译器而异(Visual Studio采用3位,部分编译器遵循C99标准采用2位)。

可通过输出控制符Output manipulators函数std::setprecision()覆盖std::cout的默认精度。输出控制符用于调整数据输出方式,定义于iomanip头文件中。

#include <iomanip> // for output manipulator std::setprecision()
#include <iostream>

int main()
{
    std::cout << std::setprecision(17); // show 17 digits of precision
    std::cout << 3.33333333333333333333333333333333333333f <<'\n'; // f suffix means float
    std::cout << 3.33333333333333333333333333333333333333 << '\n'; // no suffix means double

    return 0;
}

输出:

image

由于我们使用std::setprecision()将精度设置为17位,上述每个数字都以17位精度打印。但正如你所见,这些数字显然并非精确到17位!而且由于浮点数比双精度数精度更低,浮点数产生的误差更大。

提示
输出控制符(以及输入控制符)具有粘滞性——这意味着一旦设置,它们将保持有效状态。
唯一例外是 std::setw。某些输入输出操作会重置 std::setw,因此每次需要时都应手动调用 std::setw。

精度问题不仅影响分数,还会影响任何有效数字过多的数值。让我们考虑一个大数:

#include <iomanip> // for std::setprecision()
#include <iostream>

int main()
{
    float f { 123456789.0f }; // f has 10 significant digits
    std::cout << std::setprecision(9); // to show 9 digits in f
    std::cout << f << '\n';

    return 0;
}

输出:

image

123456792 大于 123456789。数值 123456789.0 具有 10 个有效数字位,但浮点数通常仅保留 7 位精度(而 123456792 的结果也仅精确到 7 个有效数字位)。我们丢失了精度!当数字无法精确存储导致精度损失时,这种现象称为舍入误差rounding error

因此,在使用浮点数时必须谨慎——若所需精度超出变量存储范围,便可能引发此类问题。

最佳实践
除非空间极为有限,否则优先使用双精度浮点数而非单精度浮点数,因为单精度浮点数的精度不足往往会导致计算误差。


舍入误差使浮点数比较变得棘手

浮点数难以处理,源于二进制(数据存储方式)与十进制(人类思维方式)之间的隐性差异。以分数1/10为例:在十进制中它可轻松表示为0.1,我们习惯将0.1视为仅含1个有效数字的易表示数值。但在二进制中,十进制数0.1对应的是无限序列:0.00011001100110011……因此当我们将0.1赋值给浮点数时,就会遭遇精度问题。

以下程序可直观展示这种影响:

#include <iomanip> // for std::setprecision()
#include <iostream>

int main()
{
    double d{0.1};
    std::cout << d << '\n'; // use default cout precision of 6
    std::cout << std::setprecision(17);
    std::cout << d << '\n';

    return 0;
}

这输出:

image

在上行中,std::cout 输出 0.1,这符合预期。

在下行中,当我们要求 std::cout 显示 17 位精度时,发现 d 实际上并非精确的 0.1!这是因为双精度类型受限于内存容量,不得不截断近似值。最终结果是精度达到16位有效数字(double类型保证的精度),但该数值并非精确的0.1。舍入误差可能使数值略微偏小或偏大,具体取决于截断位置。

舍入误差可能带来意想不到的后果:

#include <iomanip> // for std::setprecision()
#include <iostream>

int main()
{
    std::cout << std::setprecision(17);

    double d1{ 1.0 };
    std::cout << d1 << '\n';

    double d2{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 }; // should equal 1.0
    std::cout << d2 << '\n';

    return 0;
}

image

尽管我们可能预期d1和d2应相等,但实际结果并非如此。若在程序中比较d1和d2,程序很可能无法达到预期效果。由于浮点数往往存在不精确性,比较浮点数通常会引发问题——我们在第6.7节“关系运算符与浮点数比较”中将深入探讨该主题及其解决方案。

关于舍入误差的最后一点说明:数学运算(如加法和乘法)往往会放大舍入误差。因此,即使0.1的舍入误差出现在第17位有效数字,当我们十次累加0.1时,误差已悄然蔓延至第16位有效数字。持续运算将使该误差日益显著。

关键洞见
当数字无法精确存储时,就会产生舍入误差。即便是0.1这样简单的数字也可能出现这种情况。因此,舍入误差随时都可能发生,且确实无处不在。舍入误差并非例外——而是常态。切勿认为浮点数是精确的。
这条规则的推论是:在处理金融或货币数据时,务必谨慎使用浮点数。

相关内容
若需深入了解浮点数在二进制中的存储方式,请参阅 float.exposed 工具。
欲了解更多关于浮点数与舍入误差的知识,floating-point-gui.defabiensanglard.net 网站提供了通俗易懂的专题指南。


NaN 和 Inf

IEEE 754 兼容格式额外支持若干特殊值:

  • Inf 表示无穷大。Inf 为有符号数,可为正值(+Inf)或负值(-Inf)。
  • NaN 代表“非数值”。存在多种不同类型的 NaN(本文不作讨论)。
  • 带符号零,表示“正零”(+0.0)与“负零”(-0.0)采用独立表示形式。

不兼容IEEE 754的格式可能不支持部分(或全部)特殊值。此时,使用或生成这些特殊值的代码将产生实现定义的行为。

以下程序演示了三种特殊值:

#include <iostream>

int main()
{
    double zero { 0.0 };

    double posinf { 5.0 / zero }; // positive infinity
    std::cout << posinf << '\n';

    double neginf { -5.0 / zero }; // negative infinity
    std::cout << neginf << '\n';

    double z1 { 0.0 / posinf }; // positive zero
    std::cout << z1 << '\n';

    double z2 { -0.0 / posinf }; // negative zero
    std::cout << z2 << '\n';

    double nan { zero / zero }; // not a number (mathematically invalid)
    std::cout << nan << '\n';

    return 0;
}

使用Clang的测试结果:

image

请注意,打印 Inf 和 NaN 的结果因平台而异,因此您的结果可能有所不同(例如 Visual Studio 会将最后一个结果打印为 -nan(ind))。

最佳实践
避免除以0.0,即使您的编译器支持该操作。


结论

简而言之,关于浮点数有两点需要牢记:

  1. 浮点数适用于存储极大或极小的数值,包括含有小数部分的数值。
  2. 浮点数常存在微小舍入误差,即使数值有效位数少于精度要求时亦然。这些误差往往因数值过小且输出时被截断而难以察觉。然而浮点数比较可能导致结果偏差,对这些数值进行数学运算将使舍入误差逐渐累积放大。
posted @ 2026-02-14 08:24  游翔  阅读(2)  评论(0)    收藏  举报