Урок №42. Операторы сравнения

  Юрий  | 

  |

  Обновл. 11 Сен 2021  | 

 129886

 ǀ   23 

В языке 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).

Вот несколько примеров использования этих операторов на практике:

Результат выполнения программы:

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

Всё просто!

Сравнение чисел типа с плавающей точкой

Сравнение значений типа с плавающей точкой с помощью любого из этих операторов — дело опасное. Почему? Из-за тех самых небольших ошибок округления, которые могут привести к неожиданным результатам. Например:

Вот так раз:

d1 > d2

В вышеприведенной программе d1 = 0.0100000000000005116, а d2 = 0.0099999999999997868. Значения обоих этих чисел очень близки к 0.1, но d1 больше d2. Они не равны.

Иногда сравнение чисел типа с плавающей точкой бывает неизбежным. В таком случае следует использовать операторы >, <, >= и <= только если значения этих чисел сильно отличаются друг от друга. А вот если два операнда почти равны, то результат уже может быть неожиданный. В вышеприведенном примере последствия неправильного результата незначительны, а вот с оператором равенства дела обстоят хуже, так как даже при самой маленькой неточности результат сразу меняется на противоположный ожидаемому. Не рекомендуется использовать операторы == или != с числами типа с плавающей точкой. Вместо них следует использовать функцию, которая вычисляет, насколько эквивалентны эти два значения. Если разницей между ними можно пренебречь, то мы считаем их равными. Значение разницы между числами, которой можно пренебречь, называется эпсилоном. Оно, обычно, небольшое (например, 0.0000001).

Очень часто начинающие разработчики пытаются писать свои собственные функции определения равенства чисел:

Примечание: Функция fabs() — это функция из заголовочного файла cmath, которая возвращает абсолютное значение (модуль) параметра. fabs(а − b) возвращает положительное число как разницу между а и b.

Функция isAlmostEqual() из примера, приведенного выше, сравнивает разницу (a − b) и эпсилон, вычисляя, таким образом, можно ли считать эти числа равными. Если разница между а и b очень мала, то функция возвращает true.

Хоть это и рабочий вариант, но он не идеален. Эпсилон 0.00001 подходит для чисел около 1.0, но будет слишком большим для чисел типа 0.0000001 и слишком малым для чисел типа 10000. Это означает, что каждый раз при вызове функции нам нужно будет выбирать наиболее соответствующий входным данным функции эпсилон.

Дональд Кнут, известный учёный, предложил следующий способ в своей книге «Искусство программирования, том 2: Получисленные алгоритмы» (1968):

Здесь, вместо использования эпсилона как абсолютного числа, мы используем его как умножитель, чтобы подстроиться под входные данные.

Рассмотрим детально, как работает функция approximatelyEqual(). Слева от оператора <= абсолютное значение (а − b) сообщает нам разницу между а и b (положительное число). Справа от <= нам нужно вычислить эпсилон, т.е. наибольшее значение разности чисел, которое мы готовы принять. Для этого алгоритм выбирает большее из чисел а и b (как приблизительный показатель общей величины чисел), а затем умножает его на эпсилон. В этой функции эпсилон представляет собой процентное соотношение. Например, если разница между числами а и b находится в пределах 1% (больше или меньше), то мы вводим эпсилон 1% (1% = 1/100 = 0.01). Его значение можно легко регулировать, в зависимости от обстоятельств (например, 0.01% = эпсилон 0.0001). Чтобы сделать неравенство (!=) вместо равенства — просто вызовите эту функцию, используя логический оператор НЕ (!), чтобы перевернуть результат:

Но и функция approximatelyEqual() тоже не идеальна, особенно, когда дело доходит до чисел, близких к нулю:

Возможно, вы удивитесь, но результат:

1
0

Второй вызов не сработал так, как ожидалось. Математика просто ломается, когда дело доходит до нулей.

Но и этого можно избежать, используя как абсолютный эпсилон (то, что мы делали в первом способе), так и относительный (способ Кнута) вместе:

Здесь мы добавили новый параметр — absEpsilon. Сначала мы сравниваем а и b с absEpsilon, который должен быть задан как очень маленькое число (например, 1e-12). Таким образом, мы решаем случаи, когда а и b — нулевые значения или близки к нулю. Если это не так, то мы возвращаемся к алгоритму Кнута.

Протестируем:

Результат:

1
0
1

С удачно подобранным absEpsilon, функция approximatelyEqualAbsRel() обрабатывает близкие к нулю и нулевые значения корректно.

Сравнение чисел типа с плавающей точкой — сложная тема, и нет одного идеального алгоритма, который подойдет в любой ситуации. Однако для большинства случаев, с которыми вы будете сталкиваться, функции approximatelyEqualAbsRel() должно быть достаточно.


Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (458 оценок, среднее: 4,81 из 5)
Загрузка...

