Palabras clave: Datos estructurados, Registros.

Preguntas de repaso
  • ¿Qué es una estructura de datos?

  • ¿Qué es un array y qué se utiliza para gestionar sus datos?

No sigas si no conoces las respuestas.

1. Concepto de registro

Son registros: tu DNI, un contacto de tu agenda telefónica, los datos de un alumno en una lista de asistencia a clase, el billete de entrada al cine o el del vuelo de un avión, etc …​ Todos tienen en común que recopilan las diferentes características de determinados objetos.

Un registro recolecta varios datos y no todos los datos tienen que ser del mismo tipo.

Cada uno de los datos del registro recibe el nombre de campo del registro.

2. Declaración de registros

Un registro se declara en Processing como:

class TipoRegistro {
    <tipo_de_dato> campo1;
    <tipo_de_dato> campo2;
    ...
}

donde cada campo se declara como una variable que se corresponde a un tipo de dato.

Ejemplo 1. Declaración del registro Fecha en Processing

Imagina que se quiere tener un registro de las fechas de una cierta actividad. Para ello podemos utilizar 3 variables de tipo de dato entero:

1 2 3
int dia; int mes; int año;

Pero así declarado nada nos dice de que los valores alamacenados en cada una de estas variables se corresponde a un fecha concreta (una fecha es única y relaciona un día, un mes y un año). Para poner de manifiesto el vínculo de estas 3 variables utilizaremos un registro, al que llamaremos Fecha (empieza en mayúsculas):

1 2 3 4 5
clase Fecha { int dia; int mes; int año; }

3. Datos de tipo de dato registro

Cuando defines un registro defines un nuevo tipo de dato (estructurado) que será reconocido por el lenguaje de programación y por tanto puedes declarar variables que respondan a ese tipo. La sintaxis general es:

TipoRegistro variable;
Ejemplo 2. Declaración de variables de tipo Fecha

Continuando con el ejemplo anterior, podemos definir una variable, llamémosla unaFecha, que puede ser del tipo de dato Fecha. Basta añadir al código anterior la instrucción:

1
Fecha unaFecha;

Cuando declaramos una variable, dicha variable toma el valor NULL para indicar que no sabe a qué parte de la memoria (del Heap) tiene que referenciar para recuperar o guardar datos.

unFechaNull

4. Inicialización de registros

Cuando declaras una variable de "tipo Registro", la variable está preparada para almacenar una referencia al lugar de la memoria donde se almacenarán los valores de sus campos.

Cuando se trabaja con estructuras de datos siempre se tiene que reservar de forma explícita la memoria necesaria para almacenar todos los datos que guardará el tipo de dato estructurado.

Siempre hemos de usar la instrucción new con el fin de reservar una porción de memoria a cada una de los campos del registro y se le asigne una referencia a la variable para "que apunte" a esa porción de memoria y así poder acceder a sus diferentes campos (o valores).

La sintaxis general es:

variable = new TipoRegistro();  // Observa los paréntesis ()
Ejemplo 3. Declaración de variables de tipo Fecha

Continuando con el ejemplo anterior, para inicializar la variable unaFecha, escribiremos la instrucción:

1
unaFecha = new Fecha();

Cuando usamos la instrucción new, se reserva espacio para tantos campos como tenga el registro y la variable toma el valor de la referencia de memoria que indica dónde se almacenarán esos datos.

unFechaNew

No olvides añadir los paréntesis después del tipo de dato cuando uses la instrucción new.

5. Acceso a los campos de un registro

Si en un programa utilizaras un variable de tipo registro, como por ejemplo,

class TipoRegistro {
    <tipo_de_dato> campo1;
    <tipo_de_dato> campo2;
    ...
}

TipoRegistro = variable;

necesitarás almacenar y recuperar la información de cada uno de sus campos; es decir, tendrás que "acceder" a los distintos campos de la variable variable.

El acceso a los valores de los distintos campos usa las expresiones

variable.campo1
variable.campo2
...

y utiliza dichas expresiones igual que si fueran variables simples.

5.1. Asignación de valores a los campos de un registro

Si la asignación de una variable simple es:

variable = valor;

la asignación de valores a los campos de una varible de tipo registro es

variable.campo = valor;

Observa que el lado izquierdo de = responde al acceso al campo campo del registro registro.

Ejemplo 4. Asignación de valores a variables de tipo Fecha

Continuando con el ejemplo anterior, para asignar a la variable unaFecha la fecha "18 de febrero de 2001" escribiremos la instrucción:

1 2 3
unaFecha.dia = 18; unaFecha.mes = 2; unaFecha.año = 2001;

Cuando usamos la instrucción de asignación con la notación punto, se guarda en el campo indicado el valor del resultado de la expresión del lado derecho de =.

unaFechaAsignacion

