¿Qué puedo usar para la conversión de entrada en lugar de scanf?

125

Con mucha frecuencia he visto personas que desaniman a otros de usar scanfy dicen que hay mejores alternativas. Sin embargo, todo lo que termino viendo es "no usar scanf" o "aquí hay una cadena de formato correcta" , y nunca se mencionan ejemplos de las "mejores alternativas" .

Por ejemplo, tomemos este fragmento de código:

scanf("%c", &c);

Esto lee el espacio en blanco que quedó en la secuencia de entrada después de la última conversión. La solución habitual sugerida para esto es usar:

scanf(" %c", &c);

o no usar scanf.

Como scanfes malo, ¿cuáles son algunas opciones de ANSI C para convertir formatos de entrada que scanfgeneralmente pueden manejar (como enteros, números de punto flotante y cadenas) sin usar scanf?

SS Anne
fuente

Respuestas:

87

Las formas más comunes de lectura de entrada son:

  • usar fgetscon un tamaño fijo, que es lo que generalmente se sugiere, y

  • usando fgetc, lo que puede ser útil si solo estás leyendo uno char.

Para convertir la entrada, hay una variedad de funciones que puede usar:

  • strtoll, para convertir una cadena en un entero

  • strtof/ d/ ld, para convertir una cadena en un número de coma flotante

  • sscanf, que no es tan malo como simplemente usarlo scanf, aunque tiene la mayoría de las desventajas mencionadas a continuación

  • No hay buenas maneras de analizar una entrada separada por delimitador en ANSI C. sin formato. Utilice strtok_rPOSIX o strtok, lo que no es seguro para subprocesos. También puedes rodar el tuyo variante segura para subprocesos utilizando strcspny strspn, como strtok_rno implica ningún soporte especial del sistema operativo.

  • Puede ser excesivo, pero puede usar lexers y analizadores ( flexy bisonson los ejemplos más comunes).

  • Sin conversión, simplemente use la cadena


Como no entré exactamente por qué scanf es malo en mi pregunta, elaboraré:

  • Con los especificadores de conversión %[...]y %c, scanfno consume espacios en blanco. Aparentemente, esto no se conoce ampliamente, como lo demuestran los muchos duplicados de esta pregunta .

  • Existe cierta confusión sobre cuándo utilizar el &operador unario cuando se hace referencia ascanf los argumentos de (específicamente con cadenas).

  • Es muy fácil ignorar el valor de retorno de scanf . Esto podría causar fácilmente un comportamiento indefinido al leer una variable no inicializada.

  • Es muy fácil olvidar evitar el desbordamiento del búfer scanf. scanf("%s", str)es tan malo como, si no peor que,gets .

  • No puede detectar desbordamiento al convertir enteros con scanf. De hecho, el desbordamiento provoca un comportamiento indefinido en estas funciones.


SS Anne
fuente
56

Por que es scanf malo?

El principal problema es que scanfnunca tuvo la intención de tratar con la entrada del usuario. Está destinado a ser utilizado con datos formateados "perfectamente". Cité la palabra "perfectamente" porque no es completamente cierto. Pero no está diseñado para analizar datos que no son tan confiables como la entrada del usuario. Por naturaleza, la entrada del usuario no es predecible. Los usuarios malinterpretan las instrucciones, hacen errores tipográficos, presionan accidentalmente enter antes de que terminen, etc. Uno podría preguntarse razonablemente por qué una función que no debe usarse para las entradas de usuario lee stdin. Si usted es un usuario experimentado de * nix, la explicación no será una sorpresa, pero podría confundir a los usuarios de Windows. En los sistemas * nix, es muy común crear programas que funcionen a través de tuberías,stdoutstdindel segundo. De esta manera, puede asegurarse de que la salida y la entrada sean predecibles. Durante estas circunstancias, en scanfrealidad funciona bien. Pero cuando trabaja con datos impredecibles, corre el riesgo de todo tipo de problemas.

