Странное поведение при приведении int к плавающему в C
У меня есть сомнения относительно вывода следующей программы на Си. Я попытался скомпилировать его, используя Visual C++ 6.0 и MinGW32 (gcc 3.4.2).
#include <stdio.h>
int main() {
int x = 2147483647;
printf("%f\n", (float)2147483647);
printf("%f\n", (float)x);
return 0;
}
Выход:
2147483648.000000
2147483647.000000
Мой вопрос: почему обе линии разные? При преобразовании целочисленного значения 2147483647 в формат IEEE 754 с плавающей запятой оно приближается к 2147483648.0. Итак, я ожидал, что обе строки будут равны 2147483648.000000.
РЕДАКТИРОВАТЬ: значение "2147483647.000000" не может быть значением с плавающей запятой одинарной точности, поскольку число 2147483647 не может быть точно представлено в формате с плавающей запятой IEEE 754 без потери точности.
5 ответов
В обоих случаях код стремится преобразовать некоторый целочисленный тип в float
а затем double
.. double
преобразование происходит как это float
значение передается в функцию с переменным числом.
Проверьте настройки FLT_EVAL_METHOD
подозреваю, что оно имеет значение 1 или 2 (отчеты OP 2
по крайней мере с одним компилятором). Это позволяет компилятору оценить float
"... операции и константы дальности и точности" больше, чем float
,
Ваш компилятор оптимизирован (float)x
идти прямо int
в double
арифметика. Это улучшение производительности во время выполнения.
(float)2147483647
приведение времени компиляции и компилятор оптимизирован для int
в float
в double
Точность как производительность не является проблемой здесь.
[Edit2] Интересно, что спецификация C11 более специфична, чем спецификация C99 с добавлением "За исключением назначения и приведения...". Это означает, что компиляторы C99 иногда позволяли int
в double
прямое преобразование без предварительного прохождения float
и что C11 был изменен, чтобы явно не допустить пропуска актеров.
С C11, формально исключающим это поведение, современные компиляторы не должны этого делать, а более старые, такие как OP, - таким образом, ошибка по стандартам C11. Если в какой-либо другой спецификации C99 или C89 не указано иное, это может быть допустимым поведением компилятора.
[Редактировать] Принимая комментарии вместе @Keith Томпсон, @tmyklebu, @Matt McNabb, компилятор, даже с ненулевым FLT_EVAL_METHOD
, следует ожидать, чтобы произвести 2147483648.0...
, Таким образом, либо флаг оптимизации компилятора явно переопределяет правильное поведение, либо компилятор имеет угловую ошибку.
C99dr §5.2.4.2.2 8 Значения операций с плавающими операндами и значениями, подлежащими обычному арифметическому преобразованию и плавающими константами, оцениваются в формате, диапазон и точность которого могут быть больше, чем требуется типом. Использование форматов оценки характеризуется значением реализации FLT_EVAL_METHOD:
-1 неопределимо;
0 оценивать все операции и константы только для диапазона и точности типа;
1 оценить операции и константы типа float
а также double
в диапазоне и точности double
тип, оценить long double
операции и константы в диапазоне и точности long double
type`;
2 оценить все операции и константы для диапазона и точности long double
тип.
C11dr §5.2.4.2.2 9 За исключением присваивания и приведения (которые удаляют весь дополнительный диапазон и точность), значения, полученные операторами с плавающими операндами, и значениями, подлежащими обычному арифметическому преобразованию и плавающим константам, оцениваются в формате, диапазон которого и точность может быть больше, чем требуется типом. Использование форматов оценки характеризуется значением, определенным реализацией FLT_EVAL_METHOD
-1 (такой же как C99)
0 (так же, как C99)
1 (такой же, как C99)
2 (так же, как C99)
Это, безусловно, ошибка компилятора. От стандарта C11 у нас есть следующие гарантии (C99 был похож):
- Типы имеют набор представимых значений (подразумевается)
- Все значения представлены
float
также могут быть представленыdouble
(6.2.5/10) - преобразование
float
вdouble
не меняет значение (6.3.1.5/1) - Кастинг
int
вfloat
когда значение int находится в наборе представимых значений дляfloat
, дает это значение. - Кастинг
int
вfloat
, когда величина значения int меньше, чемFLT_MAX
иint
не представимое значение дляfloat
, вызывает либо следующий самый высокий или следующий самый низкийfloat
значение, которое будет выбрано, и то, которое выбрано, определяется реализацией. (6.3.1.4/2)
Третий из этих пунктов гарантирует, что float
значение предоставляется printf
не изменяется при продвижении аргумента по умолчанию.
Если 2147483647
представима в float
, затем (float)x
а также (float)2147483647
должен дать 2147483647.000000
,
Если 2147483647
не представлен в float
, затем (float)x
а также (float)2147483647
должен либо дать следующий самый высокий или следующий самый низкий float
, Им не нужно делать одинаковый выбор. Но это означает, что распечатка 2147483647.000000
не допускается1, каждый из которых должен иметь либо более высокое значение, либо более низкое значение.
1 Хорошо - теоретически возможно, что следующий самый низкий поплавок был 2147483646.9999999...
поэтому, когда значение отображается с точностью до 6 цифр printf
тогда это округляется, чтобы дать то, что было замечено. Но это не так в IEEE754, и вы можете легко экспериментировать, чтобы обесценить эту возможность.
Во-первых printf
Преобразование из целого числа в число с плавающей точкой выполняется компилятором. На втором это выполняется библиотекой времени выполнения C. Нет особой причины, по которой они должны давать идентичные ответы в пределах своей точности.
Visual C++ 6.0 был выпущен в прошлом веке, и я считаю, что он предшествует стандарту C++. Совершенно неудивительно, что VC++ 6.0 демонстрирует нарушенное поведение.
Вы также заметите, что gcc-3.4.2 относится к 2004 году. Действительно, вы используете 32-битный компилятор. GCC на x86 играет довольно быстро и свободно с математикой с плавающей точкой. Технически это может быть оправдано стандартом C, если gcc устанавливает FLT_EVAL_METHOD
к чему-то ненулевому.
Некоторые из вас, ребята, сказали, что это ошибка оптимизации, но я не согласен. я думаю, что это разумная ошибка точности с плавающей точкой и хороший пример, показывающий людям, как работает плавающая точка.
возможно, OP попытается вставить мою программу в ваш компьютер и попытаться скомпилировать с вашим компилятором и посмотреть, что произойдет. или попробуйте:
(с явным преобразованием числа с плавающей запятой).
во всяком случае, если мы посчитаем ошибку, это 4.656612875245797e-10
так много, и следует считать довольно точным.
это может быть связано с предпочтением printf
тоже.