У мові C++ є 6 операторів порівняння:
Оператор | Символ | Приклад | Операція |
Більше | > | x > y | true, якщо x більше y, в протилежному випадку — false |
Менше | < | x < y | true, якщо x менше y, в протилежному випадку — false |
Більше/Дорівнює | >= | x >= y | true, якщо x більше/дорівнює y, в протилежному випадку — false |
Менше/Дорівнює | <= | x <= y | true, якщо x менше/дорівнює y, в протилежному випадку — false |
Дорівнює | == | x == y | true, якщо x дорівнює y, в протилежному випадку — false |
Не дорівнює | != | x != y | true, якщо x не дорівнює y, в протилежному випадку — false |
Ви вже могли їх бачити в коді. Вони досить прості. Кожен з цих операторів обчислюється в логічне значення true
(1
) або false
(0
).
Ось декілька прикладів використання цих операторів на практиці:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#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; } |
Результат виконання програми:
Enter an integer: 4
Enter another integer: 5
4 does not equal 5
4 is less than 5
4 is less than or equal to 5
Все просто!
Порівняння значень типу з плаваючою крапкою
Порівняння значень типу з плаваючою крапкою за допомогою будь-якого з вищевказаних операторів — справа делікатна. Чому? А саме через ті невеликі помилки округлення, які можуть призвести до несподіваних результатів, наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> int main() { double d1(100 - 99.99); // повинно бути 0.01 double d2(10 - 9.99); // повинно бути 0.01 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
У вищенаведеній програмі d1 = 0.0100000000000005116
, а d2 = 0.0099999999999997868
. Обидва цих числа дуже близькі до 0.1
, але d1
більше d2
. Вони не є рівними.
Іноді порівняння чисел типу з плаваючою крапкою буває неминучим. В такому випадку слід використовувати оператори >
, <
, >=
і <=
, тільки якщо значення цих чисел дуже різняться між собою. А ось якщо два операнди майже рівні, то результат вже може бути несподіваний. У вищенаведеному прикладі наслідки неправильного результату незначні, а ось з оператором рівності справи йдуть гірше, тому що навіть при найменшій неточності результат відразу змінюється на протилежний очікуваному. Не рекомендується використовувати оператори ==
чи !=
зі значеннями типу з плаваючою крапкою. Замість них краще використовувати функцію, яка обчислює, наскільки еквівалентні ці два значення. Якщо різницею між ними можна знехтувати, то ми вважаємо їх рівними. Значення різниці між числами, якою можна знехтувати, називається епсилоном. Воно, зазвичай, невелике (наприклад, 0.0000001
).
Дуже часто початківці намагаються писати свої власні функції визначення рівності чисел:
1 2 3 4 5 6 7 |
#include <cmath> // для fabs() bool isAlmostEqual(double a, double b, double epsilon) { // Якщо різниця між a і b менше значення епсилону, то тоді a і b можна вважати рівними return fabs(a - b) <= epsilon; } |
Примітка: Функція fabs() — це функція з заголовкового файлу cmath, яка повертає абсолютне значення (модуль) параметра. fabs(а − b)
повертає додатне число як різницю між а
і b
.
Функція isAlmostEqual() з вищенаведеного прикладу порівнює різницю (а − b)
з епсилоном, вираховуючи, таким чином, чи можна вважати ці числа рівними. Якщо різниця між а
і b
дуже мала, то функція повертає true
.
Хоча це і робочий варіант, але він не ідеальний. Епсилон 0.0000001
підходить для чисел близьких до 1.0
, але буде занадто великим для чисел типу 0.0000001
і занадто малим для чисел типу 10000
. Це означає, що кожен раз, при виконанні функції, нам потрібно буде вибирати найбільш відповідний вхідним даним функції епсилон.
Дональд Кнут, відомий вчений, запропонував наступний спосіб у своїй книзі “Искусство программирования, том 2: Получисленные алгоритмы” (1968):
1 2 3 4 5 6 7 |
#include <cmath> // для fabs() // Повертаємо true, якщо різниця між a і b знаходиться в межах відсотка епсилону bool approximatelyEqual(double a, double b, double epsilon) { return fabs(a - b) <= ( (fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * epsilon); } |
Тут, замість використання епсилона як абсолютного числа, ми використовуємо його як множник, щоб підлаштуватися під вхідні дані.
Розглянемо детально, як працює функція approximatelyEqual(). Зліва від оператора <=
абсолютне значення (а − b)
повідомляє нам різницю між а
і b
(додатне число). Праворуч від <=
нам потрібно обчислити епсилон, тобто найбільше значення різниці чисел, яке ми готові прийняти. Для цього алгоритм вибирає більше з чисел а
і b
(як приблизний показник загальної величини чисел), а потім множить його на епсилон. У цій функції епсилоном є відсоткове співвідношення. Наприклад, якщо різниця між числами а
і b
знаходиться в межах 1% (більше або менше), то ми вводимо епсилон 1% (1% = 1/100 = 0.01). Його значення можна легко регулювати, в залежності від обставин (наприклад, 0.01% = епсилон 0.0001). Щоб зробити нерівність (!=
) замість рівності, просто викличте цю функцію, використовуючи логічний оператор НЕ (!
), щоб “перевернути” результат:
1 2 |
if (!approximatelyEqual(a, b, 0.001)) std::cout << a << " is not equal to " << b << "\n"; |
Але і функція approximatelyEqual() також не ідеальна, особливо, коли справа доходить до чисел, близьких нулю:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> #include <cmath> // для fabs() // Повертаємо true, якщо різниця між a і b знаходиться в межах відсотка епсилону bool approximatelyEqual(double a, double b, double epsilon) { return fabs(a - b) <= ((fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * epsilon); } int main() { // Значення a дуже близьке до 1.0, але, через помилки округлення, трохи менше 1.0 double a = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1; // По-перше, давайте порівняємо значення a (майже 1.0) з 1.0 std::cout << approximatelyEqual(a, 1.0, 1e-8) << "\n"; // По-друге, давайте порівняємо значення a - 1.0 (майже 0.0) з 0.0 std::cout << approximatelyEqual(a - 1.0, 0.0, 1e-8) << "\n"; } |
Можливо, ви здивуєтеся, але результат:
1
0
Другий виклик функції спрацював не так, як очікувалося. Математика просто ламається, коли справа доходить до нулів.
Але і цього можна уникнути, використовуючи як абсолютний епсилон (те, що ми робили в першому способі), так і відносний (спосіб Кнута) разом:
1 2 3 4 5 6 7 8 9 10 11 |
// Повертаємо true, якщо різниця між a і b менше absEpsilon або знаходиться в межах relEpsilon bool approximatelyEqualAbsRel(double a, double b, double absEpsilon, double relEpsilon) { // Перевіряємо числа на їх рівність один одному - це потрібно в випадках, коли порівнювані числа є нульовими або "біля нуля" double diff = fabs(a - b); if (diff <= absEpsilon) return true; // В протилежному випадку повертаємося до алгоритму Кнута return diff <= ( (fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * relEpsilon); } |
Тут ми додали новий параметр — absEpsilon
. Спочатку ми порівнюємо а
і b
з absEpsilon
, який повинен бути встановлений як дуже маленьке число (наприклад, 1e-12
). Таким чином, ми вирішуємо випадки, коли а
і b
— нульові значення або близькі до нуля. Якщо це не так, то ми повертаємося до алгоритму Кнута.
Протестуємо:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <iostream> #include <cmath> // для fabs() // Повертаємо true, якщо різниця між a і b знаходиться в межах відсотка епсилону bool approximatelyEqual(double a, double b, double epsilon) { return fabs(a - b) <= ((fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * epsilon); } // Повертаємо true, якщо різниця між a і b менше absEpsilon або знаходиться в межах relEpsilon bool approximatelyEqualAbsRel(double a, double b, double absEpsilon, double relEpsilon) { // Перевіряємо числа на їх рівність один одному - це потрібно в випадках, коли числа, що порівнюються, є нульовими або "біля нуля" double diff = fabs(a - b); if (diff <= absEpsilon) return true; // В протилежному випадку повертаємося до алгоритму Кнута return diff <= ((fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * relEpsilon); } int main() { // Значення a дуже близьке до 1.0, але, через помилки округлення, трохи менше 1.0 double a = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1; std::cout << approximatelyEqual(a, 1.0, 1e-8) << "\n"; // порівнюємо "майже 1.0" з 1.0 std::cout << approximatelyEqual(a - 1.0, 0.0, 1e-8) << "\n"; // порівнюємо "майже 0.0" з 0.0 std::cout << approximatelyEqualAbsRel(a - 1.0, 0.0, 1e-12, 1e-8) << "\n"; // порівнюємо "майже 0.0" з 0.0 } |
Результат:
1
0
1
З вдало підібраним absEpsilon
, функція approximatelyEqualAbsRel() обчислює нульові значення правильно.
Порівняння значень типу з плаваючою крапкою — складна тема, і немає одного ідеального алгоритму, який підійде в будь-якій ситуації. Однак для більшості випадків, з якими ви стикатиметеся, функції approximatelyEqualAbsRel() повинно бути достатньо.