Недостатки 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();. Попробуйте сделать это с таким количеством нажатий клавиш, используя какой-то другой инструмент ;)