Tips para Lenguaje C con cadenas y limpieza del búffer – Cómo garantizar que el código funcione y no morir en el intento

Tips para Lenguaje C con cadenas y limpieza del búffer – Cómo garantizar que el código funcione y no morir en el intento

Comments Off

C es un sin lugar a dudas un lenguaje maravilloso, al menos eso es lo que siempre me ha parecido (incluso cuando era estudiante). Lo más fascinante de C es que existe en una delgada línea, entre los lenguajes de alto nivel y los de bajo nivel. Esto permite que se pueda utilizar el lenguaje para hacer cosas propias de lenguajes de alto nivel o incluso meterle mano a direcciones de memoria y manipulación de bits. En este post explicaré algunos tips que pueden ser de utilidad particularmente para manejar cadenas y limpieza del búffer. Veamos.

Manejo de cadenas

En C las cadenas (String) son vistas como arreglos de caracteres. Esto permite un ahorro en las definiciones del lenguaje ya que con una sola estructura (char) es posible generar colecciones de caracteres (String). Adicionalmente manejar los String de esta manera implica que será necesaria la utilización de ciertas funciones para manipular los arreglos de caracteres, como por ejemplo:

  • strcpy / memcpy (para copiar cadenas)
  • strlen (para obtener la longitud de una cadena)
  • strcat (para concatenar cadenas)

Muchas veces cuando estamos trabajando con arreglos de caraceres, la aplicación no parece ejecutarse correctamente. Cuando revisamos a fondo qué está ocurriendo nos percatamos que las cadenas están llenas de “basura” (caracteres que aparecen dentro de la cadena, pero que no fueron introducidos por nosotros). Para ilustrar la idea veamos el siguiente código:

ejemploManejoCadenas00.jpg

En este caso estamos utilizando una misma cadena de 20 posiciones (de 0 a 19) para capturar el ingreso por pantalla de una línea de texto. Las líneas número 13 y 19 permiten, a través del ciclo, capturar incluso espacios es blanco, ya que agregar el caracter leído al arreglo mientras lo que se lea sea distinto de EOL (‘\n’). Veamos que ocurre cuando compilamos y ejecutamos el código:

terminal.jpg

Podemos ver que el código en principio no está haciendo lo que debería (o lo hace, pero a medias). Esto es producto de la utilización de la misma cadena para leer dos String diferentes. ¿Cuál podría ser la solución para esto?. La solución más sencilla sería decir “bueno, creemos entonces dos arreglos”. ¿Será esto lo más apropiado?. ¿qué sentido tendría definir dos cadenas para cada lectura?. Lo más sensato en este tipo de casos es inicializar el arreglo cada vez que se vaya a hacer una lectura. Es más, resulta una buena (y muy buena) práctica de programación inicializar los arreglos antes de utilizarlos. Lo hacemos todo el tiempo con las variables (eso nos dicen las buenas prácticas) entonces, ¿Por qué no hacerlo en este caso también?. Para inicializar una cadena en C, basta con hacer una pequeña modificación en el código. Veamos:

ejemploManejoCadenasv2.jpg

Las líneas 12 y 19 permiten “inicializar” una cadena. Básicamente el caracter ‘\0′ indica el fin de un arreglo de caracteres. Por ende resulta bastante obvio entender que básicamente le estamos diciendo a C que el fin de la cadena empieza desde la posición 0 de la misma, es decir, la cadena está vacía. Si ejecutamos de nuevo nuestra aplicación, pues… vaya, no veremos ningún cambio importante, ya que la salida será la misma. Pero entonces, ¿Por qué no funcionó?. La respuesta es fácilmente deducible. Sólo indicamos que la casilla 0 era igual a fin de cadena, pero en memoria el resto de las casillas siguen teniendo cada uno de los caracteres que se leyeron en el primer ciclo. Lo que necesitaríamos sería inicializar todas las casillas del arreglo. Para ello podemos generar un ciclo haciendo que cada casilla sea igual a ‘\0′, o bien podemos utilizar una función predeterminada en C. Veamos:

ejemploManejoCadenasv3.jpg

