Неинтуитивный результат присвоения числа двойной точности переменной int в C
Может кто-нибудь дать мне объяснение, почему я получаю два разных числа, соответственно. 14 и 15, как результат из следующего кода?
#include <stdio.h>
int main()
{
double Vmax = 2.9;
double Vmin = 1.4;
double step = 0.1;
double a =(Vmax-Vmin)/step;
int b = (Vmax-Vmin)/step;
int c = a;
printf("%d %d",b,c); // 14 15, why?
return 0;
}
Я ожидаю получить 15 в обоих случаях, но кажется, что мне не хватает основополагающих принципов языка.
Я не уверен, что это актуально, но я делал тест в CodeBlocks. Однако, если я нахожу те же строки кода в некотором онлайновом компиляторе (например, это), я получаю ответ от 15 для двух печатных переменных.
Ответы
Ответ 1
... почему я получаю два разных номера...
Помимо обычных проблем с плавающей точкой пути вычислений к b
и c
поступают по-разному. c
вычисляется, сначала сохраняя значение как double a
.
double a =(Vmax-Vmin)/step;
int b = (Vmax-Vmin)/step;
int c = a;
C позволяет вычислить математическую математику с плавающей запятой, используя более широкие типы. Проверьте значение FLT_EVAL_METHOD
с <float.h>
.
За исключением назначения и литья (которые удаляют весь дополнительный диапазон и точность),...
-1 неопределимы;
0 оценивать все операции и константы только по диапазону и точности типа;
1 оценивать операции и константы типа float
и double
диапазон и точность double
типа, оценивать long double
операции и константы в диапазоне и точности long double
типа;
2 оценивают все операции и константы в диапазоне и точности long double
типа.
C11dr §5.2.4.2.2 9
ОП сообщили 2
Сохраняя частное в double a = (Vmax-Vmin)/step;
, точность принудительно double
тогда как int b = (Vmax-Vmin)/step;
мог вычислить как long double
.
Это тонкое различие получается из (Vmax-Vmin)/step
(вычисленного, возможно, как long double
), который сохраняется как double
а не long double
. Один из 15 (или только выше), а другой чуть ниже 15. int
усечение усиливает эту разницу до 15 и 14.
В другом компиляторе результаты могут быть одинаковыми из-за FLT_EVAL_METHOD < 2
или других характеристик с плавающей запятой.
Преобразование в int
из числа с плавающей запятой является строгим с числами около целого числа. Часто лучше round()
или lround()
. Лучшее решение зависит от ситуации.
Ответ 2
Это действительно интересный вопрос, вот что происходит именно на вашем оборудовании. Этот ответ дает точные расчеты с точностью поплавков double
точности IEEE, то есть 52 бит мантиссы плюс один неявный бит. Подробнее о представлении см. В статье wikipedia.
Итак, вы сначала определите некоторые переменные:
double Vmax = 2.9;
double Vmin = 1.4;
double step = 0.1;
Соответствующие значения в двоичном выражении будут
Vmax = 10.111001100110011001100110011001100110011001100110011
Vmin = 1.0110011001100110011001100110011001100110011001100110
step = .00011001100110011001100110011001100110011001100110011010
Если вы подсчитаете биты, вы увидите, что я дал первый бит, который установлен плюс 52 бита вправо. Это точно та точность, с которой ваш компьютер хранит double
. Обратите внимание, что значение step
было округлено.
Теперь вы делаете математику по этим цифрам. Первая операция, вычитание, приводит к точному результату:
10.111001100110011001100110011001100110011001100110011
- 1.0110011001100110011001100110011001100110011001100110
--------------------------------------------------------
1.1000000000000000000000000000000000000000000000000000
Затем вы делите step
за step
, который был округлен вашим компилятором:
1.1000000000000000000000000000000000000000000000000000
/ .00011001100110011001100110011001100110011001100110011010
--------------------------------------------------------
1110.1111111111111111111111111111111111111111111111111100001111111111111
Из-за округления step
результат составляет чуть ниже 15
. В отличие от предыдущего, я не закруглялся сразу, потому что именно там происходит интересное: ваш CPU действительно может хранить числа с плавающей запятой большей точности, чем double
, поэтому округление не происходит сразу.
Таким образом, когда вы конвертируете результат (Vmax-Vmin)/step
непосредственно в int
, ваш процессор просто отсекает биты после дробной точки (так подразумевается, что неявное преобразование double → int
определяется языковыми стандартами):
1110.1111111111111111111111111111111111111111111111111100001111111111111
cutoff to int: 1110
Однако, если вы сначала сохраните результат в переменной типа double, округление имеет место:
1110.1111111111111111111111111111111111111111111111111100001111111111111
rounded: 1111.0000000000000000000000000000000000000000000000000
cutoff to int: 1111
И это именно то, что вы получили.
Ответ 3
"Простой" ответ заключается в том, что эти, казалось бы, простые числа 2.9, 1.4 и 0.1 представлены внутренне как двоичная с плавающей запятой, а в двоичном выражении число 1/10 представлено как бесконечно повторяющаяся двоичная доля 0.00011001100110011... [ 2] , (Это аналогично тому, как 1/3 в десятичном конце заканчивается 0,3333333333...). Преобразованные обратно в десятичные числа, эти исходные номера в итоге становятся такими, как 2.8999999999, 1.3999999999 и 0.0999999999. И когда вы делаете дополнительную математику, они.0999999999 имеют тенденцию к размножению.
И тогда дополнительная проблема заключается в том, что путь, по которому вы что-то вычисляете - сохраняете ли вы его в промежуточных переменных определенного типа или вычисляете его "все сразу", что означает, что процессор может использовать внутренние регистры с большей точностью, чем тип double
- может в конечном итоге внести существенный вклад.
Суть в том, что когда вы конвертируете double
спину в int
, вы почти всегда хотите округлить, а не усекать. Здесь произошло то, что (по сути) один путь вычисления дал вам 15.0000000001, который уселся до 15, а другой дал вам 14.999999999, который уселся до 14.
См. Также вопрос 14.4a в списке часто задаваемых вопросов.
Ответ 4
Эквивалентная проблема анализируется при анализе программ на C для FLT_EVAL_METHOD == 2.
Если FLT_EVAL_METHOD==2
:
double a =(Vmax-Vmin)/step;
int b = (Vmax-Vmin)/step;
int c = a;
вычисляет b
путем вычисления long double
выражения, а затем обрезает его до int
, тогда как для c
он вычисляет из long double
, обрезая его, чтобы double
а затем до int
.
Таким образом, оба значения не получаются при одном и том же процессе, и это может привести к разным результатам, поскольку плавающие типы не обеспечивают обычной точной арифметики.