Вычисление с плавающей запятой дает разные результаты с float, чем с double

У меня есть следующая строка кода.

hero->onBeingHit(ENEMY_ATTACK_POINT * (1.0 - hero->getDefensePercent()));
  • Метод void onBeingHit(int decHP) принимает целое число и обновляет очки здоровья.
  • float getDefensePercent() — это геттер-метод, возвращающий процент защиты героя.
  • ENEMY_ATTACK_POINT — макропостоянный коэффициент, определяемый как #define ENEMY_ATTACK_POINT 20.

Допустим, hero->getDefensePercent() возвращает 0.1. Итак, расчет

20 * (1.0 - 0.1)  =  20 * (0.9)  =  18

Всякий раз, когда я пробовал это со следующим кодом (без добавления f 1.0)

hero->onBeingHit(ENEMY_ATTACK_POINT * (1.0 - hero->getDefensePercent()));

У меня 17.

Но для следующего кода (f добавляется после 1.0)

hero->onBeingHit(ENEMY_ATTACK_POINT * (1.0f - hero->getDefensePercent()));

У меня 18.

В чем дело? Важно ли вообще иметь f, хотя hero->getDefensePercent() уже находится в плавающем состоянии?


person haxpor    schedule 15.02.2013    source источник
comment
Вы не можете точно сохранить .9 или .1 (с типами данных с плавающей запятой). Вероятно, меньшая точность в двойном числе приведет к чему-то вроде 18.00x вместо 17.999xxx. Обратите внимание, что floating point --> int всегда находится на нижнем уровне.   -  person Zeta    schedule 15.02.2013
comment
Не слишком хорошо знаю С++, но я ожидаю, что ваш литерал интерпретируется как double и, таким образом, заставляет вычисления выполняться в doubles, а не floats.   -  person Damien_The_Unbeliever    schedule 15.02.2013
comment
ENEMY_ATTACK_POINT — целое число. Вы получаете ошибки округления, так как используете целочисленную математику.   -  person Pubby    schedule 15.02.2013
comment
liveworkspace.org/code/1KmxNy$0   -  person BoBTFish    schedule 15.02.2013
comment
liveworkspace.org/code/uNqnq$7   -  person BoBTFish    schedule 15.02.2013
comment
Зета и Дэмиен Имеет смысл и сопровождается ответом Лимеса ниже. Спасибо за быстрый ответ.   -  person haxpor    schedule 15.02.2013
comment
@Pubby Но другая переменная имеет более высокую точность, так что это не должно быть проблемой.   -  person haxpor    schedule 15.02.2013
comment
@BoBTFish Спасибо за ваши усилия по размещению там примеров кода. +1 за это.   -  person haxpor    schedule 15.02.2013


Ответы (3)


Что происходит? Почему целочисленный результат не равен 18 в обоих случаях?

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

0.1 не может быть представлено точно как значение с плавающей запятой (в обоих случаях). Компилятор выполняет преобразование в двоичное число с плавающей запятой IEEE754 и решает, следует ли округлить в большую или меньшую сторону до представляемого значения. Затем процессор умножает это значение во время выполнения, и результат округляется, чтобы получить целочисленное значение.

Хорошо, но поскольку и double, и float ведут себя так, почему я получаю 18 в одном из двух случаев, а 17 — в другом? Я в замешательстве.

Ваш код берет результат функции 0.1f (число с плавающей запятой), а затем вычисляет 20 * (1.0 - 0.1f), которое является двойным выражением, а 20 * (1.0f - 0.1f) — выражением с плавающей запятой. Теперь версия с плавающей запятой оказывается немного больше 18.0 и округляется до 18, в то время как двойное выражение немного меньше 18.0 и округляется до 17.

Если вы не знаете точно, как двоичные числа с плавающей запятой IEEE754 строятся из десятичных чисел, это будет в значительной степени случайным, если оно будет немного меньше или немного больше, чем десятичное число, которое вы ввели в свой код. Так что не стоит на это рассчитывать. Не пытайтесь исправить такую ​​проблему, добавляя f к одному из чисел и говоря: «Теперь это работает, поэтому я оставлю это f там», потому что другое значение снова ведет себя по-другому.

Почему тип выражения зависит от наличия этого f?

Это связано с тем, что литерал с плавающей запятой в C и C++ имеет тип double по умолчанию. Если вы добавите f, это будет число с плавающей запятой. Результат выражения с плавающей запятой имеет тип "больше". Результат двойного выражения и целого числа по-прежнему является двойным выражением, а int и float будут числом с плавающей запятой. Таким образом, результатом вашего выражения является либо число с плавающей точкой, либо двойное число.

Хорошо, но я не хочу округлять до нуля. Я хочу округлить до ближайшего числа.

Чтобы устранить эту проблему, добавьте половину к результату, прежде чем преобразовывать его в целое число:

