Con mucha frecuencia he visto personas que desalientan a otros a 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 menciona ningún ejemplo de las "mejores alternativas" .
Por ejemplo, tomemos este fragmento de código:
scanf("%c", &c);Esto lee el espacio en blanco que quedó en el flujo 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 números enteros, números de coma flotante y cadenas) sin usar scanf ?
Otras respuestas brindan 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. Trate de describir la entrada con una sintaxis formal; con suerte, encontrará que se puede describir usando 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 finito que reconozca e interprete cada línea de comando, un carácter a la vez. Luego, 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 escaneada hasta ahora en un número o lo que sea. Probablemente puedas 'hacer tu propio rollo' si es así de simple; si encuentra que necesita una gramática libre de contexto completa, es mejor que descubra cómo usar las herramientas de análisis existentes (re: lex y yacc o sus variantes).
Este es un ejemplo del uso de flex para escanear una entrada simple, en este caso un archivo de números de punto flotante ASCII que pueden 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); }Las formas más comunes de lectura de entrada son:
usando fgets con un tamaño fijo, que es lo que se suele sugerir, y
usando fgetc , que puede ser útil si solo está 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 fallas que se mencionan a continuación
No hay buenas formas de analizar una entrada separada por delimitadores en ANSI C simple. strtok_r de POSIX o strtok , que no es seguro para subprocesos. También puede lanzar su propia variante segura para subprocesos usando strcspn y strspn , ya que strtok_r no implica ningún soporte especial para el sistema operativo.
Puede ser excesivo, pero puede usar lexers y parsers ( flex y bison son los ejemplos más comunes).
Sin conversión, simplemente use la cadena
Como no entré exactamente por qué scanf es malo en mi pregunta, explicaré:
Con los especificadores de conversión %[...] y %c , scanf no consume espacios en blanco. Aparentemente, esto no es muy conocido, como lo demuestran los muchos duplicados de esta pregunta .
Existe cierta confusión sobre 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 olvidarse de evitar el desbordamiento del búfer en scanf . scanf("%s", str) es tan malo, si no peor, que gets .
No puede detectar el desbordamiento al convertir enteros con scanf . De hecho, el desbordamiento provoca un comportamiento indefinido en estas funciones.
scanf es increíble cuando sabes que tu entrada siempre está bien estructurada y se comporta bien. De lo contrario...
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 (intentar leer más entradas de las que un búfer tiene el tamaño 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 rechazara esa entrada, pero no lo hace: convierte y asigna con éxito el 12 , dejando w4 en el flujo de entrada para estropear la próxima 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 una cantidad máxima 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 fgets es que almacenará la nueva línea final en el búfer si hay espacio, por lo que puede verificar fácilmente si alguien ingresó más información de la que esperaba:
char *newline = strchr( input, '\n' ); if ( !newline ) { // input longer than we expected } Cómo lidiar con eso depende de usted: puede rechazar toda la entrada sin control y sorber cualquier entrada restante con getchar :
while ( getchar() != '\n' ) ; // empty loopO puede procesar la entrada que obtuvo 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: strtok modifica su entrada (sobrescribe los delimitadores con el terminador de cadena) y no puede conservar su estado (es decir, usted no puede tokenizar parcialmente una cadena, luego comenzar a tokenizar otra, luego continuar donde lo dejó en la cadena original). Hay una variante, strtok_s , que 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. strtol y strtod convertirán representaciones de cadenas de enteros y números reales en sus respectivos tipos. También le permiten detectar el problema 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; fgets es para obtener la entrada. sscanf es para analizarlo después. scanf intenta hacer ambas cosas al mismo tiempo. Esa es una receta para los problemas. Leer primero y analizar después.
scanf es malo? 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 cierta. Pero no está diseñado para analizar datos que son tan poco confiables como la entrada del usuario. Por naturaleza, la entrada del usuario no es predecible. Los usuarios malinterpretan las instrucciones, cometen errores tipográficos, accidentalmente presionan enter antes de que terminen, etc. Uno podría preguntarse razonablemente por qué una función que no debe usarse para la entrada del usuario lee desde stdin . Si es un usuario experimentado de *nix, la explicación no le sorprenderá, pero podría confundir a los usuarios de Windows. En los sistemas *nix, es muy común crear programas que funcionan a través de tuberías, lo que significa que envía la salida de un programa a otro canalizando la salida stdout del primer programa a la stdin del segundo. De esta manera, puede asegurarse de que la salida y la entrada sean predecibles. En estas circunstancias, scanf realmente funciona bien. Pero cuando se trabaja con entradas impredecibles, se arriesga a 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 asumo que los viejos hackers incondicionales de C 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 desde stdin . La mayoría de las veces, pasa todas las entradas 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 una cosa muy pequeña.
En primer lugar, gets NO es una alternativa. Es peligroso y NUNCA debe usarse. Lea aquí por qué: ¿Por qué la función gets es tan peligrosa que no debería usarse?
Mi favorito es fgets en combinación con sscanf . Una vez escribí una respuesta sobre eso, pero volveré a publicar el código completo. Aquí hay un ejemplo con una verificación y un análisis de errores decentes (pero no perfectos). 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 forma natural. Como por ejemplo
printf("Enter the price in the format <dollars>.<cent>: "); fgets(buffer, bsize, stdin);y luego usesscanf(buffer "%d.%d", &dollar, ¢). Nunca haría algo comoprintf("Enter height and base of the triangle: "). El punto principal de usarfgetsa 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, "%f%f", &f, &g)) != 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ía:
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 . Puede comprobarlo con if(buffer[strlen(buffer)-1] != '\n') . Si desea eliminar la nueva línea, puede hacerlo con buffer[strcspn(buffer, "\n")] = 0 .
En general, recomendaría no esperar que el usuario ingrese la entrada en algún formato extraño que deba analizar en diferentes variables. Si desea asignar las variables height y width , no solicite ambos 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 asigna automáticamente un búfer del tamaño apropiado, puede usar getline() . El inconveniente es que necesitará free el resultado después. El estándar no garantiza que esta función exista, pero POSIX la tiene. También puede implementar el suyo propio o encontrar uno en SO. ¿Cómo puedo leer una cadena de entrada de longitud desconocida?
Si se toma en serio la creación de programas en C con la entrada del usuario, le recomendaría echar un vistazo a una biblioteca como ncurses . Porque entonces 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 brinda la capacidad de leer una pulsación de tecla instantáneamente en lugar de esperar a que el usuario presione Intro.
Aquí hay una diatriba sobre scanf : https://web.archive.org/web/20201112034702/http://sekrit.de/webdocs/c/beginners-guide-away-from-scanf.html
Indiquemos 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 cualquier entrada es rechazada, es necesario proporcionar al usuario un mensaje descriptivo que explique (en un lenguaje claro "fácil de entender para personas normales que no son programadores") por qué fue rechazada (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 se rechace la entrada del usuario son:
También definamos "la entrada contenía caracteres inaceptables" correctamente; y decir que:
A partir de esto podemos determinar que se necesitan los siguientes mensajes de error:
A partir de este punto, podemos ver que una función adecuada para convertir una cadena en un número entero necesitaría distinguir entre tipos de errores muy diferentes; y que algo como " scanf() " o " atoi() " o " strtoll() " es total y absolutamente inútil 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 varios cientos de líneas de código por sí sola.
Ahora, esto fue 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.
En esta respuesta, supondré que está leyendo e interpretando líneas de texto . Tal vez le esté preguntando al usuario, que está escribiendo algo y presionando RETORNO. O tal vez esté leyendo líneas de texto estructurado de un archivo de datos de algún tipo.
Dado que está leyendo líneas de texto, tiene sentido organizar su código en torno a una función de biblioteca que lee, 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 lee una línea de texto y la vuelve a imprimir. Tal como está escrito, tiene un par de limitaciones, a las que llegaremos en un minuto. También tiene una característica muy buena: el número 512 que pasamos como segundo argumento a fgets es el tamaño de la line de matriz que le estamos pidiendo a fgets que lea. Este hecho, que podemos decirle a fgets cuánto puede leer, significa que podemos estar seguros de que fgets no desbordará la matriz al leer demasiado en ella.
Así que ahora sabemos cómo leer una línea de texto, pero ¿qué pasaría si realmente quisiéramos leer un número entero, un número de punto flotante, un solo carácter o una sola palabra? (Es decir, ¿qué pasaría si la llamada scanf que estamos tratando de mejorar hubiera 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 número entero, la forma más simple (aunque imperfecta) de hacerlo es llamar a atoi() . Para convertir a un número de coma flotante, existe atof() . (Y también hay mejores formas, como veremos en un minuto). He aquí 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 (quizás y o n como 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; ignora silenciosamente cualquier carácter adicional que se haya escrito).
Finalmente, si desea que el usuario escriba una cadena que definitivamente no contenga 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, metí un poco la pata, 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 tres cosas que me salteé.
(1) Hemos estado llamando
fgets(line, 512, stdin); para leer en la line matriz, y donde 512 es el tamaño de la line de matriz para que fgets sepa que no debe desbordarse. Pero para asegurarse de que 512 es el número correcto (especialmente, para verificar si alguien modificó el programa para cambiar el tamaño), debe volver a leer donde se declaró la line . Eso es una molestia, por lo que hay dos formas mucho mejores de mantener los tamaños sincronizados. Podría, (a) usar el preprocesador para hacer un nombre para el tamaño:
#define MAXLINE 512 char line[MAXLINE]; fgets(line, MAXLINE, stdin); O bien, (b) utilice el operador sizeof de C:
fgets(line, sizeof(line), stdin); (2) El segundo problema es que no hemos estado verificando 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 solicitaste, lo indica devolviendo un puntero nulo. Así que 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 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 " Ese " 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 a 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 querremos quitar esa nueva línea. Hay varias formas de hacerlo, a las 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 esta otra forma 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 me vengas a llorar cuando te falle debido a una de sus 17 peculiaridades y debilidades, o entre en un bucle infinito debido a la entrada de tu no esperaba, o cuando no sabe cómo usarlo para hacer algo más complicado. Y echemos un vistazo a las molestias reales de fgets :
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.
Tienes que comprobar el valor de retorno. En realidad, eso es un lavado, porque para usar scanf correctamente, también debe verificar su valor de retorno.
Tienes que quitar el \n hacia atrás. Esto es, lo admito, una verdadera molestia. Desearía que hubiera una función estándar que pudiera indicarle que no tuviera este pequeño problema. (Por favor, que nadie mencione gets ). Pero en comparación con las 17 molestias diferentes de scanf's , tomaré esta molestia de fgets cualquier día.
Entonces , ¿ cómo quitas esa nueva línea? Hay muchas maneras:
(a) Manera obvia:
char *p = strchr(line, '\n'); if(p != NULL) *p = '\0';(b) Manera complicada y compacta:
strtok(line, "\n");Desafortunadamente, esto no funciona del todo bien en líneas vacías.
(c) Otra forma compacta y ligeramente oscura:
line[strcspn(line, "\n")] = '\0'; Y hay otras formas también. Yo, siempre uso (a), ya que es simple y obvio, aunque menos que conciso. Consulte esta pregunta , o esta pregunta , para obtener más (mucho más) sobre cómo eliminar el \n de lo que fgets le brinda.
Y ahora que eso 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 strtol y strtod . strtol también le permite usar una base diferente a 10, lo que significa que puede obtener el efecto de (entre otras cosas) %o 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 narrativa 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 carácter. ¿Qué sucede si desea leer una línea que contiene dos números, o varias palabras separadas por espacios en blanco, o una puntuación de encuadre específica? Ahí es donde las cosas se ponen interesantes, y donde las cosas probablemente se estaban complicando si intentabas hacer cosas usando scanf , y donde hay muchas más opciones ahora que has leído limpiamente una línea de texto usando fgets , aunque la historia completa en todos esas opciones probablemente podrían llenar un libro, por lo que solo podremos arañar la superficie aquí.
Mi técnica favorita es dividir la línea en "palabras" separadas por espacios en blanco, y 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 como una discusión completamente separada). Mi propia preferencia es una función dedicada para construir una serie 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 seguir procesando cada una, quizás con las mismas atoi / atof / strtol / strtod que ya hemos visto.
Paradójicamente, a pesar de que hemos dedicado una buena cantidad de tiempo y esfuerzo aquí a descubrir 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 forma, obtendrá la mayoría de las ventajas de scanf , pero sin la mayoría de las desventajas.
Si su sintaxis de entrada es particularmente complicada, podría ser apropiado usar una biblioteca "regexp" para analizarla.
Finalmente, puede usar cualquier solución de análisis ad hoc que le convenga. Puede moverse a través de la línea un carácter a la vez con un char * puntero comprobando los caracteres que espera. O puede buscar caracteres específicos usando funciones como strchr o strrchr , o strspn o strcspn , o strpbrk . O puede analizar/convertir y omitir grupos de caracteres de dígitos usando las funciones strtol o strtod que omitimos anteriormente.
Obviamente, se podría decir mucho más, pero esperamos que esta introducción lo ayude a comenzar.
¿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 puede detectar simplemente si todo el formato se escaneó con éxito y que no hubo basura adicional que no sea un espacio 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. }