Buenas prácticas en Ansi C (1)


Hay muchas cosas a tener en cuenta cuando se desarrolla un programa. El problema es que en C estas primeras decisiones son cruciales para conseguir un poco de orden y concierto.

La falta de espacios de nombres y de jerarquía hace que todo sea un cajón de sastre. La falta de clases y objetos provoca interfaces poco definidas y accesos incorrectos.

Llevo unos 6 años programando en Ansi C y voy a exponer algunas de las buenas prácticas que he detectado en este tiempo.

No entraré en temas de estilo, sino en cuestiones prácticas.

No están todas las que son, pero son todas las que están.

Nombrado de variables

Las variables deben tener nombres cortos, concisos y descriptivos de su funcionalidad.

Como regla general, no deben indicar su tipo, ya que si cambiamos el tipo tendríamos que cambiar el nombre a la variable y esto no es lógico.

Como he dicho, por regla general". Hay algunos casos en los que sí me gusta establecer un carácter o dos indicando el tipado. Por ejemplo, cuando programo usando "JNI , me gusta utilizar una simple "j" para mostrar qué son "tipos java" y qué no, a fin de no mezclarlos a la ligera.

Ámbito de las variables

Las variables deben ser locales, y no es mala idea definirlas siempre al principio de las funciones.

Las variables globales son un error. No deben usarse en ninguna circunstancia. Una variable global impide poder transformar un módulo en una librería, ya que no se puede reusar correctamente.

Nombrado de funciones

Las funciones suelen ser un problema, ya que no deben coincidir en tooooooodo nuestro entorno.

Por lo tanto, yo suelo utilizar un sistema de nombrado jerárquico, tratando de evitar nombres excesivamente largos. Es un tira y afloja un poco difícil, y a veces no funciona, pero es lo mejor que he encontrado.

En este sistema jerárquico comienzo por una abreviatura de la librería que estoy usando, separada del resto por un guión bajo. Si la librería tuviera distintas funcionalidades, entonces diferenciaría cada una de ellas con otra palabra indicando a cuál de éstas hace referencia, terminando con el nombre de la función propiamente dicho en camelcase, comenzando en minúsculas. Después de toda esta verborrea, uno ejemplitos:

// Librería libtypeutilities.a
void types_bigint_new ();
void types_bigint_calculateChecksum ();
void types_string_concat();
// Librería libencryption.a
void crypt_aes_new ();
void crypt_md5_get ();

Ámbito de las funciones

Cuando una función sólo se utiliza en el archivo en el que se implementa, debería indicarse con la palabra static, que para eso está.

Cuando quereremos utilizar una función que está en otro archivo, nunca la declararemos en el archivo.c, sino que crearemos un archivo.h para tal efecto. La razón es muy sencilla: si cambia la función, nuestro programa no lo detectará hasta devolver un segmentation fault, como poco.

La mejor manera de defendernos de estos problemas es definir las funciones que se comparten en archivos de cabecera e incluir éstos tanto en el archivo que las utiliza como en el que las define. De esta manera el compilador detectará cualquier incongruencia y no dejará seguir, en lugar de encontrarnos el problema durante la ejecución.

Tamaño de las funciones

Las funciones deben ser pequeñas. Más pequeñas. Aún más pequeñas.

Es una lástima que en C no sea posible tener funciones de 5-6 líneas en casi ningún caso, ya que la comprobación de nulos y alguna cosa más ya nos hace superar este límite. Si hay que reservar y liberar memoria, nos pasaremos con toda seguridad.

Pero un límite de 40-50 líneas es más que asumible en la mayoría de los casos.

El "goto"

Siempre se ha dicho que usar "goto" es de malos programadores. Realmente sólo se trata de una herramienta más, que debe utilizarse con cuidado.

Se puede utilizar siempre y cuando no se salte a ningún sitio extraño. Puede venir bien para saltar al final de una función, a la zona donde se libera memoria.

Y una vez he dicho todo esto... ¿Para qué #### quieres usar un #### "goto" en una función de 40 líneas? Estoy seguro de que no te hace falta.

Programación por contrato

Es una buena práctica realizar comprobaciones al comienzo de la función y terminar la ejecución de ésta. No importa si hay varios return.

Eso sí: Una vez se ha reservado memoria, será mejor evitar todo tipo de return hasta el final.

Una sola función sólo debería reservar memoria en un punto, ya sea a una o varias variables.

void funcion (int param)
{
   // definición de variables
   int i = 0;
   // comprobaciones al comienzo
   if ( param > 5 )
     return;

   // cuerpo de la función
   return;
}

Gestión de memoria

Libera siempre memoria al mismo nivel que la reserves. Si tienes una función que reserva memoria, crea una que la libere en el mismo archivo. Aunque sólo haga un free. Quedará todo más homogéneo.

Protección de archivos de cabecera

La maraña de archivos de cabecera puede ser peligrosa, ya que es sencillo llegar a incluir varias veces el mismo archivo.

Por eso se debe proteger siempre el archivo de cabecera:

// Archivo "a.h"
#ifndef _A_H_
#define _A_H_

// rellene aquí sus contenido

#endif /* _A_H_ */

He visto casos en los que se protege la inclusión en lugar de proteger el archivo de cabecera. La comparación que he puesto es como proteger los tarjeteros y no los cajeros automáticos. Lo primero es que hay muchos más, y lo segundo, que es sencillísimo violar la seguridad.

Número de funciones por archivo

Nuevamente tenemos un problema, ya que en C tienden a salir muuuuchas funciones en seguida.

No daré una cifra, sino una regla: Todas las funciones de un mismo archivo deben estar relacionadas en cuanto a la funcionalidad.

Portabilidad: los defines

Es muy común encontrarse funciones que sólo están disponibles en un sistema operativo determinado y usamos "#define" para realizar la operación de una manera o de otra.

Pues bien... debemos acostumbrarnos a evitar los define dentro de nuestro código.

Irán al comienzo del archivo, definiendo macros o constantes, y después sólo utilizaremos estas macros o constantes.

Macros y constantes

Deben ser cortas y claras.

Su tamaño máximo es una línea.

Si tienen demasiada funcionalidad, deben transformarse en funciones.

A ser posible, una macro no debe relacionarse con otras macros (aunque sí pueden hacerlo con constantes).

En las macros, siempre protegeremos los argumentos entre paréntesis:

bc. #define max(a,b) (a)>(b) ? (a) : (b)

Y nunca llamaremos a una macro con parámetros que contengan operadores unarios. Si queréis saber por qué, compilad esto:

#include <stdio.h>
#define MIN(a,b) a<b?a:b

int main()
{
   int a = 1;
   int b = 1;
   printf("min %d, %d = %d\
", a, b, MIN (a++, b++));
   printf("min %d, %d = %d\
", a, b, MIN (a++, b++));
   printf("min %d, %d = %d\
", a, b, MIN (a++, b++));
   return 0;
}

El resultado será:

min 2, 3 = 2
min 4, 4 = 3
min 5, 6 = 5

¿Sabríais decir por qué? Probablemente no sea el resultado esperado o, al menos, no es el más intuitivo.

Continuará

Esto ha sido el primer plato. En el siguiente se explicarán las "buenas prácticas al hacer librerías en ansi C":link://slug/buenas-practicas-c-2.

Admito sugerencias.


Comentarios

Comments powered by Disqus