hero->onBeingHit(ENEMY_ATTACK_POINT * (1.0 - hero->getDefensePercent()) + 0.5);

В C++11 для этого есть std::round(). В предыдущих версиях стандарта не было такой функции для округления до ближайшего целого числа. (Подробности см. в комментариях.)

Если у вас нет std::round, вы можете написать его самостоятельно. Будьте осторожны при работе с отрицательными числами. При преобразовании в целое число будет усечено (округлено до нуля), что означает, что отрицательные значения будут округлены вверх, а не вниз. Таким образом, мы должны вычесть половину, если число отрицательное:

int round(double x) {
    return (x < 0.0) ? (x - .5) : (x + .5);
}
person leemes    schedule 15.02.2013
comment
Спасибо за проницательное объяснение. - person haxpor; 15.02.2013
comment
Вам не нужно добавлять 0.5 или писать свою собственную функцию. Просто используйте std::round()<cmath>). В общем, я бы рекомендовал всегда использовать одну из стандартных функций (round, floor, ciel или trunc); это делает ваше намерение ясным (и заставляет вас думать о том, что вы на самом деле хотите сделать). - person James Kanze; 15.02.2013
comment
@JamesKanze Это только C ++ 11. Я отредактировал ответ соответственно, спасибо. - person leemes; 15.02.2013
comment
@leemes Это также C99 (и Posix), поэтому он присутствует в большинстве реализаций C ++ 03 или даже C ++ 98. Microsoft может быть исключением, поскольку они явно не поддерживают современный C. Но Microsoft более или менее лидирует в поддержке C++11, поэтому любой последний компилятор Microsoft должен их поддерживать. И, конечно же, поскольку они также являются Posix, они должны быть в любом компиляторе Unix. - person James Kanze; 15.02.2013
comment
@JamesKanze Исправил. Я этого не знал. Но в C++03 это не std::round, а просто round. В ответ я теперь ссылаюсь на комментарии. - person leemes; 15.02.2013
comment
@leemes Да. Возможно, вам придется включить <math.h> и использовать только round. (И что произойдет, если заголовок, который вы включаете, включает <math.h>, и вы используете свое определение? Как правило, вы никогда не должны определять функцию, которая имеет то же имя, что и что-либо в стандартной библиотеке C --- или в любой другой C API, который вы используете, например, Posix. Но вы можете знать их все?) - person James Kanze; 15.02.2013

1.0 интерпретируется как двойное, в отличие от 1.0f, которое рассматривается компилятором как число с плавающей запятой.

Суффикс f просто сообщает компилятору, какое число является числом с плавающей запятой, а какое — двойным.

Как следует из названия, двойное число имеет в 2 раза большую точность, чем число с плавающей запятой. Как правило, тип double имеет точность от 15 до 16 десятичных знаков, а тип float — только 7.

Эта потеря точности может привести к ошибкам усечения, которые намного легче всплывать.

См. MSDN (C++)

person Community    schedule 15.02.2013
comment
double имеет как минимум такую ​​же точность, как float. То же самое, меньше двух раз, два раза или больше двух раз определяется реализацией. В обычных форматах IEEE double имеет точность 52 бита, float — 24 бита, double — 53, поэтому double имеет более чем в два раза большую точность. Но есть процессоры (хотя большинство программистов на C++ никогда их не увидят), где double и float идентичны. (Также обратите внимание, что на большинстве мэйнфреймов машинные числа с плавающей запятой не двоичные, а основаны на 16 или 8.) - person James Kanze; 15.02.2013
comment
Кроме того (опять же просто собирая гниды — в вашем ответе нет ничего плохого): то, что подразумевается под десятичной точностью, различается. Для кругового пути (преобразование в десятичное и обратно, гарантированное получение того же значения) с IEEE вам нужно 9 (float) или 17 (double) цифр. В качестве альтернативы вы можете рассмотреть поездку туда и обратно в другом направлении, что дает 6 и 15. (Они соответствуют digits10 и max_digits10 в numeric_limits.) - person James Kanze; 15.02.2013
comment
Это нормально :) Мне нравится новая информация :) - person ; 15.02.2013
comment
@JamesKanze Спасибо за информацию. - person haxpor; 15.02.2013

Причина, по которой это происходит, заключается в более точном результате при использовании double, т.е. 1.0.

Попробуйте округлить свой результат, что приведет к более точному интегральному результату после преобразования:

hero->onBeingHit(ENEMY_ATTACK_POINT * (1.0 - hero->getDefensePercent()) + 0.5);

Обратите внимание, что добавление 0.5 и усечение до int сразу после этого приведет к округлению результата, поэтому к тому времени, когда ваш результат будет 17.999..., он станет 18.499..., который будет усечен до 18

person LihO    schedule 15.02.2013