Palabras clave: Datos estructurados, Registros.

Preguntas de repaso
  • ¿Qué es la programación estructurada?

  • ¿Qué es el anidamiento de funciones?

  • ¿Que es el anidamientos de estructuras?

No sigas si no conoces las respuestas.

1. Datos estructurados

Cuando se estudió el tema de las variables se indicaba:

int, float, char y boolean son tipos de datos que almacenan valores simples; pero String es un tipo de dato que almacena valores compuestos (en concreto una secuencia de char).

Los String son un tipo de dato estructurado porque almacena más de un dato.

Los datos estructurados o estructuras de datos son tipos de datos que recolectan varios datos simultáneamente.

Existen muchos tipos de datos estructurados: array, string, registros, conjuntos, diccionarios, ficheros, objetos, etc …​

Estudiaremos con algo más de profunidad los siguientes:

Array

Los arrays son conjuntos de datos donde todos son del mismo tipo de dato. Para referirse a un dato concreto, se utiliza un índice; así, un array puede contemplarse como una colección de datos de la forma \(\{a_0,a_2,\ldots, a_n\} \).

Por ejemplo, el conjunto de datos \(\{2,4,6\}\) se puede almacenar en el ordenador utilizando un estructura de array, con lo que \(a_0=2\), \(a_1=4\) y \(a_2=6\)

String

Este tipo de dato, ya parcialmente estudiados, sirve para representar secuencias de caracteres. Cada dato (o carácter) concreto de la secuencia tiene asociado un índice al igual que en los array; así un string puede contemplarse como la secuencia "\(a_0a_2\ldots a_n\)".

El tipo de dato String es el que debes de utilizar si quieres almacenar oraciones, frases, párrafos, …​

Registro

Los registros recogen varios datos y cada uno de ellos tiene su propio tipo de dato. Para referirse a un dato concreto, se utiliza un nombre particular que el programador le asigna y que recibe el nombre de campo.

Por ejemplo, los datos personales de una persona se puede almacenar con un registro de la forma:

Persona

String nombre;

String apellido;

int edad;

Fecha fecha;

Dicho registro se ha denominado Persona y conta de 4 campos llamados nombre, apellido, edad y fecha. El campo fecha se ha definido de tipo estructurado Fecha (que es otro registro).

2. Instrucción new

Para asignar valores a una variables de tipo de dato simple basta realizar previamente una declaración de dicha de variable como una variable que almacenará valores del tipo de dato deseado. De hecho, vimos que ambas acciones se pueden llevar a cabo simultáneamente. Por ejemplo, una declaración y asignación de variable real puede ser:

float calificación = 8.5;

La asignación se puede hacer porque Processing, al realizar la declaración float calificación, te reservó el espacio de memoria suficiente para guardar reales. Processing, automáticamente, te preparó el terreno. Pero esto no lo hace con los tipos de datos estructurados.

Esta diferencia de comportamiento entre cómo Processing trata los datos simples y los datos estructurados se debe al modo en que se gestiona el ciclo de vida de los valores de una variable. En general, los lenguajes de programación tienen este comportamiento:

  • Los valores de los tipos de datos primitivos se almacenan en una zona de la memoria que se llama Stack (Pila). En el Stack se guardan los parámetros y las variables locales de todas las funciones, salvo que las variables sean de tipo estructurado. Todo lo que se almacene en el Stack está controlado por el lenguaje de programación, en el sentido de que hace las reservas de memoria pertinentes para cada valor.

  • Los datos estructurados se almacenan en una zona de la memoria que se llama Heap (Montículo). En el Heap se guardan los datos dinámicos, como los arrays o los registros. A diferencia del Stack donde todo está controlado por el lenguaje de programación, el control del Heap se delega en el programador. Esto quiere decir que tú, como programador, deberás expresamente dar la orden de que se reserve el espacio de memoria necesario para guardar tus datos.

