¿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 usesscanf(buffer "%d.%d", &dollar, ¢)
. Nunca haría algo comoprintf("Enter height and base of the triangle: ")
. El punto principal del uso defgets
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:
-
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 verificar 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 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 conscanf''s
17 molestias diferentes descanf''s
, tomaré esta molestia defgets
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í.
-
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 mismasatoi
/atof
/strtol
/strtod
que ya hemos analizado. -
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 confgets
es pasarla asscanf
. De esta manera, terminas con la mayoría de las ventajas descanf
, 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 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 comostrchr
ostrrchr
, ostrspn
ostrcspn
, ostrpbrk
. O puede analizar / convertir y omitir grupos de caracteres de dígitos utilizando las funcionesstrtol
ostrtod
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 solochar
.
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 usarscanf
, 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
Utilicestrtok_r
de POSIX ostrtok_s
del Anexo K., que no está ampliamente implementado. También puede rodar el suyo usandostrcspn
ystrspn
, ya que no involucrar cualquier soporte especial del sistema operativo. -
Puede ser excesivo, pero puede usar lexers y analizadores (
flex
ybison
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 descanf
(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, segets
. -
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 conprintf
): 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 como12w4
, esperaría quescanf
rechace esa entrada, pero no lo hace: convierte y asigna con éxito el12
, dejandow4
en la secuencia de entrada paraw4
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;