SlideShare una empresa de Scribd logo
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 cur sos, 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 r elació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 lectu ra 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 intr oduzca 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 ine sperada, hasta tener bugs muy difíciles de
encontrar (por ejemplo, si se sobrescribe una variable) además de que algún malware pod rí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 introduc e 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 qu e 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 re comienda 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 t eclado)
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 desbo rdamientos 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 c uenta 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 eje mplo anterior, introdujéramos el
nombre "Jorge", la cadena quedaría así (omito el caracter nulo):
"Jorgen"
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 se ncilla, 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 qu iera 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 disponib les 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 t odos 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 qu e 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 mom ento 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í:
50n
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 es pecificador 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í:
25n
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 e s 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 indefi nido. 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ól o 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 has ta 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 s e 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 funci ona 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 invoca rí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, pa ra 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 pos ició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 cu al 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 s e 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.

Más contenido relacionado

PDF
Chuleta de lenguaje C para principiantes
PDF
Tema 3 sentencias de control de java por gio
PDF
03 - Entrada y salida en lenguaje C
PDF
Matlab 2
PDF
02 - Conceptos fundamentales sobre tipos de datos en lenguaje C
PPTX
la instrucción if , Leer cadenas, caracteres
PDF
Instrucciones de control de salto
PDF
DATOS LENGUAJE C
Chuleta de lenguaje C para principiantes
Tema 3 sentencias de control de java por gio
03 - Entrada y salida en lenguaje C
Matlab 2
02 - Conceptos fundamentales sobre tipos de datos en lenguaje C
la instrucción if , Leer cadenas, caracteres
Instrucciones de control de salto
DATOS LENGUAJE C

La actualidad más candente (19)

DOCX
Printf23
DOCX
DOCX
métodos procedimimientos estructuras de control java
PPT
Curso Java Inicial 3 Sentencias De Control De Flujo
PPT
Estructuras de control
PPTX
Sentencias de repetición en Java
PDF
13 PHP. Un Ejemplo Con Constantes
PPT
Estructuras de control en Java
PPT
Iv unidad estructuras de control
PDF
Apunte pseudocodigo bucles y arrays v1.1
PPTX
Lenguajes de Programación: Clases y objetos
PDF
Programacion en java
DOCX
Estructuras de repetición en programacion
PPT
Estructuras De Control
 
PDF
Java - Sintaxis Básica 2015
PDF
04 - Sentencias de control condicionales y ciclos en lenguaje C: for, while, ...
PDF
05 - Funciones en lenguaje C
PPTX
Sentencias de Programacion
Printf23
métodos procedimimientos estructuras de control java
Curso Java Inicial 3 Sentencias De Control De Flujo
Estructuras de control
Sentencias de repetición en Java
13 PHP. Un Ejemplo Con Constantes
Estructuras de control en Java
Iv unidad estructuras de control
Apunte pseudocodigo bucles y arrays v1.1
Lenguajes de Programación: Clases y objetos
Programacion en java
Estructuras de repetición en programacion
Estructuras De Control
 
Java - Sintaxis Básica 2015
04 - Sentencias de control condicionales y ciclos en lenguaje C: for, while, ...
05 - Funciones en lenguaje C
Sentencias de Programacion
Publicidad

Destacado (20)

