I. DISEÑO DE PROGRAMAS EN C






MODULARIDAD
Una propiedad importante de C es la modularidad, es decir, la posibilidad de organizar un programa en un conjunto de bloques de código (funciones) que son gobernados desde el programa principal. Estos bloques (módulos) pueden organizarse en diferentes ficheros, si ello es necesario, para facilitar la gestión del programa desde su creación hasta su puesta a punto con la compilación, debugging y generación del ejecutable. Este proceso se realiza con la creación de un proyecto. La forma más razonable de construirlo es realizando la siguiente partición del programa:

(1) cabecera.h : fichero que agrupará todos los #include y todos los #define que requiere el programa, las definiciones de las variables globales y las estructuras empleadas y, finalmente, las declaraciones de las funciones que se usan a lo largo del programa.
(2) main.c : fichero que incluirá el programa principal “main{....}” precedido del “cabecera.h”.
(3) funciones.c : fichero, o en su caso ficheros, dependiendo del número de subrutinas con el que se haya diseñado el programa, y que puede ser más conveniente dividirlo en varios ficheros, que contiene las definiciones de todas las funciones creadas por el programador para ese proyecto concreto.

El entorno visual Code::Blocks ofrece la posibilidad de generar proyectos de una forma sencilla. Basta crear el proyecto desde la pestaña File, darle nombre y guardarlo en el directorio correspondiente, añadir los ficheros que componen el proyecto teniendo cuidado que el nombre de ninguno de ellos sea idéntico al del proyecto y después compilar y linkar con las pestañas habituales. Hacer simplemente la observación que en este caso al compilar se guardan los ficheros objeto porque cada vez que se corrige uno de los ficheros que componen el proyecto solo se recompilan aquellos que se han modificado, para volver a relinkarlo todo de nuevo en el nuevo ejecutable. Al final, a la hora de guardar el proyecto en su forma final se pueden eliminar, si se desea, los ficheros objeto.

Aquí se puede ver un ejemplo sencillo de un proyecto que implementa la construcción de una cola de cadenas de caracteres.



PORTABILIDAD
Suele ser habitual transplantar un programa entre diferentes máquinas. También puede darse el caso que dichas máquinas tengan diferente procesador y componentes, así como estar gobernadas por diferente sistema operativo. Un programa se dice que es portable si funciona en diferentes máquinas. Normalmente esta propiedad es difícil de conseguir porque casi todos los programas contienen fragmentos de código que dependen de la implantación, y solo funcionan con ese sistema operativo o con ese procesador particular. El lenguaje C ofrece la posibilidad de construir código portable pero aún así es necesario tener en cuenta ciertos consejos.

Uso de #define: La forma de mejorar la portabilidad es situar en macros con #define todas aquellas directivas de sustitución de ‘números particulares’ que dependen del sistema o del procesador. Estos ‘números’ incluyen los tamaños de los registros de acceso aleatorio a disco, las órdenes sobre el manejo de la pantalla y teclado, asignación dinámica de memoria, etc. Al portar el programa bastará con cambiar estas directivas al precompilador.

fread(buf, 128, 1, fp) ; /* sentencia no portable */
#define TAM_BUF 128 /* código portable */
………………
fread(buf, TAM_BUF, 1, fp) ;

Dependencias del sistema operativo: Todos los programas tienen dependencia del sistema operativo. Mejora la portabilidad si toda la interacción del programa con el sistema va incluida en una o varias funciones específicas que en caso de ser transplantado el programa a otra máquina bastaría con efectuar los cambios correspondientes en dichas funciones dejando el resto de código intacto.

void llamada_sist_operativo(int op, int x, int y)
{
switch(op)
{
case 1:borrar_pantalla( ) ;
break ;
case 2:borrar_linea( ) ;
break ;
case 3:goto_xy(x, y) ;
break ;
...................
}
}

Diferencias en el tamaño de los datos: El tamaño de una palabra en un procesador de 32 bits es de 32 bits y en un procesador de 64 bits será de 64 bits. Como el tamaño de una palabra tiene relación directa con el tamaño por defecto de un entero (tipo int), si se desea hacer código portable no deben hacerse suposiciones sobre el tamaño de un determinado tipo de datos. Para ello C dispone del operador sizeof( ) cuando el programa requiera conocer el número de bytes de alguna variable concreta.

/* escribir de forma portable un entero en disco */
fwrite(&i, sizeof(int), 1, stream) ;

De cualquier manera la construcción de código portable no es tarea fácil porque tarde o temprano todo proceso tendrá que concretarse e implantarse en un entorno determinado que obligará a manejar las particularidades de ese entorno.



DEPURACIÓN
Una buena programación requiere de una buena depuración de errores. De hecho, el producto final será bueno y fiable cuando haya pasado un control de calidad exhaustivo, en este caso la depuración.

Hoy en día, en los entornos interactivos a los que ya estamos acostumbrados se usa la prueba incremental como método más efectivo de depuración. Consiste en mantener siempre una unidad operativa que funciona e ir añadiendo nuevo código que se va probando hasta su correcto funcionamiento. De esta forma, siempre tendremos la seguridad de que los nuevos errores están en el nuevo código introducido. Así, podemos estimar, grosso modo, que el tiempo de depuración es proporcional a la superficie que ocupa el programa, es decir, proporcional al cuadrado del número de líneas de código. Ello supone que yendo añadiendo nuevas líneas de a poco gastaremos menos tiempo que agregando un bloque largo de un solo golpe. Vale pues, en este terreno, como en tantas otras facetas no solo de la programación, el dicho italiano: piano, piano, si arriva lontano.