Ahora las líneas 12 y 19 hacen una llamada a la función memset, la cual está disponible en la librería string.h. Esta función permite inicializar una cadena (arreglo de caracteres) con un valor en particular. El primer parámetro representa la cadena que se quiere inicializar, el segundo el valor que se le quiere asociar a cada casilla del arreglo (para las cadenas en C, el valor 0 es equivalente a ‘\0′) y el tercer parámetro indica cuántas casillas (partiendo de la 0) se desean inicializar. Lo más usual es inicializar todo el arreglo, pero pueden haber casos donde sólo nos interese inicializar una parte. Cuando ejecutemos nuestra aplicación obtendremos la siguiente salida:

terminal.jpg

En este caso ya no existen caracteres acumulados en la cadena que produzcan procesamientos erróneos en nuestro código. Ahora bien, podemos ver al momento de leer la cadena, también se contempla el salto de línea (por ello el símbolo @ aparece justo debajo de la cadena al momento de imprimirla). Si nos interesara remover este caracter podemos implementar una función sencilla que asigne a la última casilla de la cadena el valor ‘\0′.

Conclusiones

Con este ejemplo sencillo podemos concluir dos cosas importantes:

  • Es una buena práctica de programación (no sólo propia de C) inicializar siempre (sí, SIEMPRE) una cadena antes de ser usada.
  • Inicializar una cadena con la sintaxis cadena[0] = ‘\0′ es perfectamente válido, pero sólo si no pensamos usar la cadena para dos lecturas diferentes. Para cualquier otro caso es recomendable utilizar la función memset.

 

Limpieza del búffer

¿Nos nos ha pasado alguna vez que cuando programamos en C y corremos nuestro proyecto el mismo comienza a comportarse de forma extraña?. A veces saltando segmentos de código donde se hacen lecturas por el teclado o bien “fusionando” cadenas leídas, generando todo esto un caos. Lo primero que tengo que decir en este caso es que a) La máquina no está poseída y b) C no es “temperamental”. Analicemos lo que realmente ocurre. Cada vez que se hacen lecturas por teclado, es altamente recomendable limpiar el búffer estándar de este periférico (en el caso de C stdin). Esto evita que el búffer pueda colapsar y generar comportamientos inesperados. Esto puede hacer se gracias a una función bastante útil, llamada fflush, la cual se encarga de limpiar el búffer que se le pase como parámetro. En particular si estamos trabajando con el búffer del teclado debemos usar la sintaxis fflush(stdin) inmediatamente después de cada lectura. Eso es lo que nos dice la teoría y ciertamente está en lo correcto. En sistemas operativos como Windows esto funciona perfectamente. Pero cuando trabajamos con sistemas operativos basados en UNIX (entiéndase Ubuntu o MAC OS X por ejemplo) el resultado es distinto: el fflush NO funciona, aún cuando sea invocado. El misterio de esto es algo que aún no he resuelto, pero que espero poder develar algún día. Mientras tanto, la pregunta razonable sería: ¿Cómo hacer para limpiar el búffer de entrada del teclado en un sistema operativo basado en UNIX que no ejecute correctamente fflush?. Pues bien la limpieza del búffer es algo que puede hacerse a “mano”, con una función tal como ésta:

cleanBuffer.jpg

En este caso se utiliza una macro para determinar en cuál sistema operativo se está ejecutando la aplicación (se asume que si la macro no está definida se está ejecutando el código en Windows). Si el sistema operativo está basado en UNIX, se genera un ciclo haciendo lecturas hasta que el caracter leído sea igual a \n, es decir, a salto de línea. Si se ejecuta la aplicación bajo un sistema operativo distinto a UNIX, se ejecuta el respectivo fflush. Con esta solución podemos invocar la función cleanBuffer justo después de cada lectura del teclado, garantizando así que siempre mantendremos el búffer limpio evitando así comportamiento inesperados en nuestra aplicación (al menos por este motivo, ya lo que se programa es otra cosa).

Enjoy it!


NOTA: Los ejemplos mostrados en este post fueron realizados utilizando MAC OS X v10.6.6 y gcc.

Back to Top