PDF
Medicos homeopatas.
DOCX
Informe de investigación acción
DOCX
Protocolos de red
PPT
Muertos en el bosque
PPTX
Métodos de estudio
PPT
Monumentos del mundo
PDF
Proyectopedaggicoderecreacinytiempolibreienss 091112090738-phpapp02
PPTX
La amistad
PPT
1 actitudes hacia el trabajo
PPTX
PDF
Pleno (04) 12 marzo-2013,1ª parte
PDF
Borrador pleno (11) 30 agosto-2013, 4ª parte
ODP
Presentació película titanic
PDF
Resolucioncd.390
PPT
Nancy patricia vanegas-soffy_cristina_espinosa-carmen_elena_henao_-teoria_de_...
PPTX
El estado, pnud, etnicidad y lengua umg 15'
PPT
Flickr angii montiel sofi aguilera (1) (2)
PDF
Tema 10 excepciones
PDF
Intervencion u py d pleno extraordinario 1 febrero
Medicos homeopatas.
Informe de investigación acción
Protocolos de red
Muertos en el bosque
Métodos de estudio
Monumentos del mundo
Proyectopedaggicoderecreacinytiempolibreienss 091112090738-phpapp02
La amistad
1 actitudes hacia el trabajo
Pleno (04) 12 marzo-2013,1ª parte
Borrador pleno (11) 30 agosto-2013, 4ª parte
Presentació película titanic
Resolucioncd.390
Nancy patricia vanegas-soffy_cristina_espinosa-carmen_elena_henao_-teoria_de_...
El estado, pnud, etnicidad y lengua umg 15'
Flickr angii montiel sofi aguilera (1) (2)
Tema 10 excepciones
Intervencion u py d pleno extraordinario 1 febrero
Publicidad

Similar a Lectura de cadenas en c (20)

DOCX
Librerias de dev c++
PPTX
Entrada Salida de Cadenas Fundamentos de la programación
PDF
Unidad-2.pptx.pdf
PPTX
Cadenas
PPT
Capitulo 8 Cadenas
PPT
Fpr Tema 4 www.fresymetal.com
PDF
Cadena de caracteres
PDF
08 strings o cadenas
DOC
Programacion c
PPTX
Cadenas y funciones de cadena
PPTX
Cadenas y funciones de cadena
DOCX
Tarea pagweb
DOC
PROGRAMACION EN C
DOC
PROGRAMACIÓN EN C
PDF
Funciones C (gnu/linux)
PDF
Introducción
PPT
Objetivo 01 Archivos de Texto
PPT
El lenguaje c
PDF
manual de turbo c ++.pdf, para programar
Librerias de dev c++
Entrada Salida de Cadenas Fundamentos de la programación
Unidad-2.pptx.pdf
Cadenas
Capitulo 8 Cadenas
Fpr Tema 4 www.fresymetal.com
Cadena de caracteres
08 strings o cadenas
Programacion c
Cadenas y funciones de cadena
Cadenas y funciones de cadena
Tarea pagweb
PROGRAMACION EN C
PROGRAMACIÓN EN C
Funciones C (gnu/linux)
Introducción
Objetivo 01 Archivos de Texto
El lenguaje c
manual de turbo c ++.pdf, para programar

Más de jbersosa (20)

DOCX
Las excepciones standar
DOCX
Mas sobre excepciones
DOCX
Estructuras de control try catch
DOCX
Main
PDF
Clasen1java
PDF
Programación java1
RTF
Tercercortesistop
PDF
Encapsulacion
DOCX
Administracion de la memoria principal
PPTX
Auditoria 2
PDF
Auditoriasistemasi 150703002656-lva1-app6891
PDF
Auditoria informatica
PPTX
Auditoria de sistemas (1)
PPTX
Auditoría de sistemas de información presentación
PDF
Realizar investigación y hacer un análisis por cada tema asignado al particip...
DOCX
Sistemas operativos
PDF
PDF
Estructura de una red
DOCX
Proyectodeprogramacinidesegundocorte2015 2
DOCX
Bases de datos mysql y repotes usando jasper report
Las excepciones standar
Mas sobre excepciones
Estructuras de control try catch
Main
Clasen1java
Programación java1
Tercercortesistop
Encapsulacion
Administracion de la memoria principal
Auditoria 2
Auditoriasistemasi 150703002656-lva1-app6891
Auditoria informatica
Auditoria de sistemas (1)
Auditoría de sistemas de información presentación
Realizar investigación y hacer un análisis por cada tema asignado al particip...
Sistemas operativos
Estructura de una red
Proyectodeprogramacinidesegundocorte2015 2
Bases de datos mysql y repotes usando jasper report

Último (20)

