Почему fprintf вызывает утечку памяти и ведет себя непредсказуемо при отсутствии аргумента width - PullRequest
3 голосов
/ 29 мая 2019

Следующая простая программа ведет себя непредсказуемо.Иногда он печатает «0,00000», иногда печатает больше «0», чем я могу сосчитать.Иногда он использует всю память в системе, прежде чем система либо убивает какой-либо процесс, либо завершается с ошибкой с bad_alloc.

#include "stdio.h"

int main() {
  fprintf(stdout, "%.*f", 0.0);
}

Я знаю, что это некорректное использование fprintf.Должен быть другой аргумент, определяющий ширину форматирования.Просто удивительно, что такое непредсказуемое поведение.Иногда кажется, что он использует ширину по умолчанию, а иногда очень плохо.Разве это не может быть сделано, чтобы всегда терпеть неудачу или всегда использовать какое-то поведение по умолчанию?

Я натолкнулся на подобное использование в некотором коде на работе и потратил много времени, выясняя, что происходит.Казалось, что это происходит только с отладочными сборками, но не происходит при отладке с помощью gdbДругое любопытство заключается в том, что запуск его через valgrind будет последовательно приводить к печати многих случаев «0», что в противном случае случается довольно редко, но проблема использования памяти никогда не возникнет.

Я работаю в Red HatКорпоративный Linux 7, скомпилированный с gcc 4.8.5.

Ответы [ 4 ]

5 голосов
/ 29 мая 2019

Формально это неопределенное поведение.

Что касается того, что вы наблюдаете на практике:
Я предполагаю, что fprintf заканчивается использованием неинициализированного целого числа в качестве числа десятичных разрядов для вывода.Это потому, что он попытается прочитать число из места, где вызывающий не записал какое-либо конкретное значение, так что вы просто получите все биты, которые там будут храниться.Если это огромное число, fprintf попытается выделить много памяти для внутреннего хранения строки результата.Это объясняет часть «нехватки памяти».

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

* 1009И, наконец, если случайное целочисленное значение окажется равным 5, вы получите 0.00000.

Вероятно, Valgrind последовательно инициализирует память, которую видит ваша программа, поэтому поведение становится детерминированным.

Разве это не может быть сделано, чтобы всегда терпеть неудачу

Я уверен, что он даже не скомпилируется, если вы используете gcc -pedantic -Wall -Wextra -Werror.

3 голосов
/ 29 мая 2019

Неопределенное поведение не определено.

Однако в x86-64 System-V ABI хорошо известно , что аргументы не передаютсястек но в регистрах.Переменные с плавающей запятой передаются в регистры с плавающей запятой , а целые числа передаются в регистры общего назначения .В стеке нет хранилища параметров, поэтому ширина аргументов не имеет значения.Так как вы никогда не передавали integer в части аргумента переменной, регистр общего назначения, соответствующий первому аргументу, будет содержать любой мусор, из которого он был раньше.

Эта программа покажет, как с плавающей запятойзначения и целые числа передаются отдельно:

#include <stdio.h>

int main() {
    fprintf(stdout, "%.*f\n", 42, 0.0);
    fprintf(stdout, "%.*f\n", 0.0, 42);
}

Скомпилированные в x86-64, GCC + Glibc, оба printf s будут выдавать одинаковый вывод :

0.000000000000000000000000000000000000000000
0.000000000000000000000000000000000000000000
2 голосов
/ 29 мая 2019

Строка формата не соответствует параметрам, поэтому поведение fprintf не определено. Google "неопределенное поведение C" для получения дополнительной информации о "неопределенном поведении".

Это было бы правильно:

// printf 0.0 with 7 decimals
fprintf(stdout, "%.*f", 7, 0.0);

Или, может быть, вы просто хотите это:

// printf 0.0 with de default format
fprintf(stdout, "%f", 0.0);

Об этой части вашего вопроса: Иногда кажется, что используется ширина по умолчанию, а иногда очень плохо. Разве это не может быть сделано, чтобы всегда терпеть неудачу или всегда использовать какое-то поведение по умолчанию?

Не может быть никакого поведения по умолчанию, fprintf читает аргументы в соответствии со строкой формата. Если аргументы не совпадают, fprintf заканчивается случайными значениями.


Об этой части вашего вопроса: Другое любопытство заключается в том, что запуск его через valgrind будет последовательно приводить к печати многих случаев "0", что в противном случае происходит довольно редко, но тогда проблема использования памяти никогда не возникнет либо :.

Это просто еще одно проявление неопределенного поведения, с valgrind условия совершенно разные, и поэтому фактическое неопределенное поведение может отличаться.

1 голос
/ 29 мая 2019

Это неопределенное поведение в стандарте. Это означает, что «все честно», потому что вы делаете неправильные вещи.

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

Это противоположно тому, что обозначают C и C ++: вы платите за то, что используете. Если вы хотите оплатить стоимость, вы должны выполнить проверку.

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

Например, когда вы вызываете функцию на машине, это просто означает, что вы указываете микропроцессору перейти в определенное место кода.

В некоторых готовых сборках и ABI printf("%.*f", 5, 1); будет выглядеть как

mov A, STR_F ; // load into register A the 32 bit address of the string "%.*f"
mov B, 5 ; // load second 32 bit parameter into B 
mov F0, 1.0 ; // load first floating point parameter into register F0
call printf ; // call the function

Теперь, если вы пропустите какой-либо параметр, в данном случае B, он примет любое значение, которое было там раньше.

С такими функциями, как printf, дело в том, что они допускают что угодно в своем списке параметров (это printf(const char*, ...), поэтому все допустимо). Вот почему вы не должны использовать printf на C ++: у вас есть лучшие альтернативы, такие как потоки. printf избегает проверок компилятора. потоки лучше осведомлены о типах и могут быть расширены для ваших собственных типов. Кроме того, именно поэтому ваш код должен компилироваться без предупреждений.

...