Недостатки 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] = '';
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
трудно (хотя и возможно) использовать правильно и безопасно со строковыми буферами. И это невозможно использовать безопасно для арифметического ввода. Последняя является реальной проблемой. Первое-это просто неудобство.
П. С. выше в предполагалось, что это будет о всей семье scanf
функции (в том числе и fscanf
и sscanf
). С scanf
в частности, очевидная проблема заключается в том, что сама идея использования строго отформатированной функции для чтения потенциально интерактивные ввод довольно сомнителен.
из комп.ленг.с вопросы и ответы: почему все говорят не использовать scanf? Что я должен использовать вместо этого?
scanf
есть ряд проблем-см. вопросы 12.17, 12.18 a и 12.19. Кроме того, его%s
формат имеет ту же проблему, что иgets()
(см. вопрос 12.23) - трудно гарантировать, что принимающий буфер не переполнится. [сноска]в целом,
scanf
предназначен для относительно структурированного, форматированного ввода (его название фактически происходит от"Scan formatted"). Если вы обратите внимание, он скажет вам, удалось ли ему это или нет, но он может сказать вам только приблизительно, где он потерпел неудачу, а вовсе не как или почему. У вас очень мало возможностей для восстановления ошибок.однако интерактивный пользовательский ввод является наименее структурированным входом. Хорошо продуманный пользовательский интерфейс позволит возможность ввода пользователем практически всего-не только букв или знаков препинания, когда ожидались цифры, но и больше или меньше символов, чем ожидалось, или вообще никаких символов (то есть, просто ключ возврата), или преждевременный EOF, или что-нибудь. Почти невозможно изящно справиться со всеми этими потенциальными проблемами при использовании
scanf
; гораздо легче читать целые строки (сfgets
или тому подобное), затем интерпретируйте их, используяsscanf
или какой-то другой методы. (Функции, такие какstrtol
,strtok
иatoi
часто полезны; см. также вопросы 12.16 и 13.6.) Если вы используетеscanf
variant, обязательно проверьте возвращаемое значение, чтобы убедиться, что ожидаемое количество элементов было найдено. Кроме того, если вы используете%s
, обязательно защитите от переполнения буфера.заметьте, кстати, что критика
scanf
не обязательно обвиненияfscanf
иsscanf
.scanf
читаетstdin
, который обычно является интерактивной клавиатурой и поэтому наименее ограничен, что приводит к большинству проблем. Когда файл данных имеет известный формат, с другой стороны, может быть целесообразно прочитать его с помощьюfscanf
. Идеально подходит для разбора строк с помощьюsscanf
(пока возвращаемое значение проверено), потому что так легко восстановить контроль, перезапустить сканирование, отменить ввод, если он не соответствует и т. д.Дополнительные ссылки:
ссылки: K & R2 SEC. 7.4 p. 159
Да, вы правы. Существует серьезный недостаток безопасности в scanf
семьи(scanf
,sscanf
, fscanf
..etc) esp при чтении строки, потому что они не учитывают длину буфера (в который они читают).
пример:
char buf[3];
sscanf("abcdef","%s",buf);
явно буфер buf
может держать MAX 3
char. Но ... --3--> постараюсь поставить "abcdef"
в него вызывает переполнение буфера.
это очень трудно сделать 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()
семья:
- потенциал переполнения буфера с %s и %[ спецификаторы преобразования. Да, вы можете указать максимальную ширину поля, но в отличие от С
printf()
, вы не можете сделать то аргумента вscanf()
вызов; он должен быть жестко закодирован в спецификаторе преобразования. - потенциал для арифметического переполнения С %d, %i и т. д.
- ограниченная способность обнаруживать и отклонять плохо сформированный вход. Например, "12w4" не допустимое целое число, но
scanf("%d", &value);
будет успешно конвертировать и назначить 12 вvalue
, оставив "w4" застрявшим во входном потоке, чтобы испортить будущее чтение. В идеале вся входная строка должна быть отклонена, ноscanf()
не дает Вам удобный механизм для этого.
если вы знаете, что ваш вход всегда будет хорошо сформирован с строками фиксированной длины и числовыми значениями, которые не флиртуют с переполнением, то scanf()
является отличным инструментом. Если вы имеете дело с interactive ввод или ввод, который не гарантированно будет хорошо сформирован, затем используйте что-то еще.
многие ответы здесь обсуждают потенциальные проблемы переполнения использования 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
функции - отсутствие любой безопасность типов. То есть, вы можете этот код:
int i;
scanf("%10s", &i);
черт, даже это "хорошо":
scanf("%10s", i);
Это хуже, чем printf
-подобные функции, потому что scanf
ожидает указатель, поэтому сбои более вероятны.
конечно, есть некоторые шашки спецификатора формата, но они не идеальны и хорошо, они не являются частью языка или стандарта библиотека.
преимущество scanf
Как только вы узнаете, как использовать инструмент, как вы всегда делать в C, он имеет очень полезную навигации. вы можете узнать, как использовать scanf
и друзья по чтению и пониманию руководство. Если вы не можете пройти через это руководство без серьезных проблем с пониманием, это, вероятно, указывает на то, что вы не знаете C очень хорошо.
scanf
и друзья пострадали от неудачного дизайна выбор это затрудняло (а иногда и невозможно) правильное использование без чтения документации, как показали другие ответы. К сожалению, это происходит во всем C, поэтому, если бы я не советовал использовать scanf
тогда я бы не советовал использовать С.
одним из самых больших недостатков, похоже, является чисто репутация, которую он заработал среди непосвященных; как и многие полезные функции 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();
. Попробуйте сделать это с таким количеством нажатий клавиш, используя какой-то другой инструмент ;)