Что я могу использовать для преобразования входных данных вместо scanf?

Я очень часто видел, как люди отговаривали других от использования scanf и говорили, что есть лучшие альтернативы. Однако все, что я в итоге вижу, это либо «не использовать scanf» , либо «вот правильная строка формата» , и никогда не будет примеров «1110]» лучше альтернативы " упоминается.

Например, давайте возьмем следующий фрагмент кода:

scanf("%c", &c);

Это считывает пробелы, которые остались во входном потоке после последнего преобразования. Обычно предлагаемое решение этого заключается в том, чтобы использовать:

scanf(" %c", &c);

или не использовать scanf.

Так как scanf плохо, каковы некоторые опции ANSI C для преобразования входных форматов, которые scanf обычно могут обрабатывать (такие как целые числа, числа с плавающей точкой и строки) без использования scanf?

123
задан JL2210 16 October 2019 в 17:06
поделиться

6 ответов

Наиболее распространенные способы чтения входа:

  • использование fgets с фиксированным размером, который является тем, что обычно предлагается, и

  • использование fgetc, который может быть полезным, если Вы только читаете сингл char.

Для преобразования входа существует множество функций, которые можно использовать:

  • strtoll, чтобы преобразовать строку в целое число

  • strtof / d / ld, преобразовать строку в число с плавающей запятой

  • sscanf, которое не является как [1 130] плохо как простое использование scanf, хотя оно действительно имеет большинство крушений упомянутым ниже

  • , нет никаких хороших способов проанализировать разделенный от разделителя вход в плоскости ANSI C. Или используйте strtok_r от POSIX или strtok, который не ориентирован на многопотоковое исполнение. Вы могли также самокрутка ориентированный на многопотоковое исполнение вариант с помощью [1 111] и strspn, поскольку strtok_r не включает специальной поддержки ОС.

  • Это может быть излишество, но можно использовать лексические анализаторы и синтаксические анализаторы (flex и bison являющийся наиболее распространенными примерами).

  • Никакое преобразование, просто просто используйте строку

<час>

, Так как Вы не вошли точно , почему scanf плохо в Вашем вопросе, я уточню:

<час>
80
ответ дан 24 November 2019 в 01:17
поделиться

Почему scanf плохо?

основная проблема состоит в том, что scanf никогда не предназначался для контакта с вводом данных пользователем. Это предназначается, чтобы использоваться с "совершенно" отформатированными данными. Я заключил слово в кавычки "отлично", потому что это не абсолютно верно. Но это не разработано для парсинга данных, которые так же ненадежны как ввод данных пользователем. По своей природе ввод данных пользователем не предсказуем. Пользователи неправильно понимают инструкции, делает опечатки, случайно нажмите Enter, прежде чем они будут сделаны и т.д. Можно было бы обоснованно спросить, почему функция, которая не должна использоваться для чтений ввода данных пользователем от stdin. Если Вы - опытное *, отклоняют пользователя, объяснение не станет неожиданностью, но оно могло бы смутить пользователей Windows. В *отклоняют системы, очень распространено создать программы, которые работают через передачу по каналу, что означает, что Вы отправляете вывод одной программы другому путем передачи по каналу stdout из первой программы к stdin из вторых. Таким образом, можно удостовериться, что вывод и ввел, предсказуемы. Во время этих обстоятельств, scanf на самом деле работы хорошо. Но при работе с непредсказуемым входом, Вы рискуете всеми видами проблемы.

Итак, почему не там никакие простые в использовании стандартные функции для ввода данных пользователем? Можно только предположить здесь, но я предполагаю, что старый хардкор C хакеры просто думал, что существующие функции были достаточно хороши, даже при том, что они являются очень неуклюжими. Кроме того, когда Вы смотрите на типичные терминальные приложения, они очень редко читают ввод данных пользователем от stdin. Чаще всего Вы передаете весь ввод данных пользователем как параметры командной строки. Несомненно, существуют исключения, но для большинства приложений, ввод данных пользователем является очень незначительной вещью.

Поэтому, что можно сделать?

Мой фаворит fgets в сочетании с sscanf. Я однажды записал ответ об этом, но я повторно отправлю полный код. Вот пример с достойным (но не прекрасен) проверка ошибок и парсинг. Это достаточно хорошо для отладки целей.

Примечание

мне особенно не нравится просить, чтобы пользователь ввел две разных вещи на одной одной строке. Я только делаю это, когда они принадлежат друг другу естественным способом. Как, например printf("Enter the price in the format <dollars>.<cent>: ") и затем используют sscanf(buffer "%d.%d", &dollar, &cent). Я никогда не делал бы чего-то как [1 112]. Основной момент использования fgets ниже должен инкапсулировать исходные данные, чтобы гарантировать, что один вход не влияет на следующее.

#define bsize 100

