Lenguaje de programación. Tipos de datos

Anuncio
TEMA 4: Tipos de Datos
A partir de Algol-68 todos los lenguajes ofrecen una serie de tipos básicos y unos constructores para poder
formar nuevos datos estructurados.
4.1
Tipos de datos primitivos
• Son tipos no definidos en términos de otros tipos. Con estos tipos de datos primitivos y con los
constructores de tipos se pueden definir tipos estructurados. Los tipos de datos primitivos que
aparecen en la mayorÃ−a de los lenguajes de programación son:
• Entero
• Real o Flotante
• Booleano
• Carácter
• Entero: Ada, C: short, long ( para cambiar el rango )
C: unsignal (enteros sin signo )
• Real o Flotante: real o float
double ( precisión )
• Booleano: True, False -> Rango de este tipo
(excepción -> C ) => 0 : Falso
!= 0 : Verdadero
• Carácter: se almacena el código asociado al carácter.
4.2
Tipos ordinales definidos por el usuario
• Un tipo ordinal es aquel cuyo rango de posibles valores puede asociarse fácilmente con el conjunto
de los enteros positivos. Podemos establecer un orden dentro del tipo.
• En Pascal son ordinales: Entero, Booleano y Carácter.
• Muchos lenguajes le permiten al usuario definir nuevos tipos ordinales. Formas:
• Mediante Enumerados
• Subrango
4.2.1. ENUMERACION.
• En un tipo enumeración el programador enumera en la definición del tipo los posibles valores
(constantes simbólicas).
Ej: ( Pascal )
1
type
dia = {Lunes, Martes, Miercoles, Jueves, Viernes, Sábado, Domingo }
• ¿ A la hora de diseñar el lenguaje, en un mismo entorno de referencia, puedo definir un tipo ?. ¿
Un mismo literal puede pertenecer a dos grupos diferentes ?.
type
fin_de_semana = { Sábado, Domingo }
• Pascal y C no permiten que un mismo literal pertenezca a dos tipos distintos en un mismo entorno de
referencia.
• Ada si lo permite. A este problema se le llama Sobrecarga de Literales: están sobrecargando los
literales sábado y domingo. Ada lo resuelve analizando el contexto en el que aparezca el literal
sobrecargado. Y si lo puede resolver lo resuelve, si no da un error de ambigüedad. Si no el
programador dispone de una herramienta para resolver el problema ( lo soluciona de forma
explÃ−cita ).
dia ` Sabado
fin_de_semana ` Sábado caso de que el compilador no pueda resolverlo.
• Implementación del tipo enumeración. Asignar un entero no negativo diferente a cada uno de los
literales. La mayorÃ−a de los lenguajes prohibe realizar operaciones ( sumar, multiplicar, ... ) entre
los literales. C es una excepción. Entre literales solo pueden hacerse operaciones de comparación (
= , < , <= , >= , > ,... ). Los literales ni pueden leerse ni imprimirse.
4.2.2. SUBRANGO.
• Un subrango es una subsecuencia contigua de un tipo ordinal.
Ej: ( Pascal )
type
mayúsculas = `A' ... `Z';
indice = 1 ... 100;
laborable = Lunes ... Viernes;
• Para realizar la conexión entre el tipo de que proviene y el tipo subrango se asocia el tipo carácter a
mayúsculas, mirando de cual proviene.
Además en tiempo de ejecución tendrá que comprobar que esa asignación ( el valor ) está dentro del
rango lo que supone un sobrecarga.
4.3
Tipos Compuestos
• Un tipo compuesto es un tipo estructurado o formado por valores simples. Los lenguajes de
programación aportan gran cantidad de tipos estructurados: uniones, registros, conjuntos, arrays,
2
lista, árboles, archivos, ...
• Se pueden estudiar de una manera formal y reducirse a:
• Producto Cartesiano: tuplas, registros.
• Uniones Disjuntas: registros variantes, uniones.
• Aplicaciones: arrays.
• Conjuntos Potencia: conjunto.
• Tipos recursivos: estructuras dinámicas.
4.3.1. PRODUCTO CARTESIANO.
• El producto cartesiano de dos tipos S y T: S x T es el conjunto de todos los pares ordenados de
valores tal que el primer valor pertenece al conjunto S y el segundo valor pertenece al conjunto T.
S x T = { (x, y) / x â
S, y â
T}
• La cardinalidad ( número de elementos ) del conjunto producto cartesiano:
|| S x T || = || S || â
|| T || || S ||: cardinalidad de S.
• Para n conjuntos:
S1 x S2 x ... x Sn --> n tuplas (S1, S2, ... , Sn)
S x S = S2
S x S x S = S3
... Si n=0
S x ... x S = Sn S0 = ( ) ó 0-tupla ó Unit
( n veces ) tupla vacÃ−a
• PASCAL y ADA: implementan el producto cartesiano con registro.
• C: estructuras.
Ej: (Pascal)
Type
Fecha = record
Mes:mes;
DÃ−a:1..31;
End;
Estamos definiendo: { enero, ... , diciembre } x { 1 ... 31 }
3
• Para un conjunto cartesiano de n conjuntos existen n Operaciones de Proyección. Permiten extraer el
valor que ocupa la posición i-ésima. En el ejemplo de Pascal:
f.mes
f.dia Ambas son operaciones de proyección.
• En el momento de la ejecución del programa, las celdas de memoria que se asocian para cada
campo, ocupan posiciones contiguas. En general, cada uno de estos campos puede ser de un tipo
distinto, lo que implica que cada campo tendrá un tamaño diferente. Por ello hay que guardar:
• Tipo correspondiente a cada campo.
• Desplazamiento correspondiente a cada campo a partir de la dirección base ( para poder hacer una
operación de proyección ).
La estructura:
DESCRIPTOR TIPO DE LA ESTRUCTURA REGISTRO EN TIEMPO DE COMPILACIÃ N.
( en tiempo de ejecución no se necesita ningún descriptor ).
Nombre Registro
Nº Componentes
Nombre1
Tipo1
Desplazamiento1
Nombre2
Tipo2
Desplazamiento2
.......
NombreN
TipoN
DesplazamientoN
Dirección
Dirección base en la que comienza el registro.
4.3.2. UNIONES DISJUNTAS O SUMA.
• La unión disjunta o suma de valores pertenecientes a dos tipos S y T (se denota S+T) es el conjunto
de valores en el que cada valor se elige de cada uno de los conjuntos S ó T y se etiqueta para indicar
el conjunto del que ha sido elegido.
S y T, S + T etiqueta
S + T = { ( true, x ) | x â
S } â ª { ( false , y ) | y â
T}
“true y false son etiquetas”
• Cardinalidad: || S + T || = || S || + || T ||
• Sobre los valores del conjunto S + T se pueden realizar dos operaciones básicas:
4
• Comprobar el valor de la etiqueta para conocer el conjunto del que proviene el segundo elemento.
• Proyección del valor original en S ó T.
• Las uniones se pueden extender: S1 + S2 + ... + Sn ( necesitamos una etiqueta que tome n valores
diferentes ).
• ¿ Como se ven reflejadas las uniones disjuntas en los lenguajes de programación ?
• Registros Variantes: Ada, Pascal.
• Uniones: C.
• Cuando un lenguaje de programación decide adoptar la unión disjunta se debe plantear 2
questiones:
• Problema de la comprobación de tipos.
• Forma concreta que adopta la union.
• La mayoria de los lenguajes de programación incluyen la unión disjunta relacionada con la
estructura registro ( es el caso de Ada o Pascal ). Sin embargo en C, la estructura unión es una
estructura diferente de la estructura registro. En C, no se definen etiquetas para las uniones.
Ej de registro variante en Pascal:
Type
Forma = ( punto, circulo, rectangulo );
Figura = record
x,y: real;
case forma_figura: forma of
punto: ( ); “tupla vacia ( Unit ) ”
circulo: ( radio: real );
rectangulo: ( lado1, lado2: real );
end;
Figura = real x real x ( Unit + real + ( real x real ) )
Producto cartesiano: real x real
Unión Disjunta: Unit + real + ( real x real )
El conjunto de valores que yo estoy definiendo para el tipo figura es más que un producto cartesiano, es un
producto cartesiano por una unión disjunta.
• Implementación:
Se utiliza la misma dirección base para cada uno de los elementos variantes, y se reserva espacio para el
5
elemento variante de mayor tamaño. Tendremos que guardar en el descriptor que empleamos en tiempo de
compilación: la etiqueta, el tipo de la etiqueta y la asociación entre el tipo de la etiqueta y la tabla de casos.
Descriptor:
• Si el lenguaje no hace una comprobación de tipos en tiempo de ejecución entonces en tiempo de
ejecución no se almacena nada. ( C y Pascal ).
4.3.3.APLICACIONES.
• Consideremos una aplicación m que aplica cada valor x del conjunto S a un valor en el conjunto T.
Ese valor en el conjunto T se llama imagen de x bajo m, m(x).
m: S T
entonces
S T = conjunto de todas las posibles aplicaciones posibles de S en T.
ST={m/xâ
Sâ
m(x) â
T}
• Cardinalidad: || S T || = || T || || S ||
• ¿ En qué estructura de datos se ve reflejada la aplicación ?. En los arrays (vector, matriz o
tabla).
El array es lo que se llama una aplicación finita por que es una aplicación que va desde un conjunto finito
que se denomina conjunto Ã−ndice a otro conjunto finito que se llama conjunto componente.
Ej Pascal:
array [ S ] of T;
S T ( estamos definiendo esta aplicación ).
• Muchos lenguajes de programación permiten definir arrays multidimensionales ( matrices ).
Ej: (Pascal) (array n-dimensional)
Type
a = array[ i1 .. s1, i2 .. s2, ... , in .. sn ] of T
( aplicación que va desde un solo Ã−ndice al conjunto T ).
a = { i1 .. s1} x { i2 .. s2 } x ... x { in .. sn } T
( aplicación entre una n-tupla y el conjunto T ).
• Problemas con los que nos encontramos a la hora de adoptar una aplicación:
• Tipo de los Ã−ndices ( de qué tipo puede ser S ) y limite inferior de los Ã−ndices.
6
• En C: el tipo de los indices es un subrango de los enteros.
• En Pascal o Ada: el tipo de los indices es cualquier tipo ordinal.
• Algunos lenguajes fijan ellos el lÃ−mite inferior ( 0 ó 1 ). C lo fija a 0 y en otros lenguajes lo decide
el programador.
• Vinculación de Ã−ndices y categorias de arrays. La vinculación del tipo del Ã−ndice a un array
normal se hace de manera estática. Sin embargo, el rango de valores que puede adoptar ese Ã−ndice
a veces no se vincula de manera estática al array, sino de manera dinámica. Hay cuatro tipos de
arrays:
• Array estático: Vinculación de valores de los Ã−ndices y asignación de espacio al array estática.
Ej:
Pascal: array global â
C: static â
array estático.
array estático.
• Array dinámico de pila fijo: Vinculación de rango de valores de los Ã−ndices estática y
asignación de espacio dinámica ( en la pila de ejecución ).
Ej:
Pascal, C, Ada: arrays definidos en un subprograma ( arrays locales ).
• Array dinámico de Pila: Vinculación de rango de Ã−ndices y asignación de espacio dinámicas.
Pero una vez se fija no puede cambiarse.
Ej: (Ada)
get (long _ lista); leemos un dato entero.
declare se crea un nuevo bloque en la pila de ejecución.
lista: array ( 1 ..long_lista ) of integer;
begin
{ cuerpo de la ejecución }
end;
• Array dinámico de heap: Vinculación del rango de Ã−ndices y asignación de espacio dinámicas
( en tiempo de ejecución). Pero ahora puede cambiar el tamaño o la dirección en tiempo de
ejecución.
• Implementación del tipo array: Los elementos del array se almacenan en celdas contiguas de
memoria. El código para acceder al elemento del array se genera en tiempo de compilación y es en
tiempo de ejecución cuando se produce el direccionamiento a ese código.
• Arrays unimensionales; Para calcular la dirección de un elemento del array:
7
@v[ k] = @v[ li ] + ( k - li ) * e @v[ li ]: direccion base del array.
â
@v[ k] = ( @v[ li ] - li*e ) + k * e ( @v[ li ] - li*e ): es cte y puede calcularse en t de comp
li: limite inferior.
e:tamaño de un elemento del array.
• Descriptor de la estructura en t. de compilación:
Nombre del Array
Tipo del elemento
Tipo del Ã−ndice
LÃ−mite Inferior
LÃ−mite Superior
Dirección base del array
Si alguna de estas estructuras se vincula en tiempo de ejecución, tendriamos que mantener esta parte del
descriptor en tiempo de ejecución.
• Arrays Multidimensionales:
Ej: una matriz; Hay dos posibilidades:
• Almacenamiento por Filas.
• Almacenamiento por Columnas.
El programador no deberÃ−a obviar esto del lenguaje de programación. Un uso ineficiente del lenguaje
podrÃ−a provocar un fallo de página ( por la paginación ).La mayoria de los lenguajes de programación
hacen un almacenamiento por filas.
La dirección del elemento ij:
@m[ i, j ] = @m [ fi, ci ] + ( (i - fi ) n + ( j - ci ) ) * e
m: nombre de la matriz
@m [ fi, ci ]: dirección base del primer elemento de la matriz.
e: tamaño del elemento de la matriz.
â
@m[ i, j] = @m[ fi, ci ] - ( fi*n + ci )*e + (i*n + j) *e
(i*n + j) *e: parte variable
@m[ fi, ci ] - ( fi*n + ci )*e: parte constante. Se puede calcular en tiempo de compilación ( si la asignación
de espacio se realiza de manera estática).
Descriptor en tiempo de compilación:
Nombre Array
8
Tipo Elemento
Numero Dimensiones
Tipo Indice 1
Lim_Inf Indice 1
Lim_Sup Indice 1
.................
Tipo Indice N
Lim_Inf Indice N
Lim_Sup Indice N
Dirección
Si alguna de las partes del descriptor no se conociera hasta el tiempo de ejecución, deberá mantenerse el
descriptor hasta el tiempo de ejecución.
4.3.4. CONJUNTOS POTENCIA.
• Consideremos un conjunto S. Al conjunto de todos los conjuntos de S se le denomina conjuntos
potencia o conjunto de las partes de S. Formalmente:
P(s) = { s | s â
S}
• Operaciones con los conjuntos Potencia:
• Pertenencia
• Inclusión (contenido)
• Unión
• Intersección
• Cardinalidad: || P(s) || = 2 || S ||
• El único lenguaje imperativo que implementa directamente los conjuntos potencia es Pascal: set of T
Ej:
Type
Color = ( rojo, verde, azul );
Var
C = set of color; P(color)
valores que puede tomar C (son ellos mismos conjuntos):
{ }, { rojo }, { verde }, { azul }, { rojo, verde }, { rojo, azul }, { verde, azul },
{ rojo, verde, azul }.
En Pascal solo se pueden construir conjuntos de tipo ordinal.
9
• Implementación: Una posible implementacion del tipo conjuntos potencia es representar los
conjuntos como cadenas de bits:
Ej:
Type
Nota = set of `A' .. ` E `;
[ `A', `D', `E' ] 1 0 0 1 1 ( representación asociada a ese conjunto:
ABCDE)
( cada elemento del conjunto nota se representa con 5 bits ).
• Ventajas: Permite que las operaciones tÃ−picas de conjuntos puedan realizarse mediante operaciones
lógicas que permiten todos los tipos de lenguajes máquina.
• Unión conjuntos OR
• Pertenencia
`B' in nota AND
01000
4.3.5. TIPOS RECURSIVOS.
• Un tipo recursivo es aquel cuyos valores están compuestos de valores del mismo tipo. Un tipo
recursivo está definido en términos de él mismo, (T = .... T ....), se define mediante una
ecuación de conjuntos recursiva.
• La cardinalidad de un tipo recursivo es siempre infinita ( no podemos enumerar todos sus valores)
aunque la cardinalidad de los conjuntos que aparecen en la parte de la derecha se finita.
• Implementación. Si el lenguaje es imperativo: (Pascal, Ada, C) debe realizarse la implementación
utilizando el tipo puntero.
• Si el lenguaje es funcional (Haskell) sobre todo los que se llaman modernos, debe realizarse la
implementación definiendo tipos recursivos directamente.
Ej: Tipo recursivo lista.
Lista: secuencia de valores de cualquier numero de componentes incluido ninguno.
Longitud: numero de componentes de la lista.
Lista vacÃ−a: lista de longitud cero (sin componentes).
Lista homogenea: todos los elementos son del mismo tipo.
Lista Enteros:
ListaEnteros = Unit + ( Entero * ListaEnteros )
10
0-tupla ( )
( de otra manera ):
ListaEnteros = { ( nil, ( ) ) } â ª { ( cons, ( i, l ) ) | i â
Entero, l â
ListaEnteros }
etiqueta elto etiqueta par
PodrÃ−amos intentar buscar soluciones sucesivas a esa ecuación. Los elementos del conjunto solución a
esa ecuación:
( nil, ( ) ) â ¡ nil ( para simplificar )
( cons, ( i, nil ) ) i â
entero
( cons, ( i, cons( j, nil) ) ) i, j â
entero
(cons, ( i, cons, ( j, cons( k, nil) ) ) ) i, j, k â
entero
• Algunos autores incluyen un notación especial para hacer referencia a un lista: S*
S* = Unit + ( S x S* ) S: tipo base de la lista
• ¿ Cómo definirÃ−amos una lista en Haskell ?
data Bool = True | False ( definición del tipo booleano ).
| alternativa
data [a] = [ ] | a: [a] : constructor
data Lista a = Nil | a `cons' ( lista a )
`cons' operador de construcción
( lista a ) constructor
Otro ejemplo: Definición del tipo recursivo árbol binario:
ArbolBEnteros = Unit + ( Entero x ArbolBEnteros x ArbolBEnteros )
subárbol izq. subárbol dcho
• Soluciones a esta ecuación:
( nil, ( ) ) â ¡ nil
( nodo, ( i, nil, nil ) ) i â
Entero Ôrbol con 1 elemento.
( nodo, ( i, nodo( j, nil, nil ), nil ) ) i, j â
Entero
( nodo, ( i, nodo( j, nil, nil ), nodo( k, nil, nil ) ) ) i, j, k â
Entero
11
• Una definición para este tipo en Haskell:
Data ArbolB t = Nil | Nodo t ( ArbolB t ) ( ArbolB t )
Tipo Puntero
4.4
• El tipo puntero define variables cuyo rango toma valores que hacen referencia a direcciones de
memoria y a un valor especial: nil.
• Nil es una dirección inválida que se emplea para indicar que el puntero no hace referencia a
ningún objeto válido.
• Uso de los punteros:
• Para realizar direccionamiento indirecto. Es muy utilizado en el lenguaje ensamblador. Caso del C
incorpora este uso al tipo puntero.
• Administración del almacenamiento dinámico en el Heap.
• Operaciones con los punteros:
• Asignación. En la operación de asignación lo que se hace es fijar el puntero a la dirección de un
objeto.
• Dar referencia. En la operación dar referencia cuando un puntero aparece en una expresión o bien
hace referencia al contenido de la celda de memoria a la que se está vinculado, o bien hace
referencia a un objeto cuya dirección de memoria se almacena en la celda a la que está vinculada el
puntero.
Este último caso es el de Dar referencia (lo que hace es resolver la referencia indirecta ).
• Problemas a la hora de decidir los punteros:
• Comprobación de tipos: El tipo del objeto al que pueda apuntar un puntero se denomina “tipo
dominio“.
• PL / I: Primer lenguaje imperativo que incorporo el tipo puntero. Permitio que cambiara de manera
dinámica ( tiempo de ejecución ) el tipo dominio de un puntero. A partir de entonces se resolvió
este problema ( se asigna de manera estática el tipo dominio ).
• Punteros colgantes ( Danghing pointers ). Un puntero o referencia colgante es un puntero que contiene
una referencia a una variable dinámica que ya ha sido desasignada.
Ej: C
Int *p, j;
Void f (void)
{
int i;
p = &i;
.....
12
}
.....
f( );
j = *p + 5;
Otro tipo de manifestación de los punteros colgantes:
Ej: Pascal
var
suma, p: ^real;
.....
new (suma);
p := suma;
dispose (suma);
Varios apuntadores apuntando a una variable dinámica y hacemos un dispose de esa variable dinámica.
• Objetos pérdidos. Un objeto perdido es un objeto de asignación dinámica que ya no es accesible
por el programa de usuario ( aunque ese objeto contenga datos ). A estos objetos perdidos se le llama
basura por:
• Por definición no se pueden acceder.
• El sistema no puede reincorporarlo a la zona de espacio disponible en el heap.
Ej Pascal
Var
Suma:^real;
..................
new (suma)
suma^:27.7;
..................
new (suma); ó suma:=p;
2 formas de crear un objeto perdido
Se le asigna otra dirección
13
Los tipos puntero se incluyen en la mayorÃ−a de los LP imperativos (si no seria imposible definir tipos
recursivos de datos).
• Implementación del tipo Puntero:
• 1) SOLUCIONES AL PROBLEMA DE LAS REFERENCIAS COLGANTES:
Cuando el problema: haber hecho un dispose de una variable dinámica (2º uso de los punteros
únicamente)
• Utilización de “lapidas”
HEAP
P
VA Variable dinámica
q
r
Hago un dispose ( se libera memoria)
• El resto de punteros hacen referencia a una dirección invalida (problema).
• Solución: Ahora los punteros hacen referencia a otro objeto del Heap: Lapida
P
VA
q
r Variable dinámica
Dispose (r)
Nunca se libera espacio que ocupa lapida ahora apunta a NILL
• Esta solución conlleva un costo en tiempo en espacio:
• Hay que reservar espacio en el Heap para cada una de las variables dinámicas (la Lapida y las
variables dinámicas)
• En tiempo: Cada referencia necesita resolver 2 indirecciones.
b) “Cerradura y llave”
• En este caso los apuntadores se implementan como un par ordenado:
(llave, dirección) La del elemento al que hace referencia en el Heap
14
Entero
• A las variables dinámicas del Heap se le añade una cabecera,
en esta se almacena un valor entero que se llama cerradura.
• En el momento de la asignación : New (p)
• Se crea un valor entero
• Se reserva espacio en el Heap para
• Se coloca en “dirección”, la dirección del objeto
P (17, ) r (17, )
Si hacemos una asignación q:=p
(17, )
• Todos los apuntadores que hacen referencia a ese objeto tienen el mismo valor en la llave. Para
realizar un acceso se comprueba que el valor de la llave del apuntador coincide con el valor de la
cerradura.
• A la hora de realizar la desasignacion, Dispose (r), se le asigna a la cerradura un valor invalido.
P (17, )
Entonces
r(-1, )
• 2) SOLUCIONES AL PROBLEMA DE LOS OBJETOS PERDIDOS
• Cuando tenÃ−amos un objeto en el Heap al que no podÃ−amos hacer referencia desde ninguna
variable del programa. Ni tampoco podrÃ−a reutilizarse ese espacio. Soluciones:
a) Método del contador de referencias (Garbage collection)
Necesitamos reservar espacio en el heap para almacenar el objeto, reservamos también espacio para el
contador (numero de punteros que referenciamos a ese objeto).
PP
Qq
AsÃ− es fácil localizar objetos perdidos objetos con el contador a cero
b) Método de la recogida de Basura
• La asignación de espacio en el heap se realiza cuando se pide (no se tiene precaución).
Problema: Cuando ya no hay espacio en el heap.
15
Algoritmo de Recogida de basura.
Necesitemos espacio adiciona en el heap.
1º) Marcar todos los objetos del heap como basura
2º) Se inspeccionan los punteros del programa
Los objetos a los que apuntan estos apuntadores se desmarcan.
Se desmarcan
.
.
.
3º) Los objetos que siguen marcados se incorporan a la lista de espacio disponible.
Este algoritmo tiene un costo de tiempo bastante fuerte.
Tema 4: Tipos de Datos LPR
1
16
X
Lapida
X
Cabecera
Objeto dinámico
Entero
Objeto dinámico
Cerradura
Objeto dinámico
Cerradura -1
Objeto dinámico
Se libera este espacio
16
Como no coincide 17 con -1 ya no se puede acceder.
Imposible acceder a una referencia invalida
0
Objeto
2
Objeto
X
Objeto
Objeto
Objeto
Objeto
Este objeto podrÃ−a contener referencia a otros objetos del heap
Toma la dirección de i (objeto dinámico de pila: acaba con la ejecución de la función ) y se la asigna al
puntero p.
Error o referencia no válida. *p ya no está dentro de la función f.
17
Descargar