Versión 3.0

800x600 mínimo
En esta lección:

¿Qué es la asignación dinámica?
-----
Un array de punteros
-----
Una lista enlazada
-----


Otras secciones:

Conceptos básicos
-----
Programando en C
-----
Programando en C++
-----
Programando Windows 9x.
-----
Teoría electrónica
-----
Circuitos electrónicos
-----
Actividades adicionales
-----
Hipervínculos
-----


Contácteme:

Dudas y comentarios
-----


Hola mundo

¿Qué es la asignación dinámica?

Hasta este punto del tutorial de C las variables que se han utilizado en los programas son de tipo estáticas. (Algunas de ellas han sido automáticas y fueron asignadas dinámicamente para Usted por el sistema pero esta operación pasó inadvertida para Usted). En este capítulo estudiaremos algunas variables asignadas dinámicamente, éstas son variables que no existen cuando se carga el programa pero se crean dinámicamente cuando son necesarias al correr el programa. Es posible, utilizando éstas técnicas crear tantas variables como sea necesario, utilizarlas y removerlas de su espacio en memoria para que sea utilizado por otras variables, como es costumbre, un ejemplo habla bién del concepto:


#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct animal
{
    char nombre[25];
    char raza[25];
    int  edad;
}
*mascota1, *mascota2, *mascota3;

int main()
{
    mascota1 = (struct animal *)malloc(sizeof(struct animal));
    /* Es un error no checar la asignacion, consulte el texto. */
    /* Checaremos la asignacion en el siguiente programa. */
    strcpy(mascota1->nombre, "General");
    strcpy(mascota1->raza, "Colicondela");
    mascota1->edad = 1;

    /* mascota2 ahora señala a la construccion de arriba */
    mascota2 = mascota1;
    mascota1 = (struct animal *)malloc(sizeof(struct animal));
    strcpy(mascota1->nombre, "Francisco");
    strcpy(mascota1->raza, "Labrador Cobrador");
    mascota1->edad = 3;

    mascota3 = (struct animal *)malloc(sizeof(struct animal));
    strcpy(mascota3->nombre, "Cristal");
    strcpy(mascota3->raza, "Pastor Aleman");
    mascota3->edad = 4;

    /* Desplegamos los datos */
    printf("%s es un %s, y su edad es de %d.\n",
    mascota1->nombre, mascota1->raza, mascota1->edad);
    printf("%s es un %s, y su edad es de %d.\n",
    mascota2->nombre, mascota2->raza, mascota2->edad);
    printf("%s es un %s, y su edad es de %d.\n",
    mascota3->nombre, mascota3->raza, mascota3->edad);

    /* mascota1 señala a la misma estructura que mascota3 */
    mascota1 = mascota3;
    free(mascota3);         /* Esto libera una estructura */
    free(mascota2);         /* Esto libera otra estructura*/
    /* free(mascota1); esto no se puede hacer, consulte el texto */

    return 0;
}

Empezamos definiendo una estructura llamada animal con algunos campos en relación a unos perros, no definimos ninguna variable de éste tipo sólo tres punteros, si Usted busca en el resto del código del programa no encontrará ninguna variable definida por lo que no tenemos en donde almacenar datos, todo lo que tenemos para trabajar son tres punteros, cada uno de los cuales es capaz de señalar a variables de la estructura definida llamada animal. Para hacer cualquier cosa con el programa necesitamos algunas variables por lo que las crearemos dinámicamente

Creación dinámica de variables

El enunciado en la línea 15 asigna algo al puntero mascota1 creará una estructura dinámica conteniendo tres variables, el corazón del enunciado es la función malloc ( ) gravada en el centro del enunciado. Esta es una función para asignar memoria que requiere el resto del código de la línea para cumplir su objetivo. La función malloc ( ), por defecto, asignará una parte de memoria en una pila (heap) de "n" caracteres de longitud y será de tipo char. La "n" debe especificarse como el único argumento a la función, discutiremos "n" en breve, pero primero necesitamos definir la pila (heap).

¿Qué es la pila (heap)?

La pila (heap) es una área predefinida de memoria que puede ser accesada por los programas para almacenar datos y variables, éstos son asignados a la pila (heap) por el sistema cuando se realizan llamadas a malloc ( ). El sistema mantiene un registro del lugar donde los datos son almacenados, los datos y las variables pueden ser desasignadas al gusto dejando "agujeros" en la pila, el sistema sabe la ubicación de los agujeros y los puede utilizar para almacenamiento adicional de datos con llamadas adicionales a malloc ( ), la estructura de la pila (heap) es por tanto una entidad en constante cambio, dinámica.

