Недостатки scanf - PullRequest
       48

Недостатки scanf

56 голосов
/ 12 марта 2010

Хочу узнать недостатки scanf().

На многих сайтах я читал, что использование scanf может привести к переполнению буфера. Что является причиной этого? Есть ли другие недостатки у scanf?

Ответы [ 9 ]

53 голосов
/ 12 марта 2010

Большинство ответов пока сосредоточены на проблеме переполнения буфера строк. В действительности, спецификаторы формата, которые можно использовать с функциями scanf, поддерживают явную настройку ширина поля , которая ограничивает максимальный размер ввода и предотвращает переполнение буфера. Это делает популярные обвинения в опасностях переполнения буфера строк, присутствующих в scanf, практически безосновательными. Утверждать, что scanf является аналогом gets в этом отношении, совершенно неверно. Существует существенное качественное различие между scanf и gets: scanf предоставляет пользователю функции предотвращения переполнения строкового буфера, в то время как gets нет.

Можно утверждать, что эти scanf функции сложны в использовании, поскольку ширина поля должна быть встроена в строку формата (нет способа передать ее через аргумент с переменным числом аргументов, как это можно сделать в printf) , Это на самом деле правда. scanf действительно довольно плохо разработан в этом отношении. Но, тем не менее, любые заявления о том, что scanf каким-то образом безнадежно нарушен в отношении безопасности переполнения буфера строк, являются полностью поддельными и обычно делаются ленивыми программистами.

Реальная проблема с scanf имеет совершенно другую природу, хотя она также составляет около переполнение . Когда функция scanf используется для преобразования десятичных представлений чисел в значения арифметических типов, она не обеспечивает защиту от арифметического переполнения. Если переполнение происходит, scanf производит неопределенное поведение. По этой причине единственный правильный способ выполнить преобразование в стандартной библиотеке C - это функции из семейства strto....

Итак, чтобы подвести итог вышесказанному, проблема с scanf заключается в том, что трудно (хотя возможно) правильно и безопасно использовать строковые буферы. И это невозможно безопасно использовать для арифметического ввода. Последнее является реальной проблемой. Первое - это просто неудобство.

P.S. Вышеприведенное предназначено для всего семейства функций scanf (включая также fscanf и sscanf). Что касается scanf, то очевидной проблемой является то, что сама идея использования строго отформатированной функции для чтения потенциально интерактивного ввода довольно сомнительна.

52 голосов
/ 12 марта 2010

Проблемы с 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;

    // 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]
12 голосов
/ 12 марта 2010

Из comp.lang.c FAQ: Почему все говорят, что не следует использовать scanf? Что я должен использовать вместо этого?

scanf имеет ряд проблем - см. Вопросы 12,17 , 12,18a и 12,19 . Кроме того, его формат %s имеет ту же проблему, что и gets() (см. Вопрос 12.23 ) - трудно гарантировать, что приемный буфер не будет переполнен. [примечание]

В более общем смысле, 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

5 голосов
/ 12 марта 2010

Очень трудно заставить 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.

5 голосов
/ 12 марта 2010

Да, вы правы. Существует серьезный недостаток безопасности в семействе scanf (scanf, sscanf, fscanf .. и т. Д.), Особенно при чтении строки, потому что они не принимают длину буфера (в который они читают ) в учетную запись.

Пример:

char buf[3];
sscanf("abcdef","%s",buf);

ясно, что буфер buf может содержать MAX 3 char. Но sscanf попытается вставить в него "abcdef", что приведет к переполнению буфера.

3 голосов
/ 22 марта 2016

Преимущество 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, то мы столкнулись с той же реальностью: мы, скорее всего, откажемся от ввода (обычно вплоть до следующего символа новой строки)! Yuuuuuuck!

К сожалению, scanf одновременно делает сложным (неинтуитивным) и легким (наименьшее количество нажатий клавиш) отбрасывание ввода таким способом. Столкнувшись с этой реальностью отказа от пользовательского ввода, некоторые пытались scanf("%*[^\n]%*c");, не понимая, что делегат %*[^\n] потерпит неудачу, когда встретится только с новой строкой, и, следовательно, новая строка все равно останется на поток.

Небольшая адаптация, разделив два делегата формата, и мы видим здесь некоторый успех: scanf("%*[^\n]"); getchar();. Попробуйте сделать это с таким небольшим количеством нажатий клавиш, используя другой инструмент;)

3 голосов
/ 13 октября 2015

Существует одна большая проблема с scanf -подобными функциями - отсутствие любого типа безопасности. То есть вы можете закодировать это:

int i;
scanf("%10s", &i);

Черт, даже это "хорошо":

scanf("%10s", i);

Это хуже, чем printf -подобные функции, потому что scanf ожидает указатель, поэтому сбои более вероятны.

Конечно, есть некоторые средства проверки спецификаций формата, но они не идеальны и хороши, они не являются частью языка или стандартной библиотеки.

3 голосов
/ 03 октября 2014

Многие ответы здесь обсуждают потенциальные проблемы переполнения при использовании 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, поэтому он пока остается довольно непереносимым.

3 голосов
/ 12 марта 2010

Проблемы с семьей *scanf():

  • Потенциал для переполнения буфера с% s и% [спецификаторами преобразования. Да, вы можете указать максимальную ширину поля, но в отличие от printf(), вы не можете сделать это аргументом в вызове scanf(); он должен быть жестко задан в спецификаторе преобразования.
  • Потенциал арифметического переполнения с% d,% i и т. Д.
  • Ограниченная способность обнаруживать и отклонять плохо сформированный ввод. Например, «12w4» не является допустимым целым числом, но scanf("%d", &value); успешно преобразует и присваивает 12 значению value, в результате чего «w4» застрял во входном потоке, чтобы испортить будущее чтение. В идеале вся входная строка должна быть отклонена, но scanf() не дает вам простого механизма для этого.

Если вы знаете, что ваши входные данные всегда будут правильно сформированы со строками фиксированной длины и числовыми значениями, которые не совпадают с переполнением, тогда scanf() - отличный инструмент. Если вы имеете дело с интерактивным вводом или вводом, который не гарантированно будет правильно сформирован, используйте что-то другое.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...