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

заявление

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

выводит 0.

, заявление
printf("%f\n",0);

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

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

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

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

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

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

10 ответов


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

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

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

N1570 7.21.6.1 пункт 9:

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

аргументы типа float звание double, который является, почему printf("%f\n",0.0f) строительство. Аргументы целочисленных типов уже, чем int звание int или unsigned int. Настоящие правила продвижения (указанные в пункте 6.5.2.2 N1570) не помогают в случае printf("%f\n", 0).


во-первых, как было затронуто в нескольких других ответах, но не, на мой взгляд, достаточно ясно: это тут работа для предоставления целого числа в большинство контексты, в которых библиотечная функция принимает double или


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


почему использование целочисленного литерала вместо литерала float вызывает такое поведение?

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

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

у вас будет такое же неопределенное поведение, как и при пытаюсь!--6-->

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

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

если спецификация преобразования является недопустимым, поведение неопределено. C11dr §7.21.6.1 9

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

  1. это UB за спецификацию, и компиляция отвратительна, - сказал Нуф.

  2. double и int различных размеров.

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

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


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

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


Я не уверен, что сбивает с толку.

ваша строка формата ожидает double; вы предоставляете вместо int.

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


"%f\n" гарантирует предсказуемый результат только при втором


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

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

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

основная причина этой проблемы "неопределенное значение" стоит в приведении указателя на 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);



ссылки

  1. gnu project glibc реализация "printf" (vprintf))
  2. пример кода семплификации printf
  3. пример кода семплификации va_arg