Los errores más comunes cuando se usa el lenguaje C como en cualquier otro lenguaje son los típicos de sintaxis, diseño, etc., así como particulares del lenguaje, como los surgidos en el manejo de punteros.

Errores en el orden de procesamiento: Los operadores incremento y decremento, como ya explicamos en su día, actúan de modo distinto según precedan o no a su operando.
Problemas con punteros: Los más frecuentes vienen del desconocimiento en sí de su manejo y en otras ocasiones del uso accidental de punteros inválidos.
Errores de sintaxis: Es típico y habitual dejarse punto y coma en numerosas líneas, pero sobre todo, al final de la definición de una estructura. Hay otros más inhabituales o inesperados como una función que se le exige devolver un tipo de dato incorrecto.
Errores de indexación: No olvidar que el índice de un array de n elementos comienza en 0 y acaba en n-1.
Errores de límites: Rebasar los límites de un array o de capacidad de una variable siempre generará problemas difíciles de detectar.
Omisión de los prototipos de funciones: Saber que una función que no ha sido declarada se toma por defecto como entera, pero cualquier otro tipo debe ser declarado obligatoriamente.
Errores de argumentos: Hay que asegurarse de que los argumentos que se pasan a las funciones son del mismo tipo que los declarados en los prototipos.
Colisión pila-montón (stack-heap): Un programa compilado en C crea y usa cuatro regiones lógicas diferentes de memoria que realizan diferentes funciones. La primera de las regiones contendrá el código del programa, la segunda contendrá las variables globales y las otras dos regiones son la pila (stack) y el montón (heap). La pila almacena las variables locales y direcciones de regreso de las funciones, y el montón es la zona de memoria libre donde se realiza, por ejemplo, la asignación dinámica de memoria. Aunque inusual, puede ocurrir colisión por desbordamiento entre ambas zonas de memoria.



EFICIENCIA
La eficiencia de un programa se refiere a su velocidad de ejecución y al uso que hace dicho programa de los recursos del sistema: RAM, espacio en disco, papel de impresora, etc. Es un concepto subjetivo que depende de los objetivos y de las necesidades del programador. La regla general a tener en cuenta es que optimizar unos aspectos puede provocar la pérdida de efectividad en otros. La eficiencia consiste en una ecuación donde no es posible minimizar todas las variables, y el hacerlo en unas provoca el aumento de otras.

Así, por ejemplo, es posible aumentar la rapidez de un programa reduciendo al mínimo la llamada a funciones, y una solución sería incluir en el programa principal el código de la función correspondiente allí donde se llama, pero ello provoca que el código del programa se alarga inevitablemente dificultando su depuración y manejo. Otro ejemplo puede ser también que un uso eficiente de disco requiere la compactación de los datos que hay que almacenar, pero ello implica que su posterior acceso se ve dificultado por necesitar ser descompactados , y por tanto el acceso a disco se hace más lento.

Existen pequeños trucos que pueden hacer que los programas sean más pequeños y rápidos:

(1) El uso de los operadores incremento, x ++, o decremento, x --, frente a las sentencias directas, x = x+1 ó x = x-1, requiere menos RAM y es más rápido.

(2) El uso de las variables registro en los bucles agiliza su ejecución:

register int i;
for( i=0 ; i < 100 ; i++ ) … ;

(3) El uso de punteros frente a la indexación de los array hace que la ejecución sea más rápida y el código más comprimido.

int a, *p, array[ ] ;
…..
a = *p++
….
a = array[t++]

(4) La introducción directa del código de pequeñas funciones en la estructura del programa puede ahorrar tiempo en su ejecución. Es fácil de entenderlo si tenemos en cuenta que con el lenguaje C todas las variables locales y los parámetros de las funciones así como la dirección de vuelta usan la pila como almacenamiento temporal en la secuencia de llamada. Cuando se vuelve de la función en la secuencia de retorno toda esa información: variables, parámetros y dirección de vuelta deben ser eliminadas de la pila. Está claro que todo este proceso consume tiempo y si es posible evitarlo aumentaremos la rapidez del programa.

for( i=1 ; i<100 ; ++i ) t = calcular(i) ;
……….

float calcular(int q)
{
return abs(sin(q)*cos(q)/3.1416) ;
}

inclusión directa en programa:
for( i=1 ; i<100 ; ++i ) t = abs(sin(i)*cos(i)/3.1416) ;



MANTENIMIENTO
Una vez diseñado, escrito, compilado, depurado, probado y aprobado, el programa queda dispuesto para su uso. Aquí termina la fase de desarrollo y comienza la fase de mantenimiento. El programador dedicado al mantenimiento de software tiene dos responsabilidades básicas: corregir errores y proporcionar protección al código fuente.

Corrección de errores: Casi ningún programa está exento de errores. En cuanto a su importancia podemos clasificarlos en tres grupos: (1) categoría_1: aquellos que se deben arreglar obligatoriamente, (2) categoría_2: aquellos que sería deseable arreglar y (3) categoría_3: aquellos que no son preocupantes. Organizar los errores de software según esta escala permitirá al encargado de su mantenimiento de planificar y gestionar de forma adecuada su tiempo.

Protección del código fuente: La imprudencia en el manejo del código fuente puede pagarse caro. Imaginar que el programador intenta subsanar el error A y al hacerlo, elimina, por error, un bloque de código del programa fuente, que pierde al grabar la nueva versión. Al ir a arreglar el error B descubrirá las otras pérdidas que ya no puede momentáneamente subsanar y que, quizás, su posterior arreglo le consumirá un tiempo importante. Así parece lógico recomendar que desde un principio hay que ir guardando las diferentes versiones del programa fuente.

Volver al principio