Puedes pensar que es un fastidio esto de tener que ir reservando memoria en el Heap. Bueno, no lo es tanto si tienes en cuenta que hay lenguajes de programación donde no solo tienes que hacer la reserva de memoria, sino que además tienes que dar la orden de liberar la memoria una vez que dejes de utilizarla. Si siempre reservaras, pero nunca liberaras podrías ocupar toda la memoria del ordenador y sin memoria …​ tus programas dejarían de funcionar. Por suerte, Processing, esta segunda acción, la de liberar la memoria, la controla él y utiliza lo que llama un garbage collector (recolector de basura); pero no te libra de que ordenes las correspondientes reservas de memoria antes de usar una variable de tipo estructurado.

Tienes que ordenar a Processing que te reserve memoria para guardar los valores de una estructura de datos.

En Processing, la instrucción para hacer la correspondiente reserva de memoria se llama new y se debe de realizar después de hacer la correspondiente declaración. El esquema general que deberás de utilizar antes de hacer las correspondientes asignaciones a una variable de tipo estructurado es:

Recuerda
TipoDeDatoEstructurado  variable; (1)

variable = new ReservaMemoriaParaEste_TipoDeDatoEstructurado; (2)
1 Responde a una instrucción de declaración de variable. Observa que se hace exáctamente igual que los tipos de datos simples.
2 new es la instrucción de reserva de memoria. Lo que se indica a continuación del new y con ese nombre tan largo dependerá del tipo de dato estructurado que se vaya a utilizar.

Con la instrucción new el lenguaje reservará el espacio de memoria adecuado en el Heap necesario para la cantidad de datos que están recolectados por el tipo de dato estrucutrado.

3. Stack vs Heap

La Stack (pila) es la zona de la memoria del ordenador que almacena la información de forma temporal: cuando en una función o método se declara una variable se añade la información que almacena a la pila, pero la información desaparace cuando la función o método se ha ejecutado.

Gráficamente podemos representar la Stack como un conjunto de datos apilados donde cada dato se corresponde con el valor de una variable. El siguiente dibujo representa gráficamente la situación de la Stack para tres variables (¿de qué tipo son?)

aparienciaStack

La pila inicialmente está vacía y conforme se declaran variables se van apilando los valores asignados a las mismas. Como ejemplo, consideremos el siguiente código

1 2 3
int x = 10; x = 5; boolean ok = true;

La ejecución línea a linea y su repercusión en la Stack se muestra en la siguiente secuencia de imágenes:

  1. int x = 10; Al declarar la variable se reserva un espacio en la Stack para guardar un entero. Como además se asigna un valor, dicho valor se almacena en la Stack.

    primeraEjecucion
  2. Con x = 5; cambiamos el valor almacenado en la Stack.

    segundaEjecucion
  3. boolean ok = true; realiza lo mismo que la línea 1: añade un elmento a la pila pero ahora para un valor booleano.

    terceraEjecucion

Las variables referidas a datos simples acceden al valor almacenado en la pila. Una variable de tipo int accede a un valor de tipo de dato entero, una variable de tipo char accede a un valor de tipo de dato char, etc …​

De forma análoga, las variables referidas a datos estructurados acceden al valor almacenado en la pila, pero los valores que se almacenan en una pila para un variable de tipo de dato estructurado es una referencia.

El término referencia se utiliza para referirse a una localización de la memoria donde se almacenan los datos estructurados.

El valor de una variable de tipo-referencia o es null o es una dirección de memoria de la Heap.

null significa que no se está referenciando a ninguna localización de la memoria.

Cuando se ejecuta la correspondiente instrucción new, la variable tipo-referencia almacenará la referencia que se le asigne de la Heap.

Como ejemplo, consideremos el siguiente pseudo-código.

1 2 3
int x = 5; TDEstructurado var; var = new TDEstructurado;

Si suponemos que TDEstructurado es una estructura de datos que recopila dos datos enteros y un carácter, la ejecución línea a linea y su repercusión en la Stack y en el Heap se muestra en la siguiente secuencia de imágenes:

  1. int x = 5; declara una variable por lo que se reserva un espacio en la Stack para guardar un entero. Como además se asigna un valor, dicho valor se almacena en la Stack.

    bprimeraEjecucion
  2. TDEstructurado var; declara una variable por lo que se reserva un espacio en la Stack para guardar una referencia. Como no se asigna ningún valor, la variable var aún "no sabe" a qué lugar de la memoria de la Heap se debe de referenciar para guardar o recuperar valores. Esto se indica con el valor null (apunta a ninguna parte)

    bsegundaEjecucion
  3. var = new TDEstructurado; reserva la memoria necesaria para guardar tantos datos como recopile el tipo de dato TDEstructurado, además dicha referencia se guarda en la zona de la Stack destianda a la variable var. Supuesto que TDEstructurado almacena dos enteros y un carácter, podemos representar la situación como sigue:

    bterceraEjecucion