Espero que la breve explicación de la pila y la asignación dinámica sea suficiente para entender lo que estamos haciendo con la función malloc ( ). Simplemente le solicita al sistema un bloque de memoria del tamaño especificado regresando un puntero que señala al primer elemento del bloque, el único argumento en el paréntesis es el tamaño deseado del bloque que en nuestro caso, deseamos un bloque que almacene una de las estructuras que definimos al principio del programa. El operador sizeof es nuevo, al menos para nosotros en éste curso, regresa el tamaño en bytes del argumento entre paréntesis, regresa por lo tanto, el tamaño de la estructura llamada animal y ése número es utilizado como parámetro en la llamada a malloc ( ), al completar ésta llamada tenemos un bloque asignado en la memoria con el puntero llamado mascota1 señalando el principio de éste bloque.

¿Qué hacer si malloc ( ) falla?

Si no hay suficiente memoria disponible para el bloque solicitado, malloc ( ) no retorna un puntero válido, en su lugar retorna el valor de NULL. El valor retornado siempre debe ser checado antes de intetar utilizarlo, en el ejemplo no realizamos prueba alguna por dos razones: en primer lugar y más importante es el hecho de presentar los tópicos uno a la vez para facilitar el estudio, en segundo, estamos solicitando una pequeña cantidad de memoria por lo que nuestro programa no causa problema. Sin embargo tenga siempre en cuenta que toda la memoria asignada dinámicamente debe ser cuidadosamente verificada.

¿Qué es el reparto (cast)?

Al principio de la llamada a la función malloc ( ) podemos observar el llamado reparto (Traducción en éste tutorial del término en inglés "cast"). La función malloc ( ) retorna por defecto un puntero de tipo void, como no se puede utilizar un puntero que señale a nada, debe ser cambiado a otro tipo. Usted puede definir el tipo de puntero con la construcción dada en el ejemplo, en éste caso deseamos un puntero que señale a una estructura de tipo animal, así que se lo decimos al compilador con éste reparto. Aún en el caso de omitir el reparto, la mayoría de los compiladores retornan un puntero correctamente dando un mensaje de advertencia y produciendo un ejecutable de todas maneras, es mejor práctica indicarle al compilador el reparto adecuado.

Utilizando la estructura asignada dinámicamente

En las lecciónes en estructuras y punteros vimos que si tenemos una estructura con un puntero señalandola podemos tener acceso a cualquier variable dentro de la estructura, en las líneas 18 a la 20 del programa asignamos algunos valores con propósito de ilustración, observe que son similares a la asignación de variables estáticas. En la línea 22 asignamos el valor de mascota1 a mascota2, esto no crea un nuevo dato, simplemente tenemos dos punteros al mismo objeto. Como mascota2 señala a la estructura creada para mascota1, ésta puede ser utilizada de nueva cuenta para asignar dinámicamente otra estructura, lo mismo se puede decir para mascota2. A la nueva estructura le asignamos algunos valores para ilustración en las líneas 24 a la 26. Finalmente asignamos datos a mascota3 de la misma manera yá explicada y desplegamos los resultados en pantalla.

Desechando los datos asignados dinámicamente

Para desechar los datos asignados dinámicamente y liberar el espacio para su reutilización utilizamos la función free ( ), para ésto simplemente llámela utilizando el puntero del bloque asignado dinámicamente como único parámetro y el bloque es liberado. Para ilustrar otro aspecto de la asignación dinámica y la liberación de espacio, he incluido una etapa adicional en el programa, en la línea 41 se le asigna a mascota1 el valor de mascota3, al hacer ésto, el bloque que señalaba a mascota1 se pierde ya que no existe puntero que señale al bloque de memoria, por lo tanto no podemos referirlo, cambiarlo ó desasignarlo, éste bloque de memoria, el cual es parte de la pila, se ha desperdiciado, ésto no debe hacerse en un programa, lo mostramos a manera de ilustración. La primera llamada a la función free ( ) remueve el bloque de datos señalado por mascota1 y mascota3 lo que desasigna la estructura y libera el espacio de memoria para su uso posterior. La segunda llamada a free ( ) remueve el bloque señalado por mascota2, en este punto del programa hemos perdido todo acceso a los datos generados al principio del programa.

