Por que es scanf
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 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,stdout
stdin
del segundo. De esta manera, puede asegurarse de que la salida y la entrada sean predecibles. Durante estas circunstancias, en scanf
realidad 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á 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 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, ¢)
. Nunca haría algo así printf("Enter height and base of the triangle: ")
. El punto principal de usar a 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);
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 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 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 necesitarás 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.
(r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2
no detecta tan mal el texto no numérico final.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".scanf
está 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.scanf
podría ser apropiada para la entrada de flujo formateada , pero no es del todo buena para nada basada en líneas.scanf
Es 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
%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
%d
especificador de conversión y escribe algo así12w4
, esperaríascanf
rechazar esa entrada, pero no lo hace: convierte y asigna con éxito12
, dejandow4
en 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:Una peculiaridad
fgets
es 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:Depende de usted cómo lidia con eso: puede rechazar la entrada completa sin control y sorber cualquier entrada restante con
getchar
: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:strtok
modifica 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 variantestrtok_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
ystrtod
convertirá representaciones de cadenas de enteros y números reales a sus respectivos tipos. También le permiten captar el12w4
problema que mencioné anteriormente: uno de sus argumentos es un puntero al primer carácter no convertido en la cadena:fuente
%*[%\n]
, lo cual es útil para tratar con líneas demasiado largas más adelante en la respuesta).snprintf()
),.isspace()
allí: acepta caracteres sin signo representados comoint
, por lo que debe emitirunsigned char
para evitar UB en plataformas dondechar
está firmado.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 (incluidasgetline
). Y luego el siguiente paso es interpretar esa línea de texto de alguna manera.Aquí está la receta básica para llamar
fgets
para leer una línea de texto: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
fgets
es el tamaño de la matriz en laline
que estamos pidiendofgets
leer. Este hecho, que podemos decirfgets
cuánto está permitido leer, significa que podemos estar seguros de quefgets
no 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
scanf
llamada 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, hayatof()
. (Y también hay mejores formas, como veremos en un minuto). Aquí hay un ejemplo muy simple:Si desea que el usuario escriba un solo carácter (tal vez
y
on
como respuesta sí / no), literalmente puede tomar el primer carácter de la línea, así:(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
como la cadena
"hello"
seguida de otra cosa (que es lo que habría hecho elscanf
formato%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
para leer en la matriz
line
, y donde 512 es el tamaño de la matriz,line
por lo quefgets
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 dondeline
se 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:O (b) use el
sizeof
operador de C :(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ó, esto lo indica al devolver un puntero nulo. Entonces deberíamos haber estado haciendo cosas comoFinalmente, 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\n
carácter que termina la línea, y también llena el\n
carácter en su matriz . Puede ver esto si modifica ligeramente nuestro ejemplo anterior:Si ejecuto esto y escribo "Steve" cuando me lo solicita, se imprime
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
atoi
oatof
, 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. Perofgets
está empezando a parecer una molestia. ¡Llamarscanf
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 afgets
las molestias reales: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.
Tienes que verificar el valor de retorno. En realidad, eso es un lavado, porque para usarlo
scanf
correctamente, también debe verificar su valor de retorno.Tienes que quitarle la
\n
espalda. 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 mencionegets
). Pero en comparación conscanf's
17 molestias diferentes, tomaré esta molestia defgets
cualquier día.Entonces, ¿cómo no se tira de ese salto de línea? Tres maneras:
(a) Forma obvia:
(b) Manera complicada y compacta:
Lamentablemente este no siempre funciona.
(c) Otra forma compacta y ligeramente oscura:
Y ahora que está fuera del camino, podemos volver a otra cosa que omití: las imperfecciones de
atoi()
yatof()
. 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, sonstrtol
ystrtod
.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
o%x
conscanf
. 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 usandofgets
, 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í.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 funcionesatoi
/atof
/strtol
/strtod
que ya hemos analizado.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 leerfgets
es pasarlasscanf
. De esta manera, terminas con la mayoría de las ventajasscanf
, 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
char *
puntero que busca los caracteres que espera. O puede buscar caracteres específicos usando 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.
fuente
sizeof (line)
lugar de simplementesizeof line
? ¡El primero hace que parezcaline
un nombre de tipo!sscanf
como motor de conversión pero recolectar (y posiblemente masajear) la entrada con una herramienta diferente. Pero quizás valga la pena mencionarlogetline
en este contexto.fscanf
molestias reales", ¿quiere decirfgets
? Y la molestia n. ° 3 realmente me molesta, especialmente dado quescanf
devuelve 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).sizeof
estilo. 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 esostrtok(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 ystrtok()
devuelve nulo. Es una verdadera penafgets()
que no devuelva un valor más útil para que podamos saber si la nueva línea está allí o no.En lugar de
scanf(some_format, ...)
considerarfgets()
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.fuente
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:
Definamos también "entrada contenida caracteres inaceptables" correctamente; y decir eso:
5" se tratará como "5")
A partir de esto, podemos determinar que se necesitan los siguientes mensajes de error:
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:
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...
Escriba (potencialmente miles de líneas) de código usted mismo, para satisfacer sus necesidades.
fuente
Aquí hay un ejemplo de cómo usar
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:fuente
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:
lex
y /yacc
o sus variantes).fuente
errno == EOVERFLOW
después de usarstrtoll
).