Está en la página 1de 24

Capítulo 4

Tipos de datos

Cualquier programa, no importa qué lenguaje use, se puede considerar como la especificación
de un conjunto de operaciones que se van a aplicar a ciertos datos en un orden determinado.
Existen diferencias básicas entre los lenguajes en cuanto a los tipos de datos permisibles, los
tipos de operaciones disponibles y los mecanismos que se suministran para controlar el orden
en el que las operaciones se aplican a los datos. Estas tres áreas, datos, operaciones y control,
forman la base de los cínco capítulos siguientes y también conforman el marco para gran
parte del análisis y comparación de lenguajes en la parte 11. Este capítulo considera los datos,
tipos y operaciones que se incorporan comúnmente en los lenguajes, en tanto que en el capítulo
5 se amplía ese concepto a las extensiones definidas por el programador a estos tipos básicos
de datos, un concepto que llamamos czbs/rcíccz.ó#. En el capítulo 6 se examinan las operaciones
individuales y el ordenamiento secuencial que controlan la ejecución de cada componente.
En el capítulo 7 se estudia cómo se organizan las abstracciones en torno a componentes de
programas que se conocen ordinariamente como swbprogrcJwczs o procec7Í.mJ.e#/os y, por último,
en e] capítulo s se presentan los conceptos de ¢ere#c/.c} y mé/ocÍos para permitir al programador
ampliar las capacidades del lenguaje de programación con técnicas or;.c#/czc7czs cz o¿y.e/os.

4.1 PROPIEDADES DETIPOS Y OBJETOS

En primer término se investigan las propiedades que definen objetos de datos en un lenguaje
de programación. Después se analizan tipos representativos en casi todos los lenguajes de
programación en términos de características disponibles a partir del hardware de la compu-
tadora real (es decir, tipos elementales de datos) y características que por lo general se simulan
con softwareh(es decir, tipos de datos estructurados).

4.1.1 0bjetos de datos, variables y constantes


Las áreas de almacenamiento de datos de una computadora real, como la memoria, registros
internos y medios. externos, tienen por lo general una estructura relativamente simple como
series de bits agrupados en bytes o palabras. Sin embargo, el almacenamiento de datos de la
computadora virtual para un lenguaje de programación tiende a tener una organización más com-
pleja, con arreglos, pilas, números, cadenas de caracteres y otras formas de datos que existen
en diferentes puntos durante la ejecución de un programa. Es conveniente usar el término

107
108 Tiposdeddtos Cdp. 4

AE ioooi A 0000000000010001

(a) Objeto de datos: Una localidad (b) Valor del dato: Un (c) Variable enlazada: Objeto
en la memoria de la computadora con patrón de bits usado por el de datos enlazado al valor de
el nombre A. traductor siempre que se dato 17.
usa en el programa el
número 17.

Figura 4.1. Un objeto de datos de variable simple con valor de 17.

o¿y.e/o c7e c7oÍos para referirse a un agrupamiento en tiempo de ejecución de uno o más datos
en una computadora virtual. Durante la ejecución de un programa, existen muchos objetos de
datos de distintos tipos. Más aún, en contraste con la organización relativamente estática de
las áreas de almacenamiento subyacentes de una computadora real, estos objetos de datos y
sus interrelaciones cambian de manera dinámica durante la ejecución del programa.
Algunos de los objetos de datos que existen durante la ejecución de programas son c7e##i.c7os
por e/ programc}dor; es decir, son variables, constantes, arreglos, archivos, etc., que el pro-
gramador crea y manipula de manera explícita a través de declaraciones y enunciados en el
programa. Otros objetos de datos son c7e##z.cJos por e/ s;.s/emc}; es decir, son objetos de datos
que la computadora virtual construye para ``mantenimiento" durante la ejecución de programas
y a los cuales el programador no tiene acceso directo, como pilas de almacenamiento en tiem-
po de ejecución, registros de activación de subprogramas, memorias intermedias de archivos
y listas de espacio libre. Los objetos de datós definidos por el sistema se generan ordinariamente
de manera automática según se requieren durante la ejecución de programas, sin especificación
explícita por parte del programador.
Un objeto de datos representa un recj.pj.en# pcwc} vcr/ores c7e c7czÍos, un lugar donde los
valores de datos se pueden guardar y más tarde recuperar. Un objeto de datos se caracteriza
por un conjunto de oírj.bw/os, el más importante de los cuales es su Íj.po c7e c7ofos. Los atributos
determinan el número y tipo de valores que el objeto de datos puede contener y también pres-
criben la organización lógica de esos valores.
Un vcz/or c7e c7cr/os puede ser un sólo número, carácter o, posiblemente, un apuntador a
otro objeto de datos. Un valor de datos se representa ordinariamente por medio de un patrón
particular de bits en el almacenamiento de la computadora. Por ahora, consideramos que dos
valores son iguales si los patrones de bits que los representan en el almacenamiento son idén-
ticos; sin embargo, esta sencilla definición es insuficiente para datos más complejos. Es fácil
confundir objetos de datos con valores de datos, y en muchos lenguajes la distinción no se
hace con claridad. La distinción se aprecia tal vez con más facilidad advirtiendo las diferencias
de implementación: un objeto de datos se representa comúnmente como almacenamiento en
la memoria de la computadora. Un valor de datos está representado por un patrón de bits.
Decir que un objeto de datos Á contiene el valor 8 significa que se hace que el bloque de al-
macenamiento que representa a Á contenga el patrón particular de bits que representa a 8,
como se muestra en la figura 4.1.
Si se observa el curso de la ejecución de un programa, ciertos objetos de datos existen al
principio de la ejecución y otros se crean dinámicamente durante la misma. Algunos objetos
de datos se destruyen durante la ejecución; otros persisten hasta que el programa concluye.
Sec. 4.1. Propiedóc]es de tipos y objetos 109

Así pues, cada objeto tiene un /z.empo cíe vi.c7a durante el cual puede usarse para guardar
valores de datos. Un objeto de datos es e/eme#/c}/ si contiene un valor de datos que siempre se
manipula como una unidad. Se trata de una esfrztc/cm c7e c7c}/os si es un agregado de otros
objetos de datos.
Un objeto de datos participa en varios e#/czces durante su tiempo de vida. Mientras que los
atributos de un objeto de datos no varían durante su tiempo de vida, los enlaces pueden
cambiar de manera dinámica. Los atributos y enlaces más importantes son:
1. rí.po. Esta asociación se hace por lo común en el tiempo de traducción del programa
asociando el objeto de datos con el conjunto de valores de datos que el objeto puede
tomar.
2. £occ7/j.c7czcJ. El enlace con una localidad de almacenamiento en la memoria donde el
objeto de datos está representado no es ordinariamente modificable por el programador,
sino que es establecido y puede ser modificado por las rutinas de gestión de almacena-
miento de la computadora virtual, como se expone en la sección 5.4.
3. ycz/oJ'. Este enlace es por lo general resultado de una operación de asignación.

4. Nomóre. El enlace a uno o más nombres por medio de los cuales se puede hacer refe-
rencia al objeto durante la ejecución del programa se establece por lo común mediante
declaraciones y se modifica a través de llamadas y devoluciones de subprogramas, co-
mo se expone en el capítulo 7.

5. Compo#e#/e. El enlace de un objeto de datos a uno o más objetos de datos de los


cuales es un componente se suele representar por un valor de apuntador, y se puede
modificar a través de un cambio en el apuntador, como se explica en la sección 4.3.9.

Variables y constantes

Un objeto de datos que el programador define y nombra explícitamente en un programa se