DOCX
Tipos de bares y licores, cerveza, enología y descripción del personal en á...
PDF
Sistema de Acumulación de Costos finanzas
PPTX
Secuencia del calculo de dieta normal.pptx
PPTX
presentacion transtornos alimenticios.pptx
PPTX
nutricion deportiva tips para cualquier curso deportivo en la UC
PPTX
Inducción Manejo higiénico de los alimentos
PDF
Interpretación - Actualización FSSC 22000 V5.1 2021.pdf
PDF
2222222222222222222222222222222222222222222.pdf
PPTX
Metodos-de-Conservacion-de-Los-Alimentos
PPTX
CARNES.pptxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
PPTX
PROGRAMA AGUA POTABLE EN ALIMENTOS PLANTAS
PPTX
DIPLOMADO MANEJO DE RESIDUOS SOLIDOS EN PLANTAS DE ALIMENTOS
PPT
todo sobre Alimentacion a partir de los 6 meses
PPTX
Educacion en nutricion unidad III UPE HER.pptx
PPTX
presentación de la alimentación de no sé
PPTX
OBESIDAD ASOCIADA A DIABETES MELLILTUS TIPO 2.pptx
PPTX
Tipos de envases-y-embalajes-para-alimentos
DOCX
PLAN DE CAPACITACIÓN ATP - CERDOS SAN MIGUEL MALVAS .....docx
PPT
Metodos-Fisicos-de-Conservacion de alimentos
PDF
Brown Beige and Red Vibrant Organic Delicious Creations Presentation.pdf
Tipos de bares y licores, cerveza, enología y descripción del personal en á...
Sistema de Acumulación de Costos finanzas
Secuencia del calculo de dieta normal.pptx
presentacion transtornos alimenticios.pptx
nutricion deportiva tips para cualquier curso deportivo en la UC
Inducción Manejo higiénico de los alimentos
Interpretación - Actualización FSSC 22000 V5.1 2021.pdf
2222222222222222222222222222222222222222222.pdf
Metodos-de-Conservacion-de-Los-Alimentos
CARNES.pptxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
PROGRAMA AGUA POTABLE EN ALIMENTOS PLANTAS
DIPLOMADO MANEJO DE RESIDUOS SOLIDOS EN PLANTAS DE ALIMENTOS
todo sobre Alimentacion a partir de los 6 meses
Educacion en nutricion unidad III UPE HER.pptx
presentación de la alimentación de no sé
OBESIDAD ASOCIADA A DIABETES MELLILTUS TIPO 2.pptx
Tipos de envases-y-embalajes-para-alimentos
PLAN DE CAPACITACIÓN ATP - CERDOS SAN MIGUEL MALVAS .....docx
Metodos-Fisicos-de-Conservacion de alimentos
Brown Beige and Red Vibrant Organic Delicious Creations Presentation.pdf

Lectura de cadenas en c

  • 1. 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 cur sos, 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 r elació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 lectu ra 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 intr oduzca 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 ine sperada, hasta tener bugs muy difíciles de encontrar (por ejemplo, si se sobrescribe una variable) además de que algún malware pod ría aprovechar esta vulnerabilidad para poner en riesgo la seguridad del sistema operativo. Es por lo tanto una función que se debe evitar.
  • 2. Aquí cabe decir que scanf en su forma típica tiene el mismo problema al usarlo con cadenas; si el usuario introduc e 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 qu e 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 re comienda 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 t eclado) 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 desbo rdamientos 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);
  • 3. Como se ve, es muy fácil de usar, pero debemos tener en c uenta 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 eje mplo anterior, introdujéramos el nombre "Jorge", la cadena quedaría así (omito el caracter nulo): "Jorgen" 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 se ncilla, 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 qu iera 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 disponib les para que el programa pueda leerlos mediante scanf, getchar, etc.
  • 4. 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 t odos 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 qu e 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: ");
  • 5. 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 mom ento 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í: 50n 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 es pecificador 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í: 25n 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 e s 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
  • 6. 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 indefi nido. 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ól o 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 has ta 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 s e 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 funci ona 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 invoca ríamos de esta forma: char nombre[30]; printf("Escribe un nombre: ");
  • 7. 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;
  • 8. } 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, pa ra 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 pos ición 0 de cad, e inicializamos i a 1,
  • 9. 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);
  • 10. 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 cu al 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;
  • 11. 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; }
  • 12. 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 s e 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.