epelpad

El post que buscas se encuentra eliminado, pero este también te puede interesar

Lectura de cadenas en C

La forma correcta de leer cadenas es un tema que casi nunca se trata a profundidad en los libros de C, y menos aún en los cursos, así que no es raro que sea una de las cosas que más problemas ocasionan a quienes están aprendiendo este lenguaje.

En este post vamos a ver cómo trabajan algunas de las principales funciones de entrada de C, así como sus pros y contras en relación a la lectura de cadenas. Al final se muestra una función de ejemplo que se puede utilizar para facilitarnos la programación.

El post está enfocado a la lectura de cadenas desde el teclado. Algunos de los conceptos o ideas podrían variar para la lectura de datos desde archivos, pero en general, lo aquí descrito debería aplicar también para leer desde archivos de texto.

Alternativas

La primera función de lectura de datos que se enseña es scanf. Se puede usar para leer cadenas, salvo por un detalle conocido por todos los programadores en C: no acepta cadenas con espacios. La recomendación que la mayoría hace, es que se utilice gets, pero se trata de una función insegura. Muchos la consideran el defecto más grande del lenguaje C, al grado de que ya ha sido declarada obsoleta. El problema de esta función es que lee todo lo que el usuario introduzca y lo almacena en la cadena, sin verificar si hay espacio suficiente. Es decir, si tenemos esto:

char nombre[30];
printf("Escribe tu nombre: ");
gets(nombre);

y el usuario introduce un nombre de 30 o más caracteres, gets almacena todo en la variable nombre. Como sólo le caben 30 caracteres (29 más el caracter nulo o de fin de cadena '\0'), los restantes sobrescribirán lo que sea que esté en las posiciones de memoria más allá del byte 30 de la variable nombre. Es lo que se conoce como un desbordamiento de buffer. A partir de aquí, pueden ocurrir varias cosas, desde la menos grave, esto es, que el programa termine su ejecución de forma inesperada, hasta tener bugs muy difíciles de encontrar (por ejemplo, si se sobrescribe una variable) además de que algún malware podría aprovechar esta vulnerabilidad para poner en riesgo la seguridad del sistema operativo. Es por lo tanto una función que se debe evitar.

Aquí cabe decir que scanf en su forma típica tiene el mismo problema al usarlo con cadenas; si el usuario introduce un nombre que no tiene espacios, y es mayor a la capacidad de nuestra variable, habrá desbordamiento.

¿Cuál es la solución entonces? En realidad hay varias. Una forma de hacerlo es usando scanf con una serie de modificadores que sí permiten leer cadenas con espacios, incluso de forma segura, pero es algo complicada y propensa a errores por todo lo que hay que teclear. La función que casi siempre se recomienda es fgets, que es una buena opción si se sabe utilizar. Tiene el siguiente prototipo:

char *fgets(char *s, int n, FILE *stream);
Sus parámetros son:

s - cadena donde se almacenarán los caracteres
n - el tamaño de s
stream - el archivo de donde se leerán los caracteres. Si se especifica stdin, se leerán de la entrada estándar (normalmente el teclado)

Hay que recalcar que el parámetro n especifica la cantidad de caracteres a leer, más el caracter de fin de cadena o caracter nulo '\0'. Es decir, esta función lee un máximo de n-1 caracteres, o hasta encontrar un caracter de nueva línea (el Enter con el que se finaliza la entrada de datos), lo que ocurra primero. Y finalmente cierra la cadena, agregando un '\0' justo después del último caracter leído. En otras palabras, siempre tendremos una cadena perfectamente formada y sin desbordamientos de buffer.

Vamos a ver un ejemplo de su uso. En este código, fgets lee un máximo de 29 caracteres de la entrada estándar:

char nombre[30];
printf("Escribe tu nombre: ");
fgets(nombre, 30, stdin);

Como se ve, es muy fácil de usar, pero debemos tener en cuenta algunos detalles que casi nunca se mencionan cuando se recomienda esta función. Cuando leemos una cadena con fgets, puede darse cualquiera de estos casos:

1. El número de caracteres introducidos por el usuario es menor a n-1.
Cuando esto suceda, la cadena incluirá al final el caracter de nueva línea (cosa que no pasaría con scanf o gets). Si al ejecutar el código de ejemplo anterior, introdujéramos el nombre "Jorge", la cadena quedaría así (omito el caracter nulo):

"Jorge\n"
Cuando se imprima la variable, siempre se meterá una línea nueva al final. Esto signfica, por ejemplo, que no será posible, al menos de forma sencilla, imprimir en una misma línea el nombre y la edad de una persona. Que esto sea aceptable o no, dependerá del uso que se le quiera dar al programa. En cualquier caso, siempre se puede verificar si la cadena contiene ese caracter, y eliminarlo, poniendo en su lugar el caracter nulo.

2. El número de caracteres introducidos es exactamente n-1.
La cadena no contendrá el caracter de nueva línea, el cual se quedará en el buffer de entrada (más sobre esto en la siguiente sección).

3. El número de caracteres introducidos es mayor a n-1.
fgets leerá únicamente los primeros n-1 caracteres y los asignará a la cadena, dejando en el buffer todos los caracteres no leídos.

Que fgets funcione de esta manera no es casualidad ni capricho. Es útil para saber si se leyó completo el valor introducido. Si después de llamar a esta función, la cadena contiene un caracter de nueva línea al final (caso 1) significa que se leyeron todos los caracteres introducidos por el usuario, así que el buffer de entrada está limpio. Si no hay caracter de nueva línea en la cadena, significa que quedó "basura" en el buffer. Como mínimo, el '\n' (caso 2), pero podrían ser más caracteres (caso 3).

Limpieza de buffer

Lo común en programas de consola o modo texto, es que para la entrada de datos por el teclado se use un buffer manejado por líneas. Esto significa que el programa no lee los caracteres directamente tal cual se van tecleando, sino que se guardan en un buffer o memoria intermedia, y hasta que se presiona Enter quedan disponibles para que el programa pueda leerlos mediante scanf, getchar, etc.

Las funciones de lectura de cadenas no se llevan muy bien con funciones de entrada más generales como scanf, debido a que manejan de diferente forma la lectura del buffer.

La función scanf funciona así: después de revisar el especificador de formato que le enviamos (%d, %f, etc.) lee y descarta todos los espacios en blanco que haya en el buffer de entrada* (esto incluye tabulaciones y caracteres de nueva línea) y a continuación lee los valores introducidos (o espera a que se introduzcan, si aún no se ha hecho) y los almacena. En cambio, gets y fgets no descartan nada, sino que leen lo que haya hasta que encuentran un caracter de nueva línea (o, en el caso de fgets, hasta leer el máximo de caracteres indicados). Y es ahí donde aparece el problema, porque scanf deja en el buffer de entrada el caracter de nueva línea, a diferencia de gets (y fgets, si se da el caso 1 de la sección anterior).

