Недостатки scanf
Я хочу знать недостатки scanf()
,
На многих сайтах я читал, что с помощью scanf
может вызвать переполнение буфера. Что является причиной этого? Есть ли другие недостатки с scanf
?
9 ответов
Проблемы с scanf (как минимум):
- с помощью
%s
получить строку от пользователя, что приводит к возможности того, что строка может быть длиннее, чем ваш буфер, вызывая переполнение. - возможность неудачного сканирования, оставляя указатель вашего файла в неопределенном месте.
Я очень предпочитаю использовать fgets
читать целые строки так, чтобы вы могли ограничить объем прочитанных данных. Если у вас есть буфер 1K, и вы читаете строку в него с 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;
// 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.
size_t lastPos = strlen(buff) - 1;
if (buff[lastPos] != '\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[lastPos] = '\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;
}
Наконец, тестовый прогон, чтобы показать это в действии:
$ ./tstprg
Enter string>[CTRL-D]
No input
$ ./tstprg
Enter string> a
OK [a]
$ ./tstprg
Enter string> hello
OK [hello]
$ ./tstprg
Enter string> hello there
Input too long [hello the]
$ ./tstprg
Enter string> i am pax
OK [i am pax]
Кажется, что большинство ответов до сих пор сосредоточены на проблеме переполнения буфера строк. В действительности, спецификаторы формата, которые могут быть использованы с scanf
функции поддерживают явную настройку ширины поля, которая ограничивает максимальный размер ввода и предотвращает переполнение буфера. Это делает популярные обвинения в опасностях переполнения буфера строк, присутствующих в scanf
практически безосновательно. Утверждая, что scanf
как-то аналогично gets
в отношении совершенно неверно. Есть большая качественная разница между scanf
а также gets
: scanf
действительно предоставляет пользователю функции предотвращения переполнения буфера строки, в то время как gets
не делает.
Можно утверждать, что эти scanf
Функции сложно использовать, так как ширина поля должна быть встроена в строку формата (нет способа передать ее через аргумент с переменным числом аргументов, как это можно сделать в printf
). Это на самом деле правда. scanf
на самом деле довольно плохо разработан в этом отношении. Но тем не менее любые претензии, что scanf
каким-то образом безнадежно нарушается в отношении безопасности переполнения буфера строк, полностью поддельные и обычно сделаны ленивыми программистами.
Настоящая проблема с scanf
имеет совершенно другую природу, хотя это также о переполнении. когда scanf
Функция используется для преобразования десятичных представлений чисел в значения арифметических типов, она не обеспечивает защиту от арифметического переполнения. Если переполнение происходит, scanf
производит неопределенное поведение. По этой причине единственный правильный способ выполнить преобразование в стандартной библиотеке C - это функции из strto...
семьи.
Итак, подведя итог вышесказанному, проблема с scanf
в том, что его трудно (хотя и возможно) правильно и безопасно использовать со строковыми буферами. И это невозможно безопасно использовать для арифметического ввода. Последнее является настоящей проблемой. Первый просто неудобство.
PS Вышесказанное предназначено для всей семьи scanf
функции (в том числе также fscanf
а также sscanf
). С scanf
в частности, очевидная проблема заключается в том, что сама идея использования строго отформатированной функции для чтения потенциально интерактивного ввода довольно сомнительна.
Из comp.lang.c FAQ: Почему все говорят, что не следует использовать scanf? Что я должен использовать вместо этого?
scanf
имеет ряд проблем - см. вопросы 12.17, 12.18a и 12.19. Кроме того, его%s
формат имеет ту же проблему, чтоgets()
has (см. вопрос 12.23) - трудно гарантировать, что приемный буфер не переполнится. [примечание]В более общем смысле,
scanf
предназначен для относительно структурированного, отформатированного ввода (его название на самом деле происходит от "отформатированного сканирования"). Если вы обратите внимание, он скажет вам, был ли он успешным или неудачным, но он может сказать вам только приблизительно, где он потерпел неудачу, а не вообще, как и почему. У вас очень мало возможностей для устранения ошибок.Тем не менее, интерактивный пользовательский ввод является наименее структурированным. Хорошо продуманный пользовательский интерфейс позволит пользователю печатать практически что угодно - не только буквы или знаки препинания, когда ожидаются цифры, но также большее или меньшее количество символов, чем ожидалось, или вообще никаких символов (т. Е. Только ВОЗВРАТ ключ), или преждевременный 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
семья (scanf
,sscanf
, fscanf
..etc) esp при чтении строки, потому что они не принимают во внимание длину буфера (в который они читают).
Пример:
char buf[3];
sscanf("abcdef","%s",buf);
явно буфер buf
может держать Макс 3
голец. Но sscanf
постараюсь поставить "abcdef"
в это вызывает переполнение буфера.
Преимущество scanf
как только вы научитесь использовать инструмент, как вы всегда должны делать в C, он имеет чрезвычайно полезные варианты использования. Вы можете научиться использовать scanf
и друзья, читая и понимая руководство. Если вы не можете прочитать это руководство без серьезных проблем с пониманием, это, вероятно, будет означать, что вы не очень хорошо знаете C.
scanf
и друзья страдали от неудачного выбора дизайна, что делало его трудным (а иногда и невозможным) для правильного использования без чтения документации, как показали другие ответы. К сожалению, это происходит по всему C, поэтому, если бы я рекомендовал не использовать scanf
тогда, вероятно, я бы посоветовал не использовать C.
Кажется, что одним из самых больших недостатков является исключительно репутация, которую он заработал среди непосвященных; как и со многими полезными функциями C, мы должны быть хорошо информированы, прежде чем использовать его. Ключевым моментом является осознание того, что, как и в случае с остальной частью языка C, оно выглядит лаконичным и идиоматическим, но это может быть слегка обманчивым. Это распространено в C; новичкам легко написать код, который, по их мнению, имеет смысл и может даже работать для них изначально, но не имеет смысла и может привести к катастрофическим ошибкам.
Например, непосвященные обычно ожидают, что %s
делегат может вызвать чтение строки, и хотя это может показаться интуитивно понятным, это не всегда так. Более уместно описать поле, читаемое как слово. Чтение руководства настоятельно рекомендуется для каждой функции.
Каким будет любой ответ на этот вопрос, если не упомянуть отсутствие безопасности и риск переполнения буфера? Как мы уже рассмотрели, C не является безопасным языком и позволит нам срезать углы, возможно, применить оптимизацию за счет корректности или, скорее, потому что мы ленивые программисты. Таким образом, когда мы знаем, что система никогда не получит строку, размер которой превышает фиксированное число байтов, мы получаем возможность объявить массив такого размера и отказаться от проверки границ. Я действительно не вижу в этом падения; это вариант. Опять же, чтение руководства настоятельно рекомендуется и откроет нам эту возможность.
Ленивые программисты не единственные, кого ужалили scanf
, Нередки случаи, когда люди пытаются читать float
или же double
значения с использованием %d
, например. Они обычно ошибаются, полагая, что реализация будет выполнять какое-то преобразование за кулисами, что имело бы смысл, потому что подобные преобразования происходят во всем остальном языке, но здесь это не так. Как я уже говорил ранее, scanf
а друзья (да и вся остальная часть С) обманчивы; они кажутся краткими и идиоматическими, но это не так.
Неопытные программисты не обязаны учитывать успех операции. Предположим, что пользователь вводит что-то совершенно нечисловое, когда мы сказали scanf
читать и преобразовывать последовательность десятичных цифр, используя %d
, Единственный способ, которым мы можем перехватить такие ошибочные данные, - это проверить возвращаемое значение, и как часто мы пытаемся проверить возвращаемое значение?
Так же, как fgets
, когда scanf
и друзья не смогут прочитать то, что им говорят, поток останется в необычном состоянии; - В случае fgets
, если недостаточно места для хранения полной строки, тогда оставшаяся непрочитанная строка может быть ошибочно воспринята, как если бы это была новая строка, если ее нет.
- В случае scanf
и друзья, преобразование не удалось, как описано выше, ошибочные данные остаются непрочитанными в потоке и могут быть ошибочно обработаны, как если бы они были частью другого поля.
Это не проще в использовании scanf
и друзья, чем использовать fgets
, Если мы проверим на успех, ища '\n'
когда мы используем fgets
или проверяя возвращаемое значение, когда мы используем scanf
и друзья, и мы находим, что мы прочитали неполную строку, используя fgets
или не удалось прочитать поле с помощью scanf
, тогда мы столкнулись с той же реальностью: мы, скорее всего, откажемся от ввода (обычно вплоть до следующего символа новой строки)! Yuuuuuuck!
К несчастью, scanf
оба одновременно делают сложным (неинтуитивным) и легким (наименьшее количество нажатий клавиш) таким способом отбрасывать ввод. Столкнувшись с этой реальностью отказа от пользовательского ввода, некоторые пытались scanf("%*[^\n]%*c");
, не понимая, что %*[^\n]
делегат потерпит неудачу, когда встретится только с новой строкой, и, следовательно, новая строка все равно останется в потоке.
Небольшая адаптация, разделив два делегата формата, и мы видим здесь некоторый успех: scanf("%*[^\n]"); getchar();
, Попробуйте сделать это с таким небольшим количеством нажатий клавиш, используя другой инструмент;)
Есть одна большая проблема с scanf
Подобные функции - отсутствие какого- либо типа безопасности. То есть вы можете кодировать это:
int i;
scanf("%10s", &i);
Черт, даже это "хорошо"
scanf("%10s", i);
Это хуже чем printf
-подобные функции, потому что scanf
ожидает указатель, поэтому сбои более вероятны.
Конечно, есть некоторые средства проверки спецификаций формата, но они не идеальны и хорошо, они не являются частью языка или стандартной библиотеки.
Проблемы у меня с *scanf()
семья:
- Потенциал переполнения буфера с%s и%[спецификаторами преобразования. Да, вы можете указать максимальную ширину поля, но в отличие от
printf()
Вы не можете сделать это аргументом вscanf()
вызов; он должен быть жестко задан в спецификаторе преобразования. - Потенциал для арифметического переполнения с%d, %i и т. Д.
- Ограниченная способность обнаруживать и отклонять плохо сформированный ввод. Например, "12w4" не является допустимым целым числом, но
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, поэтому оно пока остается довольно непереносимым.