void error_function(const char *buffer, int no_conversions) {
        fprintf(stderr, "An error occurred. You entered:\n%s\n", buffer);
        fprintf(stderr, "%d successful conversions", no_conversions);
        exit(EXIT_FAILURE);
}

char c, buffer[bsize];
int x,y;
float f, g;
int r;

printf("Enter two integers: ");
fflush(stdout); // Make sure that the printf is executed before reading
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);

// Unless the input buffer was to small we can be sure that stdin is empty
// when we come here.
printf("Enter two floats: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);

// Reading single characters can be especially tricky if the input buffer
// is not emptied before. But since we're using fgets, we're safe.
printf("Enter a char: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%c", &c)) != 1) error_function(buffer, r);

printf("You entered %d %d %f %c\n", x, y, f, c);

Выполнение как это устранит типичную проблему, которая является запаздывающей новой строкой, которая может смешать с входом вложенного множества. Но это имеет другую проблему, которая является, если строка - дольше, чем [1 114]. Можно проверить это с [1 115]. Если Вы хотите удалить новую строку, можно сделать это с [1 116].

В целом, я советовал бы, чтобы не ожидать, что пользователь введет вход в некоторый странный формат, который необходимо проанализировать к различным переменным. Если Вы хотите присвоить переменные height и width, не просите обоих одновременно. Позвольте пользователю нажимать Enter между ними. Кроме того, этот подход является очень естественным в одном смысле. Вы никогда не будете получать вход от [1 119], пока Вы не совершите нападки, входят, итак, почему не всегда читает целую строку? Конечно, это может все еще привести к проблемам, если строка длиннее, чем буфер. Я не забывал упоминать, что ввод данных пользователем является неуклюжим в C? :)

Для предотвращения проблем со строками дольше, чем буфер можно использовать функцию, которая автоматически выделяет буфер соответствующего размера, можно использовать getline(). Недостаток состоит в том, что Вам будет нужно к [1 121] результат впоследствии.

Усиление игры

, Если бы Вы серьезно относитесь к созданию программ в C с вводом данных пользователем, я рекомендовал бы взглянуть на библиотеку как [1 122]. Поскольку затем Вы, вероятно, также хотите создать приложения с некоторой терминальной графикой. К сожалению, Вы потеряете некоторую мобильность, если Вы сделаете это, но она дает Вам намного лучший контроль ввода данных пользователем. Например, это дает Вам способность считать нажатие клавиши немедленно вместо того, чтобы ожидать пользователя для нажатия Enter.

52
ответ дан 24 November 2019 в 01:17
поделиться

Вот пример использования flex для сканирования простого входа, в этом случае файл чисел с плавающей точкой ASCII, которые могли бы быть или в США (n,nnn.dd) или в европейце (n.nnn,dd) форматы. Это просто копируется с намного большей программы, таким образом, могут быть некоторые некорректные ссылки:

/* This scanner reads a file of numbers, expecting one number per line.  It  */
/* allows for the use of European-style comma as decimal point.              */

%{
  #include <stdlib.h>
  #include <stdio.h>
  #include <string.h>
  #ifdef WINDOWS
    #include <io.h>
  #endif
  #include "Point.h"

  #define YY_NO_UNPUT
  #define YY_DECL int f_lex (double *val)

  double atofEuro (char *);
%}

%option prefix="f_"
%option nounput
%option noinput

EURONUM [-+]?[0-9]*[,]?[0-9]+([eE][+-]?[0-9]+)?
NUMBER  [-+]?[0-9]*[\.]?[0-9]+([eE][+-]?[0-9]+)?
WS      [ \t\x0d]

%%