*Si el especificador de formato tiene %c, %[, o %n, scanf no descartará los espacios iniciales.

Vamos a ver un ejemplo de lo que ocurre cuando se utilizan estas funciones. Todo esto se explicará desde el punto de vista del programador y del programa, ya que internamente el sistema operativo puede realizar algunas tareas más, (por ejemplo, convertir el Enter a '\n') pero no son relevantes aquí.

Si ejecutamos un programa con este código:

int num1, num2;
char nombre[30];
printf("Escribe un numero: ");
scanf("%d", &num1);
printf("Escribe otro numero: ");
scanf("%d", &num2);
printf("Escribe tu nombre: ");
fgets(nombre, 30, stdin);

el primer scanf analizará el especificador de formato que le mandamos, que en este ejemplo es %d, así que primero descartará cualquier espacio que haya en el buffer de entrada (en este caso, ninguno) y después intentará leer un valor entero desde el buffer de entrada. Como en este momento el buffer está vacío, la ejecución se detendrá, a la espera de que se introduzca un valor. Si escribimos 50 y presionamos Enter, en el buffer se tendrá algo así:

50\n
en este momento, scanf leerá el '5' y el '0', que son caracteres válidos para un entero, y a continuación encontrará el '\n', que no es un caracter válido para un entero, así que terminará de leer, y asignará el valor 50 a num1, dejando el buffer así:

\n
A continuación se ejecutará el segundo scanf, que, de nuevo, verificará el especificador de formato (%d), por lo que descartará el '\n' que hay actualmente en el buffer, y a continuación esperará a que introduzcamos un valor. Si ahora escribimos 25 y presionamos Enter, el buffer quedará así:

25\n
y scanf leerá el '2' y el '5', y se detendrá al encontrar el otro '\n'. Entonces asignará el valor 25 a num2, y el buffer quedará así:

\n
Hasta aquí todo funciona bien, ya que se ha utilizado únicamente scanf, pero la siguiente instrucción de entrada es fgets y es entonces donde aparece el problema. Puesto que fgets lee lo que haya en el buffer sin descartar nada, se encontrará directamente con un '\n' (justo el caracter que le indica que deje de leer), así que lo asignará a la cadena, y saltará a la siguiente instrucción, sin habernos dejado escribir nada. La solución pasa por limpiar el buffer de entrada después de un scanf, si la siguiente instrucción es una lectura de cadena con gets o fgets. Hay quienes sugieren utilizar para esto fflush(stdin), pero es incorrecto. Como el propio estándar del lenguaje C dice, fflush es una función para vaciar flujos de salida (stdin es de entrada, obviamente). Si lo usamos con flujos de entrada, el resultado queda indefinido. En Linux y sistemas tipo Unix no funciona. En Windows suele funcionar, pero no podemos contar con eso, porque realmente estamos usando la función de forma incorrecta. Imaginemos que mediante alguna extraña técnica pudiéramos usar printf para leer datos en vez de imprimirlos, ¿tendría justificación hacer semejante disparate sólo porque "a mí me funciona"?

La manera recomendada de limpiar la entrada estándar es la siguiente:

int c;
while ((c = getchar()) != '\n' && c != EOF);

este código es como un fflush(stdin) pero correcto y estándar. Lo que hace es leer un caracter hasta que encuentra un caracter de nueva línea o de fin de archivo (EOF). En realidad, cuando estamos leyendo desde la entrada estándar, no deberíamos encontrarnos nunca con un fin de archivo, así que se podría omitir esta parte, pero es preferible dejarlo tal cual, ya que es posible redirigir a stdin el contenido de un archivo, que obviamente sí tiene fin. El código anterior funciona en ambos casos.

Función

Nuestra función de ejemplo tiene el siguiente prototipo:

int leecad(char *cad, int n);
Acepta dos parámetros: la variable donde almacenaremos la cadena, y su tamaño (incluyendo el caracter nulo). Es decir, que la invocaríamos de esta forma:

char nombre[30];
printf("Escribe un nombre: ");
leecad(nombre, 30);

Además, devuelve un valor de tipo entero, que servirá para verificar si hubo un error.

Podemos separar su funcionamiento en tres partes:

1. Comprobación del buffer
2. La lectura en sí de la cadena
3. Limpieza de buffer

Comprobación del buffer

Puesto que queremos una función que se pueda usar de forma más o menos general, primero hay que verificar si el buffer está limpio, o si hay un '\n' dejado por scanf y, en ese caso, limpiarlo:

    int i, c;

    /* Comprobación */
    c=getchar();
    if (c == EOF) {
        cad[0] = '\0';
        return 0;
    }

    if (c == '\n')
        i = 0;
    else {
        cad[0]=c;
        i = 1;
    }


Tenemos dos variables: i, que es el típico contador usado en los ciclos for, y c, que es donde guardaremos los caracteres individuales que vayamos leyendo y después se irán agregando a la cadena cad.

Empezamos leyendo sólo el primer caracter que haya en la entrada. Si es EOF, significa que no hay nada por leer, así que cerramos la cadena, dejándola "vacía" y salimos de la función retornando un valor de 0 ó falso, para indicar que hubo un error.

Si el valor leído es '\n', significa que había un caracter de nueva línea dejado por un scanf o función similar. Simplemente inicializamos i a 0, para indicar que los siguientes caracteres que leamos los iremos asignando a partir del primer caracter de la cadena.

Si no había un '\n', significa que el caracter que leímos es el primer caracter de la cadena introducida. En este caso, lo guardamos en la posición 0 de cad, e inicializamos i a 1, porque en este caso, como ya tenemos el primer caracter de la cadena, continuaremos agregando caracteres a partir del segundo.

Lectura de la cadena

Pasamos ahora a la lectura de la cadena:

    for (; i < n-1 && (c=getchar())!=EOF && c!='\n'; i++)
       cad[i] = c;

    cad[i] = '\0';

el for empieza con un ; porque estamos omitiendo la inicialización del contador, ya que fue inicializado en el punto anterior.
Este código lee un caracter a la vez, lo agrega a cad, y se repite hasta que encuentre un fin de línea, fin de archivo, o haya leído la cantidad máxima de caracteres que se le indicó. Luego, cierra la cadena agregando un '\0' al final. Todo esto es muy similar a la forma en que los compiladores suelen implementar la función fgets, sólo que en lugar de getchar usan getc o fgetc.

Limpieza del buffer

Finalmente, limpiamos el buffer si es necesario:

    if (c != '\n' && c != EOF)
        while ((c = getchar()) != '\n' && c != EOF);

la variable c contiene el último caracter leído. Recordemos que había 3 formas de salir del for: que hayamos encontrado un '\n', un EOF, o que hayamos llegado al máximo de caracteres que debemos leer. Si se da cualquiera de los dos primeros casos, significa que leímos todo lo que había en el buffer, por lo que no hay nada que limpiar. En el tercer caso, el usuario escribió más caracteres de los debidos, que aún están en el buffer, por lo que hay que quitarlos, para lo cual usamos el método que vimos poco más arriba.

Juntándolo todo, tenemos la función:

int leecad(char *cad, int n) {
    int i, c;

    c=getchar();
    if (c == EOF) {
        cad[0] = '\0';
        return 0;
    }

    if (c == '\n')
        i = 0;
    else {
        cad[0] = c;
        i = 1;
    }

    for (; i < n-1 && (c=getchar())!=EOF && c!='\n'; i++)
       cad[i] = c;

    cad[i] = '\0';

    if (c != '\n' && c != EOF)
        while ((c = getchar()) != '\n' && c != EOF);

    return 1;
}


Notas finales y sugerencias

Aunque puesta aquí sólo a manera de ejemplo, la función está lista para ser usada tal cual en sus programas. Como usa sólo código estándar, debe funcionar en cualquier plataforma que tenga una implementación de C.

Por simplicidad, funciona únicamente para leer de la entrada estándar, pero fácilmente se podría modificar para que trabaje también con archivos. Para eso habría que agregar un tercer argumento: FILE *stream, y reemplazar los getchar() por fgetc(stream).

Finalmente, muchos programadores consideran que, si se quiere tener un programa lo más correcto y tolerante a fallas posible (en cuanto a lectura de datos), se deberían leer todas las variables (incluso de tipo int, float, etc.) en una cadena temporal, usando, por ejemplo, fgets (o incluso nuestra función de ejemplo) y después extraer de esta cadena los datos a leer, mediante sscanf, que funciona igual a scanf, pero en vez de leer los datos del teclado, los lee desde la cadena que le especifiquemos. Esto se deja como un ejercicio para quien quiera implementarlo.

30 comentarios - Lectura de cadenas en C

sceik
Como te dije aquel día en el que me ayudaste en el Tema de Inyección acá van mis puntos... Veo que te olvidaste de avisar pero no importa anoté tu user en un Bloc de Notas.. Y va Recomendación porque un capo del C como vos tiene que ser Full user... Abrazo y espero te den muchos puntos !!
rainor90
Mismo comentario que @scelk un Capo!
rainor90 +1
por algun motivo no me deja dar puntos... deje de usar taringa por un tiempo y ahora no me deja apesar de ser NFU apenas arregle eso t dejo mis 10 inaceptable que alguien q sepa tanto no sea NFU cuando hay tanto pendejo dando vuelta con 1209090231 puntos
_rkm
+10 y reco master!
leoelgordo
Hola capo... la verdad yo no entiendo un joraca, pero si me gustaria aprender pero bien desde cero.. te dejo mis +5.. y si en algun momento tienes algun tema o pagina que pueda aprender te lo voy a agradecer... saludos xD
caelito
Buena info... Soy novato asi que todos mis puntos de hoy (solo 3 )... Para el concurso ACM ICPC esto es super!!! Saludos y a favoritos!!
X0X_Corei
Como puedo ingresar texto pero sin tener que presionar enter???, me explico:
Ingreso el texto en una consola con el Dragon Naturally Speaking 10(en modo dictado), pero uso cin y tengo que presionar enter cada vez que quiero que interprete el texto el programa. Lo que quiero es que cada tanto tiempo(con algun temporizador tipico) el programa lea eso que yo ingrese ya sea con el teclado o con la voz. Existe alguna funcion (estandar o no) para hacer eso??
matiasromero7739
proba con este codigo,:
#include <stdio.h>
#include <unistd.h>
#include <termios.h>

int mygetch();

int main(){

char ch;

while((ch=mygetch())!='*'){
printf("n%cn", ch);
}

return 0;
}
PinEchiN_Es
@matiasromero7739 eso es de hace cuatro años, este compi de seguro ya hasta se graduó
ATINAo5
Che, tenes idea como puedo buscar una cadena determinada dentro de un fichero?
PinEchiN_Es
Bien por el post ahi te dejo 5 puntos , justo ahora hago los parciales y me tocan estos temas.
rrnum7
Gracias. Ojalá te sirva.
susanita21
buena informacion ahora comprendo porque no funcionaba un cliclo for con la fincion gets()
leoslax
Hola amigo, me has ayudado mucho con este aporte, no tengo puntos por ser novato, pero yo pregunto, podría devolverte el favor de una manera mas útil? Si necesitas algo comunícate conmigo. Un abrazo.
rrnum7
Por cierto, que no se malinterprete mi primer comentario de este post. Me refería a que me toca a mí devolver el favor de lo mucho que aprendí cuando empezaba, compartiendo lo que sé. No espero que nadie me "devuelva" nada, ni mucho menos, sino simplemente que les sirva.
leoslax
Ya tenes 10+! Pude darte los puntos. Un abrazo y felicitaciones por el post.
rrnum7
@leoslax Ok, gracias.
SonidoCristalino
¡Increíble Post!... Estaba buscando otra cosa relacionado a fgets() pero comencé a leer tu post y no pude dejar de leerlo, y fuiste el primero que leí en habla hispana que hace mención a la mala utilización de fflush() para limpiar el buffer cuando se tienen este tipo de funciones( en la facultad ese método se enseña como valedero, poniendo llamadas a esa función a lo largo de todo el código ).
Me gustó la influencia de Dennis Ritchie en el código (cosa que yo nunca pude aprender).
Post totalmente claro, y paso a paso. ¡Van mis 1010 puntos!
GE-NIO
rrnum7 +1
Gracias. Sí, en muchos lados se enseña el fflush(stdin), aunque creo que su uso se ha ido reduciendo mucho, en parte, me imagino, porque cada vez más programadores empiezan a usar otros sistemas operativos aparte de Windows, y se dan cuenta del error.
juliodion12345
Gracias por la info, me ha servido bastante para mis proyectos
EAMP14
Gracias, me sirvio para tener una idea de como funciona la Lectura de cadenas en C. Espero sigas haciendo mas Posts.
Nito18 +1
gracias, me sirvio
famardones
Hola, disculpa, tengo una consulta, en el caso de querer leer una frase desde la entrada estandar, la unica forma para saber si existe un salto de linea, es ver si está el codigo ascii de la tecla <enter>?? es decir leer caracter por caracter e identificar si está el numero 10? o existe otra forma de hacerlo?
rrnum7
No hace falta hacerlo caracter por caracter. Puesto que el fin de línea termina la entrada de datos (a menos que hagas algo "raro", como usar un especificador complejo con scanf), sólo puede existir al final de la cadena. Entonces puedes hacer algo así:

int longitud = strlen(cadena);
if (cadena[longitud-1] == '\n') /* Hay Enter */

Juanpax96 +1
Muchisimas gracias! me ayudo mucho a comprender la utilizacion del buffer y el porque de mis problemas, GRACIAS
mistermalvavisco +1
muchas gracias por la explicacion, me parece muy buena tu forma de darte a entender porque explicas como funciona exactamente y eso facilita mucho la comprension en lugar de la memorizacion. +10
Rogerio_Ceni
La forma correcta de leer cadenas es un tema que casi nunca se trata a profundidad en los libros de C, y menos aún en los cursos, así que no es raro que sea una de las cosas que más problemas ocasionan a quienes están aprendiendo este lenguaje.


asi es C , se aprende de oficio a los golpes el tema cadenas de caracteres , sigo traumado desde que lo vi en la facultad
poponuts
muchisimas gracias! una explicacion impecable, muy clara. Me sirvio mucho.
Unbr0ken
Esa función tiene errores...

Yo creé una semejante, basándome en la que el libro de K&R crearon.

http://foro.elhacker.net/programacion_cc/c_limpiando_la_stdin_correctamente-t424539.0.html

Saludos.
rrnum7
Sí, tiene sus problemas, e incluso hace mucho pensé en quitarla o cambiarla, pero preferí dejarla porque creo que aún asi ilustra bien el punto del post. Y hay un par de cosas que yo llamaría simplemente debatibles, por ejemplo, siempre devuelve una cadena válida, aún si el usuario manda un parámetro incorrecto. Y dado que quise hacerla más o menos general, funciona tanto si antes de invocarla había un fin de línea en el buffer o no. Esto también tiene una consecuencia negativa en ci
rrnum7
ertos casos. De cualquier forma, es imposible, sólo con la biblioteca estándar, escribir una función que se comporte bien en todos los casos (a menos, como comento al final, que leamos sólo cadenas y hagamos parsing). Tengo de hecho una función que, salvo por la validación del parámetro, es prácticamente idéntica a la tuya (K&R FTW ), pero para este post en particular me parecio que era irme un poco a lo obvio. Gracias por comentar.
Unbr0ken
@rrnum7 Ja, sí, K&R (D.E.P. Ritchie), crearon un lenguaje fenomenal, y su libro es incomparable.

Saludos.
edypastorres
Hermano se te agradece esta información, buscaba algo que me ayudara a obtener cadenas con espacio, un saludo
FranJ7966
Buen día; Primeramente debo decir que no soy usuario recurrente de Taringa!, pero suelo leer ciertos post a menudo. Por lo que veo esto es algo viejo pero espero puedan responder. El caso que atañe en este momento está relacionado con este post. Estoy realizando un programa en C que sirva de inventario para registrar los componentes básicos de ciertas computadoras y necesito una función que, después de ya haber escrito los datos en un archivo, lea una linea especifica de este, es decir, que el usuario pueda consultar específicamente una linea del archivo y leerla para mostrar en pantalla esos datos. He hecho esto para leer carácter por carácter pero no hallo cómo hacerlo con lineas completas y que comience a leer desde el principio de la linea.

cont=num-1;
fseek(arch,0,SEEK_SET);
while(cont>0){
while (fgetc(arch)=='n'){
cont--;
}
}
act=getc(arch);

Gracias de antemano!