Я хочу знать недостатки scanf()
.
На многих сайтах я читал, что использование scanf
может вызвать переполнение буфера. Что является причиной этого? Есть ли другие недостатки у scanf
?
Я хочу знать недостатки scanf()
.
На многих сайтах я читал, что использование scanf
может вызвать переполнение буфера. Что является причиной этого? Есть ли другие недостатки у scanf
?
Проблемы с scanf (как минимум):
%s
для получения строки от пользователя, что приводит к возможности того, что строка может быть длиннее вашего буфера, вызывая переполнение.Я очень предпочитаю использовать fgets
для чтения целых строк, чтобы вы могли ограничить объем считываемых данных. Если у вас есть буфер размером 1 КБ, и вы читаете в него строку с помощью fgets
, вы можете определить, была ли строка слишком длинной, по тому факту, что в ней нет завершающего символа новой строки (несмотря на последнюю строку файла без новой строки).
Затем вы можете пожаловаться пользователю или выделить больше места для остальной части строки (при необходимости непрерывно, пока у вас не будет достаточно места). В любом случае нет риска переполнения буфера.
Как только вы прочитали строку, вы знаете, что находитесь на следующей строке, так что здесь нет проблем. После этого вы можете sscanf
продублировать вашу строку в свое удовольствие, не сохраняя и не восстанавливая указатель файла для повторного чтения.
Вот фрагмент кода, который я часто использую, чтобы предотвратить переполнение буфера при запросе информации у пользователя.
Его можно легко настроить для использования файла, отличного от стандартного ввода, если это необходимо, и вы также можете выделить ему собственный буфер (и продолжать увеличивать его, пока он не станет достаточно большим), прежде чем вернуть его вызывающему (хотя вызывающий тогда будет нести ответственность за освобождение, конечно).
#include <stdio.h>
#include <string.h>
#define OK 0
#define NO_INPUT 1
#define TOO_LONG 2
#define SMALL_BUFF 3
static int getLine (char *prmpt, char *buff, size_t sz) {
int ch, extra;
// Size zero or one cannot store enough, so don't even
// try - we need space for at least newline and terminator.
if (sz < 2)
return SMALL_BUFF;
// Output prompt.
if (prmpt != NULL) {
printf ("%s", prmpt);
fflush (stdout);
}
// Get line with buffer overrun protection.
if (fgets (buff, sz, stdin) == NULL)
return NO_INPUT;
// Catch possibility of `\0` in the input stream.
size_t len = strlen(buff);
if (len < 1)
return NO_INPUT;
// If it was too long, there'll be no newline. In that case, we flush
// to end of line so that excess doesn't affect the next call.
if (buff[len - 1] != '\n') {
extra = 0;
while (((ch = getchar()) != '\n') && (ch != EOF))
extra = 1;
return (extra == 1) ? TOO_LONG : OK;
}
// Otherwise remove newline and give string back to caller.
buff[len - 1] = '\0';
return OK;
}
И тест-драйвер для него:
// Test program for getLine().
int main (void) {
int rc;
char buff[10];
rc = getLine ("Enter string> ", buff, sizeof(buff));
if (rc == NO_INPUT) {
// Extra NL since my system doesn't output that on EOF.
printf ("\nNo input\n");
return 1;
}
if (rc == TOO_LONG) {
printf ("Input too long [%s]\n", buff);
return 1;
}
printf ("OK [%s]\n", buff);
return 0;
}
Наконец, тестовый запуск, чтобы показать его в действии:
$ printf "\0" | ./tstprg # Singular NUL in input stream.
Enter string>
No input
$ ./tstprg < /dev/null # EOF in input stream.
Enter string>
No input
$ ./tstprg # A one-character string.
Enter string> a
OK [a]
$ ./tstprg # Longer string but still able to fit.
Enter string> hello
OK [hello]
$ ./tstprg # Too long for buffer.
Enter string> hello there
Input too long [hello the]
$ ./tstprg # Test limit of buffer.
Enter string> 123456789
OK [123456789]
$ ./tstprg # Test just over limit.
Enter string> 1234567890
Input too long [123456789]
if (fgets (buff, sz, stdin) == NULL) return NO_INPUT;
Почему вы использовали NO_INPUT
в качестве возвращаемого значения? fgets
возвращает NULL
только в случае ошибки.
- person Fabio Carello; 29.03.2013
char *buf; scanf("%ms", &buf);
, который выделит достаточно места для вас с помощью malloc
(поэтому его необходимо освободить позже), что поможет предотвратить переполнение буфера.
- person dreamlax; 03.10.2014
getLine
с 1
в качестве параметра sz
? if (buff[strlen(buff)-1] != '\n')
вот где возникает проблема. Возможно, if (!sz) { return TOO_LONG; } if (buff[sz = strcspn(buff, "\n")] == '\n' || getchar() == '\n') { buff[sz] = '\0'; return OK; } unsigned char c; while (fread(&c, 1, 1, stdin) == 1 && c != '\n'); return TOO_LONG;
, который действительно не переполнится, когда вы пройдете sz <= 1
, и имеет дополнительное преимущество удаления '\n'
для вас с нулевыми накладными расходами, хотя следует отметить, что ваш код может быть улучшен за счет стратегического использования из scanf
...
- person autistic; 12.07.2018
size_t lastPos = strlen(buff) - 1; if (buff[lastPos] != '\n') {
можно использовать как UB, если сначала ввести нулевой символ.
- person chux - Reinstate Monica; 31.08.2020
printf "\0" | exeName
для проверки исходной проблемы и ее устранения. Думаю, я никогда не проверял с таким безумным сценарием ввода (но, черт возьми, должен был это сделать). Спасибо за внимание.
- person paxdiablo; 01.09.2020
Большинство ответов до сих пор, похоже, сосредоточены на проблеме переполнения строкового буфера. На самом деле спецификаторы формата, которые можно использовать с функциями scanf
, поддерживают явную настройку ширины поля, которая ограничивает максимальный размер ввода и предотвращает переполнение буфера. Это делает популярные обвинения в опасности переполнения строкового буфера, присутствующие в scanf
, практически беспочвенными. Утверждение, что scanf
чем-то похоже на gets
в этом отношении, совершенно неверно. Между scanf
и gets
есть большая качественная разница: scanf
предоставляет пользователю функции предотвращения переполнения строкового буфера, а gets
— нет.
Можно возразить, что эти возможности scanf
сложны в использовании, так как ширина поля должна быть встроена в строку формата (нет возможности передать ее через аргумент с переменным числом аргументов, как это можно сделать в printf
). Это действительно так. scanf
действительно довольно плохо спроектирован в этом отношении. Но, тем не менее, любые заявления о том, что scanf
каким-то образом безнадежно нарушен в отношении безопасности переполнения строкового буфера, являются полностью фиктивными и обычно исходят от ленивых программистов.
Настоящая проблема с scanf
имеет совершенно другую природу, хотя она тоже связана с переполнением. Когда функция scanf
используется для преобразования десятичных представлений чисел в значения арифметических типов, она не обеспечивает защиты от арифметического переполнения. Если происходит переполнение, scanf
приводит к неопределенному поведению. По этой причине единственный правильный способ выполнить преобразование в стандартной библиотеке C — это функции из семейства strto...
.
Итак, подытоживая вышесказанное, проблема с scanf
заключается в том, что его сложно (хотя и возможно) правильно и безопасно использовать со строковыми буферами. И нельзя смело использовать для арифметического ввода. Последнее является настоящей проблемой. Первое просто неудобство.
P.S. Вышеупомянутое предназначено для всего семейства функций scanf
(включая также fscanf
и sscanf
). В частности, с scanf
очевидная проблема заключается в том, что сама идея использования строго отформатированной функции для чтения потенциально интерактивного ввода довольно сомнительна.
scanf
чем-то похоже на gets в этом отношении, совершенно неверно. Я понимаю, scanf
по крайней мере действительно позволяет указать максимальный размер поля, но идеологическое использование of %s
, безусловно, имеет те же проблемы, что и gets
, и, как и многие другие опасные, но полезные инструменты в C, ими легко злоупотреблять. Даже у strtoul
есть свои опасности, так что вместо того, чтобы предлагать людям прекратить использовать части языка C, не можем ли мы сразу предложить людям прекратить использование всего языка C?
- person autistic; 12.07.2018
Из часто задаваемых вопросов comp.lang.c: Почему все говорят не использовать scanf? Что я должен использовать вместо этого?
scanf
имеет ряд проблем — см. вопросы 12.17, 12.18a и 12.19. Кроме того, его формат%s
имеет ту же проблему, что иgets()
(см. вопрос 12.23) — это трудно гарантировать, что принимающий буфер не переполнится. [сноска]В более общем смысле,
scanf
предназначен для относительно структурированного, форматированного ввода (на самом деле его название происходит от «отформатированного сканирования»). Если вы обратите внимание, то оно скажет вам, удалось это или не удалось, но может сказать вам лишь приблизительно, где это не удалось, а вовсе не как и почему. У вас очень мало возможностей для исправления ошибок.Тем не менее, интерактивный пользовательский ввод — это наименее структурированный ввод. Хорошо продуманный пользовательский интерфейс позволяет пользователю вводить что угодно — не только буквы или знаки препинания, когда ожидалось ввести цифры, но и больше или меньше символов, чем ожидалось, или вообще не вводить символы (т.е. /em>, просто клавиша RETURN), или преждевременный EOF, или что-то еще. Почти невозможно изящно справиться со всеми этими потенциальными проблемами при использовании
scanf
; гораздо проще читать целые строки (сfgets
и т.п.), а затем интерпретировать их, используя либоsscanf
, либо какие-либо другие приемы. (Часто бывают полезны такие функции, какstrtol
,strtok
иatoi
; см. также вопросы 12.16 и 13.6.) Если вы используете какой-либо вариантscanf
, обязательно проверьте возвращаемое значение, чтобы уверен, что ожидаемое количество элементов было найдено. Кроме того, если вы используете%s
, обязательно предохраняйтесь от переполнения буфера.Обратите внимание, кстати, что критика
scanf
не обязательно является обвинениемfscanf
иsscanf
.scanf
считывается сstdin
, которая обычно представляет собой интерактивную клавиатуру и, следовательно, наименее ограничена, что приводит к наибольшему количеству проблем. С другой стороны, когда файл данных имеет известный формат, может быть уместно прочитать его с помощьюfscanf
. Совершенно уместно анализировать строки с помощьюsscanf
(пока возвращаемое значение проверяется), потому что так легко восстановить контроль, перезапустить сканирование, отбросить ввод, если он не соответствует, и т. д.Дополнительные ссылки:
Ссылки: K&R2 Sec. 7,4 р. 159
Очень трудно заставить scanf
делать то, что вы хотите. Конечно, можно, но такие вещи, как scanf("%s", buf);
, так же опасны, как и gets(buf);
, как все говорят.
Например, то, что делает paxdiablo в своей функции чтения, можно сделать с помощью чего-то вроде:
scanf("%10[^\n]%*[^\n]", buf));
getchar();
Вышеупомянутое будет читать строку, сохранять первые 10 символов, отличных от новой строки, в buf
, а затем отбрасывать все до (и включая) новой строки. Итак, функция paxdiablo может быть записана с использованием scanf
следующим образом:
#include <stdio.h>
enum read_status {
OK,
NO_INPUT,
TOO_LONG
};
static int get_line(const char *prompt, char *buf, size_t sz)
{
char fmt[40];
int i;
int nscanned;
printf("%s", prompt);
fflush(stdout);
sprintf(fmt, "%%%zu[^\n]%%*[^\n]%%n", sz-1);
/* read at most sz-1 characters on, discarding the rest */
i = scanf(fmt, buf, &nscanned);
if (i > 0) {
getchar();
if (nscanned >= sz) {
return TOO_LONG;
} else {
return OK;
}
} else {
return NO_INPUT;
}
}
int main(void)
{
char buf[10+1];
int rc;
while ((rc = get_line("Enter string> ", buf, sizeof buf)) != NO_INPUT) {
if (rc == TOO_LONG) {
printf("Input too long: ");
}
printf("->%s<-\n", buf);
}
return 0;
}
Еще одна проблема с scanf
— его поведение в случае переполнения. Например, при чтении int
:
int i;
scanf("%d", &i);
вышеуказанное нельзя безопасно использовать в случае переполнения. Даже в первом случае чтение строки гораздо проще сделать с помощью fgets
, чем с scanf
.
Да, ты прав. Существует серьезный недостаток безопасности в scanf
family(scanf
,sscanf
, fscanf
..etc) esp при чтении строки, потому что они не учитывают длину буфера (в который они считываются).
Пример:
char buf[3];
sscanf("abcdef","%s",buf);
ясно, что буфер buf
может содержать MAX 3
символов. Но sscanf
попытается поместить в него "abcdef"
, что вызовет переполнение буфера.
scanf
, является проблемой с этим кем-то, а не с scanf
. Совершенно не имеет отношения к рассматриваемому вопросу. В конце концов, это C, а не Java.
- person AnT; 12.03.2010
scanf()
должна быть жестко запрограммирована в спецификаторе преобразования; с printf()
вы можете использовать *
в спецификаторе преобразования и передать длину в качестве аргумента. Но так как *
означает что-то другое в scanf()
, это не работает, поэтому вам в основном нужно генерировать новый формат для каждого чтения, как это делает Алок в своем примере. Это просто добавляет больше работы и беспорядка; можно также использовать fgets()
и покончить с этим.
- person John Bode; 12.03.2010
Проблемы, которые у меня есть с семьей *scanf()
:
printf()
, вы не можете сделать его аргументом в вызове scanf()
; это должно быть жестко закодировано в спецификаторе преобразования.scanf("%d", &value);
успешно преобразует и присвоит 12 value
, оставив «w4» застрявшим во входном потоке, чтобы испортить будущее чтение. В идеале вся входная строка должна быть отклонена, но scanf()
не дает вам простого механизма для этого.Если вы знаете, что ваш ввод всегда будет правильно сформирован со строками фиксированной длины и числовыми значениями, которые не заигрывают с переполнением, то scanf()
— отличный инструмент. Если вы имеете дело с интерактивным вводом или вводом, который не гарантируется правильно сформированным, используйте что-то другое.
Во многих ответах здесь обсуждаются потенциальные проблемы переполнения при использовании scanf("%s", buf)
, но последняя спецификация POSIX более или менее решает эту проблему, предоставляя символ назначения-распределения m
, который можно использовать в спецификаторах формата для форматов c
, s
и [
. Это позволит scanf
выделить столько памяти, сколько необходимо с помощью malloc
(поэтому ее необходимо освободить позже с помощью free
).
Пример его использования:
char *buf;
scanf("%ms", &buf); // with 'm', scanf expects a pointer to pointer to char.
// use buf
free(buf);
См. здесь. Недостатком этого подхода является то, что это относительно недавнее дополнение к спецификации POSIX и вообще не указано в спецификации C, поэтому на данный момент он остается непереносимым.
У scanf
-подобных функций есть одна большая проблема — отсутствие безопасности типов any. То есть вы можете закодировать это:
int i;
scanf("%10s", &i);
Черт, даже это "хорошо":
scanf("%10s", i);
Это хуже, чем printf
-подобные функции, потому что scanf
ожидает указатель, поэтому сбои более вероятны.
Конечно, есть некоторые средства проверки спецификаторов формата, но они не идеальны и не являются частью языка или стандартной библиотеки.
Преимущество scanf
заключается в том, что после того, как вы научитесь использовать этот инструмент, как вы всегда должны делать в C, у него есть чрезвычайно полезные варианты использования. Вы можете научиться использовать scanf
и друзей, прочитав и поняв руководство. Если вы не можете пройти это руководство без серьезных проблем с пониманием, это, вероятно, указывает на то, что вы не очень хорошо знаете C.
scanf
и друзья пострадали от неудачного выбора дизайна, из-за которого было трудно (а иногда и невозможно) правильно использовать без чтения документации, как показали другие ответы. К сожалению, это происходит во всем C, поэтому, если бы я советовал не использовать scanf
, я бы, вероятно, посоветовал не использовать C.
Одним из самых больших недостатков, по-видимому, является репутация, которую он заработал среди непосвященных; как и со многими полезными функциями C, мы должны быть хорошо информированы, прежде чем использовать его. Ключ в том, чтобы понять, что, как и в остальной части C, он кажется кратким и идиоматичным, но это может слегка ввести в заблуждение. Это распространено в C; новичкам легко написать код, который, по их мнению, имеет смысл и может даже работать на них изначально, но не имеет смысла и может привести к катастрофическим сбоям.
Например, непосвященные обычно ожидают, что делегат %s
вызовет чтение строки, и хотя это может показаться интуитивно понятным, это не обязательно так. Поле лучше описать как слово. Настоятельно рекомендуется прочитать руководство для каждой функции.
Каким был бы ответ на этот вопрос, если бы не упоминание о его небезопасности и риске переполнения буфера? Как мы уже говорили, C не является безопасным языком и позволит нам срезать углы, возможно, применить оптимизацию за счет корректности или, что более вероятно, потому что мы ленивые программисты. Таким образом, когда мы знаем, что система никогда не получит строку больше фиксированного числа байтов, нам предоставляется возможность объявить массив такого размера и отказаться от проверки границ. Я действительно не вижу в этом падения; это вариант. Опять же, настоятельно рекомендуется прочитать руководство, которое откроет нам эту возможность.
Не только ленивые программисты страдают от scanf
. Нередко можно увидеть, как люди пытаются прочитать значения float
или double
, используя, например, %d
. Обычно они ошибаются, полагая, что реализация будет выполнять какое-то преобразование за кулисами, что имеет смысл, поскольку подобные преобразования происходят во всем остальном языке, но здесь это не так. Как я уже говорил ранее, scanf
и друзья (и, конечно же, остальные члены C) обманчивы; они кажутся краткими и идиоматическими, но это не так.
Неопытные программисты не обязаны учитывать успех операции. Предположим, пользователь вводит что-то совершенно нечисловое, когда мы сказали scanf
прочитать и преобразовать последовательность десятичных цифр, используя %d
. Единственный способ перехватить такие ошибочные данные — это проверить возвращаемое значение, а как часто мы утруждаем себя проверкой возвращаемого значения?
Подобно fgets
, когда scanf
и друзья не могут прочитать то, что им велят прочитать, поток останется в необычном состоянии; – В случае fgets
, если не хватает места для сохранения полной строки, то оставшаяся часть строки, оставшаяся непрочитанной, может быть ошибочно воспринята как новая строка, когда это не так. - В случае scanf
и друзей, преобразование завершилось неудачно, как описано выше, ошибочные данные остаются непрочитанными в потоке и могут быть ошибочно обработаны, как если бы они были частью другого поля.
Использовать scanf
и друзей не проще, чем fgets
. Если мы проверим успех, ища '\n'
, когда мы используем fgets
, или проверим возвращаемое значение, когда мы используем scanf
и других, и обнаружим, что мы прочитали неполную строку, используя fgets
, или не смогли прочитать поле, используя scanf
, то мы сталкиваемся с той же реальностью: мы, вероятно, отбрасываем ввод (обычно до следующей новой строки включительно)! Юууууук!
К сожалению, scanf
одновременно усложняет (неинтуитивно) и упрощает (наименьшее количество нажатий клавиш) отбрасывание ввода таким образом. Столкнувшись с этой реальностью отказа от пользовательского ввода, некоторые пытались использовать , не понимая, что делегат scanf("%*[^\n]%*c");
%*[^\n]
потерпит неудачу, если не встретит ничего, кроме новой строки, и, следовательно, новая строка все равно останется в потоке.
Небольшая адаптация путем разделения двух делегатов формата, и мы видим здесь некоторый успех: scanf("%*[^\n]"); getchar();
. Попробуйте сделать это с таким количеством нажатий клавиш, используя какой-нибудь другой инструмент;)
scanf()
. - person Jonathan Leffler   schedule 19.07.2017