Комментариев: 23

  1. WinglessFantasy:

    Не понимаю эту функцию

    Я написал её в майн чтоб чекнуть её работоспособность и вот что получилось. Получилось что она полностью не рабочая при задаче показателей 0.001 и 0.0012 при епсилоне 0.01 в переводе 0.0001 получалось всегда след результат. При 0.001 выдавало тру при любом изменении нолей спереди или сзади любого числа на 1 оно выдавало ошибку. Как понять где эта черта лимита? Насколько я думал что епсилоном мы задаем черту за которой отличие одного числа от другого не важно на это не так! Ибо числа должны быть 1 к 1 что высветилось тру.

    поясните плиз на примерах как оно работает я не втыкаю сижу уже 2 часа тыцяю пробую и так и сяк оно просто рандомно выдает 1 или 0

  2. KILYAV:

    Проблема из ничего.

    Приводим float/double к __int32/__int64 и совершаем над ним операцию AND посредством которой отбрасываем определенное количество младших битов, на ваше усмотрение.

    Сравниваем полученные числа.

  3. Николай:

    Если в С++ такая проблема со сравнением дробных чисел, не будет ли логичнее создать отдельный класс? Чтобы каждый объект его состоял из трёх целых чисел (целая часть, дробная часть и количество цифр справа от запятой), а значит не возникало необходимости придумывать функции типа "приблизительно равно" и т.п.

  4. Андрей:

    Здравствуйте!
    Как правильно сравнивать высоту ( в дес. дробях 0,00 м) саму с собой через одну секунду?
    Задача поймать точку прохождения апогея (максимальной высоты).
    Написали такое, можете что получше подсказать?

  5. Андрей:

    А почему нельзя взять взять за вычисляемый эпсилон среднее арифметическое абсолютных значений сравниваемых величин умноженное на эпсилон? Код вроде попроще будет.

    1. Sasha:

      Можно и так наверно, но мне кажется тут берется большее число, потому что всегда надо рассматривать худший случай

  6. Эдуард:

    Если при сравнении чисел указать тип float вместо double, то результатом будет true, даже при обычном сравнении. Это специфика компилятора или есть еще что-то?

    1. Александр:

      Я тоже заметил что float точный, думаю нужно просто запомнить что double и long double имеют такие костыли.

    2. Борис:

      Почему так уверены? У float будет всё то же самое. Принцип хранения таких чисел ведь одинаковый, что флоат что дабл. А в данном случае у вас просто удачное совпадение. Попробуйте с другими числами и найдёте "неудачные".

  7. Артём:

    Возможно, вы удивитесь, но результат:

    1
    0

    Второй вызов не сработал так, как ожидалось. Математика просто ломается, когда дело доходит до нулей.
    Почему?

    1. Sasha:

      Потому что почти 1(допустим 0.9) — 1 = -0.1. Да это действительно меньше нуля и функция по логике должна возвращать true, но если посмотреть внимательнее можно заметить. что там берется модуль. То есть: fabs(-0.1) = 0.1, а это уже больше нуля

  8. Алексей:

    Тяжеловата тема, но интересно.
    Наибольшая сложность — не знаешь сразу куда применять.

  9. Сергей:

    Тема интересная, но не сразу дается. Код понял "примерно" т.е. поверхностно, чует сердце, буду к нему еще возвращаться. Принцип понятен сразу: как в тестере крутилка: 2 вольта, 20 вольт, 200 вольт и т.д. Воспоминание о аналоговых входах МК меня немного огорчило: там как раз и надо сравнивать небольшие напряжения. Например АКБ -зарядился или нет, сел или еще пойдет… теперь понимаю, почему так часто врут индикаторы заряда батарей. Спасибо, очередной интересный урок!

    1. Фото аватара Юрий:

      Пожалуйста 🙂 Главное — не зацикливайтесь, если что — вернётесь позже к этому уроку.

  10. Вячеслав:

    интересно для написания торгового робота на криптобирже нужно применять функцию approximatelyEqualAbsRel() или нет?

    1. Фото аватара Юрий:

      Вы пишете ботов на С++ для криптобирж?

      1. Вячеслав:

        хочу попробовать

  11. Дед Максим:

    Первый урок, который я вообще не понял :). Видимо, из-за того, что не выспался. Код вообще не понятен. Пытаюсь — не выходит(

    1. Константин:

      Алло, Дед Максим! Ты когда пишешь рукой на листочек строку текста и приближаешься к правому краю и видишь, что последнее слово (если будешь продолжать таким же почерком) не помещается в строку, что делаешь? Правильно. Прижимистей буквы друг к другу тулишь. Это аналоговое представление значений. Цифровое же (то, которое в ЭВМ) — это когда все знаки и расстояния между ними строго одинаковы. И теперь представь себе, что точность — это ширина листа (если листок в клеточку, вообще, идеальная аналогия цифрового представления значений!) И вот тебе надо сравнить заряд электрона и заряд бозона. Что надо сделать? Правильно! Взять листочки по-ширше, т е. установить по-больше точность, иначе не влезающие цифры пропадут и вместо сравниваемых значений вообще какая-то дурь осядет. Но это ещё пол-беды! Подоплёка машинных "мансов" в том, что ЭВМ втихаря дописывает в клеточки левые цифры для заполнения пустующих после значащих цифр клеточек. Ну естественно результаты сравнения 100 — 99.99 и 10 — 9.99 с такими мансами будут не корректными! Да, дык о чём это я? А, вот пример: Требуется сравнить две трёхлитровых банки с жидкостью (молоко, самогон — по вкусу:-). Задаёмся граничным условием — если разница залитых объёмов не превышает одну пипетку (эпсилон) принимаем объёмы как равные. Пипетка — это абсолютный эпсилон, а объём пипетки/объём банки — это относительный эпсилон. А если объёмы сопоставимы с пипеткой (близки нулю)? Тогда Гулливер ловит лилипута, аннексирует у него пипетку (absEpsilon) и если разница меньше этого absEpsilon, то значения объёмов за "ноль" сойдут — не похмелишься (не наешься)!

  12. Oleksiy:

    Радует то, что в реальной жизни чаще требуется сравнивать целые числа. А когда доходит до чисел с плавающей точкой, то там почти всегда не важно ">" или ">=".

    1. Фото аватара Юрий:

      Ну это в реальной жизни 🙂 Та и в реальной жизни бывают исключения.

  13. Георгий:

    Кажется у меня отключился мозг после строчки: "Очень часто начинающие разработчики пытаются писать свои собственные функции определения равенства чисел:"

    1. Фото аватара Юрий:

      Почему? 🙂

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *