En esta respuesta, voy a suponer que estás leyendo e interpretando líneas de texto . Tal vez le estés preguntando al usuario, que está escribiendo algo y presionando RETORNO. O tal vez esté leyendo líneas de texto estructurado de algún tipo de archivo de datos.
Como está leyendo líneas de texto, tiene sentido organizar su código alrededor de una función de biblioteca que lea, bueno, una línea de texto. La función estándar es fgets()
, aunque hay otras (incluidas getline
). Y luego el siguiente paso es interpretar esa línea de texto de alguna manera.
Aquí está la receta básica para llamar 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 fgets
es el tamaño de la matriz en la
line
que estamos pidiendo fgets
leer. Este hecho, que podemos decir fgets
cuánto está permitido leer, significa que podemos estar seguros de que fgets
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, 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, así:
printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);
(Esto ignora, por supuesto, la posibilidad de que el usuario haya escrito una respuesta de varios caracteres; silenciosamente ignora cualquier carácter adicional que se haya escrito).
Finalmente, si desea que el usuario escriba una cadena que definitivamente no contiene espacios en blanco, si desea tratar la línea de entrada
hello world!
como la cadena "hello"
seguida de otra cosa (que es lo que habría hecho el scanf
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
fgets(line, 512, stdin);
para leer en la matriz line
, y donde 512 es el tamaño de la matriz, line
por lo 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 line
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:
#define MAXLINE 512
char line[MAXLINE];
fgets(line, MAXLINE, stdin);
O (b) use el sizeof
operador 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ó, esto lo indica al devolver un puntero nulo. Entonces deberíamos haber estado haciendo cosas como
printf("type something:\n");
if(fgets(line, 512, stdin) == NULL) {
printf("Well, never mind, then.\n");
exit(1);
}
Finalmente, está el problema de que para leer una línea de texto,
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:
printf("you typed: \"%s\"\n", line);
Si ejecuto esto y escribo "Steve" cuando me lo solicita, se imprime
you typed: "Steve
"
Eso "
en la segunda línea se debe a que la cadena que leyó e imprimió fue en realidad "Steve\n"
.
A veces, esa nueva línea adicional no importa (como cuando llamamos
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 había dicho que scanf
no era bueno, y que de otra manera sería mucho mejor. Pero fgets
está empezando a parecer una molestia. ¡Llamar 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 fgets
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 mencione gets
). Pero en comparación con scanf's
17 molestias diferentes, tomaré esta molestia de fgets
cualquier día.
Entonces, ¿cómo no se tira de ese salto de línea? Tres maneras:
(a) Forma obvia:
char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';
(b) Manera complicada y compacta:
strtok(line, "\n");
Lamentablemente este no siempre funciona.
(c) Otra forma compacta y ligeramente oscura:
line[strcspn(line, "\n")] = '\0';
Y ahora que está fuera del camino, podemos volver a otra cosa que omití: las imperfecciones de atoi()
y atof()
. El problema con ellos es que no le dan ninguna indicación útil de éxito o fracaso: ignoran silenciosamente la entrada no numérica final y devuelven silenciosamente 0 si no hay ninguna entrada numérica. Las alternativas preferidas, que también tienen otras ventajas, son 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
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 usando fgets
, aunque la historia completa de todas esas opciones probablemente podría llenar un libro, así que solo vamos a poder arañar la superficie aquí.
Mi técnica favorita es dividir la línea en "palabras" separadas por espacios en blanco, luego hacer algo más con cada "palabra". Una función estándar principal para hacer esto es
strtok
(que también tiene sus problemas, y que también califica una discusión completamente separada). Mi preferencia es una función dedicada para construir una matriz de punteros para cada "palabra" separada, una función que describo en
estas notas del curso . En cualquier caso, una vez que tenga "palabras", puede procesar cada una de ellas, tal vez con las mismas
funciones atoi
/ atof
/ strtol
/ 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 leer
fgets
es pasarla sscanf
. De esta manera, terminas con la mayoría de las ventajas 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 utilizar las soluciones de análisis ad hoc que más le convengan. Puede moverse a través de la línea de un carácter a la vez con un
char *
puntero que busca los caracteres que espera. O puede buscar caracteres específicos usando funciones como 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.
(r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2
no detecta tan mal el texto no numérico final.