conoce como una vcír/.c}b/e. Una vcrrj.cíb/e sj.mp/e es un objeto elemental de datos con nombre.
Se piensa por lo común que el valor (o valores) de una variable es modificable por operaciones
de asignación; es decir, el enlace de objeto de datos a valor puede cambiar durante su tiem-
po de vida.
Una co#s/cz#/e es un objeto de datos con nombre que está enlazado en forma permanente a
un valor (o valores) durante su tiempo de vida. Una /Í./ercz/ (o co#s/c}#/e /J./ercJD es una constante
cuyo nombre es simplemente la representación por escrito de su valor (por ejemplo, "21" es
la representación decimal por escrito de la constante literal que es un objeto de datos cuyo
vailor es 21). U" constante definida por el programador es una. constante cuyo nombre es
elegido por el programador en una definición del objeto de datos.
Puesto que el valor de una constante está enlazado permanentemente a su nombre durante
su tiempo de vida, el traductor también conoce ese enlace. Por consiguiente, si un programador
escribe en C: #c7e/#e Á4Á.Y 30, esa información es conocida durante la traducción. EI
compilador de C puede utilizar esa información sabiendo que el valor no puede ser alterado.
(Por ejemplo, el enunciado de asignación ^4Á.Y= 4 sería ilegal, puesto que ^4Á.Yes siempre la
110 Tiposdedd[os Cdp 4

EJEMPLO 4.1. Variables simples en C.

Un subprograma en C puede incluir la declaración:


int N.'
la cual declara un objeto de datos simple N de tipo eníero. Posteriormente en el subprogra-
ma, la asignación;
N - 2J .,
se puede usar para asignar el valor de dato 27 a N. La situación se describiría de manera
algo más compleja como sigue:

1. La declaración especifica un objeto elemental de datos de tipo eníero.

2. Este objeto de datos se va a crear al entrar al subprograma y a destruir a la salida; por


tanto, su tiempo de vida es la duración de la ejecución del subprograma.

3. Durante su tiempo de vida, el objeto de datos va a estar enlazado al nombre "N", a


través del cual se puede hacer referencia a él, como ocurre en el enunciado de
asignación anterior. Se pueden enlazar otros nombres al objeto de datos si el mismo se
pasa como argumento a otro subprograma.

4. lnicialmente, ningún valor está enlazado al objeto de datos, pero el enunciado de


asignación enlaza temporalmente el valor 27 a él hasta que alguna asignación poste-
rior a N cambia el enlace.

5. Ocultos para el programador, existen otros enlaces efectuados por la computadora


virtual: el objeto de datos N se hace un componente de un reg/.síro de ací/.vac/.Ón, un
objeto de datos que contiene todos los datos locales para el subprograma, y a este
registro de activación se le asigna almacenamiento en una p/./a en Í/.empo c/e e/.ecuc/.Ón
(otro objeto de datos oculto) en el momento en que comienza la ejecución del
subprograma. Cuando el subprograma concluye, este almacenamiento se libera para
volver a usarlo y se destruye el enlace del objeto con la localidad de almacenamiento
(esto se analiza en mayor detalle en el capítulo 7).

constante 30; tiene tanto sentido como escribir el enunciado de asignación 30 = 4.) A veces,
el compilador puede usar información acerca de valores de constantes para evitar generar
código para un enunciado o expresión. Por ejemplo, en el enunciado if:

if (MAX < 2)t ... )

el traductor ya tiene los valores de datos para las constantes A4Á.Y y 2, puede computar que es
falso que 30 sea menor que 2 y, por tanto, puede pasar completamente por alto cualquier
código para el enunciado if.

Persistencia. En la actualidad, casi todos los programas se desarrollan todavía usando el mo-
delo de "procesamiento por lotes" (sección 1.4.1). Es decir, el programador supone la serie
de sucesos siguiente:
Sec. 4.1. Propieclddes de tipos y objetos mu

EJEMPLO 4.2. Variables, constantes y literales en C.

Un subprograma en C puede incluir las declaraciones:

const int MAX=30;


int N;

Podemos entonces escribir las asignaciones:

N = 27;
N = N + MAX;

N es una variable simple; MAX, 27 y 30 son constantes. N, MAX, "27" y "30" son nombres
para objetos de datos de tipo enfero. La declaración de constante especifica que el objeto de
datos llamado MAX deberá enlazarse permanentemente (por el tiempo que dure la ejecución
del subprograma)` al valor 30. La constante MAX es una consíanfe defí.n/.da por e/ progra-
mac/or porque el programador define en forma explícita el nombre para el valor 30. El nombre
"27", por otra parte, es una /Í.Íera/ que nombra el objeto de datos que contiene el valor 27.
Estas literales se definen como parte de la definición misma del lenguaje. La distinción
importante, aunque confusa, es aquí entre el va/or27, que es un entero representado como
una serie de bits en almacenamiento durante la ejecución del programa, y el mombre "27",
que es una secuencia de dos caracteres "2" y "7" que representa el mismo número en forma
decimal, según está escrito en un programa. C tiene tanto declaraciones de constante, así en
este ejemplo, como definiciones de macro al igual que #define MAX 30, Ia cual es una
operación en tiempo de compilación que causa que todas las referencias a MAX en el programa
se cambien a la constante 30.

Adviértase que, en este ejemplo, la constante 30 tiene dos nombres, el nombre "MAX" definido
por el programador y el nombre de literal "30", y ambos se pueden usar para hacer referencia
a un obj.eto de datos que contenga un valor 30 en el programa.

También es necesario darse cuenta de que

#define MAX 30

es un mandato que el traductor usa para equiparar MAX con el valor 30, en tanto que el
atributo cons/ en C es una directiva de traductor que afirma que la variable MAX siempre
contendrá el valor 30.

1. El programa se carga en la memoria.


2. Se ponen a disposición del programa datos externos apropiados (por ejemplo, cintas,
discos).
3. Los datos de entrada pertinentes son leídos hacia variables del programa, las variables
se manipulan y luego los datos de resultado se escriben de nuevo en su formato externo.
4. El programa concluye.
112 Tiposcledótos Cdp. 4

El tiempo de vida de las variables del programa está determinado por el tiempo de ejecución
del programa; sin embargo, el tiempo de vida de los datos suele extenderse más allá de esa
única ejecución. Se dice que los datos son pers;.s/e#/es y continúan existiendo entre ejecuciones
del programa.
Muchas aplicaciones actuales no encajan fácilmente en este modelo. Considérese un sistema
de reservaciones de línea aérea. Para reservar un asiento se llama al agente de viajes, el cual
interroga al sistema de reservaciones e invoca programas que verifican horarios, precio y
destinos. Los datos y los programas coexisten esencialmente de manera indefinida. En este
caso, contar con un programa que represente datos persistentes permitiría proyectar con más
eficiencia este tipo de sistemas con base en transacciones. Con un lenguaje de este tipo, se
declararían variables cuyos tiempos de vida se extienden más allá del tiempo de ejecución del
programa. La programación sería más simple, puesto que no habría necesidad de especificar
la forma de los datos en un archivo externo antes de manipularlos. El traductor del lenguaje
ya sabría donde se guardaron los datos y conocería su forma. (Como se indicó en el capítulo
2, todavía sería necesario leer los datos en el almacenamiento secundario y procesarlos en los
registros internos de alta velocidad de la unidad central de procesamiento, pero eso sería un
nivel de detalle que maneja automáticamente el traductor del lenguaje y no un asunto del
programador.) En la sección 9.6.1 se analiza la investigación en curso sobre el desarrollo de
lenguajes persistentes. Sin embargo, en tanto esos lenguajes llegan a usarse ampliamente, en
la sección 4.3.12 se estudia el uso de archivos para transferir datos persistentes a variables
locales de programas.

4.1.2 Tiposdedatos

Un Íz.po de c7o/os es una clase de objetos de datos ligados a un conjunto de operaciones para
crearlos y manipularlos. Aunque un programa se ocupa de o¿y.e/os cJe dc}Íos particulares como
un arreglo Á, la variable entera X o el archivo F, un lenguaje de programación trata más co-
múnmente, por necesidad, con fJ.pos cJe dcííos como la clase de arreglos, enteros o archivos y
las operaciones que se suministran para manipular arreglos, enteros o archivos.
Todo lenguaje tiene un conjunto de tipos pr;.mj.Í;.vos de datos que están integrados al 1en-
guaje. Además, un lenguaje puede proveer recursos que permitan al programador definir
nuevos tipos de datos. Una de las diferencias principales entre lenguajes más antiguos como
FORTRAN y COBOL y lenguajes más modernos como C y Ada está en el área de tipos de
datos definidos por el programador, un tema que se trata en la sección 5 .3 . La tendencia más
reciente es dejar que los tipos mismos sean manipulados por el lenguaje de programación.
Esta es una característica importante incorporada al ML y es una de las que están presentes en
los modelos de programación orientada a objetos que se analizan en el capítulo 8.
Los elementos básicos de una especz#ccw;.óm de un tipo de datos son:
1. Los c}Írjówfos que distinguen objetos de datos de ese tipo,
2. Los vo/ores que los objetos de datos de ese tipo pueden tener, y
3. Las opercicí.ones que definen las posibles manipulaciones de objetos de datos
de ese tipo.
Sec 4.1 PÍopiec]dcles c]e tipos y objetos 113

Por ejemplo, si se considera ]a especificación de un tipo de datos de arreglo, los atributos


podrían incluir el número de dimensiones, el intervalo de subíndices para cada dimensión y el
tipo de datos de los componentes; los valores serían los conjuntos de números que forman
valores válidos para componentes de arreglo; y las operaciones podrían incluir la subindización
para seleccionar componentes particulares del arreglo y posiblemente otras operaciones para
crear arreglos, cambiar su forma y efectuar aritmética sobre parejas de arreglos.
Los elementos básicos de la /.mp/emc#/czcz.ó# de un tipo de datos son:

1. La represe#/czcz.Ón cíe cz/moce#omz.e#/o que se usa para representar los objetos de datos
del tipo de datos en el almacenamiento de la computadora durante la ejecución del
programa, y
2. La manera en que las operaciones definidas para el tipo de datos se representan en
términos de cr/gorítmos o procec7z.mí.e#/os particulares que manipulan la representación
de almacenamiento elegida para los objetos de datos.
La especificación de un tipo de datos corresponde aproximadamente a la especificación
de esa parte de la computadora virtual que está definida por el tipo de datos, según se des-
cribe en el capi'tulo 2. La implementación de un tipo de datos define la s/.mc//czcJ.ó# de esas.
partes de la computadora virtual en términos de las construcciones más primitivas que sumi-
nistra la capa subyacente de computadora virtual, la cual puede ser directamente la computadora
de hardware o una combinación de hardware y software definida por un sistema operativo o
microcódigo.
El último aspecto conectado con un tipo de datos se refiere a su represe#/c7c;.ó# sJ.#ÍcícÍJ.ccr.
Tanto la especificación como la implementación son en gran medida independientes de las
formas sintácticas particulares que se usan en el lenguaje mismo. Los atributos de objetos de
datos se suelen representar sintácticamente mediante c7ec/czrczcj.ones o cíe/77/.cÍ.o#cs c7e /J.po.
Los valores se pueden representar como constantes literales o definidas. Las operaciones se
pueden invocar usando símbolos especiales, procedimientos integrados o funciones como sJ.#
(seno) o reoc7 (leer), o implícitamente a través de combinaciones de otros elementos del len-
guaje. La representación sintáctica particular hace poca diferencia, pero la información presente
en la sintaxis del programa proporciona información al traductor del lenguaje que puede ser
vital para determinar el tiempo de enlazamiento de diversos atributos y, por tanto, para permitir
al traductor establecer representaciones de almacenamiento eficientes o para ejecutar revisiones
en busca de errores de tipo.
Las secciones que siguen examinan con más detalle las áreas de especificación, implemen-
tación, representación sintáctica y verificación de tipos de datos. Las representaciones sintác-
ticas particulares varían con tanta amplitud entre los lenguajes que no se intenta aquí un
estudio exhaustivo, aunque los ejemplos que se ofrecen son representativos de los lenguajes
que se describen en este libro.

4.1.3 Especificación de tipos elementales de datos

Un objeto de datos c/emen/cz/ contiene un solo valor de datos. Una clase de estos objetos de
datos en la cual se definen diversas operaciones se conoce como un ÍÍ.po e/emem/cz/ c7e cJa/os.
114 Tiposdec]dtos Cdp. 4

Aunque cada lenguaje de programación tiende a tener un conjunto algo diferente de tipos
elementales de datos, se suelen incluir los tipos e#/ero, rec7/, de ccrrác/er, óoo/ecr#o, c/e e#z¿-
mercrcz.ó# y czp%#ÍocJor, aunque la especificación exacta puede diferir en forma significativa
entre dos lenguajes. Por ejemplo, aunque casi todos los lenguajes incluyen un tipo booleano
de datos, éste se trata de manera muy distinta en FORTRAN y en C.

Atributos. Los atributos básicos de cualquier objeto de datos, como el tipo de datos y el
nombre, son ordinariamente invariables durante su tiempo de vida. Algunos de los atributos
se pueden guardar en un czescrJ.p/or (también llamado vecíor cJe c}rreg/os) como parte del
objeto de datos durante la ejecución del programa; otros se pueden usar sólo para determinar
la representación de almacenamiento del objeto de datos y no pueden aparecer de manera
explícita durante la ejecución. Adviértase que el vo/or cíe w# o/rz.bej/o de un objeto de datos es
diferente del vcr/or gwe e/ o¿y.e/o c7e cJc!/os co#/;.e#e. El valor contenido puede cambiar durante
el tiempo de vida del objeto de datos y siempre se representa en forma explícita durante la
ejecución del programa.

Valores. El tipo de objeto de datos determina el conjunto de valores posibles que puede con-
tener. En cualquier punto durante su tiempo de vida, un objeto elemental de datos contiene un
solo valor de este conjunto. Por ejemplo, el tipo en/ero de datos determina un conjunto de
valores enteros que pueden servir como valores para objetos de datos de este tipo. Este conjunto
de valores es por lo común el mismo que el conjunto de valores enteros que se pueden re-
presentar de manera conveniente en el almacenamiento en la computadora de hardware sub-
yacente, aunque no es necesario que así sea. Por ejemplo, C define las cuatro clases siguientes
de tipos enteros: z.#/ (ent), sÁor/ (corto), /o#g (largo) y cÁcw (car). Puesto que casi todo el
hardware implementa aritmética de enteros de precisión múltiple (por ejemplo, enteros de 16
bits y 32 bits o enteros de 32 bits y 64 bits), C permite al programador elegir entre estas
implementaciones definidas por el hardware. SÁor/ usa el valor más corto de la longitud de
palabra del entero, /ong usa el valor más largo que implementa el hardware, e ;.#Í usa el valor
más eficiente que el hardware implementa. Éste puede ser igual que sbor/, igual que /o#g o
algún valor intermedio entre éstos. Es interesante señalar que en C los caracteres se guardan
como enteros de s bits en el tipo c4ar, que es un subtipo entero.
El conjunto de valores que define un tipo elemental de datos es comúnmente un co#/.ct#Ío
orc7enoc7o, con un valor mínimo y un valor máximo, y para cualquier par de valores distintos,
uno es más grande que el otro. Por ejemplo, para un tipo entero de datos, existe ordinariamente
un valor máximo que corresponde al entero más grande que se puede representar en forma
conveniente en la memoria y, de manera similar, un entero mínimo, con los enteros intermedios
dispuestos en su ordenamiento numérico usual.

Operaciones. El conjunto de operaciones definido para un tipo de datos determina cómo se


pueden manipular los objetos de datos de ese tipo. Las operaciones pueden ser operczc;.omes
prJ.mJ./j.vas, lo que significa que las operaciones se especifican como parte de la definición del
lenguaje, o pueden ser ope,.ocj.ones cJe/nj.c7czs por e/ programczcJor, en forma de subprogramas
o declaraciones de método como parte de definiciones de clase. Este capítulo hace énfasis en
las operaciones primitivas; las operaciones definidas por el programador se estudian con más
detalle en capítulos subsiguientes.
Sec. 4.1 Propiec]ddes c]e tipos y objetos 115

EJEMPLO 4.3. Signaturas de operaciones simples.


(a) La adición de enteros es una operación que toma dos objetos de datos enteros como
argumentos y produce un objeto de datos entero como su resultado (un objeto de datos que
por 1o común contiene la suma de los valores de sus dos argumentos). AsÍ pues, su
especificación es:
+ .. entero x enlero + erilero

(b) El operador "=" que prueba en cuanto a igualdad de los valores de dos objetos de datos
enteros y produce un objeto de datos que contiene un resultado booleano (cierto o falso) se
especifica:
--.. entero x entero + booleano

(c) Una operación de raíz cuadrada, SQRT (square rooí), sobre objeto de datos de números
reales se especifica:
SQRT .. real + real

Una operación es una/z¿#cÍ.ón mcz/emá/j.cc}; para un orgwme#/o (o argumentos) de entrada


dado, tiene un rescj//ocJo bien definido y determinado de manera exclusiva. Cada operación
tiene un cJom7.#/.o, el conjunto de posibles argumentos de entrada con base en los cuales se
define, y un Í.#/ewcz/o, el conjunto de posibles resultados que puede producir. La c7cc/.Ón de la
operación define los resultados que se producen para un conjunto dado de argumentos.
Un cz/gor/./mo que especifica cómo calcular los resultados para cualquier conjunto dado de
argumentos es un método común para especificar la acción de una operación, pero son posibles
otras especificaciones. Por ejemplo, para especificar la acción de una operación de multi-
plicación, se podría dar una "tabla de multiplicar" que simplemente enumera el resultado de
multiplicar dos números cualesquier, en vez de un algoritmo para multiplicar cualquier par
de números.
Para especificar la s¿.g#cz/wro de una operación, se dan el número, orden y tipos de datos de
los argumentos en el dominio de una operación, así como el orden y tipo de datos del intervalo
resultante. Es conveniente usar la notación matemática habitual para esta especificación:

nombre op .. tipo arg x tipo arg x ...tipo arg + tipo de resul[ado

En C a esto se le llama el pro/o/Í.po de la función.


Una operación que tiene dos argumentos y produce un solo resiiltado se designa como una
operación b/.#czr;.c7 (o cJ/.ácJ/.cc7). Si tiene un argumento y un resultado, es una operación z/#c7r/.cz
(o mo#ác7/.ccz). Al número de argumentos para una operación se le suele llamar la c7rí.c/c7cJ de la
operación. Casi todas las operaciones primitivas en lenguajes de programación son operaciones
binarias o unarias.
Una especificación precisa de la acción de una operacíón requiere ordinariainente inás in-
formación que el simple tipo de datos de los argumentos. En particular, la representación de
almacenamiento de los tipos de argumentos determina por lo general cómo se pueden mani-
pular los argumentos de esos tipos. Por ejemplo, un algoritmo para la multiplicación de dos
números, donde los números se representan en notación binaria, es diferente de im algoritmo
de multiplicación para números decimales. Así pues, en la especificación de la operación se
116 Tiposdecldtos Cdp. 4

da ordinariamente una descripción informal de la acción. Por tanto, la especificación precisa


de la acción es parte de la implementación de la operación, una vez que se han determinado
las representaciones de almacenamiento para los argumentos.
A veces es dificil determinar una especificación precisa para una operación como función
matemática. Existen cuatro factores principales que se combinan para hacer confusa la defi-
nición de muchas operaciones de lenguaje de programación:

\. Operaciones que no están definidas para ciertas entradas. Una. operaLc.ióri que
aparentemente está definida en algún dominio puede no estar definida, de hecho, para
ciertas entradas en el dominio. Puede ser en extremo dificil especificar el dominio
exacto en el cual una operación no está definida, como, por ejemplo, los conjuntos de
números que causan exceso negativo o desbordamiento en operaciones aritméticas.

2. Árgwme#Íos j.mp/;'cÍ./os. Una operación en un programa se invoca ordinariamente con


un conjunto de argumentos explícitos. Sin embargo, la operación puede acceder a otros
argumentos implícitos a través del uso de variables globales u otras referencias no
locales de identificador. La determinación completa de todos los datos que pueden
afectar el resultado de una operación suele verse dificultada por esta clase de argumentos
implícitos.

3. E/ec/os co/cÍ/ercr/es /resw//crcJos ;.mp/Í'cj./osJ. Una operación puede devolver un resultado


explícito, por ejemplo la cantidad devuelta como resultado de una suma, pero también
puede modificar los valores guardados en otros objetos de datos, tanto definidos por el
programador como por el sistema. Estos resultados implícitos se conocen como e/ec/os
co/c}Íercr/es. Una función puede modificar sus argumentos de entrada y también devolver
un valor. Los efectos colaterales son una parte básica de muchas operaciones, en
particular de aquellas que modifican las estructuras de datos. Su presencia dificulta la
especificación exacta del ámbito de una operación.
4. Á%/omodí#ccrcj.ó# /se#sJ.bí./Í.cícrcJ c]/ Áj.s/or/.cr//. Una operación puede modificar su propia es-
tructura interna, ya sean datos locales que se retienen entre ejecuciones o su propio
código. Los resultados que la operación produce para un conjunto particular de argu-
mentos dependen entonces no sólo de esos argumentos sino del historial completo de
]lamadas anteriores durante el cómputo, y de lüs argumentos que se dan en cada llamada.
Se dice que la operación es se#sz.ó/e c7/ Á;.síor/.cÍ/ en cuanto a sus acciones. Un ejemplo
común es el ge#erczc7or c7e #úmeros c}/ecí/or;.os que se encuentra como operación en
muchos lenguajes. Típicamente, esta operación toma un argumento constante y sin
embargo devuelve un resultado distinto cada vez que se ejecuta. La operación no sólo
devuelve su resultado, sino que además modifica un #aimero sem/.//cz interno que afecta
su resultado en la próxima ejecución. La automodificación a través de cambios en los
datos locales que se retienen entre llamadas es común; la automodificación por cambios
en el código de una operación es menos común, aunque posible, en lenguajes como
LISP.

En la sección 9.4 se comenta la investigación reciente que está buscando formas de desarro-
llar especificaciones precisas de operaciones.
Sec. 4.1 Propiec]ddes c]e tipos y ob)etos 117

Subtipos. Cuando se describe un nuevo tipo de datos, se suele desear expresar que es "simi-
lar" a otro tipo de datos. Por ejemplo, C define los tipos ;.#/, /o#g, s¢or/ y cAc7r como variantes
de enteros. Todos se comportan en forma similar, y sería deseable que ciertas operaciones,
como + y x, se definieran de manera análoga. Si un tipo de datos es parte de una clase más
grande, se dice que es un sw¿/J.po de la clase mayor, y ésta es un sztpcr/;-po de este tipo de
datos. Por ejemplo, en Pascal se pueden crear subintervalos de enteros, como en:

type Smalllnteger= 1..20;

que es un subtipo en/ero con valores limitados a 1, 2, 3 ,..., 20.


Con un subtipo, se supone que las operaciones disponibles para la clase más grande de
objetos también están a disposición de la clase más pequeña. ¿Cómo se puede determinar
esto? En Pascal y Ada, los subintervalos de este tipo son explícitamente parte del lenguaje.
¿Cómo se extiende esto a otros tipos de datos que no son primitivos para el lenguaje?
El importante concepto de ¢ere#c/.c}, que se estudia en el capítulo 8, es una generalización
de esta propiedad de los subtipos. Se podría decir que el tipo cAcm de C Aerec7cJ las operacio-
nes del tipo entero Í.#/.

4.1.4 Implementación de tipos e[ementales de datos

La implementación de un tipo elemental de datos se compone de una represe#/ocj.ó# cJe c}/-


mczce7?czmJ.e#/o para objetos de datos y valores de ese tipo, y un conjunto de cz/gor/./mos o pro-
cecJ¿.mí.en/os que definen las operaciones del tipo en términos de manipulaciones de la
representación de almacenamiento.

Representación de almacenamiento. El almacenamiento de típos elementales de datos está


fuertemente influido por la computadora subyacente que va a ejecutar el programa. Por ejemplo,
la representación de almacenamiento para valores enteros o reales es casi siempre la
representación binaria de entero o de punto flotante para números que se usan en el hardware
subyacente. Para valores de carácter, se usan los códigos de caracteres del hardware o del
sistema operativo. La razón de esta elección es sencilla: si se usan las representaciones de
almacenamiento del hardware, entonces las operaciones básicas sobre datos de ese tipo se
pueden implementar usando las operaciones que suministra el hardware. En caso contrario,
las operaciones tendrán que simularse por software, y las mismas operaciones se ejecutarán
de manera mucho menos eficiente.
Los cr/r;.62¿/os de los objetos de datos elementales se tratan en forma similar:

1. Por razones de eficiencia, muchos lenguajes se proyectan de modo que los atributos de
datos sean determinados por el compilador. Los atributos mismos no se guardan en la re-
presentación de almacenamiento en tiempo de ejecución. És{a es ordinariamente una
representación que e] hardware proporciona directamente. Éste es el método usual en
C, FORTRAN y Pascal, donde la eficiencia en el uso del almacenamiento y la velocidad
de ejecución son objetivos primordiales.
118 Tiposc]ec]dtos Cdp. 4

2. Los atributos de un objeto de datos se pueden guardar en un c/escr;.píor como parte del
objeto de datos en el tiempo de ejecución. Éste es el método usual en lenguajes como
LISP y Prolog, donde la flexibilidad, más que la eficiencia, es el objetivo primario.
Puesto que casi ningún hardware suministra directamente representaciones de almace-
namiento para descriptores, los descriptores y las operaciones sobre objetos de datos
con descriptores se deben simular por software.

Ordinariamente, la representación de un objeto de datos es independiente de su localidad


en la memoria. Así, cada objeto de datos de un tipo dado (y con los mismos atributos) tiene la
misma =epresentación sin que importe su posición particular en la memoria de la computadora.
La representación de almacenamiento se describe por lo común en términos del tamaño d3l
bloque de memoria que se requiere (el número de palabras, bytes o bits de memoria necesarios)
y la disposición de los atributos y valores de datos dentro de este bloque. Por lo general, se
considera que la dirección de la primera pa]abra o byte de un bloque de memoria de esta clase
representa la localidad del objeto de datos.

lmplementación de operaciones. Toda operación definida para objetos de datos de un tipo


dado se puede implementar en una de tres formas principales:

1. D;.rec/c}me#/e como operc}c/.Ó# c7e ¢czrc7wc]re. Por ejemplo, si se guardan enteros usando
la representación de hardware para enteros, entonces la adición y la sustracción se
pueden implementar usando las operaciones aritméticas integradas en el hardware.
2. Como un subprograma de procedimiento o de función. Por e.]emplo, una. operzLc.\ón de
raíz cuadrada no se suministra comúnmente en forma directa como una operación de hard-
ware. Se podría implementar como un subprograma de raíz cuadrada que calcule la
raíz cuadrada de su argumento. Si los objetos de datos no se representan usando una
representación definida por hardware, entonces todas las operaciones se deben simular
por software, por lo común en forma de subprogramas que se suministran en una
biblioteca de subprogramas.

3 . Como una secuencia de código en línea. Una secuencia de código en línea es tamb.ién
una implementación de la operación por software, pero, en vez de usar un subprograma
muy corto, las operaciones del subprograma se copian en el programa en el punto
donde de otra manera se habría invocado el subprograma. Por ejemplo, la función de
valor absoluto sobre números, definida por:

abs(x) = if x < 0 then -x else x

se implementa ordinariamente como una secuencia de código en línea:

/cz/ Tomar el valor de x de la memoria.


/b/ Si x > 0, pasar por alto la próxima instrucción.
/c,/ Sea jc = -jx:.
/d/ Giiardar el nuevo valor de x en la memoria.

donde cada renglón se implementa a través de una sola operación de hardware.


Sec 4.1. Propiedddes de tipos y obietos 119

4.1.5 Declaraciones

Al escribir un programa, el programador determina el nombre y tipo de cada objeto de datos


que se necesita. También se debe especificar el tiempo de vida de cada objeto de datos, du-
rante qué parte de la ejecución del programa se necesita, así como las operaciones que se le
habrá de aplicar.
Una c7ec/orczc/.ó# es un enunciado de programa que sirve para comunicar al traductor del
]enguaje información acerca del nombre y tipo de los objetos de datos que se necesitan du-
rante la ejecución del programa. Por su posición en el programa, por ejemplo, dentro de un
subprograma o definición de clase particular, una declaración también puede servir para indicar
los tiempos de vida deseados de los objetos de datos. Por ejemplo, la declaración en C:

float A, 8;

al principio del subprograma indica que se necesitan dos objetos de datos del tipo/oc7/ du-
rante la ejecución del subprograma. La declaración también especifica el enlace de los objetos
de datos a los nombres Á y 8 durante sus tiempos de vida.
La declaración anterior en C es una declaración exp/;'c;./c7. Muchos lenguajes también su-.
ministran declaraciones /.mp/;'c;./czs o por omi.sz'Ón, que son las declaraciones que valen cuando
no se da una declaración explícita. Por ejemplo, en un subprograma en FORTRAN, se puede
usar una variable simple /'ND/CE sin declaración explícita, y por omisión el compilador de
FORTRAN supone que es una variable entera`porque su nombre comienza por una de las
letras I-N. Alternativamente, el programa en FORTRAN podría declarar explícitamente que
/'jvD/CE es una variable real por la declaración:

REAL ÍNDICE

Una declaración también puede especificar el valor del objeto de datos si es una constante, o
el valor inicial del objeto de datos en caso contrario. También se pueden especificar en la
declaración otros enlaces para el objeto de datos: un nombre para el objeto de datos o la po-
sición del objeto de datos como componente de un objeto de datos más grande. A veces
también se especifican detalles de implementación, como el enlace a una localidad particular
de almacenamiento o a una representación particular especializada de almacenamiento. Por
ejemplo, la designación en COBOL de una variable entera como COMPUTATIONAL (de
computación) indica por lo común que se necesita una representación de almacenamiento
binaria, en vez de una de cadena de caracteres, para el valor de ese objeto de datos (para per-
mitir el uso de operaciones aritméticas más eficientes).

Dec]araciones de operaciones
Las declaraciones también se pueden usar para especificar información acerca de operaciones
en el traductor del lenguaje. La información que se requiere durante la traducción es princi-
palmente la signatura de cada operación. Por lo común no se necesita una declaración explícita
de tipos de argumentos y tipos de resultados para operaciones primitivas que están integradas
en un lenguaje. Estas operaciones se pueden invocar conforme se necesitan al escribir un
programa, y los tipos de argumentos y resultados son determinados de manera implícita por
120 Tposdeclótos Cdp. 4

