Я хочу знать недостатки scanf()
.
На многих сайтах я читал, что использование scanf
может привести к переполнению буфера. Что является причиной этого? Существуют ли другие недостатки с scanf
?
Я хочу знать недостатки scanf()
.
На многих сайтах я читал, что использование scanf
может привести к переполнению буфера. Что является причиной этого? Существуют ли другие недостатки с scanf
?
Проблемы с 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
создает поведение undefined. По этой причине единственным правильным способом выполнения преобразования в стандартной библиотеке C являются функции из семейства strto...
.
Итак, чтобы суммировать вышеизложенное, проблема с scanf
заключается в том, что трудно (хотя бы возможно) правильно и безопасно использовать строковые буферы. И безопасно использовать для арифметического ввода. Последнее является реальной проблемой. Первый - это всего лишь неудобство.
P.S. Вышеизложенное предназначено для всего семейства функций scanf
(включая также fscanf
и sscanf
). С scanf
в частности, очевидная проблема заключается в том, что сама идея использования строго отформатированной функции для чтения потенциально интерактивного ввода довольно сомнительна.
Из comp.lang.c FAQ: Почему все говорят, что не использовать scanf? Что я должен использовать вместо этого?
scanf
имеет ряд проблем - см. вопросы 12.17, 12.18a и 12.19. Кроме того, формат%s
имеет ту же проблему, чтоgets()
имеет (см. Вопрос 12.23) - трудно гарантировать, что буфер приема не будет переполнение. [footnote]В более общем плане
scanf
предназначен для относительно структурированного отформатированного ввода (его имя фактически получено из "отформатированного сканирования" ). Если вы обратите внимание, он скажет вам, удалось ли это или не удалось, но он может сказать вам только примерно, где это не удалось, и совсем не так, как и почему. У вас очень мало возможностей для восстановления любой ошибки.Однако интерактивный пользовательский ввод является наименее структурированным вводом. Хорошо спроектированный пользовательский интерфейс позволит пользователю вводить только что-нибудь - не только буквы или знаки препинания, когда ожидались цифры, но и больше или меньше символов, чем ожидалось, или вообще никаких символов (т.е. Только 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
, sscanf
, fscanf
.. и т.д.) Esp при чтении строки, поскольку они не занимают длину буфера (в который они читаются).
Пример:
char buf[3];
sscanf("abcdef","%s",buf);
ясно, что буфер buf
может содержать MAX 3
char. Но sscanf
попытается поместить в него "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()
:
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
-подобными функциями - отсутствие безопасности любого типа. То есть вы можете закодировать это:
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
values, используя, например, %d
. Они обычно ошибаются, полагая, что реализация будет выполнять какое-то преобразование за кулисами, что имеет смысл, потому что подобные преобразования происходят по всему остальному языку, но это не так. Как я уже говорил ранее, scanf
и друзья (и даже остальная часть C) обманчивы; они кажутся лаконичными и идиоматическими, но это не так.
Неопытные программисты не вынуждены рассматривать успех операции. Предположим, что пользователь вводит что-то совершенно нечисловое, когда мы сказали scanf
читать и преобразовывать последовательность десятичных цифр, используя %d
. Единственный способ перехватить такие ошибочные данные - проверить возвращаемое значение и как часто мы пытаемся проверить возвращаемое значение?
Как и fgets
, когда scanf
и друзья не читают то, что им говорят, поток будет оставлен в необычном состоянии; - В случае fgets
, если для хранения полной строки недостаточно места, оставшаяся часть строки, оставленной непрочитанной, может быть ошибочно обработана, как если бы она была новой строкой, когда она не является. - В случае scanf
и друзей преобразование завершилось неудачно, как описано выше, ошибочные данные остаются непрочитанными в потоке и могут быть ошибочно обработаны, как если бы они были частью другого поля.
Не проще использовать scanf
и друзей, чем использовать fgets
. Если мы проверим успех, ищем '\n'
когда мы используем fgets
или проверяем возвращаемое значение при использовании scanf
и друзей, и мы обнаруживаем, что мы прочитали неполную строку с использованием fgets
или не смогли прочитать поле с помощью scanf
, то мы сталкиваемся с такой же реальностью: мы, вероятно, отменим ввод (обычно вплоть до следующей новой строки). Yuuuuuuck!
К сожалению, scanf
одновременно делает его сложным (неинтуитивным) и легким (наименьшее количество нажатий клавиш), чтобы отбросить вход таким образом. Столкнувшись с этой реальностью отказа от пользовательского ввода, некоторые попытались , не понимая, что делегат scanf("%*[^\n]%*c");
%*[^\n]
потерпит неудачу, когда он встречает только новую строку, и, следовательно, новая строка все равно останется в потоке.
Небольшая адаптация, разделив делегатов двух форматов, и мы видим здесь некоторый успех: scanf("%*[^\n]"); getchar();
scanf("%*[^\n]"); getchar();
, Попробуйте сделать это с таким количеством нажатий клавиш, используя какой-либо другой инструмент;)
Функция fgets() - хорошее решение