Неинтуитивный результат присвоения числа двойной точности переменной 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.

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