el traductor del lenguaje. Sin embargo, es común que los tipos de argumentos y resultados
para operaciones definidas por el programador se tienen que dar a conocer al traductor del
lenguaje antes de que se pueda llamar el subprograma. Por ejemplo, en C, la cabeza de una
definición de subprograma, su pro/o/Í.po, proporciona esta información:
float Sub(int X, float Y)
la cual declara que S2jó tiene la signatura:
Sub .. int x float + float

Propósitos de las declaraciones


Las declaraciones tienen varios propósitos importantes:
1. Se/eccj.Ó# c7e represe7?fczcj.o#cs c7e cz/mczcenczmí.c#/o. Si una declaración proporciona in-
formación al traductor del lenguaje acerca del típo de datos y los atributos de un ob-
jeto de datos, entonces el traductor suele poder determinar la mejor representación de
almacenamiento para ese objeto de datos, con una reducción consecuente del reque-
rimiento global de almacenamiento y del tiempo de ejecución para el programa que se
está traduciendo.
2. Ges/;.ó# cJe a/mace#czmj.en/o. La información que proporcionan las declaraciones acerca
de los tiempos de vida de objetos de datos suele hacer posible el uso de procedimientos
más eficientes de gestión de almace'namiento durante la ejecución del programa. En C,
por ejemplo, todos los objetos de datos declarados al principio del subprograma tienen
el mismo tiempo de vida (igual a la duración de la ejecución del subprograma) y por
tanto se les puede asignar almacenamiento como un solo b]oque al entrar al subprograma,
y el bloque completo se libera al salir. Otros objetos de datos en C se crean dinámica-
mente mediante el uso de una función especial, mcz//oc. Puesto que los tiempos de vida
de estos objetos de datos no se declaran, se les debe asignar almacenamiento en forma
individual en un área de almacenamiento por separado, a un costo mayor.
3. Operczc;.o#espo/;.mór/cc7s. Casi todos los lenguajes usan símbolos especiales como +
para designar cualquiera de varias operaciones diferentes, de acuerdo con los tipos de
datos de los argumentos que se suministran. En C, Á + 8, por ejemplo, significa "efectuar
suma de enteros" si Á y 8 son de típo entero o "efectuar suma de números reales" si Á
y 8 son del tipo float. De un símbolo de operación como éste se dice que tiene un
Áomó#j.mo porque no designa una operación específica, sino que denota uria operación
gcnér/.ca de ``sumar" que puede tener varias formas diferentes especz#ccH cíe/ /z.po para
argumentos de tipos distintos. En casi todos los lenguajes, los símbolos básicos de
operación como +, * y / tienen homónimos ; es decir, denotan operaciones genéricas, y
otros nombres de operación identifican de manera exclusiva una operación particular.
Sin embargo, Ada permite que el programador defina nombres de subprograma
homónimos y agregue significados adicionales a símbolos de operación ya existentes.
ML expande este concepto con pleno po//.mor4smo, donde un nombre de función puede
asumir diversas implementaciones de acuerdo con los tipos de sus argumentos. Cuan-
do se analizan los diseños orientados a objetos, el polimorfismo es una característica
Sec. 41. Propiedddes de tipos y obietos 121

