Подтвердить что ты не робот

Почему printf ( "% f", 0); дать поведение undefined?

Утверждение

printf("%f\n",0.0f);

печатает 0.

Однако утверждение

printf("%f\n",0);

выводит случайные значения.

Я понимаю, что я демонстрирую какое-то поведение undefined, но я не могу понять, почему именно.

Значение с плавающей запятой, в котором все биты равны 0, по-прежнему остается действительным float со значением 0.
float и int имеют одинаковый размер на моей машине (если это даже актуально).

Почему использование целочисленного литерала вместо литерала с плавающей запятой в printf вызывает это поведение?

P.S. такое же поведение можно увидеть, если я использую

int i = 0;
printf("%f\n", i);
4b9b3361

Ответ 1

Формат "%f" требует аргумента типа double. Вы даете ему аргумент типа int. Вот почему поведение undefined.

Стандарт не гарантирует, что all-bits-zero является допустимым представлением 0.0 (хотя это часто бывает) или любого значения double, или что int и double имеют одинаковый размер (помните double, а не float) или, даже если они имеют одинаковый размер, они передаются как аргументы переменной функции таким же образом.

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

N1570 7.21.6.1 пункт 9:

... Если какой-либо аргумент не является правильным типом для соответствующего спецификация преобразования, поведение undefined.

Аргументы типа float продвигаются до double, поэтому работает printf("%f\n",0.0f). Аргументы целых типов, более узкие, чем int, продвигаются до int или до unsigned int. Эти правила продвижения (указанные в п. 6 N.570 6.5.2.2) не помогают в случае printf("%f\n", 0).

Ответ 2

Во-первых, как было затронуто в нескольких других ответах, но нет, на мой взгляд, достаточно ясно сказано: он работает, чтобы обеспечить целое число в большинстве контекстов, где функция библиотеки принимает аргумент double или float. Компилятор автоматически добавит конверсию. Например, sqrt(0) хорошо определен и будет вести себя точно как sqrt((double)0), и то же самое верно для любого другого выражения целочисленного типа, используемого там.

printf отличается. Он отличается тем, что принимает переменное количество аргументов. Его прототип функции

extern int printf(const char *fmt, ...);

Поэтому, когда вы пишете

printf(message, 0);

у компилятора нет никакой информации о том, какой тип printf ожидает второго аргумента. У этого есть только тип выражения аргумента, который int, чтобы пройти. Поэтому, в отличие от большинства функций библиотеки, вам, программисту, нужно убедиться, что список аргументов соответствует ожиданиям строки формата.