Gráficamente, para no ir "arrastrando" el marco (rojo) de la Stack y el marco (azul) de la Heap, trazaremos flechas desde una variable a una caja para hacer referencia a un valor de la Stack, y trazaremos flechas desde una variable a una caja etiquetada con una referencia para indicar los valores de la Heap. Con este criterio, la última representación gráfica será equivalente a la siguiente:

versionSimplificada
Recuerda
  • Los datos estructurados almacenan una referencia.

  • Cuando se declara una variable de tipo estructurado se le asigna el valor null, para indicar que dicha variable aún "no apunta" a ninguna zona de la memoria.

  • Tras ejecutar la instrucción new, Processing almacenará en la variable una referencia a la dirección de memoria donde se almacenan los datos en el Heap.

4. Datos estructurados y funciones

¿Recuerdas cómo se declaraba y definía una función? El esquema general es:

<td de retorno> nombre_de_la_función (<td 1> <nombre parámetro1>, <td 2> <nombre parámetro1>, ...) {

    Instrucciones que quieras

    return var; // Donde var, debe ser del tipo <td de retorno>
}

donde td son las iniciales de tipo de dato.

En dicho esquema en nada se restringe el tipo de dato a utilizar. El tipo de dato de los parámetros y del valor de retorno puede ser simple o puede ser estructurado. Sin embargo hay ciertas diferencias.

Recuerda que el ámbito de una variable se definía como las partes de un programa donde una variable puede ser utilizada. En concreto los parámetros y variables declaradas en un función solo tienen como ámbito esa función. Esto se debe a que estos datos están almacenados en el Stack. Te lo explico:

Imgina que apilas tus libros, uno encima de otro y apoyados por el dorso. Si miras la pila "desde arriba" solo verás el libro que esté en lo más alto de la pila, y si lees su portada solo verás su título, el autor y poco más; pero no podrás ver los títulos y autores de los libros inferiores, y mucho menos el que esté abajo del todo. Solo podrás ver la carátula del segundo libro cuando hayas quitado el libro "superior" (el que está en lo alto de la pila), y solo podrás ver el primer libro cuando hayas quitado todos los que están encima.

Pila de libros

De forma análoga actúa la Stack. Cuando invocas a un función es como si pusieras un libro. Si una función invoca a otra función, es como si sobre el libro anterior pusieras otro libro encima, y "desde arriba" solo puedes ver el último libro colocado. Por analogía, en el Stack solo podrás ver los valores de las variables locales de la última función invocada y no serán visibles los valores de las variables locales referente a las funciones que "están debajo". Para visualizar las variables del nivel inmediantamente inferior, deberás de ejecutar la función "superior" por completo. Cuando una función se ejecuta por completo todas sus valores locales son eliminadas del Stack: solo se borran las variables del "último libro". Solo podrás ver las variables de un cierto nivel cuando hayas ejecutando por completo todas las funciones "superiores". Así,

  • cada vez que que invocas una función, colocas un libro: reservas memoria en la Stack para las variables locales de la función.

  • cada vez que ejecutas por completo una función, quitas un libro: destruyes las memorias locales de la función liberando memoria en la Stack.

Por contra, lo que se almacena en el Heap siempre es visible mientras que tengamos una referencia a lo almacenado. Es como si tuvieras una cometa, de esas que ves volar en la playa o en un descampado. La cometa representa los datos dinámicos almacenados, la persona que la controla es la variable y el hilo de la cometa representa la referencia.

Cometa

Si la persona pierde el hilo, se pierde la cometa. De forma análoga si no hay una variable que almacene la referencia se pierden los datos (llegará el recolector de basura y los borrará al ver que nadie los usa).