importante que permite al programador ampliar el lenguaje con nuevos tipos de


datos y operaciones.
Las declaraciones permiten ordinariamente al traductor del lenguaje determinar en
tiempo de compilación cuál es la operación particular que designa un símbolo de
operación homónimo. En C, por ejemplo, el compilador determina, a partir de las
declaraciones de las variables Á y 8, cuál de las dos posibles operaciones (suma de
enteros o suma de float) es la que designa Á + 8. No se requiere verificación en tiempo
de ejecución para determinar cuál operación se debe efectuar. En Smalltalk, por otra
parte, puesto que no hay declaraciones de tipos para las variables, la determinación de
cuál operación + se debe efectuar tiene que llevarse a cabo cada vez que se encuentra
una operación + durante la ejecución del programa.
4. yer;#cc7c;.Ó# c7e /;.pos. El propósito más importante de las declaraciones, desde el punto
de vista del programador, es permitir la verificación esící/;.co de tipos antes que la
verltiicac\ón dinámica.

4.1.6 Verificación y conversión de tipos

Las representaciones de almacenamiento que se incorporan en el hardware de la computado-


ra no incluyen ordinariamente información de tipos, y las operaciones primitivas sobre los
datos no llevan a cabo verificación de tipos. Por ejemplo, una palabra particular en la memo-
ria de la computadora durante la ejecución de ún programa puede contener la serie de bits:

11100101100...0011

Esta serie de bits podría representar un entero, un número real, una serie de caracteres o una
instrucción; no hay forma de saberlo. La operación primitiva de hardware para suma de enteros
no puede verificar si sus dos argumentos representan enteros; son simplemente series de bits,
y la operación de hardware debe suponer que representan enteros. Un error común de progra-
mación en lenguajes ensambladores o de máquina es invocar una operación como la suma de
enteros sobre argumentos del tipo erróneo. Es particularmente dificil encontrar estos errores
porque la operación no falla en alguna forma obvia. La operación "funciona", pero los
resultados que produce no tienen sentido. Sin embargo, puesto que el resultado es simplemente
otra cadena de bits sin información de tipo anexa, las operaciones subsiguientes pueden
continuar el cómputo con el resultado ``basura" para producir más "basura", hasta que fracasa
el cómputo completo. Así pues, al nivel del hardware, las computadoras convencionales son
particularmente poco confiables para detectar errores de tipo de datos.
yer/#cc7c/.ó# c7e /;.pos significa comprobar que cada operación que un programa ejecuta re-
cibe el número apropiado de argumentos del tipo correcto de datos. Por ejemplo, antes de
ejecutar el enunciado de asignación:

X-A+8*C

se debe determinar para las tres operaciones, suma, multiplicación y asígnacíón, que cada
una reciba dos argumentos del tipo de datos apropiado. Si + se define sólo para argumentos
enteros o reales, y Á nombra un objeto de datos de carácter, entonces existe un error c7e /f.po
122 Tiposdeddtos Cdp. 4