(Современные компиляторы могут просматривать строку формата и сообщать вам, что у вас есть несоответствие типов, но они не собираются вставлять конверсии для достижения того, что вы имели в виду, потому что лучше ваш код должен разорваться, когда вы 'll заметьте, чем годы спустя, когда перестроен с менее полезным компилятором.)

Теперь другая половина вопроса: Учитывая, что (int) 0 и (float) 0.0 на большинстве современных систем представлены как 32 бита, все из которых равны нулю, почему он не работает в любом случае, случайно? Стандарт C просто говорит: "Это не обязательно работать, вы сами", но позвольте мне изложить две наиболее распространенные причины, почему это не сработает; что, вероятно, поможет вам понять, почему это не требуется.

Во-первых, по историческим причинам, когда вы передаете float через список аргументов переменных, его получают до double, который в большинстве современных систем имеет ширину 64 бит. Таким образом, printf("%f", 0) передает только 32 нулевых бита вызывающей стороне, ожидающей 64 из них.

Вторая, не менее важная причина - аргументы функции с плавающей запятой могут передаваться в другом месте, чем целые аргументы. Например, большинство процессоров имеют отдельные файлы регистров для целых чисел и значений с плавающей запятой, поэтому может быть правило, что аргументы с 0 по 4 идут в регистры с r0 по r4, если они являются целыми числами, но f0-f4, если они являются плавающей точкой. Итак, printf("%f", 0) выглядит в регистре f1 для этого нуля, но он не существует вообще.

Ответ 3

Почему использование этого целочисленного литерала вместо плавающего литерала вызывает это поведение?

Потому что printf() не имеет типизированных параметров, кроме const char* formatstring, как 1-й. Он использует эллипсис c-стиля (...) для всех остальных.

Он просто решает, как интерпретировать значения, переданные там в соответствии с типами форматирования, указанными в строке формата.

У вас будет такое поведение undefined, что при попытке

 int i = 0;
 const double* pf = (const double*)(&i);
 printf("%f\n",*pf); // dereferencing the pointer is UB

Ответ 4

Обычно, когда вы вызываете функцию, ожидающую double, но вы предоставляете int, компилятор автоматически преобразует в double для вас. Этого не происходит с printf, потому что типы аргументов не указаны в прототипе функции - компилятор не знает, что нужно применять преобразование.

Ответ 5

Использование несопоставимого спецификатора printf() "%f" и типа (int) 0 приводит к поведению undefined.

Если спецификация преобразования недействительна, поведение undefined. C11dr §7.21.6.1 9

Кандидатские причины UB.

  • Это UB для каждой спецификации, а компиляция - ornery, - сказал nuf.

  • double и int имеют разные размеры.

  • double и int могут передавать свои значения, используя разные стеки (общий стек FPU).

  • A double 0.0 может не определяться шаблоном с нулевым битом. (Редко)

Ответ 6

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

$ gcc -Wall -Wextra -pedantic fnord.c 
fnord.c: In function ‘main’:
fnord.c:8:2: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
  printf("%f\n",0);
  ^

или

$ clang -Weverything -pedantic fnord.c 
fnord.c:8:16: warning: format specifies type 'double' but the argument has type 'int' [-Wformat]
        printf("%f\n",0);
                ~~    ^
                %d
1 warning generated.

Итак, printf создает поведение undefined, потому что вы передаете ему несовместимый тип аргумента.

Ответ 7

Я не уверен, что смущает.

Ваша строка формата ожидает double; вместо этого вы указываете int.

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

Ответ 8

"%f\n" гарантирует предсказуемый результат только тогда, когда второй параметр printf() имеет тип double. Далее, дополнительные аргументы вариационных функций подвержены продвижению аргументов по умолчанию. Целочисленные аргументы попадают под целую рекламу, которая никогда не приводит к значениям с плавающей запятой. А параметры float повышаются до double.

В довершение: стандарт позволяет использовать второй аргумент или float или double и ничего больше.

Ответ 9

Почему формально UB теперь обсуждался в нескольких ответах.

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

  • printf ожидает своих аргументов в соответствии со стандартным распространением vararg. Это означает, что float будет double, а все, что меньше int, будет int.
  • Вы передаете int, где функция ожидает double. Ваш int, вероятно, 32 бит, ваш double 64 бит. Это означает, что четыре байта стека, начиная с места, где должен находиться аргумент, равны 0, но следующие четыре байта имеют произвольный контент. Это то, что используется для построения отображаемого значения.

Ответ 10

Основная причина этой проблемы "неопределенного значения" заключается в указании указателя на значение int, переданном в раздел параметров переменных printf, указателю на double, который выполняется макросом va_arg,

Это приводит к ссылке на область памяти, которая не была полностью инициализирована значением, переданным как параметр printf, поскольку область буфера памяти размером double больше, чем размер int.

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


Он может рассматривать эти конкретные части реализации semplificated кода "printf" и "va_arg"...

printf

va_list arg;
....
case('%f')
      va_arg ( arg, double ); //va_arg is a macro, and so you can pass it the "type" that will be used for casting the int pointer argument of printf..
.... 


реальная реализация в vprintf (с учетом gnu impl.) кода двоичного значения кода управление:

if (__ldbl_is_dbl)
{
   args_value[cnt].pa_double = va_arg (ap_save, double);
   ...
}



va_arg

char *p = (double *) &arg + sizeof arg;  //printf parameters area pointer

double i2 = *((double *)p); //casting to double because va_arg(arg, double)
   p += sizeof (double);



ссылка