6-7 关系运算符与浮点数比较
关系运算符Relational operators是用于比较两个值的运算符。共有6种关系运算符:
| Operator | Symbol | Form | Operation |
|---|---|---|---|
| Greater than | > | x > y | true if x is greater than y, false otherwise |
| Less than | < | x < y | true if x is less than y, false otherwise |
| Greater than or equals | >= | x >= y | true if x is greater than or equal to y, false otherwise |
| Less than or equals | <= | x <= y | true if x is less than or equal to y, false otherwise |
| Equality | == | x == y | true if x equals y, false otherwise |
| Inequality | != | x != y | true if x does not equal y, false otherwise |
你已经了解了这些运算符的大部分用法,它们相当直观。每个运算符都会评估为布尔值 true(1)或 false(0)。
以下是使用这些运算符处理整数的示例代码:
#include <iostream>
int main()
{
std::cout << "Enter an integer: ";
int x{};
std::cin >> x;
std::cout << "Enter another integer: ";
int y{};
std::cin >> y;
if (x == y)
std::cout << x << " equals " << y << '\n';
if (x != y)
std::cout << x << " does not equal " << y << '\n';
if (x > y)
std::cout << x << " is greater than " << y << '\n';
if (x < y)
std::cout << x << " is less than " << y << '\n';
if (x >= y)
std::cout << x << " is greater than or equal to " << y << '\n';
if (x <= y)
std::cout << x << " is less than or equal to " << y << '\n';
return 0;
}
以下是样本运行的结果:

这些运算符在比较整数时极其简单易用。
布尔条件值
默认情况下,if 语句或条件运算符(以及其他一些地方)中的条件计算为布尔值。
许多新程序员会写这样的语句:
if (b1 == true) ...
这是多余的,因为 == true 实际上并没有为条件添加任何值。相反,我们应该这样写:
if (b1) ...
同样,以下内容:
if (b1 == false) ...
更好地写成:
if (!b1) ...
最佳实践
不要向条件添加不必要的 == 或 !=。它使它们更难以阅读而不提供任何附加价值。
计算浮点值的比较可能会出现问题
考虑以下程序:
#include <iostream>
int main()
{
constexpr double d1{ 100.0 - 99.99 }; // should equal 0.01 mathematically
constexpr double d2{ 10.0 - 9.99 }; // should equal 0.01 mathematically
if (d1 == d2)
std::cout << "d1 == d2" << '\n';
else if (d1 > d2)
std::cout << "d1 > d2" << '\n';
else if (d1 < d2)
std::cout << "d1 < d2" << '\n';
return 0;
}
变量 d1 和 d2 的值都应为 0.01。但该程序输出结果出乎意料:

若在调试器中检查d1和d2的值,你会发现d1 = 0.010000000000005116而d2 = 0.0099999999999997868。两者都接近0.01,但d1大于该值而d2小于该值。
使用任何关系运算符比较浮点数都可能导致危险结果。这是因为浮点数本身不具备绝对精确性,浮点数操作数中的微小舍入误差可能使其实际值略小或略大于预期值,从而干扰关系运算符的判断。
相关内容
我们在第4.8课---浮点数 中讨论过舍入误差问题。
浮点数小于与大于运算
当小于(<)、大于(>)、小于等于(<=)和大于等于(>=)运算符用于浮点数时,在大多数情况下(当操作数值不相似时)会产生可靠的结果。然而,若操作数极为接近,这些运算符则不可靠。例如上例中d1 > d2恰巧返回真值,但若数值误差方向不同,同样可能返回假值。
若操作数相近时出现错误结果的后果可接受,则使用这些运算符尚可容忍。此为应用场景特定的决策。
以《太空侵略者》类游戏为例:当需要判断两个移动物体(如导弹与外星人)是否相交时,若两者距离较远,这些运算符将返回正确结果;但当物体极度接近时,结果可能出现偏差。在这种情况下,错误结果可能根本不会被察觉(仅表现为擦肩而过或近乎命中),游戏仍会继续进行。
浮点数的相等与不等
相等运算符(== 和 !=)则麻烦得多。以运算符 == 为例,它仅在操作数完全相等时返回真值。由于即使最微小的舍入误差也会导致两个浮点数不相等,因此当预期为真时,运算符 == 极易返回假值。运算符!=同样存在此类问题。
#include <iostream>
int main()
{
std::cout << std::boolalpha << (0.3 == 0.2 + 0.1); // prints false
return 0;
}