5.2. Recuperación de valores de los campos de un registro

Si la recuperación del dato almacenado en un variable se consigue mencionando a la variable, en un variable de tipo registro bastará mencionar o acceder a su campo para recuperar su valor almacenado.

Mencionar a la variable de tipo registro no recupera todos los datos asociados al registro. Recuerda que una variable de tipo registro almacena una referencia al lugar de la memoria donde se encuentran todos los valores asociados a sus campos.
Ejemplo 5. Recuperación de valores de una variable de tipo Fecha

Continuando con el ejemplo anterior, para imprimir (previa recuperación) los valores de los campos de la variable unaFecha basta escribir las instrucciones:

1 2 3
print("Dia: "); println(unaFecha.dia); print("Mes: "); println(unaFecha.mes); print("Año: "); println(unaFecha.fecha);
Ejemplo 6. Simulación simple

Analiza el siguiente código que muestra la animación de una simulación simple de un objeto que se deplaza en el plano.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
/* Introducción al Sw Científico y a la Programación Bota la bola */ // Creamos un tipo de dato nuevo. // Es un registro, al que llamamos Bola, // y que está formado por: class Bola { float x; // Coordenada x en la pantalla float y; // Coordenada y en la pantalla float vel; // La velocidad de la bola en la pantalla float diametro;// El diámetro de la bola } // Y definimos una variable global de tipo Bola // Observa que se llaman igual, pero Processing las distingue: // -- una está en mayúscula, Bola, es un registro; y // -- la otra está en minúscula, bola, es una variable Bola bola; // La función principal se encarga de inicializar las variables void setup () { size(200, 200); // Tamaño de la ventana gráfica: 300px de lado. bola = new Bola(); // Reservamos espacio de memoria en el Heap bola.x = width/2; // Colocamos la bola en el centro de la pantalla bola.y = height/2; bola.vel = 3; // En cada frame "avanza" 3px bola.diametro = 15;// Establecemos un diámetro de 15px } // La función de dibujo hará lo siguiente: void draw() { background(255); // Aplica un fondo de color blanco. // Dibuja una esfera con los datos de la bola ellipse(bola.x, bola.y, bola.diametro, bola.diametro); // Actualizamos la posición de la bola de forma proporcional a // su velocidad (con alteraciones aleatorias). bola.x = bola.x + bola.vel*random(0,1); bola.y = bola.y + bola.vel*random(0,1); // Si las nuevas coordenadas hacen que la bola salga de la pantalla, // entonces cambiamos la dirección de su velocidad if ((bola.x<0) || (bola.x>width) || (bola.y<0) || (bola.y>height)) bola.vel = -1*bola.vel; }

Observa la secuencia de pasos:

  1. se define un nuevo tipo de dato (de tipo registro),

  2. se declara una variable de tipo estructurado,

  3. se reserva la memoria correspondiente para dicha variable,

  4. para acceder a las variables del registro se usa la sintaxis:`var.campo`.

Predice el comportamiento de la bola y comprueba tus conclusiones:

6. Matemáticas y registros

Podemos definir muchos objetos y expresiones matemáticas como registros. Por ejemplo: los puntos del plano, los vectores, propiedades cinemáticas de objetos físicos, la ecuación de una recta o de un plano, las figuras geométricas, los números racionales o complejos, curvas polares, intervalos de la recta real, cada una de las ecuaciones de un sistema de ecuaciones, una función, los grafos, los cuaterniones, autómatas celulares, proposiciones lógicas, datos estadísticos, representaciones gráficas, desigualdades, etc …​

¿Cómo representaría un punto en el espacio mediante registros?

¿Y los números racionales?

7. Registros y funciones

No olvides que si en una función

  • Si en una función un parámetro responde a registro, puedes usarlo como una vía de entrada de datos a la función, pero también nos sirve para retornar datos de salida.

    • Si este es tu propósito, nunca apliques un new sobre el parámetro, pues perderás la referencia original.

    • No olvides nunca que si tienes aliasing sobre el argumento de una función, la modificación de los valores asociados a este argumento también modificará a la otra variable.

  • Si en una función quieres retornar un tipo de dato registro tendrás que

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

    2. reservar la memoria correspondiente para dicha variable, var = new TipoRegistro();,

    3. asignar el conjunto de valores a la variable mediante var.campo,

    4. retornar el conjunto de valores con return var;

Ejemplo 7. Sobre productos escalares

Se define el producto escalar de dos vectores de dimensión \(n\) como

\[\mathbf{v}\cdot\mathbf{w}= \sum_{i=1}^n v_i \cdot w_i\]

Además se sabe que ambos vectores son paralelos si \(\mathbf{v}\cdot\mathbf{w} = \|\mathbf{v}\|\cdot \|\mathbf{w}\|\), donde \(\|\mathbf{v}\|=\sqrt{\sum_{i=1}^{n} v_i^2}\).