Volver al principio

Hola mundo

Un array de punteros

Parece que la explicación de la asignación dinámica en el anterior ejemplo fué muy extensa, la buena noticia es que se ha dicho prácticamente todo al respecto, claro está que aún falta por aprender algunas técnicas en el uso de la asignación dinámica y esto es lo que haremos en los siguientes ejemplos, empezando con el siguiente código:


#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct animal
{
    char nombre[25];
    char raza[25];
    int  edad;
}
*mascota[12], *puntero; /* Esto define 13 punteros, no variables */

int main()
{
    int indice;

    /* primero rellenar dinamicamente las estructuras con cualquier cosa */
    for (indice = 0 ; indice < 12 ; indice++)
    {
        mascota[indice] = (struct animal *)malloc(sizeof(struct animal));
        if (mascota[indice] == NULL)
        {
            printf("Falla en la asignacion de memoria.\n");
            exit (EXIT_FAILURE);
        }
        strcpy(mascota[indice]->nombre, "Fido");
        strcpy(mascota[indice]->raza, "Pastor para tacos");
        mascota[indice]->edad = 4;
    }

    mascota[4]->edad = 12;      /* estas lineas son para */
    mascota[5]->edad = 15;      /* poner algunos datos */
    mascota[6]->edad = 10;      /* en algunos de los campos */

    /* Desplegamos los datos */
    for (indice = 0 ; indice < 12 ; indice++)
    {
        puntero = mascota[indice];
        printf("%s Es un %s, y tiene %d anos.\n",
          puntero->nombre, puntero->raza, puntero->edad);
    }

    /* es buena practica de programacion liberar el espacio */
    /* dinamicamente asignado en el programa */

    for (indice = 0 ; indice < 12 ; indice++)
    free(mascota[indice]);
    return EXIT_SUCCESS;
}

Este programa es muy parecido al ejemplo anterior ya que se utiliza la misma estructura pero en el presente caso definimos un array de punteros para ilustrar los mecanismos para construir una base de datos grande utilizando un array de punteros en lugar de un puntero sencillo a cada elemento, para mantener el ejemplo sencillo definimos 12 elementos en el array y otro puntero adicional llamado puntero. El enunciado *mascota[12] es nuevo así que es conveniente hacer algunos comentarios. Lo que hemos definido es un array de 12 punteros siendo el primero mascota[0] y el último mascota[11], como un array es en sí un puntero, el nombre mascota es un puntero constante a otro puntero, ésto es válido en C, no hay limites en cuanto al número de niveles señalados, así, por ejemplo, la expresión int ****pt es legal ya que se trata de un puntero a un puntero a un puntero a un puntero a una variable de tipo entero, se recomienda tal uso de los punteros hasta haber ganado suficiente experiencia en su manejo.

Ahora que tenemos 12 punteros los cuales pueden usarse como cualquier otro, es sencillo escribir un bucle para asignar dinámicamente un bloque de datos para cada puntero y rellenar los respectivos campos con los datos deseados, en éste caso rellenamos los espacios con datos de propósito ilustrativo pero bién podría tratarse de una base de datos, de información proveniente de algún equipo de prueba de laboratorio ó cualquier otra fuente de datos. Notará que en el ejemplo checamos cuidadosamente el valor retornado por la función malloc ( ) para verificar que no contenga un valor de cero, si retorna un valor NULL, desplegamos un mensaje indicandolo y terminando el programa, recuerde que en un programa real no basta con terminar el programa, es aconsejable generar un reporte de errores y darle al usuario la oportunidad de corregirlos y continuar el proceso iniciado antes de cerrar el programa. Volviendo al análisis del código, en las líneas 31 a la 33 escogimos algunos campos al azar para ilustrar el uso de enunciados sencillos y que los datos son desplegados en el monitor. El puntero puntero se utiliza en el bucle para desplegar datos sólo para ilustración, fácilmente se hubiera podido utilizar mascota[indice] en su lugar. Finalmente los 12 bloques son liberados de la memoria antes de terminar el programa, el resultado de la ejecución es el siguiente, tenga en cuenta que mi compilador no maneja la letra eñe:

Un array asignado dinámicamente

Volver al principio