de argumento. La verificación de tipos se puede hacer en tiempo de ejecución (verz#cczc;.ón


dinámica de tipos) o en tiempo de complla.ción (verificación estática de tipos). Una. venta,ia,
importante del uso de un lenguaje de alto nivel para programar es que la implementación del
lenguaje puede proporcionar verificación de tipos para todas (o casi todas) las operaciones, y
en esta forma el programador está protegido contra esta forma particularmente insidiosa de
error de programación.
La verJ#cczc;.ó# c7j.#ámz.ccr c7e /;.pos es verificación de tipos en tiempo de ejecución que por
lo común se efectúa inmediatamente antes de la ejecución de una operación particular. Este
tipo de verificación se implementa por lo general guardando una marca de tipo en cada objeto
de datos, la cual indica el tipo de datos del objeto. Por ejemplo, un objeto de datos de enteros
contendría tanto el valor entero como una marca de tipo de "entero". Cada operación se im-
plementa entonces de modo que comience con una secuencia de verificación de tipos en la
cual se verifica la marca de tipo de cada argumento. La operación se efectúa sólo si los tipos
de argumento son correctos; de otro modo se indica un error. Cada operación también debe
anexar las marcas de tipo apropiadas a sus resultados para que las operaciones subsiguientes
las puedan verificar.
Ciertos lenguajes de programación como LISP y Prolog están diseñados para requerir ve-
rificación dinámica de tipos. En estos lenguajes, no se dan declaraciones de variables y no se
supone una declaración de tipo por omisión (en contraste con las estructuras de tipificación
por omisión de FORTRAN). Los tipos de datos de variables como Á y 8 en la expresión Á +
8 pueden cambiar en el curso de la ejecución del programa. En estas circunstancias, los tipos
de .4 y 8 se deben verificar dinámicamente cada vez que se efectúa la suma en tiempo de eje-
cución. En lenguajes sin declaraciones, a veces se dice que las variables son sJ.n Íz.po, puesto
que no tienen un tipo fijo.
La ventaja principal de los tipos dinámicos es la flexibilidad en el diseño de programas:
no se requieren declaraciones y el tipo de objeto de datos asociado con un nombre de variable
puede cambiar según se requiere durante la ejecución del programa. El programador queda
libre de casi todas las preocupaciones acerca de tipos de datos. Sin embargo la verificación
dinámica de tipos tiene varias desventajas importantes:
1. Es difícil depurar los programas y eliminar por completo todos los errores de tipo de
argumento. Puesto que la verificación dinámica de tipos comprueba los tipos de datos
en el momento de ejecución de una operación, las operaciones que están en rutas de
ejecución de programa que no se ejecutan nunca se verifican. Durante la prueba del
programa no se pueden revisar todos los caminos de ejecución posibles, en general.
Las rutas de ejecución no probadas pueden contener errores de tipo de argumento, y
estos errores pueden aparecer sólo mucho tiempo después durante el uso del programa,
cuando algún usuario confiado suministre datos de entrada que lleven el programa a lo
largo de una ruta no probada.
2. La verificación dinámica de tipos requiere que la información de tipos se conserve du-
rante la ejecución del programa. El almacenamiento adicional que se requiere puede
ser considerable.
3. La verificación dinámica de tipos se debe implementar ordinariamente en software,
puesto que el hardware subyacente rara vez proporciona apoyo. Puesto que la com-
Sec. 41. Propiecldc]es c]e tipos y objetos 123

probación se debe hacer antes de cada ejecución de cada operación, es probable que la
velocidad de ejecución del programa se reduzca en forma considerable.

Casi todos los ]enguajes intentan eliminar o reducir al mínimo la verificación dinámica de
tipos efectuando la verificación de tipos durante la compilación. La ver/#cc7c/.Ón es/cí//.co
cJc /Í.pos se efectúa durante la traducción de un programa. Por lo común, la información ne-
cesaria se suministra en parte mediante declaraciones que proporciona el programador y en
parte por otras estructuras del lenguaje. La información que se requiere es:
\ . Para cada operación, el número, orden y tipos de datos de sus argurneníos y resultados.
Para operaciones primitivas, esta información es parte de la definición básica del len-
guaje, pero dentro de subprogramas y clases el programador la debe especificar de
manera explícita.

2. Para cada variable, el tipo de objeio de datos nombrado. E:l tipo del ob.]eto de daiios
asociado con un nombre variable debe permanecer también sin cam.bio durante la
ejecución del programa, de manera que al verificar una expresión como "A+B" se pue-
da suponer que el tipo objeto de datos que nombra A es el mismo en cada ejecución de
la expresión, aunque la expresión se ejecute repetidamente con distintos enlaces de A
hacia objetos particulares de datos (por ejemplo, en caso de que A sea un parámetro
formal de un subprograma).

3. E/ /Í.po c7e cczcJcz o¿y.e/o cJe c7c7/os co#s/c7#/e. La forma sintáctica de una literal indica
comúnmente su tipo; por ejemplo, "2" es un entero, "2.3" es un número real. Cada
constante definida debe equipararse con su definición para determinar su tipo.

Durante las fases iniciales de traducción de un programa. el compilador (u otro traductor)


recoge información de declaraciones del programa en diversas tablas, principalmente iina
"tabla de símbolos" (véase el capítulo 3) que contiene Ínformación de tipos acerca de varia-

bles y operaciones. Después de reunir toda la información de tipos, se verifica cada operación
que invoca el programa para determinar si el tipo de cada argumento es válido. Adviértase que
si la operación es polimórfica, como se expuso anteriorinente, entonces puede ser válido
cualquiera de varios tipos de argumentos. Si los tipos de argumentos son válidos, entonces se
determinan los tipos de resultados y el compilador guarda esta información para verificar
operaciones poste.riores. Obsérvese que, al mismo tiempo. el nombre de la operación polimór-
fica puede reemplazarse por el nombre de la operc7c/.Ó# espccí/7cc7 c/e /Í.po particular que usa
argumentos de los tipos designados.
Puesto que la verificación estática de tipos incluye todas las operaciones que aparecen en
cualquier enunciado de subprograma. se verifican todas las rutas de ejecución posibles y no
se necesita una revisión adicional en busca de errores de tipo. Por tanto. no se requieren mar-
cas de tipo en los objetos de datos en tiempo de ejecución y no es necesaria una verificación
dinámica de tipos; el resultado es una ganancia considerable en eficiencia de uso de alma-
cenamiento y velocidad de ejecución.
La preocupación por la verificación estática de tipos tiende a afectar miichos aspectos del
lenguaje: declaraciones, estructuras de control de datos y recursos para compilación de sub-
programas por separado. por citar unos cuantos. En casi todos los lengiiajes, la verificación
124 Tiposdeddtos Cdp. 4

estática de tipos no es posible para ciertas construcciones de lenguaje en ciertos casos. Estos
"defectos" en la estructura de verificación de tipos se pueden tratar de dos maneras:

1. Por verÉ#cc}cJ.Ó# c7j.#cím;.ccz c7e Í/.pos. El costo de almacenamiento de esta opción suele
ser alto porque las marcas de tipo para los objetos de datos se deben guardar en tiempo
de ejecución, no obstante que estas marcas se verifican muy raras veces.

2. Dejando las operaciones sin verficar. La,s opera,clones s.m veritiica.r pueden czLusa.r
errores de programa serios y sutiles, como ya se ha señalado, pero a veces se aceptan
cuando el costo de la verificación dinámica se considera demasiado alto.

Tipos fuertes. Si se pueden detectar estáticamente todos los errores de tipo de un programa,
se dice que el lenguaje es de /j.pos/wer/es. En general, los tipos fuertes proporcionan un nivel
de seguridad a los programas. Se dice que una función/, con signatura/: S + R, es segem e#
c%cz#Ío a /z.pos si la ejecución de/no puede generar un valor fuera de R. Para cualesquier
operaciones seguras en cuanto a tipos, se sabe estáticamente que los resultados serán del tipo
correcto y que no es necesario efectuar una verificación dinámica. Es obvio que, si todas las
operaciones son seguras en cuanto a tipos, el lenguaje es de tipos fuertes.
Pocos lenguajes son verdaderamente de tipos fuertes. Por ejemplo, en C, si .Yy y son de
tipo sÁor/ (es decir, enteros cortos), entonces .Y+ yy .Y * y pueden tener un resultado fuera
del intervalo permisible para enteros cortos y causar un error de tipo. Aunque es dificil tener
tipos verdaderamente fuertes, si se restrihge la conversión entre un tipo y otro, se está cerca
de alcanzar el objetivo. Se analiza esta clase de conversión en la subsección siguiente.

