В языке 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() из примера, приведенного выше, сравнивает разницу (a − b)
и эпсилон, вычисляя, таким образом, можно ли считать эти числа равными. Если разница между а
и b
очень мала, то функция возвращает true.
Хоть это и рабочий вариант, но он не идеален. Эпсилон 0.00001 подходит для чисел около 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() должно быть достаточно.
Не понимаю эту функцию
Я написал её в майн чтоб чекнуть её работоспособность и вот что получилось. Получилось что она полностью не рабочая при задаче показателей 0.001 и 0.0012 при епсилоне 0.01 в переводе 0.0001 получалось всегда след результат. При 0.001 выдавало тру при любом изменении нолей спереди или сзади любого числа на 1 оно выдавало ошибку. Как понять где эта черта лимита? Насколько я думал что епсилоном мы задаем черту за которой отличие одного числа от другого не важно на это не так! Ибо числа должны быть 1 к 1 что высветилось тру.
поясните плиз на примерах как оно работает я не втыкаю сижу уже 2 часа тыцяю пробую и так и сяк оно просто рандомно выдает 1 или 0
Проблема из ничего.
Приводим float/double к __int32/__int64 и совершаем над ним операцию AND посредством которой отбрасываем определенное количество младших битов, на ваше усмотрение.
Сравниваем полученные числа.
Если в С++ такая проблема со сравнением дробных чисел, не будет ли логичнее создать отдельный класс? Чтобы каждый объект его состоял из трёх целых чисел (целая часть, дробная часть и количество цифр справа от запятой), а значит не возникало необходимости придумывать функции типа "приблизительно равно" и т.п.
Здравствуйте!
Как правильно сравнивать высоту ( в дес. дробях 0,00 м) саму с собой через одну секунду?
Задача поймать точку прохождения апогея (максимальной высоты).
Написали такое, можете что получше подсказать?
А почему нельзя взять взять за вычисляемый эпсилон среднее арифметическое абсолютных значений сравниваемых величин умноженное на эпсилон? Код вроде попроще будет.
Можно и так наверно, но мне кажется тут берется большее число, потому что всегда надо рассматривать худший случай
Если при сравнении чисел указать тип float вместо double, то результатом будет true, даже при обычном сравнении. Это специфика компилятора или есть еще что-то?
Я тоже заметил что float точный, думаю нужно просто запомнить что double и long double имеют такие костыли.
Почему так уверены? У float будет всё то же самое. Принцип хранения таких чисел ведь одинаковый, что флоат что дабл. А в данном случае у вас просто удачное совпадение. Попробуйте с другими числами и найдёте "неудачные".
Возможно, вы удивитесь, но результат:
1
0
Второй вызов не сработал так, как ожидалось. Математика просто ломается, когда дело доходит до нулей.
Почему?
Потому что почти 1(допустим 0.9) — 1 = -0.1. Да это действительно меньше нуля и функция по логике должна возвращать true, но если посмотреть внимательнее можно заметить. что там берется модуль. То есть: fabs(-0.1) = 0.1, а это уже больше нуля
Тяжеловата тема, но интересно.
Наибольшая сложность — не знаешь сразу куда применять.
Тема интересная, но не сразу дается. Код понял "примерно" т.е. поверхностно, чует сердце, буду к нему еще возвращаться. Принцип понятен сразу: как в тестере крутилка: 2 вольта, 20 вольт, 200 вольт и т.д. Воспоминание о аналоговых входах МК меня немного огорчило: там как раз и надо сравнивать небольшие напряжения. Например АКБ -зарядился или нет, сел или еще пойдет… теперь понимаю, почему так часто врут индикаторы заряда батарей. Спасибо, очередной интересный урок!
Пожалуйста 🙂 Главное — не зацикливайтесь, если что — вернётесь позже к этому уроку.
интересно для написания торгового робота на криптобирже нужно применять функцию approximatelyEqualAbsRel() или нет?
Вы пишете ботов на С++ для криптобирж?
хочу попробовать
Первый урок, который я вообще не понял :). Видимо, из-за того, что не выспался. Код вообще не понятен. Пытаюсь — не выходит(
Алло, Дед Максим! Ты когда пишешь рукой на листочек строку текста и приближаешься к правому краю и видишь, что последнее слово (если будешь продолжать таким же почерком) не помещается в строку, что делаешь? Правильно. Прижимистей буквы друг к другу тулишь. Это аналоговое представление значений. Цифровое же (то, которое в ЭВМ) — это когда все знаки и расстояния между ними строго одинаковы. И теперь представь себе, что точность — это ширина листа (если листок в клеточку, вообще, идеальная аналогия цифрового представления значений!) И вот тебе надо сравнить заряд электрона и заряд бозона. Что надо сделать? Правильно! Взять листочки по-ширше, т е. установить по-больше точность, иначе не влезающие цифры пропадут и вместо сравниваемых значений вообще какая-то дурь осядет. Но это ещё пол-беды! Подоплёка машинных "мансов" в том, что ЭВМ втихаря дописывает в клеточки левые цифры для заполнения пустующих после значащих цифр клеточек. Ну естественно результаты сравнения 100 — 99.99 и 10 — 9.99 с такими мансами будут не корректными! Да, дык о чём это я? А, вот пример: Требуется сравнить две трёхлитровых банки с жидкостью (молоко, самогон — по вкусу:-). Задаёмся граничным условием — если разница залитых объёмов не превышает одну пипетку (эпсилон) принимаем объёмы как равные. Пипетка — это абсолютный эпсилон, а объём пипетки/объём банки — это относительный эпсилон. А если объёмы сопоставимы с пипеткой (близки нулю)? Тогда Гулливер ловит лилипута, аннексирует у него пипетку (absEpsilon) и если разница меньше этого absEpsilon, то значения объёмов за "ноль" сойдут — не похмелишься (не наешься)!
Радует то, что в реальной жизни чаще требуется сравнивать целые числа. А когда доходит до чисел с плавающей точкой, то там почти всегда не важно ">" или ">=".
Ну это в реальной жизни 🙂 Та и в реальной жизни бывают исключения.
Кажется у меня отключился мозг после строчки: "Очень часто начинающие разработчики пытаются писать свои собственные функции определения равенства чисел:"