Hola mundo

Una lista enlazada

Finalmente llegamos a una de las técnicas de programación famosa por su intimidante nombre, una lista enlazada dinámicamente, con un poco de tiempo invertido en el estudio del código se verá que no es otra cosa que una técnica más de programación hecha de simples componentes y que puede ser una poderosa herramienta, antes de presentar el código, considere ésta analogía: es el día de su cumpleaños y su hermana le ha dado un presente, al abrirlo encuentra una nota que dice "Busca en el armario", Usted vá al armario y encuentra otra nota que dice "Busca en la televisión", debajo de la televisión encuentra otra nota, "...debajo de la cafetera", Usted continúa la búsqueda y finalmente encuentra un par de calcetines debajo del plato del perro. Lo que Usted hizo fué ejecutar una lista enlazada, el punto de inicio fué al abrir el regalo y terminó debajo del plato de comida del perro, ahí terminó porque no había más notas. En nuestro programa haremos lo mismo que su hermana lo forzó a hacer, pero más rápido y dejaremos una pequeña cantidad de datos en cada punto intermedio del camino a recorrer. Tenemos además la posibilidad de regresar al principio y recorrer la lista completa una y otra vez si así lo deseamos. El código de nuestro programa es éste:


#include <stdio.h>      /* Necesario para definir NULL */
#include <string.h>
#include <stdlib.h>

#define REGISTROS 6

struct animal
{
    char nombre[25];          /* El nombre del animal */
    char raza[25];            /* El tipo de animal */
    int  edad;                /* La edad del animal */
    struct animal *siguiente; /* señala a otra estructura de este tipo */
}
*puntero, *inicio, *previo;   /* Se definen tres punteros */

int indice;                   /* Una variable global */

int main()
{
    /* El primer registro siempre es un caso especial */
    inicio = (struct animal *)malloc(sizeof(struct animal));
    if (inicio == NULL)
    {
        printf("Falla en la asignacion de memoria\n");
        exit (EXIT_FAILURE);
    }

    strcpy(inicio->nombre, "General");
    strcpy(inicio->raza, "Pastor para tacos");
    inicio->edad = 4;
    inicio->siguiente = NULL;
    previo = inicio;

    /* Una vez iniciada la lista, se puede utilizar un bucle */
    /* para rellenar los bloques */
    for (indice = 0 ; indice < REGISTROS ; indice++)
    {
        puntero = (struct animal *)malloc(sizeof(struct animal));
        if (puntero == NULL)
        {
            printf("Falla en la asignacion de memoria\n");
            exit (EXIT_FAILURE);
        }

        strcpy(puntero->nombre, "Pancho");
        strcpy(puntero->raza, "Labrador");
        puntero->edad = 3;
        /* señala al ultimo "siguiente" de este registro */
        previo->siguiente = puntero;
        puntero->siguiente = NULL; /* señala este "siguiente" a NULL */
        previo = puntero;           /* Este es ahora el registro previo */
    }

    /* Se despliegan los datos descritos */
    puntero = inicio;
    do
    {
        previo = puntero->siguiente;
        printf("%s es un %s, y tiene %d anos de edad.\n",
          puntero->nombre, puntero->raza, puntero->edad);
        puntero = puntero->siguiente;
    }
    while (previo != NULL);

    /* Es buena practica liberar el espacio utilizado */
    puntero = inicio;       /* primer bloque del grupo */
    do
    {
        previo = puntero->siguiente;    /* siguiente bloque */
        free(puntero);                  /* libera el actual bloque */
        puntero = previo;               /* señala al siguiente */
    }
    while (previo != NULL); /* termina hasta que el siguiente sea NULL */

    return EXIT_SUCCESS;
}

Este programa inicia de una manera similar a los ejemplos anteriores con la adición de una declaración constante para uso posterior, la estructura es casi la misma excepto por la adición de otro campo dentro de la estructura en la línea 12, se trata de un puntero a otra estructura del mismo tipo y será utilizada para señalar a la siguiente estructura en orden, de acuerdo a la analogía de arriba, éste puntero señalará a la siguiente nota, que a su vez contendrá un puntero a la siguiente nota. Definimos tres punteros y una variable para utilizarla como contador y con ésto estamos listos para empezar a utilizar la estructura definida, una vez más con datos sin sentido, sólo para propósitos de ilustración.

El primer campo