[!@#%&*/].*\n

^{WS}*{EURONUM}{WS}*  { *val = atofEuro (yytext); return (1); }
^{WS}*{NUMBER}{WS}*   { *val = atof (yytext); return (1); }

[\n]
.


%%

/*------------------------------------------------------------------------*/

int scan_f (FILE *in, double *vals, int max)
{
  double *val;
  int npts, rc;

  f_in = in;
  val  = vals;
  npts = 0;
  while (npts < max)
  {
    rc = f_lex (val);

    if (rc == 0)
      break;
    npts++;
    val++;
  }

  return (npts);
}

/*------------------------------------------------------------------------*/

int f_wrap ()
{
  return (1);
}
4
ответ дан 24 November 2019 в 01:17
поделиться

scanf является потрясающим, когда Вы знаете , Ваш вход всегда хорошо структурируется и хорошего поведения. Иначе...

IMO, вот самые большие проблемы с scanf:

  • Риск переполнения буфера - если Вы не указываете ширину поля для %s и %[ спецификаторы преобразования, Вы рискуете переполнением буфера (пытающийся считать более вход, чем буфер измерен для содержания). К сожалению, нет никакого хорошего способа указать, что как аргумент (как с printf) - Вы имеете или к hardcode это как часть спецификатора преобразования или делаете некоторые макро-интриги.

  • Принимает исходные данные, которые должны быть отклоненными - Если Вы читаете вход с %d спецификатор преобразования, и Вы вводите что-то как [1 110], Вы были бы ожидать scanf отклонять тот вход, но он не делает - он успешно преобразовывает и присваивается эти 12, уезжая w4 во входном потоке, чтобы испортить следующее чтение.

Так, что необходимо использовать вместо этого?

я обычно рекомендую читать весь интерактивный вход как текст с помощью [1 114] - он позволяет Вам указывать максимальное количество символов для чтения за один раз, таким образом, можно легко предотвратить переполнение буфера:

char input[100];
if ( !fgets( input, sizeof input, stdin ) )
{
  // error reading from input stream, handle as appropriate
}
else
{
  // process input buffer
}

Одна причуда [1 115] - то, что это сохранит запаздывающую новую строку в буфере, если будет комната, таким образом, можно сделать легкую проверку, чтобы видеть, ввел ли кто-то в более входе, чем Вы ожидали:

char *newline = strchr( input, '\n' );
if ( !newline )
{
  // input longer than we expected
}

то, Как Вы имеете дело с этим, ваше дело - можно или отклонить целый вход из руки и хлебать любой остающийся вход с [1 116]:

while ( getchar() != '\n' ) 
  ; // empty loop

Или можно обработать вход, который Вы получили до сих пор, и читайте снова. Это зависит от проблемы, которую Вы пытаетесь решить.

К [1 130] маркируют вход (разделите его на основе одного или нескольких разделителей), можно использовать strtok, но остерегаться - strtok, изменяет его вход (он перезаписывает разделители со строковым разделителем), и Вы не можете сохранить его состояние (т.е. Вы не можете частично маркировать одну строку, затем начать маркировать другого, затем брать, где Вы кончили в исходной строке). Существует вариант, strtok_s, который сохраняет состояние токенизатора, но AFAIK, его реализация является дополнительной (необходимо будет проверить, что __STDC_LIB_EXT1__ определяется, чтобы видеть, доступно ли это).

, После того как Вы маркировали свой вход, если необходимо преобразовать строки в числа (т.е. "1234" => 1234), у Вас есть опции. strtol и strtod преобразует строковые представления целых чисел и вещественных чисел к их соответствующим типам. Они также позволяют Вам ловить эти 12w4 проблема, которую я упомянул выше - одним из их аргументов является указатель на первый символ не преобразованный в строке:

char *text = "12w4";
char *chk;
long val;
long tmp = strtol( text, &chk, 10 );
if ( !isspace( *chk ) && *chk != 0 )
  // input is not a valid integer string, reject the entire input
else
  val = tmp;
16
ответ дан 24 November 2019 в 01:17
поделиться

, Что я могу использовать для парсинга входа вместо scanf?

Вместо scanf(some_format, ...), рассмотрите fgets() с sscanf(buffer, some_format_and %n, ...)

При помощи " %n", код может просто обнаружить, если весь формат был успешно просканирован и что никакой дополнительный непробельный спам не был в конце.

// scanf("%d %f fred", &some_int, &some_float);
#define EXPECTED_LINE_MAX 100
char buffer[EXPECTED_LINE_MAX * 2];  // Suggest 2x, no real need to be stingy.

if (fgets(buffer, sizeof buffer, stdin)) {
  int n = 0;
  // add ------------->    " %n" 
  sscanf(buffer, "%d %f fred %n", &some_int, &some_float, &n);
  // Did scan complete, and to the end?
  if (n > 0 && buffer[n] == '\0') {
    // success, use `some_int, some_float`
  } else {
    ; // Report bad input and handle desired.
  }
6
ответ дан 24 November 2019 в 01:17
поделиться

Другие ответы предоставляют правильную подробную информацию низкого уровня, таким образом, я ограничу меня более высоким уровнем: Во-первых, проанализируйте , на что Вы ожидаете каждая входная строка быть похожими. Попытайтесь описать вход с формальным синтаксисом - с удачей, Вы найдете, что это может быть описано с помощью регулярная грамматика , или по крайней мере контекстно-свободная грамматика . Если регулярная грамматика достаточна, то можно кодировать конечный автомат , который распознает и интерпретирует каждую командную строку один символ за один раз. Ваш код затем считает строку (как объяснено в других ответах), затем просканирует символы в буфере через конечный автомат. В определенных состояниях Вы останавливаете и преобразовываете подстроку, просканированную к настоящему времени к числу или что бы то ни было. Вы можете, вероятно, 'самокрутка', если это - это простое; если Вы находите требование полной контекстно-свободной грамматики, Вы - более обеспеченное выяснение, как использовать существующие инструменты парсинга (ре: lex и yacc или их варианты).

-4
ответ дан 24 November 2019 в 01:17
поделиться
Другие вопросы по тегам:

Похожие вопросы: