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...

miércoles, 10 de octubre de 2012

Érase una vez la malloc()
cómo usar la malloc() en C

Hoy vamos a hablar de la asignación dinámica de memoria. Si hacemos una búsqueda rápida en Google del mal reputado malloc() y su uso (probar con "why use malloc", por ejemplo), veremos que hay un montón de preguntas sobre el tema que van desde preguntas sencillas ("¿cómo se usa?") a las dudas existenciales ("¿porqué se usa?"). Bueno, estamos en un tema pseudo-filosófico debido al hecho de que, en realidad, se puede programar en C por un largo tiempo sin tener que utilizar malloc()... pero, en este caso, ¿estamos usando bien el lenguaje? Lo siento, pero la respuesta es ¡NO!

Sin embargo, hagamos primero un poco de claridad. Cualquier persona (incluyendo yo) que se encuentre con la necesidad de escribir (muy) rápido un pequeño programa para probar algo rápidamente - quizás para una emergencia repentina durante algún mantenimiento crítico de software muy urgente (listo para ayer) - ¿como lo escribe? Por supuesto, con miles de variables automáticas, arrays sobredimensionados ("hay mucha memoria"), tal vez algunas variables estáticas y (horror, horror) variables globales. O sea, en fin, uno de esos programas que incluso mientras lo escribes lo bautizas "temp_halgo.c" porque ya sabes que vas a escribirlo de nuevo tan pronto como sea posible o tendras que borrarlo por la vergüenza (nunca se sabe que alguien lo lee por accidente).

Pero aquí, por lo general, hablamos de estilos y de cosas bien hechas: el C tiene una gestión de memoria potente y flexible (es un lenguaje con punteros!), y escribir en C, fingiendo no saberlo es un error. Pero tenga cuidado: esto no significa que tienes que utilizar siempre punteros y malloc() ("de lo contrario no eres un buen programador"), en realidad es todo lo contrario. Dentro de una función las variables automáticas siempre serán (y con razón) la mayoría, también porque (cuando sea posible y correcto) usar el stack en lugar del heap permite mejorar el rendimiento del programa y, entre otras cosas, hace que sea más fácil de escribir, leer y mantener. La asignación dinámica de memoria es una operación costosa para el sistema operativo, añade (obviamente) complicación en el código y aumenta la posibilidad de errores (levante la mano quien nunca se olvidó de utilizar la free() correspondiente de una malloc()) .

Pero, ¿cuándo y por qué utilizar malloc()? Lo siento por la respuesta un poco "perogrullada", pero yo diría:

    1) cuando es indispensable
    2) cuando es mejor

Vemos el primer punto de Pedro Grullo:

¿Cuando es indispensable? La asignación dinámica es esencial en al menos dos casos: el primero es lo de las funciones que devuelven una dirección. Vamos a ver un ejemplo sencillo de una función que duplica una cadena (hay uno casi idéntico en K&R):
char *myStrdup(
    char *src)
{
    // ejecuta un duplicado de src (con sitio para el '\0')
    char *dest = malloc(strlen(src) + 1);
    if (dest != NULL)
        strcpy(dest, src);

    return dest;
}
Simple, ¿no? No hace falta decir (pero lo diré de todos modos), que en una función como esta, hay que usar malloc() y no se puede optar por utilizar la dirección de un array automático en el stack, el cual, debido a que está en el stack, se pierde después del return.

El segundo caso con elección forzada es el de las linked list. Veamos un ejemplo muy simplificado (pero perfectamente compilable y comprobable):
// nodo de una single linked list con campo datos
typedef struct snode {
    int data;
    struct snode *next;
} node_t;

// alloca un nodo con datos y un puntero al próximo elemento
node_t *addNode(
    node_t *next,
    int    data)
{
    node_t *node = malloc(sizeof *node);
    node->data = data;
    node->next = next;
    return node;
}

// inserta un nodo en la cabeza de una lista
void insertNode(
    node_t **head,
    int    data)
{
    node_t *node = addNode(*head, data);
    *head = node;
}

// main() para test
void main() 
{
    // lista vacia
    node_t *head = NULL;

    // inserta 10 nodos con data=indice loop
    int i;
    for (i = 1; i <= 10; i++)
        insertNode(&head, i);

    // recorre la lista y enseña los valores
    while (head) {
        printf("%d\n", head->data);
        head = head->next ;
    }
}
Para aquellos que nunca la han utilizado, las linked list no son una frivolidad: son una de las herramientas más poderosas de la programación en general (y del C en particular). Los que trabajan intensamente en proyectos grandes y profesional, más pronto o más tarde, acabaran por usarlas: una razón más, por lo tanto, para familiarizarse con la asignación de memoria dinámica.

Pasando al segundo punto de Pedro Grullo:

¿Cuándo es mejor? Sin duda, cada vez que se necesite manejar datos (normalmente arrays de tipos simples o de estructuras), de los cuales no se conoce la dimensión en compile-time: si no usamos malloc() se debería asignar arrays de gran tamaño (para evitar falta de espacio en run-time). Y este detalle nos muestra otro punto: si vamos a manejar grandes cantidades de datos, no podemos confiar demasiado en el stack, donde el espacio no es infinito, incluso si trabajamos con modernos sistemas operativos en máquinas llenas de memoria (y si programamos aplicaciones embebidas con grandes limitaciones Hardware... incluso peor).

Con lo que se ha dicho hasta ahora espero, por lo menos, haber contribuído en hacer un poco de claridad sobre esta cuestión. ¡Ah!, en el post he mencionado siempre, para simplificar, malloc(), pero, como todos saben, la asignación dinámica de memoria es una familia de funciones (malloc (), calloc (), realloc () y free()) que permiten una gran flexibilidad y variedad de soluciones.

Bueno, esto es todo, y recordad: por cada malloc() hay un free(): si las cuentas no te salen empieza a preocuparte...

Hasta el próximo post.