Заявление
printf("%f\n",0.0f);
печатает 0.
Однако заявление
printf("%f\n",0);
печатает случайные значения.
Я понимаю, что проявляю какое-то неопределенное поведение, но не могу понять почему.
Значение с плавающей запятой, в котором все биты равны 0, по-прежнему допустимо float
со значением 0.
float
И int
имеет тот же размер на моей машине (если это вообще актуально).
Почему использование целочисленного литерала вместо литерала с плавающей запятой printf
вызывает такое поведение?
PS такое же поведение можно увидеть, если я использую
int i = 0;
printf("%f\n", i);
c++
c
printf
implicit-conversion
undefined-behavior
Тревор Хики
источник
источник
printf
ожидаетdouble
, и вы даете ейint
.float
иint
может иметь такой же размер на вашем компьютере, но0.0f
фактически преобразуется в adouble
при добавлении в список аргументов с переменным числом аргументов (иprintf
ожидает этого). Короче говоря, вы не выполняете свою часть сделки,printf
основываясь на спецификаторах, которые вы используете, и аргументах, которые вы предоставляете.(uint64_t)0
вместо0
и посмотреть ли вы все равно получите случайное поведение (предполагая , чтоdouble
иuint64_t
имеют одинаковый размер и выравнивание). Скорее всего, результат будет все еще случайным на некоторых платформах (например, x86_64) из-за того, что разные типы передаются в разных регистрах.Ответы:
"%f"
Формат требует аргумент типаdouble
. Вы даете ему типовой аргументint
. Вот почему поведение не определено.Стандарт не гарантирует, что все биты-ноль являются допустимым представлением
0.0
(хотя это часто бывает) или любогоdouble
значения, или чтоint
иdouble
имеют одинаковый размер (помните, что этоdouble
не такfloat
), или даже если они одинаковы size, что они передаются в качестве аргументов функции с переменным числом аргументов таким же образом.Это могло случиться с вашей системой. Это наихудший из возможных симптомов неопределенного поведения, поскольку он затрудняет диагностику ошибки.
N1570 7.21.6.1 пункт 9:
Аргументы типа
float
повышаются доdouble
, поэтомуprintf("%f\n",0.0f)
работает. Аргументы целочисленных типов уже, чемint
продвигаются доint
или доunsigned int
. Эти правила продвижения (указанные в N1570 6.5.2.2 п.6) не помогают в случаеprintf("%f\n", 0)
.Обратите внимание, что если вы передаете константу
0
невариадной функции, которая ожидаетdouble
аргумент, поведение хорошо определено, при условии, что прототип функции виден. Например,sqrt(0)
(после#include <math.h>
) неявно преобразует аргумент0
изint
вdouble
- потому что компилятор может видеть из объявления,sqrt
что он ожидаетdouble
аргумент. Нет такой информации дляprintf
. Такие функции с переменными числами, какprintf
особенные, требуют большей осторожности при написании их вызовов.источник
double
неfloat
значит, что предположение о ширине OP может не выполняться (вероятно, не выполняется). Во-вторых, предположение, что целочисленный ноль и ноль с плавающей запятой имеют одинаковый битовый шаблон, также не выполняется. Хорошая работаfloat
это повышеноdouble
, но не объяснил почему, но это было не главное.printf
, хотя в gcc, например, есть некоторые, поэтому он может диагностировать ошибки ( если строка формата является буквальной). Компилятор видит объявлениеprintf
from<stdio.h>
, в котором говорится, что первый параметр - это,const char*
а остальные обозначены, ...
. Нет,%f
это дляdouble
(иfloat
повышено доdouble
), и%lf
дляlong double
. Стандарт C ничего не говорит о стеке. Он определяет поведениеprintf
только при правильном вызове.float
объектprintf
повышается доdouble
; в этом нет ничего волшебного, это просто языковое правило для вызова вариативных функций.printf
сам знает через строку формата, что, по утверждениям, передал ему вызывающий абонент ; если это утверждение неверно, поведение не определено.l
длина модификатор «не имеет никакого эффекта на следующемa
,A
,e
,E
,f
,F
,g
, илиG
спецификатор преобразования», длина модификатор дляlong double
конверсииL
. (@ robertbristow-johnson также может быть заинтересован)Во-первых, как было сказано в нескольких других ответах, но, на мой взгляд, недостаточно четко сказано: он действительно работает, чтобы предоставить целое число в большинстве контекстов, где библиотечная функция принимает аргумент
double
илиfloat
. Компилятор автоматически вставит преобразование. Например,sqrt(0)
он четко определен и будет вести себя точно так жеsqrt((double)0)
, и то же самое верно для любого другого выражения целого типа, используемого в нем.printf
отличается. Он отличается, потому что требует переменного количества аргументов. Его функциональным прототипом являетсяextern int printf(const char *fmt, ...);
Поэтому, когда вы пишете
printf(message, 0);
компилятор не имеет информации о том, какой тип
printf
ожидает от второго аргумента. У него есть только тип выражения аргумента, которымint
нужно следовать. Поэтому, в отличие от большинства библиотечных функций, вы, программист, должны убедиться, что список аргументов соответствует ожиданиям строки формата.(Современные компиляторы могут заглянуть в строку формата и сказать вам, что у вас есть несоответствие типов, но они не собираются начинать вставку преобразований, чтобы выполнить то, что вы имели в виду, потому что лучше ваш код должен сломаться сейчас, когда вы заметите , чем годы спустя, когда был перестроен с помощью менее полезного компилятора.)
Другая половина вопроса заключалась в следующем: учитывая, что (int) 0 и (float) 0.0 в большинстве современных систем представлены как 32 бита, все из которых равны нулю, почему это все равно не работает случайно? Стандарт C просто говорит: «Это не обязательно для работы, вы сами по себе», но позвольте мне указать две наиболее распространенные причины, по которым это не сработает; это, вероятно, поможет вам понять, почему это не требуется.
Во-первых, по историческим причинам, когда вы передаете a
float
через список переменных аргументов, он продвигается к немуdouble
, который в большинстве современных систем имеет ширину 64 бита. Таким образом,printf("%f", 0)
вызываемому, ожидающему 64 из них, передается только 32 нулевых бита.Вторая, не менее важная причина состоит в том, что аргументы функции с плавающей запятой могут передаваться в другом месте, чем целочисленные аргументы. Например, большинство процессоров имеют отдельные файлы регистров для целых чисел и значений с плавающей запятой, поэтому может быть правилом, что аргументы от 0 до 4 идут в регистры с r0 по r4, если они целые числа, но с f0 по f4, если они с плавающей запятой. Итак,
printf("%f", 0)
ищем этот ноль в регистре f1, но его там совсем нет.источник
()
.AL
. (Да, это означает, что реализацияva_arg
намного сложнее, чем раньше.)ret n
инструкции 8086, гдеn
было жестко запрограммированное целое число, которое, следовательно, не применимо к функциям с переменным числом аргументов. Однако я не знаю, действительно ли какой-либо компилятор C воспользовался этим преимуществом (конечно, компиляторы, отличные от C).Обычно, когда вы вызываете функцию, которая ожидает
double
, но вы предоставляетеint
, компилятор автоматически преобразует ее вdouble
. Этого не происходитprintf
, потому что типы аргументов не указаны в прототипе функции - компилятор не знает, что нужно применить преобразование.источник
printf()
в частности , спроектирован так, чтобы его аргументы могли быть любого типа. Вы должны знать, какой тип ожидает каждый элемент в строке формата, и вы должны указать его правильно.Потому
printf()
что не имеет типизированных параметров, кромеconst char* formatstring
как 1-й....
Для всего остального он использует многоточие в стиле c ( ).Он просто решает, как интерпретировать переданные значения в соответствии с типами форматирования, указанными в строке формата.
У вас будет такое же неопределенное поведение, как при попытке
int i = 0; const double* pf = (const double*)(&i); printf("%f\n",*pf); // dereferencing the pointer is UB
источник
printf
могут работать таким образом (за исключением того, что переданные элементы являются значениями, а не адресами). Стандарт C не определяет, какprintf
работают и другие вариативные функции, он просто определяет их поведение. В частности, нет упоминания о кадрах стека.printf
есть один типизированный параметр, строка формата, которая имеет типconst char*
. Кстати, вопрос помечен как C, так и C ++, и C действительно более актуален; Я бы, наверное, не стал использоватьreinterpret_cast
в качестве примера.Использование несовпадающего
printf()
спецификатора"%f"
и типа(int) 0
приводит к неопределенному поведению.Возможные причины УБ.
Это UB по спецификации, а компиляция злобная, - сказал Нуф.
double
иint
бывают разных размеров.double
иint
могут передавать свои значения, используя разные стеки (общий или стек FPU ).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
.Совершенно неважно, имеют ли два типа одинаковую разрядность, за исключением того, что это может помочь вам избежать серьезных исключений нарушения памяти из-за такого сломанного кода.
источник
int
было бы приемлемо здесь.int
допустимым шаблоном с плавающей запятой? Два дополнения и различные кодировки с плавающей запятой почти не имеют ничего общего.0
типизированному аргументуdouble
будет работать правильно. Для новичка не очевидно, что компилятор не выполняет такое же преобразование дляprintf
слотов аргументов, адресованных%[efg]
."%f\n"
гарантирует предсказуемый результат только тогда, когда второйprintf()
параметр имеет типdouble
. Затем дополнительные аргументы вариативных функций подвергаются продвижению аргументов по умолчанию. Целочисленные аргументы подпадают под целочисленное продвижение, которое никогда не приводит к значениям с плавающей запятой. Иfloat
параметры повышаются доdouble
.В довершение всего: стандарт позволяет второму аргументу быть или
float
илиdouble
и ничем другим.источник
Почему это формально UB, теперь обсуждается в нескольких ответах.
Причина, по которой вы получаете именно такое поведение, зависит от платформы, но, вероятно, следующая:
printf
ожидает своих аргументов в соответствии со стандартным распространением vararg. Это означает, что afloat
будет a,double
а все, что меньше an,int
будетint
.int
где функция ожидаетdouble
. У васint
, вероятно, 32 бит, вашdouble
64 бит. Это означает, что четыре байта стека, начинающиеся с того места, где должен располагаться аргумент, есть0
, но следующие четыре байта имеют произвольное содержимое. Это то, что используется для построения отображаемого значения.источник
Основная причина этой проблемы с "неопределенным значением" заключается в приведении указателя к
int
значению, переданному вprintf
раздел переменных параметров, к указателю наdouble
типы, которыеva_arg
выполняет макрос.Это вызывает ссылку на область памяти, которая не была полностью инициализирована значением, переданным в качестве параметра printf, потому что
double
размер буферной области памяти большеint
размера.Следовательно, когда этот указатель разыменован, ему возвращается неопределенное значение или, лучше сказать, «значение», которое частично содержит значение, переданное в качестве параметра
printf
, а оставшаяся часть может быть получена из другой буферной области стека или даже из области кода ( вызывает исключение сбоя памяти), реальное переполнение буфера .Он может учитывать эти конкретные части реализации полупрофессионального кода «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.. ....
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);
источник