Con estas analogías, retomemos el uso de datos estructurados en funciones. Para ello imaginemos en lo que sigue que se invoca a un función con una serie de argumentos, todos variables, sin valores literales, y que dicha función retorna un valor que responde a una estructura de datos.

4.1. Funciones que retornan un tipo de dato estructurado

Para retornar un tipo de dato estructurado tendrás que realizar el proceso que ya hacías con variables simples, pero teniendo en cuenta que deberás de hacer la reserva de memoria pertinente:

  1. declara una variable local, var, del tipo estructurado,

  2. reserva la memoria correspondiente para dicha variable, var = new TipoDato;,

  3. asigna el conjunto de valores a la variable,

  4. retorna el conjunto de valores con return var;

Ejemplo 1. Otro ejemplo visual

Imagina que tienes este pseudocódigo.

void setup() {
   td a = func();
}

td func() {
   td b = new td;
   modifica los valores asociados a b;
   return b;
}

Donde td representa a un tipo de dato estructurado. Nos preguntamos que le pasará a la variable a antes y después de la invocación a la función func(). Ejecutemoslo paso a paso.

Con la instrución td a; la variable a almacenará el valor null para indicar que no apunta a ningún lugar de la memoria. Gráficamente, su situación es:

a null2

Como en la misma línea tenemos una instrucción de asignación, evaluamos la expresión que está en el lado derecho de =; por lo que invocamos a la función func(). La ejecución de la primera línea de la función genera esta situación:

b nueva

Observa el color azul de b para indicar que será destruida cuando finalice la ejecución de func(). A continuación se modifican los valores recopilados por b;:

b modifica

Por último, la función retorna el valor almacenado en b (una referencia a una dirección de memoria) que será almacenado en a:

b retornado

Pero recuerda que se destruyen todas las variables locales; por lo que la situación final es:

b retornado borrado

4.2. Funciones con parámetros de tipo de dato estructurado

Si un parámetro responde a una estructura de datos estamos suministrando a la función un valor estructurado. Por tanto, el parámetro almacena una referencia al lugar donde dichos datos estás guardados. En este punto, tanto el argumento como el parámetro tienen una referencia al lugar donde están los datos. Si el parámetro hace modificaciones en los valores estructurados entonces cuando finalice la función el parámetro será destruido por ser una variable local y estar en el Stack, pero el argumento seguirá referenciado a los mismos datos estructurados pero con valores diferentes.

Recuerda la analogía de la cometa: es como si una persona (el argumento) le pasara otro hilo de la cometa a otra persona (el parámetro). Entonces el parámetro aprovecha ese segundo hilo para coger la cometa y cambiarle el color, para a continuación cortar su hilo. La primera persona (el argumento) seguirá teniendo su hilo enganchado a la cometa de partida pero ahora tendrá un color diferente al que tuviese antes de "prestar" su cometa.

Fíjate que según lo expuesto, podemos usar un parámetro no solo como una vía de entrada de datos a la función, sino que también nos sirve para retornar datos de salida.

Ejemplo 2. Un ejemplo visual

Imagina que tienes este pseudocódigo.

void setup() {
   td a;
   a = new td;
   func(a)
}

void func(td b) {
   modifica los valores recopilados por b;
}

Donde td representa a un tipo de dato estructurado. Nos preguntamos qué le pasará a la variable a antes y después de la invocación a la función func(). Ejecutemoslo paso a paso.

Con la instrución td a; la variable a almacenará el valor null para indicar que no apunta a ningún lugar de la memoria. Gráficamente, su situación es:

a null

Con la instrucción a = new td; se hace la correspondiente reserva de memoria y a almacena una referencia a dicha reserva.

a no null

Cuando se invoca a la función con la instrucción func(a);, el parámetro b recibe una copia del valor de a:

a b aliasing principio

La variable b se indica con color azul para reflejar el hecho de que al ser una variable local de func() y que será destruida cuando finalice la ejecución de la función. Además, la varible b está "encima" de la variable a en la Stack.

Si la función func() modifica los valores del dato estructurado, modificará los datos compartidos por a y por b.

