Что происходит с переменной float, когда% d используется в printf?
Я пытаюсь выучить C, читая язык программирования C, 2-е издание. У меня есть некоторый опыт программирования, но не с C.
Сейчас я в главе 1. У меня есть следующий код:
float f;
for (f = 0.0; f <= 3; f += 1.1)
printf("A: %3f B: %6.2f\n", f, f + 0.15);
Он печатает вывод:
A: 0.000000 B: 0.15
A: 1.100000 B: 1.25
A: 2.200000 B: 2.35
Выглядит хорошо.
Теперь я изменяю printf следующим образом:
printf("A: %3d B: %6.2f\n", f, f + 0.15);
Новый вывод -
A: 0 B: 0.00
A: -1610612736 B: 0.00
A: -1610612736 B: -625777476808257557292155887552002761191109083510753486844893290688350183831589633800863219712.00
Что здесь происходит? Я ожидаю, что float будет преобразован в int, потому что я использовал% d, но это не то, что произошло. Кроме того, почему значение B также пошло не так? Что здесь произошло?
Ответы
Ответ 1
Когда вы вызываете:
printf("A: %3d B: %6.2f\n", f, f + 0.15);
C автоматически преобразует значения float
в double
(это стандартное преобразование, сделанное при вызове функции, которая принимает переменные аргументы, такие как int printf(const char *fmt, ...);
). Для аргумента будем считать, что sizeof(int)
равно 4, а sizeof(double)
равно 8 (есть исключения, но они немногочисленны и далеко друг от друга).
Таким образом, вызов переместил указатель на стек плюс 8-байтовый двойной для f
и еще один 8-байтовый двойной для f + 0.15
. Когда он обрабатывает строку формата, %d
сообщает printf()
, что вы ввели 4-байтовый int
в стек после строки формата. Поскольку это не то, что вы сделали, вы вызвали поведение undefined; все, что происходит дальше, в порядке в соответствии со стандартом C.
Однако наиболее вероятная реализация blithely читает 4 байта и печатает их, как если бы они были int
(он верит вам, чтобы сказать правду). Затем он попадает в формат %6.2f
; он будет читать 8-байтов со стека как double
. Там есть вероятность того, что это приведет к ошибке памяти для неверного доступа (для этого потребуется 64-разрядная машина с требованием выравнивания double
на 8-байтовой границе, такой как SPARC), или она будет читать 4 байты от f
и 4 байта от f + 0.15
, объединяя их для создания некоторого довольно неожиданного значения double
- как показывает ваш пример.
Ответ 2
Printf будет обрабатывать память, на которую вы указываете, как бы вы это ни говорили. Конверсии не происходит. Он обрабатывает память, которая представляет float как int. Поскольку они хранятся по-разному, вы получаете то, что по сути является случайным числом.
Если вы хотите вывести свой float как целое число, вы должны сначала его перенести:
printf("A: %3d B: %6.2f\n", (int)f, f + 0.15);
Ответ 3
Можно напечатать любое целочисленное значение независимо от параметра с плавающей запятой:
printf("A: %d B: %6.2f\n", f, f + 0.15);
Вот как вы можете печатать произвольные целые числа в архитектуре Intel:
int print_it(int, int /* nameless but printed */, float f)
{
printf("A: %d B: %6.2f\n", f, f + 0.15);
}
int main()
{
print_it(0, 12 /* will be printed */, 0.0);
print_it(0, 123 /* printed */, 1.1);
print_it(0, 1234 /* printed */ , 2.2);
}
Этот вывод:
A: 12 B: 0.00
A: 123 B: 1.10
A: 1234 B: 2.20
Объяснение: Очевидно, что несовпадающая строка формата и параметры приводят к неопределенному поведению. Тем не менее, иногда это можно предсказать. В архитектуре Intel первые несколько параметров передаются регистрами. Значения с плавающей точкой передаются в разные регистры.
Несмотря на ту же инструкцию printf
, что и в вопросе, вывод отличается. Что происходит, так это то, что 12, 123, 1234 передаются через регистр общего назначения, отвечающий за второй параметр без плавающей запятой. Поскольку printf
имеет только один параметр без плавающей запятой, регистр второго параметра без плавающей запятой не изменяется. Этот регистр сохраняет значение, полученное из второго параметра print_it(0, int_value, fp_value)
.
Но оригинал дает мусор:
for (f = 0.0; f <= 3; f += 1.1)
printf("A: %3f B: %6.2f\n", f, f + 0.15);
Это дает различный мусор, потому что printf
вызывает другие функции внутри. Эти функции уничтожают регистр общего назначения, который читает printf("... %d ...", ...)
.
Очевидно, что такое поведение имеет место только в системах, которые передают параметры с плавающей запятой в отдельный набор регистров. Очевидно, что это происходит только в том случае, если оптимизация компилятора не изменяет код каким-либо образом, потому что разрешено делать дикие вещи, когда происходит неопределенное поведение.
Ответ 4
Для большинства функций, если вы передаете float
, но функция ожидает int
, компилятор знает, как автоматически преобразовать float
в int
. Но printf
особенный (очень особенный). %d
ожидает int
, и ваша задача передать его int
. В случае printf
ни один автоматический механизм компилятора не сможет выполнить преобразование за вас.
Тем не менее, при этом хорошие компиляторы обнаруживают и предупреждают об этой проблеме. Если это не так, вам нужен лучший.
Более длинное объяснение состоит в том, что для большинства функций прототип функции дает число и тип всех аргументов, а также механизм, который позволяет компилятору узнать, что ему может потребоваться ввести преобразования. Но прототипом для printf
является
extern int printf(const char *, ...);
где эти три точки ...
буквально означают: "здесь будет переменное количество аргументов, не знаю, сколько или каких типов". К тому времени, когда printf
фактически выполняется, и он находит %d
в строке формата, говорящей ему ожидать, что int
был передан, уже слишком поздно делать какие-либо преобразования, если то, что вы передали, было чем-то отличным от int
.