lníerencia de tipos. ML tiene un enfoque interesante hacia los tipos de datos. Las decla-
raciones de tipos no son necesarias si la interpretación no es ambigua. La imp]ementación del
lenguaje inferirá cualquier información faltante en cuanto a tipos a partir de otros tipos de-
clarados. El lenguaje tiene una sintaxis relativamente común para declarar argumentos para
funciones, como en:

fun área(longitud:int, ancho:int):int = longitud * ancho;

que declara la función área para que devuelva el área (como un entero) si se dan los lados en-
teros de un rectángulo. En este caso, una vez que se determina el tipo ya sea de /o#gj./c¿cÍ,
o#c¢o o círec7, entonces los otros dos también están determinados. La exclusión de cualesquiera
dos de estas declaraciones deja todavía la función con una sola interpretación. Como sabe
que * puede multiplicar entre sí ya sea dos números reales o dos enteros, ML interpreta lo
siguiente como equivalente al ejemplo anterior:

fun área(longitud, ancho):int = longitud * ancho;


fun área(longitud:int, ancho) = longitud * ancho;
fun área(longitud, ancho:int) = longitud * ancho;

Sin embargo, lo siguiente es i]egal:

fun área(]ongitud, ancho) = longitud * ancho;


Sec. 4.1 PÍopiec]ddes de tipos yobjetos 125

puesto que ahora es ambiguo en cuanto al tipo de argumentos. Podrían ser todos Í.#Í o
todos reo/.

Conversión de tipos y coerción

Si durante la verificación de tipos ocurre una discordancia entre el tipo real de un argumento
y el tipo esperado para esa operación, entonces:
1. La discordancia de tipos se puede marcar como un error y adoptarse una acción de
error apropiada, o
2. Se puede aplicar una cocrcj.ón (o co#vers;.ó# z.mp/j'c/./o c7e /Í.po) para cambiar el tipo del
argumento real al tipo correcto.
Una co#versj.Ón cJe f;.po es una operación con la signatura:

Conversión_op .. tipo \ + tipo2

Es decir, la conversión toma un objeto de datos de un tipo y produce el objeto de datos


"correspondiente" de un tipo distinto. Casi todos los ]enguajes suministran conversiones de
tipo de dos maneras:

1. Como un conjunto de/w#c;.o#es /.#/egrc}c7czs, que el programador puede invocar de manera


específica para efectuar la conversión. Por ejemplo, Pascal suministra la función roz¿#c7,
que convierte un objeto de datos de números reales en un objeto de datos de enteros
con un valor igual al valor redondeado del número real. En C se mo/c7ccz (cczs/) una ex-
presión para forzarla a tener el tipo correcto. (int) .Ypara/ocz/.Yconvierte el valor de .Y
al tipo entero.

2. Como coercj.ones invocadas de manera automática en ciertos casos de discordancia de


tipos. Por ejemplo, en Pascal, si los argumentos para una operación aritmética como
"+" son de tipos real y entero mixtos, el objeto de datos de enteros se convierte implíci-
tamente al tipo real antes de que se efectúe la suma.

El principio básico que gobierna las coerciones es no perder información. Puesto que todo
entero corto (en C) se puede representar como un entero largo, no se pierde información al
invocar automáticamente una conversión de s¢or/ ;.#/ + /ong ¿.#/. Estas conversiones se llaman
e#scJ#cAczmJ.e#/os o promoci.o#es. De manera similar, puesto que los enteros (en casi todos los
lenguajes) se pueden representar de manera exacta como un objeto de datos de números reales,
los enteros de valor reducido se ensanchan comúnmente a números reales sin pérdida de
información.
Por otra parte, la coerción de un número real a un entero puede hacer que se pierda infor-
mación. Mientras que 1.0 es exactamente igual al entero 1,1.5 no es representable como
entero. Será conveiiido ya sea a 1 o a 2. En este caso, se describe la coerción como un es/recAc7-
mJ.enío, y se pierde información.
Con la verificación dinámica de tipos, las coerciones se hacen en el punto en que se detecta
la discordancia durante la ejecución. Para este tipo de lenguajes, las conversiones de estre-
126 Tiposdeddtos Cdp. 4

chamiento podrían permitirse si el objeto de datos tuviera un valor apropiado (por ejemplo,
1.0 se podría convertir al tipo entero, pero 1.5 no). Para la verificación estática de tipos, se in-
serta código adicional en el programa compilado para invocar la operación de conversión en
el punto apropiado durante la ejecución. Puesto que la ejecución eficiente es por lo general
un atributo deseable, las coerciones de estrechamiento se prohíben comúnmente para que no
tenga que ejecutarse código en tiempo de ejecución para determinar si la conversión sería
legal 0 no.
Una operación de conversión de tipos puede requerir cambios extensos en la representación
de almacenamiento del objeto de datos en tiempo de ejecución. Por ejemplo, en COBOL y
PL/I, los números se suelen guardar en forma de cadenas de caracteres. Para efectuar la suma
de estos números en casi cualquier máquina, la representación de almacenamiento de cade-
nas de caracteres se debe convertir a una representación de números binarios que maneje el
hardware, y el resultado se convierte de nuevo a la forma de cadenas de caracteres antes de
guardarse. En este caso, las operaciones de conversión de tipo pueden tomar cientos de veces
más tiempo que la suma real misma.
Sin embargo, los implementadores de traductores de lenguajes confunden a veces la se-
mántica de un objeto de datos y su representación de almacenamiento. Los datos decimales
en COBOL y PL/I son un caso a propósito. Los traductores de PL/I guardan normalmente los
datos FIXED DECIMAL (decimal fija) en un formato de "decimal empacado". Esto es una
representación de hardware, pero una que se ejecuta con bastante lentitud. En el compilador
de PL/C [CONWAY y WILCOX 1973], los datos FIXED DECIMAL se guardan como datos
de punto flotante de doble precisión y 16 bits. Al guardarlos como 16 dígitos, no se pierde
precisión (importante para guardar datos decimales). Por ejemplo, el cómputo de 123.45 +
543.21 requiere una suma bastante lenta de decimales empacados (o una simulación por soft-
ware aún más ]enta si los datos decimales empacados no se implementan por hardware), en
tanto que PL/C efectúa esto como una suma de punto flotante única, más rápida, de 12345 +
54321. El compilador sigue la pista del punto decimal (por ejemplo, dividir entre 102 para
obtener el valor verdadero) como un atributo del valor resultante en tiempo de compilación.
Existen dos filosofías opuestas en cuanto a la medida en que el lenguaje debe proporcionar
coerciones entre tipos de datos. En Pascal y Ada, casi no se proveen coerciones; cualquier
discordancia de tipo, con pocas excepciones, se considera un error. En C, las coerciones son
la regla; una discordancia de tipo hace que el compilador busque una operación de conversión
apropiada para insertarla en el código compilado para suministrar el cambio de tipo adecuado.
Sólo si no hay una conversión posible la discordancia se señala como un error.
La discordancia de tipo es un error menor común de programación y la conversión de
tipos es una necesidad también común, en particular en lenguajes que tienen un número grande
de tipos de datos. Existen también cuestiones sutiles en cuanto al significado de la noción de
"discordancia de tipos" (véase la sección 5.3). Las coerciones suelen liberar al programador
de preocupaciones respecto a lo que de otro modo serían detalles tediosos ---la invocación de
conversión de operaciones de tipos numerosos explícitamente en un programa. Sin embargo,
las coerciones también pueden enmascarar errores serios de programación que de otra manera
podrían hacerse notar al programador durante la compilación.
EI PL/I, en particular, tiene mala fama por la propensión de sus compiladores a tomar un
error menor de programaciún como un nombre de variable mal escrito y, a través de una
Sec. 4.1. Propieddcles de tipos yobietos 127

coerción a veces sutil, ocultar el error de modo que éste se convierte en un defecto del programa
que es difícil detectar. Puesto que las operaciones de conversión del PL/I permiten coerciones
de estrechamiento, los resultados son sorprendentes. Por ejemplo, i9+10/3 es ilegal! Para ver
esto, obsérvese que 10/3 es forzado a 3 .33333333... hasta el número máximo de dígitos definido
por la implementación. Pero 9 + 3.333... tiene un dígito adicional en el resultado, lo que da
origen a una excepción por OVERFLOW (desbordamiento). Si esta excepción se inhabilita
(es decir, se pasa por alto), entonces el resultado se convierte automáticamente a 2.333 ..., que
no es precisamente lo que se esperaba.