a b aliasing

Finalizada la ejecución de la función func() todas sus variables locales son destruídas, quedando la siguiente situación en al memoria:

a b modificacion

Es decir, después de ejecutar la función func() la variable a apunta a un lugar de la memoria cuyos datos han quedado modificados por dicha función.

Observa que si ejecutaras la instrucción new actuando sobre el parámetro, éste almacenará la referencia a otra dirección de memoria (la que retorna la instrucción new). Si dicho valor no se retorna el conjunto de datos creados será destruído (por ser el parámetro una variable local).

Ejemplo 3. Seguimos con el ejemplo visual

Imagina que tienes ahora este pseudocódigo para la función func().

void func(td b) {
   b = new td;
   modifica los valores recopilados por b;
}

En este caso, antes de modificar los valores, con la instrucción new generamos esta situación:

b apunta otrolado

Observa que a la varible b se le asigna una referencia a un lugar de la memoria diferente a la referencia asignada a b (mira las etiquetas de los arcos). Así, al modificar los valores asociados a b, no estamos modificando los valores asociados a a. Cuando "salgamos" de la función func() destruiremos la variable b y no habremos alterado el dato estructurado de a, es decir: a tendrá los mismos valores tanto antes como después de la invocación a func().

Si vas a usar un parámetro como variable de retorno de datos, ten cuidado con aplicarle un new, pues perderás la referencia a los datos estructurados de entrada.

4.3. Aliasing

Se llama aliasing a la situación en la que dos variables de tipo de dato estructurado almacenan el mismo valor (de referencia).

Ejemplo 4. Un ejemplo visual

Imagina que tienes este pseudocódigo.

   td a, b;
   a = new td;
   b = a;

Donde td representa a un tipo de dato estructurado. Gráficamente, la situación es:

a b aliasing inicial

En principio, tener aliasing no es una situación extraña, pero si se confunde entre referencia y valores referenciados esto puede generar problemas graves. Se explica con un ejemplo.

En ocasiones, cuando se observa el código que vienen a continuación algunos consideran que el programa está realizando lo siguiente:

void setup() {
   td a, b;
   a = new td; (1)
   asignar valores iniciales a la variable a.
   b = a; (2)
   modifica valores a la variable a. (3)
   func(a); (4)
}

void func(td x) {
   modifica los valores recopilados por x; (5)
}
1 Se reserva memoria para los datos que recopila a.
2 Se guardan/salvan los datos de a en b.
3 Se modifican los valores de los datos que recopila a.
4 Se invoca a la función func().
5 func() modifica los valores del argumento a (x toma una copia del valor de referencia almacenado en a).

Por tanto, según lo interpretado, tanto al ejecutarse el paso 3 como después de la ejecución de la función func() la variable a habrá quedado modificada. Esto es cierto y es correcto.

Pero pensar que gracias al paso 2, la variable b aún conserva los valores iniciales que fueron asignados a la variable a (despues de los pasos 3 y 4) es un gravísimo error. Se está interpretando mal el paso 2. Se confunde entre referencia y valores referenciados. En el paso 2 no se están guardando los datos de a en b, lo que realmente se está haciendo es:

2'. Se guarda en b la referencia que contiene a.

Así, si durante la ejecución del programa se modificaron los datos a los que hacía referencia a, al finalizar el programa también estarán modificados los datos a los que hacía referencia b - simplemente porque almacenan las mismas referencias (apuntan a los mismos datos).

5. Repaso

  • Existen muchas estructuras de datos. Hay tantas como defina el lenguaje de programación (o sus librerías).

  • La instrucción para la declaración de un variable que sea de tipo estructurado es TipoDeDatoEstructurado variable;, de forma análoga a las variables de tipo de dato simple.

  • Antes de usar una variable de tipo registro siempre hay que usar la instrucción new, para reservar espacio de memoria en el Heap.

  • En una función, no solo podemos retornar un dato (simple o estructurado) mediante la instrucción return, sino que podemos retornar datos en los parámetros de la función mientras que sean de tipo estructurado.

  • Hemos de evitar el aliasing. Así evitamos cambiar los valores asociados a una variable a través de otra.