Entonces, ¿por qué no hay funciones estándar fáciles de usar para la entrada del usuario? Uno solo puede adivinar aquí, pero supongo que los viejos hackers C incondicionales simplemente pensaron que las funciones existentes eran lo suficientemente buenas, a pesar de que son muy torpes. Además, cuando observa las aplicaciones de terminal típicas, rara vez leen la entrada del usuario stdin. La mayoría de las veces pasa toda la entrada del usuario como argumentos de línea de comando. Claro, hay excepciones, pero para la mayoría de las aplicaciones, la entrada del usuario es algo muy menor.

¿Entonces que puedes hacer?

Mi favorito está fgetsen combinación con sscanf. Una vez escribí una respuesta al respecto, pero volveré a publicar el código completo. Aquí hay un ejemplo con comprobación de errores decente (pero no perfecta) y análisis. Es lo suficientemente bueno para fines de depuración.

Nota

No me gusta especialmente pedirle al usuario que ingrese dos cosas diferentes en una sola línea. Solo hago eso cuando se pertenecen el uno al otro de una manera natural. Como por ejemplo printf("Enter the price in the format <dollars>.<cent>: ")y luego usar sscanf(buffer "%d.%d", &dollar, &cent). Nunca haría algo así printf("Enter height and base of the triangle: "). El punto principal de usar a fgetscontinuación es encapsular las entradas para garantizar que una entrada no afecte a la siguiente.

#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);

Si hace muchos de estos, podría recomendar crear un contenedor que siempre se vacíe:

int printfflush (const char *format, ...)
{
   va_list arg;
   int done;
   va_start (arg, format);
   done = vfprintf (stdout, format, arg);
   fflush(stdout);
   va_end (arg);
   return done;
}```

Hacer esto eliminará un problema común, que es la nueva línea final que puede interferir con la entrada del nido. Pero tiene otro problema, que es si la línea es más larga que bsize. Puedes verificar eso con if(buffer[strlen(buffer)-1] != '\n'). Si desea eliminar la nueva línea, puede hacerlo con buffer[strcspn(buffer, "\n")] = 0.

En general, le aconsejaría que no espere que el usuario ingrese la entrada en algún formato extraño que debe analizar en diferentes variables. Si desea asignar las variables heighty width, no solicite ambas al mismo tiempo. Permita que el usuario presione enter entre ellos. Además, este enfoque es muy natural en un sentido. Nunca obtendrá la entrada stdinhasta que presione enter, entonces, ¿por qué no leer siempre la línea completa? Por supuesto, esto aún puede generar problemas si la línea es más larga que el búfer. ¿Recordé mencionar que la entrada del usuario es torpe en C? :)

Para evitar problemas con líneas más largas que el búfer, puede usar una función que asigne automáticamente un búfer del tamaño apropiado, puede usar getline(). El inconveniente es que necesitarás freeel resultado después.

Intensificando el juego

Si te tomas en serio la creación de programas en C con la entrada del usuario, recomendaría echar un vistazo a una biblioteca como ncurses. Porque es probable que también desee crear aplicaciones con algunos gráficos de terminal. Desafortunadamente, perderá algo de portabilidad si lo hace, pero le brinda un control mucho mejor de la entrada del usuario. Por ejemplo, le da la posibilidad de leer una pulsación de tecla al instante en lugar de esperar a que el usuario presione enter.

klutt
fuente
Tenga en cuenta que (r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2no detecta tan mal el texto no numérico final.
chux - Restablece a Monica el
1
@chux Fijo% f% f. ¿Qué quieres decir con el primero?
klutt
Con fgets()de "1 2 junk", if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {no informa nada malo con la entrada a pesar de que tiene "basura".
chux - Restablece a Monica el
@chux Ah, ahora veo. Bueno, eso fue intencional.
klutt
1
scanfestá destinado a ser utilizado con datos perfectamente formateados Pero incluso eso no es cierto. Además del problema con la "basura" como lo menciona @chux, también existe el hecho de que un formato como el que "%d %d %d"está feliz de leer la entrada de una, dos o tres líneas (o incluso más, si hay líneas en blanco intermedias), que no hay La forma de forzar (por ejemplo) una entrada de dos líneas haciendo algo como "%d\n%d %d", etc. scanfpodría ser apropiada para la entrada de flujo formateada , pero no es del todo buena para nada basada en líneas.
Steve Summit
18

scanfEs increíble cuando sabes que tu aportación siempre está bien estructurada y se comporta bien. De otra manera...

OMI, aquí están los mayores problemas con scanf:

  • Riesgo de desbordamiento del búfer : si no especifica un ancho de campo para los especificadores de conversión %sy %[, corre el riesgo de un desbordamiento del búfer (al intentar leer más entradas de las que un búfer está dimensionado para contener). Desafortunadamente, no hay una buena manera de especificar eso como un argumento (como con printf): debe codificarlo como parte del especificador de conversión o hacer algunas travesuras macro.

  • Acepta entradas que deben rechazarse : si está leyendo una entrada con el %despecificador de conversión y escribe algo así 12w4, esperaría scanf rechazar esa entrada, pero no lo hace: convierte y asigna con éxito 12, dejando w4en la secuencia de entrada estropear la siguiente lectura.

Entonces, ¿qué deberías usar en su lugar?

Por lo general, recomiendo leer todas las entradas interactivas como texto fgets, ya que le permite especificar un número máximo de caracteres para leer a la vez, por lo que puede evitar fácilmente el desbordamiento del búfer:

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

Una peculiaridad fgetses que almacenará la nueva línea final en el búfer si hay espacio, por lo que puede hacer una verificación fácil para ver si alguien ingresó más información de la que esperaba:

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

Depende de usted cómo lidia con eso: puede rechazar la entrada completa sin control y sorber cualquier entrada restante con getchar:

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

O puede procesar la entrada que recibió hasta ahora y volver a leer. Depende del problema que estés tratando de resolver.

Para tokenizar la entrada (dividirla en función de uno o más delimitadores), puede usar strtok, pero tenga cuidado: strtokmodifica su entrada (sobrescribe los delimitadores con el terminador de cadena) y no puede preservar su estado (es decir, puede ' t tokenice parcialmente una cadena, luego comience a tokenizar otra, luego retome donde lo dejó en la cadena original). Hay una variante strtok_sque conserva el estado del tokenizador, pero AFAIK su implementación es opcional (deberá verificar que __STDC_LIB_EXT1__esté definido para ver si está disponible).

Una vez que haya tokenizado su entrada, si necesita convertir cadenas en números (es decir, "1234"=> 1234), tiene opciones. strtoly strtodconvertirá representaciones de cadenas de enteros y números reales a sus respectivos tipos. También le permiten captar el 12w4problema que mencioné anteriormente: uno de sus argumentos es un puntero al primer carácter no convertido en la cadena:

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;
John Bode
fuente
Si no especifica un ancho de campo ... - o una supresión de conversión (por ejemplo %*[%\n], lo cual es útil para tratar con líneas demasiado largas más adelante en la respuesta).
Toby Speight
Hay una manera de obtener la especificación en tiempo de ejecución de los anchos de campo, pero no es agradable. Terminas teniendo que construir la cadena de formato en tu código (quizás usando snprintf()),.
Toby Speight
55
Ha cometido el error más común isspace()allí: acepta caracteres sin signo representados como int, por lo que debe emitir unsigned charpara evitar UB en plataformas donde charestá firmado.
Toby Speight
9

En esta respuesta, voy a suponer que estás leyendo e interpretando líneas de texto . Tal vez le estés preguntando al usuario, que está escribiendo algo y presionando RETORNO. O tal vez esté leyendo líneas de texto estructurado de algún tipo de archivo de datos.

Como está leyendo líneas de texto, tiene sentido organizar su código alrededor de una función de biblioteca que lea, bueno, una línea de texto. La función estándar es fgets(), aunque hay otras (incluidas getline). Y luego el siguiente paso es interpretar esa línea de texto de alguna manera.

Aquí está la receta básica para llamar fgetspara leer una línea de texto:

char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);

Esto simplemente se lee en una línea de texto y lo imprime de nuevo. Tal como está escrito, tiene un par de limitaciones, que veremos en un minuto. También tiene una característica muy buena: ese número 512 que pasamos como segundo argumento fgetses el tamaño de la matriz en la lineque estamos pidiendo fgetsleer. Este hecho, que podemos decir fgetscuánto está permitido leer, significa que podemos estar seguros de que fgetsno desbordará la matriz al leer demasiado en ella.

Entonces, ahora sabemos cómo leer una línea de texto, pero ¿qué pasa si realmente quisiéramos leer un número entero, un número de coma flotante, un solo carácter o una sola palabra? (Es decir, ¿y si la scanfllamada que estamos tratando de mejorar había estado utilizando un especificador de formato como %d, %f, %c, o %s?)

Es fácil reinterpretar una línea de texto, una cadena, como cualquiera de estas cosas. Para convertir una cadena en un entero, la forma más simple (aunque imperfecta) de hacerlo es llamar atoi(). Para convertir a un número de coma flotante, hay atof(). (Y también hay mejores formas, como veremos en un minuto). Aquí hay un ejemplo muy simple:

printf("type an integer:\n");
fgets(line, 512, stdin);
int i = atoi(line);
printf("type a floating-point number:\n");
fgets(line, 512, stdin);
float f = atof(line);
printf("you typed %d and %f\n", i, f);

Si desea que el usuario escriba un solo carácter (tal vez yo ncomo respuesta sí / no), literalmente puede tomar el primer carácter de la línea, así:

printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);

(Esto ignora, por supuesto, la posibilidad de que el usuario haya escrito una respuesta de varios caracteres; silenciosamente ignora cualquier carácter adicional que se haya escrito).

Finalmente, si desea que el usuario escriba una cadena que definitivamente no contiene espacios en blanco, si desea tratar la línea de entrada

hello world!

como la cadena "hello"seguida de otra cosa (que es lo que habría hecho el scanfformato %s), bueno, en ese caso, me he fibged un poco, no es tan fácil reinterpretar la línea de esa manera, después de todo, así que la respuesta a eso parte de la pregunta tendrá que esperar un poco.

Pero primero quiero volver a las tres cosas que salté.

(1) Hemos estado llamando

fgets(line, 512, stdin);

para leer en la matriz line, y donde 512 es el tamaño de la matriz, linepor lo que fgetssabe que no debe desbordarse. Pero para asegurarse de que 512 es el número correcto (especialmente, para verificar si tal vez alguien ajustó el programa para cambiar el tamaño), debe volver a leer donde linese haya declarado. Eso es una molestia, por lo que hay dos formas mucho mejores de mantener sincronizados los tamaños. Podría, (a) utilizar el preprocesador para crear un nombre para el tamaño:

#define MAXLINE 512
char line[MAXLINE];
fgets(line, MAXLINE, stdin);

O (b) use el sizeofoperador de C :

fgets(line, sizeof(line), stdin);

(2) El segundo problema es que no hemos estado buscando errores. Cuando esté leyendo la entrada, siempre debe verificar la posibilidad de error. Si por alguna razón fgetsno puede leer la línea de texto que le solicitó, esto lo indica al devolver un puntero nulo. Entonces deberíamos haber estado haciendo cosas como

printf("type something:\n");
if(fgets(line, 512, stdin) == NULL) {
    printf("Well, never mind, then.\n");
    exit(1);
}

Finalmente, está el problema de que para leer una línea de texto, fgetslee los caracteres y los llena en su matriz hasta que encuentra el \ncarácter que termina la línea, y también llena el \ncarácter en su matriz . Puede ver esto si modifica ligeramente nuestro ejemplo anterior:

printf("you typed: \"%s\"\n", line);

Si ejecuto esto y escribo "Steve" cuando me lo solicita, se imprime

you typed: "Steve
"

Eso "en la segunda línea se debe a que la cadena que leyó e imprimió fue en realidad "Steve\n".

A veces, esa nueva línea adicional no importa (como cuando llamamos atoio atof, ya que ambos ignoran cualquier entrada no numérica adicional después del número), pero a veces importa mucho. Muy a menudo queremos quitar esa nueva línea. Hay varias formas de hacer eso, a lo que llegaré en un minuto. (Sé que he estado diciendo eso mucho. Pero volveré a todas esas cosas, lo prometo).

En este punto, puede estar pensando: "Pensé que había dicho que scanf no era bueno, y que de otra manera sería mucho mejor. Pero fgetsestá empezando a parecer una molestia. ¡Llamar scanffue tan fácil ! ¿No puedo seguir usándolo? "

Claro, puedes seguir usando scanf, si quieres. (Y para cosas realmente simples, de alguna manera es más simple). Pero, por favor, no vengas a llorar cuando te falla debido a una de sus 17 peculiaridades y debilidades, o entra en un bucle infinito debido a la entrada de tu no esperaba, o cuando no puede descubrir cómo usarlo para hacer algo más complicado. Y echemos un vistazo a fgetslas molestias reales:

  1. Siempre tiene que especificar el tamaño de la matriz. Bueno, por supuesto, eso no es una molestia en absoluto, es una característica, porque el desbordamiento del búfer es algo realmente malo.

  2. Tienes que verificar el valor de retorno. En realidad, eso es un lavado, porque para usarlo scanfcorrectamente, también debe verificar su valor de retorno.

  3. Tienes que quitarle la \nespalda. Esto es, lo admito, una verdadera molestia. Desearía que hubiera una función estándar a la que pudiera señalarle que no tuviera este pequeño problema. (Por favor, nadie mencione gets). Pero en comparación con scanf's17 molestias diferentes, tomaré esta molestia de fgetscualquier día.

Entonces, ¿cómo no se tira de ese salto de línea? Tres maneras:

(a) Forma obvia:

char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';

(b) Manera complicada y compacta:

strtok(line, "\n");

Lamentablemente este no siempre funciona.

(c) Otra forma compacta y ligeramente oscura:

line[strcspn(line, "\n")] = '\0';

Y ahora que está fuera del camino, podemos volver a otra cosa que omití: las imperfecciones de atoi()y atof(). El problema con ellos es que no le dan ninguna indicación útil de éxito o fracaso: ignoran silenciosamente la entrada no numérica final y devuelven silenciosamente 0 si no hay ninguna entrada numérica. Las alternativas preferidas, que también tienen otras ventajas, son strtoly strtod. strtoltambién le permite usar una base que no sea 10, lo que significa que puede obtener el efecto de (entre otras cosas) %oo %xconscanf. Pero mostrar cómo usar estas funciones correctamente es una historia en sí misma, y ​​sería una gran distracción de lo que ya se está convirtiendo en una narrativa bastante fragmentada, por lo que no voy a decir nada más sobre ellas ahora.

El resto de la narración principal se refiere a la entrada que podría estar tratando de analizar y que es más complicada que un solo número o personaje. ¿Qué sucede si desea leer una línea que contiene dos números, o varias palabras separadas por espacios en blanco, o puntuación de encuadre específica? Ahí es donde las cosas se ponen interesantes, y donde las cosas probablemente se complicaban si intentaba hacer cosas usando scanf, y donde hay muchas más opciones ahora que ha leído limpiamente una línea de texto usando fgets, aunque la historia completa de todas esas opciones probablemente podría llenar un libro, así que solo vamos a poder arañar la superficie aquí.

  1. Mi técnica favorita es dividir la línea en "palabras" separadas por espacios en blanco, luego hacer algo más con cada "palabra". Una función estándar principal para hacer esto es strtok(que también tiene sus problemas, y que también califica una discusión completamente separada). Mi preferencia es una función dedicada para construir una matriz de punteros para cada "palabra" separada, una función que describo en estas notas del curso . En cualquier caso, una vez que tenga "palabras", puede procesar cada una de ellas, tal vez con las mismas funciones atoi/ atof/ strtol/ strtodque ya hemos analizado.

  2. Paradójicamente, a pesar de que hemos pasado una buena cantidad de tiempo y esfuerzo descubriendo cómo alejarnos scanf, otra buena manera de lidiar con la línea de texto con la que acabamos de leer fgetses pasarla sscanf. De esta manera, terminas con la mayoría de las ventajas scanf, pero sin la mayoría de las desventajas.

  3. Si su sintaxis de entrada es particularmente complicada, podría ser apropiado usar una biblioteca "regexp" para analizarla.

  4. Finalmente, puede utilizar las soluciones de análisis ad hoc que más le convengan. Puede moverse a través de la línea de un carácter a la vez con un char *puntero que busca los caracteres que espera. O puede buscar caracteres específicos usando funciones como strchro strrchr, o strspno strcspn, o strpbrk. O puede analizar / convertir y omitir grupos de caracteres de dígitos utilizando las funciones strtolo strtodque omitimos anteriormente.

Obviamente hay mucho más que decir, pero espero que esta introducción lo ayude a comenzar.

Steve Summit
fuente
¿Hay una buena razón para escribir en sizeof (line)lugar de simplemente sizeof line? ¡El primero hace que parezca lineun nombre de tipo!
Toby Speight
@TobySpeight ¿Una buena razón? No, lo dudo. Los paréntesis son mi hábito, porque no puedo molestarme en recordar si son objetos o nombres de tipo para los que se requieren, pero muchos programadores los omiten cuando pueden. (Para mí es una cuestión de preferencia personal y estilo, y uno bastante menor).
Steve Summit
+1 para usar sscanfcomo motor de conversión pero recolectar (y posiblemente masajear) la entrada con una herramienta diferente. Pero quizás valga la pena mencionarlo getlineen este contexto.
dmckee --- ex-gatito moderador
Cuando habla de " fscanfmolestias reales", ¿quiere decir fgets? Y la molestia n. ° 3 realmente me molesta, especialmente dado que scanfdevuelve un puntero inútil al búfer en lugar de devolver el número de caracteres ingresados ​​(lo que haría que la eliminación de la nueva línea sea mucho más limpia).
supercat
1
Gracias por la explicación de tu sizeofestilo. Para mí, recordar cuándo necesitas a los padres es fácil: creo (type)que es como un elenco sin valor (porque solo nos interesa el tipo). Otra cosa: dices que eso strtok(line, "\n")no siempre funciona, pero que no es obvio cuando podría no funcionar. Supongo que está pensando en el caso en que la línea era más larga que el búfer, por lo que no tenemos nueva línea y strtok()devuelve nulo. Es una verdadera pena fgets()que no devuelva un valor más útil para que podamos saber si la nueva línea está allí o no.
Toby Speight
7

¿Qué puedo usar para analizar la entrada en lugar de scanf?

En lugar de scanf(some_format, ...)considerar fgets()consscanf(buffer, some_format_and %n, ...)

Al usarlo " %n", el código puede simplemente detectar si todo el formato se escaneó con éxito y que al final no había basura adicional que no fuera un espacio en blanco.

// 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.
  }
chux - Restablece a Monica
fuente
6

Expongamos los requisitos de análisis como:

  • la entrada válida debe ser aceptada (y convertida en alguna otra forma)

  • la entrada inválida debe ser rechazada

  • cuando se rechaza cualquier entrada, es necesario proporcionar al usuario un mensaje descriptivo que explique (en un lenguaje claro "fácilmente entendido por personas normales que no son programadores") por qué se rechazó (para que las personas puedan descubrir cómo solucionar el problema). problema)

Para mantener las cosas muy simples, consideremos analizar un solo entero decimal simple (que fue ingresado por el usuario) y nada más. Las posibles razones para que la entrada del usuario sea rechazada son:

  • la entrada contenía caracteres inaceptables
  • la entrada representa un número que es inferior al mínimo aceptado
  • la entrada representa un número que es más alto que el máximo aceptado
  • la entrada representa un número que tiene una parte fraccionaria distinta de cero

Definamos también "entrada contenida caracteres inaceptables" correctamente; y decir eso:

  • los espacios en blanco iniciales y los espacios en blanco finales se ignorarán (por ejemplo, "
    5" se tratará como "5")
  • se permite cero o un punto decimal (por ejemplo, "1234." y "1234.000" se tratan igual que "1234")
  • debe haber al menos un dígito (por ejemplo, "." se rechaza)
  • no se permite más de un punto decimal (por ejemplo, "1.2.3" se rechaza)
  • las comas que no están entre dígitos serán rechazadas (por ejemplo, ", 1234" se rechaza)
  • las comas que están después de un punto decimal serán rechazadas (por ejemplo, "1234.000,000" se rechaza)
  • se rechazan las comas que aparecen después de otra coma (por ejemplo, "1, 234" se rechaza)
  • todas las demás comas serán ignoradas (por ejemplo, "1,234" se tratará como "1234")
  • se rechaza un signo menos que no es el primer carácter que no es un espacio en blanco
  • se rechaza un signo positivo que no sea el primer carácter que no sea un espacio en blanco

A partir de esto, podemos determinar que se necesitan los siguientes mensajes de error:

  • "Carácter desconocido al inicio de la entrada"
  • "Carácter desconocido al final de la entrada"
  • "Carácter desconocido en medio de la entrada"
  • "El número es demasiado bajo (el mínimo es ...)"
  • "El número es demasiado alto (el máximo es ...)"
  • "El número no es un entero"
  • "Demasiados puntos decimales"
  • "Sin dígitos decimales"
  • "Mala coma al comienzo del número"
  • "Mala coma al final del número"
  • "Mala coma en medio del número"
  • "Mala coma después del punto decimal"

Desde este punto, podemos ver que una función adecuada para convertir una cadena en un entero necesitaría distinguir entre tipos de errores muy diferentes; y que algo como " scanf()" o " atoi()" o " strtoll()" no sirve para nada porque no le dan ninguna indicación de lo que estaba mal con la entrada (y usan una definición completamente irrelevante e inapropiada de lo que es / no es "válido" entrada").

En cambio, comencemos a escribir algo que no sea inútil:

char *convertStringToInteger(int *outValue, char *string, int minValue, int maxValue) {
    return "Code not implemented yet!";
}

int main(int argc, char *argv[]) {
    char *errorString;
    int value;

    if(argc < 2) {
        printf("ERROR: No command line argument.\n");
        return EXIT_FAILURE;
    }
    errorString = convertStringToInteger(&value, argv[1], -10, 2000);
    if(errorString != NULL) {
        printf("ERROR: %s\n", errorString);
        return EXIT_FAILURE;
    }
    printf("SUCCESS: Your number is %d\n", value);
    return EXIT_SUCCESS;
}

Para cumplir con los requisitos establecidos; convertStringToInteger()Es probable que esta función termine siendo cientos de líneas de código por sí misma.

Ahora, esto era solo "analizar un solo entero decimal simple". Imagínese si quisiera analizar algo complejo; como una lista de estructuras de "nombre, dirección, número de teléfono, dirección de correo electrónico"; o tal vez como un lenguaje de programación. Para estos casos, es posible que deba escribir miles de líneas de código para crear un análisis que no sea una broma paralizada.

En otras palabras...

¿Qué puedo usar para analizar la entrada en lugar de scanf?

Escriba (potencialmente miles de líneas) de código usted mismo, para satisfacer sus necesidades.

Brendan
fuente
5

Aquí hay un ejemplo de cómo usar flexpara escanear una entrada simple, en este caso un archivo de números de coma flotante ASCII que puede estar en formato estadounidense ( n,nnn.dd) o europeo ( n.nnn,dd). Esto se acaba de copiar de un programa mucho más grande, por lo que puede haber algunas referencias sin resolver:

/* 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);
}
jamesqf
fuente
-5

Otras respuestas dan los detalles correctos de bajo nivel, por lo que me limitaré a un nivel superior: Primero, analice cómo espera que se vea cada línea de entrada. Intente describir la entrada con una sintaxis formal; con suerte, encontrará que puede describirse utilizando una gramática regular , o al menos una gramática libre de contexto . Si una gramática regular es suficiente, puede codificar una máquina de estado finitoque reconoce e interpreta cada línea de comando un carácter a la vez. Su código leerá una línea (como se explica en otras respuestas), luego escaneará los caracteres en el búfer a través de la máquina de estado. En ciertos estados, se detiene y convierte la subcadena explorada hasta el momento en un número o lo que sea. Probablemente pueda 'rodar el suyo' si es así de simple; Si encuentra que necesita una gramática completa sin contexto, es mejor que descubra cómo usar las herramientas de análisis existentes (re: lexy / yacco sus variantes).

PMar
fuente
Una máquina de estados finitos puede ser exagerada; Son posibles formas más fáciles de detectar el desbordamiento en las conversiones (como verificar si errno == EOVERFLOWdespués de usar strtoll).
SS Anne
1
¿Por qué codificarías tu propia máquina de estados finitos, cuando flex hace que escribirlas sea trivialmente simple?
jamesqf