c scanf

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



(8)

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

En lugar de scanf(some_format, ...) , considere fgets() con sscanf(buffer, some_format_and %n, ...)

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

// 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. }

Con mucha frecuencia he visto personas que desaniman a otros de usar scanf y dicen que hay mejores alternativas. Sin embargo, todo lo que termino viendo es "no use 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 .

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


¿Por qué es malo scanf ?

El principal problema es que scanf nunca 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 no entienden 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 desde 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, lo que significa que envía la salida de un programa a otro canalizando la salida stdin del primer programa a la stdin del segundo. De esta manera, puede asegurarse de que la salida y la entrada sean predecibles. Durante estas circunstancias, scanf realmente 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 de 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 es fgets en combinación con sscanf . Una vez escribí una respuesta al respecto, pero volveré a publicar el código completo. Aquí hay un ejemplo con una comprobación y análisis de errores decente (pero no perfecto). 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. Por ejemplo, printf("Enter the price in the format <dollars>.<cent>: ") y luego use sscanf(buffer "%d.%d", &dollar, &cent) . Nunca haría algo como printf("Enter height and base of the triangle: ") . El punto principal del uso de fgets continuació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);

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 . Puede 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 height y 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 de stdin hasta 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 tendrá que free el 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.


Aquí hay un ejemplo del uso de flex para 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); }


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 (incluida getline ). Y luego el siguiente paso es interpretar esa línea de texto de alguna manera.

Aquí está la receta básica para llamar a fgets para 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 para fgets es el tamaño de la line matriz en la que pedimos que lean los fgets . Este hecho, que podemos decirle a los fgets cuánto está permitido leer, significa que podemos estar seguros de que los fgets no desbordarán la matriz al leer demasiado en ella.

Entonces, ahora sabemos cómo leer una línea de texto, pero ¿y 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, ¿qué pasa si la llamada scanf que intentamos mejorar ha estado usando 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 a 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 y o n como respuesta sí / no), literalmente puede tomar el primer carácter de la línea, de esta manera:

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 formato scanf %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 , por lo que la respuesta a esa 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 line la matriz, y donde 512 es el tamaño de la line de la matriz, line modo que fgets sabe 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 se haya declarado la line . Eso es una molestia, por lo que hay dos formas mucho mejores de mantener sincronizados los tamaños. (a) utilice el preprocesador para crear un nombre para el tamaño:

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

O (b) use el operador sizeof 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, fgets no puede leer la línea de texto que le solicitó, 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, fgets lee los caracteres y los llena en su matriz hasta que encuentra el carácter /n que termina la línea, y también llena el carácter /n 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 "Steve/n" .

A veces, esa nueva línea adicional no importa (como cuando llamamos atoi o 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 dijiste que scanf no era bueno, y que de otra manera sería mucho mejor. Pero fgets está empezando a parecer una molestia. ¡Llamar a scanf fue 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 las molestias reales de Fgets:

  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 una cosa realmente mala.

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

  3. Tienes que quitar la /n hacia atrás. 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 menciona). Pero en comparación con scanf''s 17 molestias diferentes de scanf''s , tomaré esta molestia de fgets cualquier día.

Entonces, ¿cómo se quita esa nueva 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 eso está fuera del camino, podemos volver a otra cosa que atoi() sobre 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 strtol y strtod . strtol también le permite usar una base que no sea 10, lo que significa que puede obtener el efecto de (entre otras cosas) %o %x con scanf . 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 en general esas opciones probablemente podrían llenar un libro, por lo 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 atoi / atof / strtol / strtod que ya hemos analizado.

  2. Paradójicamente, a pesar de que hemos pasado una buena cantidad de tiempo y esfuerzo aquí descubriendo cómo alejarnos de scanf , otra buena manera de lidiar con la línea de texto que acabamos de leer con fgets es pasarla a sscanf . De esta manera, terminas con la mayoría de las ventajas de 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 puntero char * busca los caracteres que espera. O puede buscar caracteres específicos utilizando funciones como strchr o strrchr , o strspn o strcspn , o strpbrk . O puede analizar / convertir y omitir grupos de caracteres de dígitos utilizando las funciones strtol o strtod que omitimos anteriormente.

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


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 "entrada válida").

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; Es probable que esta función convertStringToInteger() 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.


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

  • usando fgets con un tamaño fijo, que es lo que generalmente se sugiere, y

  • usando fgetc , que puede ser útil si solo estás leyendo un solo 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 usar scanf , aunque tiene la mayoría de las caídas mencionadas a continuación

  • No hay buenas maneras de analizar una entrada separada por delimitadores en ANSI C. sin strtok_r Utilice strtok_r de POSIX o strtok_s del Anexo K., que no está ampliamente implementado. También puede rodar el suyo usando strcspn y strspn , ya que no involucrar cualquier soporte especial del sistema operativo.

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

  • Sin conversión, simplemente use la cadena

Como no entraste exactamente por qué scanf es malo en tu pregunta, explicaré:

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

  • Existe cierta confusión acerca de cuándo usar el operador unario & cuando se hace referencia a los argumentos de scanf (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 en scanf . scanf("%s", str) es tan malo como, si no peor que, se gets .

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


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 normal es suficiente, puede codificar una máquina de estados finitos que 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: lex y yacc o sus variantes).


scanf es increíble cuando sabes que tu entrada siempre está bien estructurada y se comporta bien. De otra manera...

En mi opinión, 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 %s y %[ , 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 especificador de conversión %d y escribe algo como 12w4 , esperaría que scanf rechace esa entrada, pero no lo hace: convierte y asigna con éxito el 12 , dejando w4 en la secuencia de entrada para w4 en la siguiente lectura.

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

Por lo general, recomiendo leer todas las entradas interactivas como texto usando fgets : 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 de los fgets es que almacenará la nueva línea final en el búfer si hay espacio, por lo que puede hacer una comprobació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 getchar con getchar : puede rechazar toda la entrada 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 simular la entrada (dividirla en función de uno o más delimitadores), puede usar strtok , pero tenga cuidado: strtok modifica su entrada (sobrescribe los delimitadores con el terminador de cadena), y no puede preservar su estado (es decir, usted no puede tokenizar parcialmente una cadena, luego comenzar a tokenizar otra, luego retomar donde lo dejó en la cadena original). Hay una variante, strtok_s , que conserva el estado del tokenizer, 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. strtol y strtod convertirán representaciones de cadenas de enteros y números reales a sus respectivos tipos. También le permiten captar el problema de 12w4 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;