4.1.7 Asignación e inicialización

Casi todas las operaciones para los tipos elementales de datos comunes, en particular números,
enumeraciones, booleanos y caracteres, toman uno o dos objetos de datos de argumento del
tipo, efectúan una operación aritmética, relacional o de otro tipo relativamente simple, y pro-
ducen un objeto de datos de resultado, que puede ser del mismo tipo o de otro diferente. Sin
embargo, la operación de asignación es un poco más sutil y amerita atención especial.
La asignación es la operación básica para cambiar el enlace de un valor a un objeto de
datos. Este cambio, sin embargo, es un e/ec/o co/oíerc7/ de la operación. En ciertos lenguajes,
como C y LISP, la asignación también devuelve un valor, el cual es un objeto de datos que
contiene una copia del valor asignado. Estos factores se aclaran cuando se intenta escribir
una especificación para asignación. En Pascal, la especificación para asignación de enteros
sería:

asígnación(=) .. entero , x entero2 + vacío

con la acción: hacer que el valor contenido en el objeto de datos e#/ero, sea una copia del va-
lor contenido en el objeto de datos e#/ero2 y devolver el resultado no explícito. (El cambio a
íntegro 1 es un resultado implícito o efecto colateral.) En C, la especificación es:

asignación(--)..entero,xentero2+entero3

con la acción: hacer que el valor contenido en el objeto de datos e#/ero, sea una copia del va-
lor contenido en el objeto de datos e#/ero2 y también crear y devolver un nuevo objeto de
datos e#/ero3, que contenga una copia del nuevo valor de e#/ero2.
Considérese la asignación: .Y := jY. Lo interesante acerca de este enunciado es la difei.ente
interpretación que se da a ambas referencias de la variable Jr. La.X'de la derecha se refiere al
valor que contiene el objeto de datos nombrado. Estas referencias se suelen llamar el vc7/or
del lado derecho (del operador de asignación) o valor r de m okj;]e;+o de distos. De me[neTai sii-
milar, la Jr de la izquierda se refiere a la localidad del objeto de datos que va a contener el
nuevo vaLlor. Estais referenc,.iais se conocen c,omo valor del lado izquierdo (del operador de
czsj.g#acJ.ó#J o vcz/or /. Por tanto, se puede definir una operación de asignación como:

1. Computar el valor 1 de la expresión del primer operando.


2. Computar el valor r de la expresión del segundo operando.
128 Tiposdecldtos Cóp 4

Asignación numérjca en Pascal

AHAH
t`

Después

Asignación de apuntador en C

Después

Figura 4.2. Dos vistas de la asignación.

3. Asignar el valor r computado al objeto de datos del valor 1 computado.


4. Devolver el valor r computado como resultado de la operación.

Si se tiene un operador de asignación (como en C), se dice entonces que el operador


devuelve el valor r del objeto de datos recién asignado. Además, C contiene un conjunto de
operadores unarios para manipular valores 1 y valores r de expresiones, el cual confiere a los
programas en C la capacidad para efectuar muchas manipulaciones útiles, y extrañas, de tales
asignaciones. Este uso dual del operador de asignación, como mecanismo para cambiar el
valor de un objeto de datos (a través de su valor 1) y como función que también devuelve
un valor (su valor r) se explota intensamente en C y se analizará más a fondo en el capítulo 6.
El uso de valores 1 y valores r proporciona una manera más concisa de describir semántica
de expresiones. Considérese la asignación en C : Á = 8 para los enteros zl y 8. En C, como en
muchos otros lenguajes, esto significa "Asignar una copia del valor de la variable 8 a la
variable A" (es decir, asignar al valor 1 de Á el valor r de 8). Considérese ahora la asignación
Á = 8 donde Á y 8 son variables apuntador. Si 8 es un apuntador, entonces el valor r de 8 es
el valor 1 de algún otro objeto de datos. Esta asignación significa entonces ``Hacer que el
valor r de ,4 se refiera al mismo objeto de datos que el valor r de 8 (es decir, asignar al valor
1 de ,4 el valor r de 8 que es el valor 1 de algún otro objeto de datos. Así pues, la asignación
j4 = 8 significa "Asignar una copia del apuntador guardado en la variable 8 a la variable Á",
como se muestra en la figura 4.2.
Sec. 4.1. F)ropieclddes c]e tipos y r)bjetos 129

18ualdad y equivalencia. El enunciado de asignación es tan omnipresente en los lenguajes


que pocos cuestionan su semántica. Pero existe una cuestión importante que es necesario
resolver. Considérese la asignación de Á en cierto nuevo ]enguaje Zczr¿:

.4 + 2 + 3

¿Significa esto:

1. evaluar la expresión 2 + 3 y asignar su valor equivalente de 5 a Á?


2. asignar la operación "2+3" aÁ?

En lenguajes con tipos estáticos de datos, el tipo para Á decide cuál semántica se va a
seguir: si Á es de tipo entero, entonces sólo la asignación de 5 a Á tiene sentido; si Á es del
tipo opercrc/.Ó#, entonces sólo la segunda es apropiada. En cambio,`para un lenguaje con tipos
dinámicos, donde a Á se le confiere un tipo en virtud de la asignación de un valor a ella,
ambas semánticas pueden ser aplicables y la asignación precedente puede ser ambigua.
Esta situación se presenta exactamente dentro de Prolog. El operador Í.s significa asignar
el valor equivalente, en tanto que el operador = significa asignar el patrón. La igualdad está
determinada entonces por el valor presente de la variable asignada. Así pues, en Prolog, la
primera cláusula de más abajo tiene éxito porque a .Xse asigna primero el valor 5 y su tipo se
fija en entero, mientras que ]a segunda cláusula fracasa, puesto que a.Yse le asigna la operación
"2+3", que no es lo mismo que el entero 5:

1. X es 2 + 3, X = 5.
2. X = 2 + 3, X = 5.

Adviértase que, en este caso, el símbolo = parece representar tanto un operador de


asignación como un operador booleano de comparación, según el contexto. De hecho, este es
un ejemplo del principio de unificación de Prolog; es decir, en la segunda cláusula las dos
asignaciones de 2 + 3 y de 5 aJÍ no son mutuamente compatibles. Esto se explica más a fondo
en la sección 6.3.2.

Inicialización. Una vcz//.czb/e #o /.n/.c/.cz//.zczc/cz, o, de manera más genera], un o¿y.e/o c7c cJc7/os no
j.#Í.cJ.cz/;.zczcJo, es un objeto de datos que ha sido creado pero al que todavía no se asigna un
valor (es decir, un valor 1 sin un valor r correspondiente). La creación de un objeto de datos
implica ordinariamente sólo la asignación de un bloque de almacenamiento. Si no hay más
acciones, el bloque de almacenamiento retiene el patrón de bits, cualquiera que éste sea, que
contenía cuando se hizo la asignación. Por lo regular, se requiere una asignación explícita
para en]azar un objeto de datos a un valor válido. En ciertos lenguajes (por ejemplo, Pascal)
la inicialización se debe hacer de manera explícita con enunciados de asignación. En otros
lenguajes (por ejemplo, APL) se deben especificar valores iniciales para cada objeto de datos
cuando se crea el objeto; la asignación de valores iniciales se maneja en forma implícita sin
que el programador use la operación de asignación.
Las variab]es no inicializadas son una fuente seria de error de programación para progra-
madores tanto profesionales como principiantes. Con frecuencia, el patrón aleatorio de bits
130 Tiposclec]dtos Cdp 4

que contiene el área de almacenamiento de valores de un objeto de datos no inicializado no se


puede distinguir de un valor válido, puesto que éste también aparece como un patrón de bits.
Así, un programa suele poder computar con el "valor" de una variable no inicializada y parecer
que opera correctamente, cuando de hecho contiene un error grave. A causa del efecto de las
variables no inicializadas sobre la fiabilidad del programa, la inicialización inmediata de
valores variables al crearlos se suele considerar como buena práctica de programación, y los
lenguajes más nuevos, como Ada, suministran recursos para hacer esto con más facilidad.
Por ejemplo, en Ada, cada declaración de variable puede también incluir un valor inicial para
la variable, usando la misma sintaxis que se usa para la asignación ordinaria. Por ejemplo:

A: array(l ..3) of float := (17.2, 20.4, 23.6);

crea un arreglo Á y asigna a cada elemento, en forma explícita, un valor inicial en la declaración.
Puesto que ordinariamente el arreglo Á se crea de manera dinámica durante la ejecución del
programa, la implementación de la asignación de valor inicial requiere la generación de código
por parte del compilador, el cual, cuando se ejecuta, asigna de manera expli'cita los valores
iniciales especificados del objeto
p
n
en el hardware

También podría gustarte