Queremos hacer un programa que:

  1. determine si dos vectores 2D, y aleatorios, son paralelos o no. Utiliza una función sin argumentos para generar dichos vectores y una función que determine dicho paralelismo.

  2. modique un vector dado para que tenga el doble de su módulo mediante una función.

Comenzamos definiendo un nuevo tipo de dato que represente a los vectores 2D. Todo vector 2D tiene dos componentes, así que definimos un registro con dos campos.

1 2 3 4 5
// Definimos el tipo de dato vector en 2D class Vector { float x; // Dirección en X float y; // Dirección en Y }

Para crear un vector nuevo necesitamos declarar una variables, reservar memoria y asinar valores a los campos. Para ello utilizaremos una función sin argumentos. Esta función es:

1 2 3 4 5 6 7 8 9 10
* Función generadora de vectores aleatorios * @return, Un nuevo vector 2D */ Vector generaVector() { Vector aux; // Declaramos una variable de tipo rector aux = new Vector(); // Reservamos memoria y guardamos la referencia aux.x=random(0, 1000); // Inicializamos el campo x aux.y=random(0, 1000); // Inicializamos el campo y return aux; // Retornamos la referencia del nuevo vector }

Esta función cada vez que es invocada retorna un vector aleatorio nuevo. La utilizaremos en la función principal para crear dos vectores como puedes ver en el siguiente código:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/** * Función principal */ void setup() { // Declaramos dos variables de tipo vector Vector v, w; // Generamos vectores aleatorios v = generaVector(); w = generaVector(); if (sonParalelos(v, w)) println("Son vectores paralelos"); else println("NO son vectores paralelos"); }

La función principal no solo crea dos vectores. También invoca a una función para comprobar si ambos vectores son paralelos o no (mostrando el mensaje pertinente). Dicha función se define como sigue:

1 2 3 4 5 6 7 8 9 10 11 12 13 14
/** * Función que comprueba si dos vectores son paralelos * @param a, uno de los vectores * @param b, el otro vector * @return, true si son paralelos y false en otro caso */ boolean sonParalelos(Vector a, Vector b) { // Calculamos el producto escalar de los dos vectores float escalar = productoEscalar(a, b); // Comprobamos si está muy próximo a cero if (abs(escalar)< pow(10, -6)) return true; else return false; }

Esta función a su vez invoca a la función que calcula el producto escalar de dos números. Como sabemos que se generan errores de aproximación será muy difícil que el producto escalar valga exáctamente cero, por eso buscamos que su valor sea muy próximo a él.

La función que calcula el producto escalar es la más sencilla de todas, y se reduce a una línea.

1 2 3 4 5 6 7 8 9 10
/** * Calcula el producto escalar de dos vectores. * @param a, uno de los vectores * @param b, el otro vector * @return el producto escalar. */ float productoEscalar(Vector a, Vector b) { // Calculamos y retornamos el producto escalar de los dos vectores return a.x*b.x + a.y*b.y; }

Para la segunda parte, como se trata de modicar un vector dado, diseñaremos una función que contemple como parámetro un vector que se utilizara como via de entrada de datos y de salida de datos. En concreto construimos esta función:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/** * Función que duplica el módulo de un vector * @param a, vector a imprimir */ void duplicaVector(Vector a) { // Calculamos el módulo del vector float modulo = sqrt(productoEscalar(a,a)); // Calculamos el ángulo del vector respecto de la horizontal float angulo = atan2(a.y, a.x); // Modificamos los valores para duplicar el módulo. a.x = 2 * modulo * cos(angulo); a.y = 2 * modulo * sin(angulo); }

que será invocada en el programa principal como:

1 2
// Duplicamos la magnitud del vector v duplicaVector(v);

Extiende el programa anterior para incluir dos funciones.

  • Una imprime los valores de los campos de forma adecuada.

  • La otra modifica al doble los módulos de dos vectores dados y retorna el vector suma de los vectores modificados.

Compara tu solución con la mía. Este es mi programa completo:

Recuerda que no debes pulsar el icono hasta que tengas en firme una respuesta con la que comparar mi resultado.

8. Repaso

  • Un registro es una estructura formada por un conjunto de datos que se llaman campos.

  • Cada campo se declara como una variable, que puede responder a un tipo de dato simple o a un tipo de dato estructurado.

  • Antes de usar una variable de tipo registro siempre hay que usar la instrucción new().

  • Para acceder a un campo se usa la notación punto: nombreVariable.nombreCampo.

  • Si usas un registro como parámetro en una función lo puedes usar para suministrar datos de entrada o para modificar valores del argumento.

  • Si una función retornara un registro, deberás de declarar una variable, reservar memoria, asignar valores a los campos y aplicar return a dicha variable.