因此,通常应避免将这些运算符用于浮点数操作数。
警告
若浮点数值存在计算可能,请避免使用运算符==和运算符!=进行比较。
上述规则存在一个显著例外:当浮点数字面量与同类型变量进行比较时,若该变量初始化值与字面量同源,且双方有效位数均未超出该类型最小精度限制,则比较操作安全可靠。其中浮点数(float)的最小精度为6位有效数字,双精度浮点数(double)则为15位有效数字。
不同数据类型的精度将在第4.8节 浮点数中详述。
例如,你可能偶尔会遇到返回浮点数字面量的函数(通常为0.0,有时也为1.0)。此类情况下,直接与同类型相同字面量进行比较是安全的:
if (someFcn() == 0.0) // okay if someFcn() returns 0.0 as a literal only
// do something
除了字面量之外,我们还可以比较用字面量初始化的常量或constexpr浮点变量:
constexpr double gravity { 9.8 };
if (gravity == 9.8) // okay if gravity was initialized with a literal
// we're on earth
比较不同类型的浮点数常量通常是不安全的。例如,比较 9.8f 与 9.8 将返回 false。
提示
当浮点数字面量与同类型变量进行比较时,只要每个字面量的有效数字位数不超过该类型的最小精度,且该变量初始化值与字面量相同,则比较是安全的。浮点数(float)的最小精度为6位有效数字,双精度浮点数(double)的最小精度为15位有效数字。不同类型的浮点数字面量通常不安全比较。
比较浮点数(进阶/可选阅读)
那么如何合理地比较两个浮点数操作数以判断它们是否相等?
最常见的浮点数相等性判断方法是使用函数检测两个数是否接近。若它们“足够接近”,则视为相等。传统上,表示“足够接近”的数值称为epsilon(us /ˈep.sə.lɑːn/)。它通常定义为一个极小的正数(例如0.00000001,有时写作1e-8)。
新手开发者常尝试编写类似下面的“足够接近”函数:
#include <cmath> // for std::abs()
// absEpsilon is an absolute value
bool approximatelyEqualAbs(double a, double b, double absEpsilon)
{
// if the distance between a and b is less than or equal to absEpsilon, then a and b are "close enough"
return std::abs(a - b) <= absEpsilon;
}
std::abs() 是
虽然该函数可行,但并非理想方案。当输入值接近1.0时,0.00001的ε值适用;但对于0.0000001左右的输入则过大,而对于10,000等输入又过小。
顺带一提……
如果我们规定任何与另一个数相差在0.00001范围内的数都应视为相同,那么:
- 1和1.0001会被视为不同,但1和1.00001会被视为相同。这倒也说得通。
- 0.0000001 与 0.00001 被视为相同。这似乎不太合理,因为两者相差两个数量级。
- 10000 与 10000.0001 被视为不同。这也似乎不太合理,因为考虑到数值的大小,两者几乎没有区别。
这意味着每次调用该函数时,我们都必须根据输入数据选择合适的ε值。既然我们知道需要根据输入数据的大小成比例地调整ε值,不如直接修改函数使其自动完成这项工作。
著名计算机科学家唐纳德·克努特Donald Knuth在其著作《计算机程序设计艺术·第二卷:半数值算法》(Addison-Wesley出版社,1969年)中提出如下方法:
#include <algorithm> // for std::max
#include <cmath> // for std::abs
// Return true if the difference between a and b is within epsilon percent of the larger of a and b
bool approximatelyEqualRel(double a, double b, double relEpsilon)
{
return (std::abs(a - b) <= (std::max(std::abs(a), std::abs(b)) * relEpsilon));
}
在此情况下,epsilon不再是绝对数值,而是相对于a或b的绝对值而存在的相对量。
让我们更详细地解析这个看似复杂的函数原理。在<=运算符左侧,std::abs(a - b)以正数形式表示a与b之间的距离。
在<=运算符右侧,我们需要计算可接受的“足够接近”的最大值。为此,算法先取a和b中较大的数(作为整体数值大小的粗略指标),再乘以relEpsilon。在此函数中,relEpsilon代表一个百分比。例如若要求“足够接近”意味着 a 和 b 需小于或等于较大值的 1%,则传入 relEpsilon 值为 0.01(1% = 1/100 = 0.01)。relEpsilon 的数值可根据具体场景灵活调整(例如 epsilon=0.002 表示误差范围为 0.2%)。
若需实现不等于 (!=) 条件而非等于条件,只需调用本函数并使用逻辑非运算符 (!) 翻转结果:
if (!approximatelyEqualRel(a, b, 0.001))
std::cout << a << " is not equal to " << b << '\n';
请注意,虽然approximatelyEqualRel()函数在大多数情况下都能正常工作,但它并非完美无缺,尤其当数值接近零时:
#include <algorithm> // for std::max
#include <cmath> // for std::abs
#include <iostream>
// Return true if the difference between a and b is within epsilon percent of the larger of a and b
bool approximatelyEqualRel(double a, double b, double relEpsilon)
{
return (std::abs(a - b) <= (std::max(std::abs(a), std::abs(b)) * relEpsilon));
}
int main()
{
// a is really close to 1.0, but has rounding errors
constexpr double a{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 };
constexpr double relEps { 1e-8 };
constexpr double absEps { 1e-12 };
std::cout << std::boolalpha; // print true or false instead of 1 or 0
// First, let's compare a (almost 1.0) to 1.0.
std::cout << approximatelyEqualRel(a, 1.0, relEps) << '\n';
// Second, let's compare a-1.0 (almost 0.0) to 0.0
std::cout << approximatelyEqualRel(a-1.0, 0.0, relEps) << '\n';
return 0;
}
出乎意料的是,这返回了:

第二次调用未达到预期效果。当值接近零时,计算逻辑便会失效。
规避此问题的方案是同时采用绝对阈值(如首次方案所示)与相对阈值(如Knuth方案所示):
// Return true if the difference between a and b is less than or equal to absEpsilon, or within relEpsilon percent of the larger of a and b
bool approximatelyEqualAbsRel(double a, double b, double absEpsilon, double relEpsilon)
{
// Check if the numbers are really close -- needed when comparing numbers near zero.
if (std::abs(a - b) <= absEpsilon)
return true;
// Otherwise fall back to Knuth's algorithm
return approximatelyEqualRel(a, b, relEpsilon);
}
在此算法中,我们首先检查a和b在绝对值上是否相近,这处理了a和b都接近零的情况。absEpsilon参数应设置为非常小的数值(例如1e-12)。若此检查失败,则回退到Knuth算法,使用相对误差阈值。
以下是我们先前测试两种算法的代码:
#include <algorithm> // for std::max
#include <cmath> // for std::abs
#include <iostream>
// Return true if the difference between a and b is within epsilon percent of the larger of a and b
bool approximatelyEqualRel(double a, double b, double relEpsilon)
{
return (std::abs(a - b) <= (std::max(std::abs(a), std::abs(b)) * relEpsilon));
}
// Return true if the difference between a and b is less than or equal to absEpsilon, or within relEpsilon percent of the larger of a and b
bool approximatelyEqualAbsRel(double a, double b, double absEpsilon, double relEpsilon)
{
// Check if the numbers are really close -- needed when comparing numbers near zero.
if (std::abs(a - b) <= absEpsilon)
return true;
// Otherwise fall back to Knuth's algorithm
return approximatelyEqualRel(a, b, relEpsilon);
}
int main()
{
// a is really close to 1.0, but has rounding errors
constexpr double a{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 };
constexpr double relEps { 1e-8 };
constexpr double absEps { 1e-12 };
std::cout << std::boolalpha; // print true or false instead of 1 or 0
std::cout << approximatelyEqualRel(a, 1.0, relEps) << '\n'; // compare "almost 1.0" to 1.0
std::cout << approximatelyEqualRel(a-1.0, 0.0, relEps) << '\n'; // compare "almost 0.0" to 0.0
std::cout << approximatelyEqualAbsRel(a, 1.0, absEps, relEps) << '\n'; // compare "almost 1.0" to 1.0
std::cout << approximatelyEqualAbsRel(a-1.0, 0.0, absEps, relEps) << '\n'; // compare "almost 0.0" to 0.0
return 0;
}