Utilizando la función malloc ( ) solicitamos un bloque de almacenamiento en memoria (en el heap) y lo rellenamos con datos, checando la correcta asignación. Al campo adicional, en éste ejemplo llamado siguiente, se le asigna el valor de NULL, el cual es utilizado para indicar que éste es el final de la lista, dejaremos el puntero llamado inicio señalando a ésta estructura de tal manera que siempre señalará a la primera estructura de la lista. Asignamos además a previo el valor de inicio por razones que pronto veremos. Tenga en cuenta que los extremos de la lista enlazada serán siempre manejados de forma diferente a los elementos centrales de la lista, en éste punto tenemos un elemento en nuestra lista relleno de datos significativos.

Rellenando las estructuras adicionales

El siguiente grupo de enunciados de control y asignación están incluidos dentro de un bucle for de tal manera que podemos construir nuestra lista una vez que ha sido definida. El número de ciclos del bucle está determinado por la constante REGISTROS definida al principio del programa, en cada ciclo, asignamos memoria, hacemos la prueba de asignación, rellenamos los primeros tres campos y los punteros. En el último registro se le dá a siguiente la dirección de éste nuevo registro porque el puntero llamado previo está señalando al registro previo, entonces previo->siguiente está dando la dirección del nuevo registro que hemos rellenado. En el nuevo registro se le asigna a siguiente el valor de NULL y al puntero previo se le dá la dirección de éste nuevo registro porque la siguiente vez que generemos un nuevo registro, éste será el previo en tal ocasión, aunque la explicación está un poco confusa, estudiando el código le resultará fácil entender la mecánica de asignación. Cuando termina el bucle tenemos una lista de 7 estructuras, 6 correspondientes a la ejecuión del bucle y la estructura que generamos antes de iniciar el bucle, la lista tendrá las siguientes características:

  • El puntero llamado inicio señala a la primera estructura de la lista.

  • Cada estructura contiene un puntero a la siguiente estructura.

  • La última estructura tiene un puntero que contiene el valor de NULL el cual puede utilizarse para detectar el final de la lista.

Debe quedar claro que no es posible simplemente brincar a mitad de la lista y cambiar algunos valores, la única manera de acceder, por ejemplo, a la tercera estructura es iniciando por el principio y recorrer la lista una estructura a la vez, aunque parece un precio alto por la conveniencia de colocar algo de datos fuera del area del programa, es de hecho una buena forma de almacenar cierto tipo de datos.

Para desplegar los datos se utiliza un método similar al utilizado para generar los datos, los punteros son inicializados y entonces utilizados para avanzar de registro en registro, leyendo y desplegando cada registro a la vez. El despliegue termina cuando se encuentra un NULL, así que el programa no necesita saber siquiera cuántos registros hay en la lista. Finalmente borramos la lista completa para liberar espacio en memoria. Se debe tener cuidado de no borrar el último registro antes de checar el valor NULL, ya que sería imposible terminar el programa.

No es difícil ni tampoco trivial agregar elementos a mitad de una lista enlazada, es necesario crear un nuevo registro, rellenarlo de datos y señalar su puntero al registro que le precederá. Por ejemplo, si se desea agregar un nuevo registro entre los registros actuales tercero y cuarto, se requiere que el nuevo registro apunte hacia el cuarto registro, y el puntero del tercer registro debe señalar al registro recién creado. Agregar un nuevo registro al principio y al final de la lista son casos especiales, como ejercicio se le deja al lector considerar los pasos necesarios para agregar un nuevo registro en una lista doblemente enlazada. El resultado de la ejecución del programa es éste:

Una lista enlazada

Dos funciones más

Debemos mencionar un par de funciones, calloc ( ) y realloc ( ). La función calloc ( ) asigna un bloque de memoria inicializado todo en ceros lo cual puede ser útil en algunas circunstancias. Generalmente asignamos memoria e inmediatamente la rellenamos con datos así que es una pérdida de tiempo rellenar primero conceros el bloque de memoria y después con los datos del programa, por ésta razón, la función calloc ( ) se utiliza muy raramente. por otra parte, la función realloc ( ) se utiliza para cambiar el tamaño de un bloque asignado de memoria, a su vez es raramente utilizada, aún por programadores experimentados.

Volver al principio

Hola mundo

© 1998, 1999 Virgilio Gómez Negrete, Derechos Reservados