En los títulos y los textos vais a encontrar unas cuantas citaciones cinematográficas (y si, soy un cinéfilo). Si no os interesan podéis fingir no verlas, ya que no son fundamentales para la comprensión de los post...

Este blog es la versión en Español de mi blog en Italiano L'arte della programmazione in C. Espero que mis traducciones sean comprensibles...

jueves, 24 de enero de 2013

¿Prototipos? Sí, ¡gracias!
cómo usar los Prototipos en C

Tras los excesos de las fiestas (y, tal vez, con un par de kilos acumulados por eliminar), es mejor empezar con un tema ligero ligero: los Prototipos de Funciones. Ligero, pero no demasiado.

Después de una inspección rápida en la red me he dado cuenta de que hay una cierta confusión sobre el tema. Prototipos necesarios, quizás recomendados, a veces desconocidos... Me he dado cuenta de información dudosa hasta en apuntes Universitarios (¡ay, ay!). Por cierto, en mi pasado, he hablado con varios colegas C-Programmers que no tenían ideas claras sobre el tema. Bueno, entonces es el momento de aclarar!

Vamos a empezar con los hechos, dejando para la segunda parte las consideraciones técnicas/filosóficas. Recomiendo prestar atención, en el siguiente texto, en ciertas palabras claves que usamos y vamos a tratar de ilustrar: declaración, prototipo y definición. Y, nos referiremos a las distintas versiones de C que nos han acompañado hasta la fecha, lo que a su vez son: K&R C, ANSI C (C89/C90) y C99 (estaría, también, el C11, pero no es significativo para este post). A menos que se especifique diversamente todos las próximas afirmaciones/consideraciones se refieren al C actual, el C99.

Vamos al grano: en C los prototipos no son obligatorios. La confusión sobre este tema proviene de la doble personalidad que muchos programadores de C (yo incluido) que, a menudo, tiene que desenredarse entre C y C++, haciendo, a veces, un poco de confusión: los prototipos son obligatorios en C++, por razones estrechamente relacionadas con ciertas características del lenguaje (¿os suena el Function Overloading?).

En C, sin embargo, es obligatoria la declaración de una función.

Hagamos, pues, un ejemplo sobre las palabras claves declaración, prototipo y definición, usando sólo un tipo moderno de sintaxis (ANSI C o C99):
// declaración de función
int myFunc();

// declaración de función con prototipo
int myFunc(int val):

// definición de función con prototipo
int myFunc(int val)
{
   if (val > 5)
       return val;
   else
       return val * 2;
}
El orden en este ejemplo, como es evidente, no es casual: la declaración es el caso básico, el prototipo contiene implícitamente una declaración, y, por último, la definición contiene implícitamente un prototipo (y por lo tanto también una declaración). Como se mencionó, en el ejemplo he omitido, para no complicar innecesariamente la descripción, sintaxis permitidas pero demasiado old-fashioned, o prohibidas por el C99.

Antes de trasladarse a la parte filosófica, hacemos un breve análisis histórico: en K&R C no había obligación de declarar las funciones, así que no había ninguna comprobación en compile-time sobre el valor de retorno y, menos aún, sobre la consistencia de los parámetros pasados: en ausencia de la declaración, el compilador aplicaba un comportamiento por defecto asumiendo que la función devolvía un int. Para los parámetros se aplicaba el default argument promotion: los enteros se promoven en int, y los float se promoven en doble.

Con la llegada de ANSI C (o C89/C90), han llegado los prototipos, pero se ha mantenido la compatibilidad hacia atrás con la sintaxis antigua (para no obligar a revisar millones de líneas de código funcionante). Con esta novedad era, finalmente, posible comprobar en compile-time el uso correcto de la funciones, sea para los parámetros sea para los valores devueltos. Debido a la compatibilidad hacia atrás se mantuvo, sin embargo, la posibilidad de escribir nuevo código con la sintaxis antigua, y, además, seguía siendo válido el concepto de default return value en ausencia de declaración.

Con el C99 se ha dado otro paso adelante: vale con la búsqueda de compatibilidad con el código existente, pero el valor de retorno por defecto era un agujero demasiado grande en la solidez del lenguaje, por lo tanto se ha introducido la declaración obligatoria como se indicaba al principio del post (añado que también se hizo obligatorio el uso de prototipos en los standard headers del lenguaje, pero esa es otra historia...).

Y ahora, después de describir lo que el standard nos exige y/o permite hacer, llegamos finalmente a lo que es mejor hacer: Creo que un buen programador utiliza prototipos (de ahí, supongo, para la propiedad transitiva aquellos que no utilizan prototipos no son buenos programadores. He dicho supongo, así que si alguien se ha ofendido, no se lo tome conmigo, se lo tome con la propiedad transitiva). ¿Y por qué recomiendo encarecidamente el uso de prototipos? Bueno, C es un lenguaje tipizado, por lo tanto es tan obvio la ayuda que este mecanismo nos puede dar para producir código sin errores de tipo, al tiempo que mejora la legibilidad y facilidad de mantenimiento, que no hay necesidad de explicarlo!

Y, para añadir un toque de radicalidad que nunca sobra, añado que, para las mismas cuestiones de legibilidad y mantenibilidad del software, no es conveniente contar con el hecho de que usando definiciones con prototipo (véase el ejemplo anterior), y escribiendo el código en el orden correcto (es decir, utilizando una función sólo después de su definición), no es necesario escribir prototipos reales. No seáis perezoso en cosas útiles, por favor!

¿Y como debe de estar estructurado un buen código considerando lo dicho anteriormente? Veamos un pequeño ejemplo con tres archivos: un header, un archivo con las funciones, y un archivo que las utiliza:

Este es el archivo header:
/* myfuncs.h
 */
// prototipos globales
char *myFunc1(char *dest, const char *src);
char *myFunc2(char *dest, const char *src);
Aquí está el archivo con las funciones:
/* myfuncs.c
 */
#include "myfuncs.h"

// myFunc1()
char *myFunc1(char *dest, const char *src)
{
    ...
}

// myFunc2()
char *myFunc2(char *dest, const char *src)
{
    ...
}
Y, finalmente, el archivo utilizador:
/* usefuncs.c
 */
#include "myfuncs.h"

// prototipos locales
static int useFuncs(void);
static int anotherFunc(void);

// anotherFunc()
static int anotherFunc(void)
{
    ...
    int res = useFuncs();
    ...
}

// useFuncs()
static int useFuncs(void)
{
    ...
    char *p1 = myFunc1(dest, src);
    char *p2 = myFunc2(dest, src);
    ...
}
¡Y ya no hace falta decir nada más!

Hasta el próximo post.