你可以看到approximatelyEqualAbsRel()函数能正确处理小数值输入。
浮点数比较是个复杂的话题,并不存在适用于所有情况的通用算法。不过,当absEpsilon设为1e-12、relEpsilon设为1e-8时,approximatelyEqualAbsRel()函数应该足以应对你遇到的绝大多数情况。
使approximatelyEqual函数成为constexpr(高级特性)
在C++23中,通过添加constexpr关键字可使两个approximatelyEqual函数成为constexpr:
// C++23 version
#include <algorithm> // for std::max
#include <cmath> // for std::abs (constexpr in C++23)
// Return true if the difference between a and b is within epsilon percent of the larger of a and b
constexpr bool approximatelyEqualRel(double a, double b, double relEpsilon)
{
return (std::abs(a - b) <= (std::max(std::abs(a), std::abs(b)) * relEpsilon));
}
// Return true if the difference between a and b is less than or equal to absEpsilon, or within relEpsilon percent of the larger of a and b
constexpr bool approximatelyEqualAbsRel(double a, double b, double absEpsilon, double relEpsilon)
{
// Check if the numbers are really close -- needed when comparing numbers near zero.
if (std::abs(a - b) <= absEpsilon)
return true;
// Otherwise fall back to Knuth's algorithm
return approximatelyEqualRel(a, b, relEpsilon);
}
相关内容
我们在第F.1课——常量表达式函数中介绍了常量表达式函数。
然而在C++23之前,我们遇到一个问题:若在常量表达式中调用这些常量表达式函数,它们将失败:
int main()
{
// a is really close to 1.0, but has rounding errors
constexpr double a{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 };
constexpr double relEps { 1e-8 };
constexpr double absEps { 1e-12 };
std::cout << std::boolalpha; // print true or false instead of 1 or 0
constexpr bool same { approximatelyEqualAbsRel(a, 1.0, absEps, relEps) }; // compile error: must be initialized by a constant expression
std::cout << same << '\n';
return 0;
}
这是因为用于常量表达式的constexpr函数不能调用非constexpr函数,而std::abs在C++23之前并未实现constexpr特性。
不过这个问题很容易解决——我们只需弃用std::abs,转而实现自己的constexpr绝对值函数即可。
// C++14/17/20 version
#include <algorithm> // for std::max
#include <iostream>
// Our own constexpr implementation of std::abs (for use in C++14/17/20)
// In C++23, use std::abs
// constAbs() can be called like a normal function, but can handle different types of values (e.g. int, double, etc...)
template <typename T>
constexpr T constAbs(T x)
{
return (x < 0 ? -x : x);
}
// Return true if the difference between a and b is within epsilon percent of the larger of a and b
constexpr bool approximatelyEqualRel(double a, double b, double relEpsilon)
{
return (constAbs(a - b) <= (std::max(constAbs(a), constAbs(b)) * relEpsilon));
}
// Return true if the difference between a and b is less than or equal to absEpsilon, or within relEpsilon percent of the larger of a and b
constexpr bool approximatelyEqualAbsRel(double a, double b, double absEpsilon, double relEpsilon)
{
// Check if the numbers are really close -- needed when comparing numbers near zero.
if (constAbs(a - b) <= absEpsilon)
return true;
// Otherwise fall back to Knuth's algorithm
return approximatelyEqualRel(a, b, relEpsilon);
}
int main()
{
// a is really close to 1.0, but has rounding errors
constexpr double a{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 };
constexpr double relEps { 1e-8 };
constexpr double absEps { 1e-12 };
std::cout << std::boolalpha; // print true or false instead of 1 or 0
constexpr bool same { approximatelyEqualAbsRel(a, 1.0, absEps, relEps) };
std::cout << same << '\n';
return 0;
}
对于进阶读者
上文中的constAbs()版本是一个函数模板,它允许我们编写单一定义来处理不同类型的值。我们在第11.6节——函数模板中将详细讲解函数模板。

浙公网安备 33010602011771号