Introducción a Lenguajes de Programación
Introducción a Lenguajes de Programación
Página 1 de 78
Autor: informatica_uned@[Link]
1 Introducción
1.1 ¿Qué es un lenguaje de programación?
Básicas: son las sentencias de un lenguaje que combinan unas cuantas instrucciones de máquina
en una sentencia abstracta más comprensible. Ej: enunciado de asignación; goto.
Unitarias: consiste en efectuar abstracciones de control con la finalidad de incluir una colección
de procedimientos que proporcionan servicios relacionados lógicamente con otras partes del
programa y que forman una parte unitaria, o independiente, del programa. Un tipo de
abstracción de control que resulta difícil de clasificar son los mecanismos de programación en
paralelo, por ejemplo, las tareas (task) de ADA son una abstracción unitaria, pero los hilos o
hebras de Java se consideran estructuradas.
1
LENGUAJES DE PROGRAMACIÓN
Página 2 de 78
Autor: informatica_uned@[Link]
Programación lógica. El programa está formado por un conjunto de enunciados que describen
lo que es verdad con respecto a un resultado deseado, en oposición a dar una secuencia
particular de enunciados que deben ser ejecutados en un orden fijo para producir el resultado.
También se le conoce como programación declarativa o lenguajes de muy alto nivel. Las variables
no representan localizaciones de memoria sino que se comportan más como nombres para los
resultados de las computaciones parciales. Ej: Prolog.
Es necesario resaltar, que aunque un lenguaje de programación pudiera tener la mayor parte de
las propiedades de uno de los anteriores paradigmas, contienen generalmente características de
varios.
La definición del lenguaje se puede dividir aproximadamente en dos partes: sintaxis (estructura) y
semántica (significado).
2
LENGUAJES DE PROGRAMACIÓN
Página 3 de 78
Autor: informatica_uned@[Link]
Ej enunciado if de C:
<enunciado if>::= if (<expresión>)<enunciado>
[else<enunciado>]
o utilizando caracteres de formato especial:
enunciado if if (expresión) enunciado
[else enunciado]
Un problema íntimamente relacionado con la sintaxis de un lenguaje de programación es su
estructura léxica, la cual es similar a la ortografía del lenguaje natural. Es la estructura de las
palabras del lenguaje que generalmente se conocen como tokens.
Semántica del lenguaje. Una descripción completa de su significado en todos los contextos
puede llegar a ser extremadamente compleja. A pesar de ello se han desarrollado varios sistemas
de notación para definiciones formales: semántica operacional, la semántica denotacional y la
semántica axiomática.
La compilación es por lo menos un proceso que consta de dos pasos: el programa original
(programa fuente) es la entrada al compilador, y la salida del compilador es un nuevo programa
(programa objetivo) el cual puede ser entonces ejecutado (si está en lenguaje máquina), aunque
lo más común es que sea un lenguaje ensamblador, así que, el programa objetivo debe ser
traducido por un ensamblador en un programa objeto y posteriormente ligado con otros
programas objetos y cargado en localizaciones de memoria apropiadas antes de que pueda ser
ejecutado.
Tanto los compiladores como los intérpretes deben llevar a cabo: Primero, un analizador léxico,
es decir un rastreador, debe convertir la representación textual del programa como una secuencia
de caracteres en una forma más fácil de procesar (agrupando caracteres en tokens); acto
seguido, el analizador sintáctico o gramatical debe determinar la estructura de la secuencia de
los tokens proporcionados por el rastreador; finalmente, un analizador semántico debe
determinar lo suficiente del significado de un programa como para permitir la ejecución o la
generación de un programa objetivo. Estas fases ocurren combinadas de diversas maneras.
Un compilador puede utilizar sólo las propiedades estáticas de un lenguaje (su léxico y su
estructura sintáctica). En algunos lenguajes como C o Ada algunas propiedades semánticas
también son estáticas (tipos de datos de las variables).
Un lenguaje que sea más dinámico es más adecuado para la interpretación.
3
LENGUAJES DE PROGRAMACIÓN
Página 4 de 78
Autor: informatica_uned@[Link]
Un lenguaje que sólo tenga asignación estática puede utilizar un ambiente totalmente estático.
Para lenguajes más dinámicos debe utilizarse un ambiente más complejo totalmente dinámico. En
un punto medio está el típico ambiente basado en pilas (C y Ada).
Los errores léxicos ocurren durante el análisis léxico, generalmente están limitados al uso de
caracteres ilegales. Los errores ortográficos los detectaría el analizador sintáctico (incluyen
tokens faltantes o expresiones mal organizadas). Los errores semánticos pueden ser estáticos
(tipos incompatibles o variables no declaradas) o dinámicos (subíndice fuera de rango o la división
entre cero). Los errores lógicos de ninguna manera son errores desde el punto de vista de la
traducción del lenguaje.
La legibilidad de máquina y del ser humano son los requisitos que prevalecen en el diseño.
El reto del diseño del lenguaje de programación, es lograr la potencia, expresividad y comprensión
que requiere la legibilidad del ser humano, mientras que se conservan al mismo tiempo la
precisión y simplicidad necesarias para la traducción de máquina.
En lenguaje exitoso de programación tiene utilerías para abstracción de datos y abstracción de
control.
La meta prevaleciente de la abstracción en el diseño de lenguajes de programación es el control
de la complejidad.
Los errores se pueden clasificar de acuerdo con la etapa de traducción en la que ocurren.
Errores ortográficos como “whillle” en vez de “while”, los detectara el analizador sintáctico.
Los errores semánticos pueden ser estáticos (tipos incompatibles, variables no declaradas).
Errores lógicos: son aquellos que comete el programador, hacen que el programa se comporte de
una manera errónea.
4
LENGUAJES DE PROGRAMACIÓN
Página 5 de 78
Autor: informatica_uned@[Link]
3.2 Eficiencia
Optimizabilidad o eficiencia del código: El diseño del lenguaje debe ser tal que un traductor
pueda generar un código ejecutable eficiente.
Como ejemplo: las variables con tipos estáticos permiten generar código que las asignan y las
referencias con eficiencia.
Eficiencia de la traducción: ¿Permite el diseño del lenguaje que el código fuente se traduzca
con eficiencia, esto es, con rapidez y con un traductor de tamaño razonable?, ¿Permite el diseño
de un lenguaje que se escriba un compilador de una sola pasada? En Pascal y en C la variables
deben ser declaradas antes de que se utilicen, sin embargo en C++ esta restricción se ha liberado
por lo que se necesita una segunda pasada de compilación.
En ocasiones los lenguajes incluyen reglas extremadamente difíciles de verificar en tiempo de
traducción, o incluso en tiempo de ejecución.
En general la verificación de errores puede ser un problema grave de eficiencia, sin embargo el
ignorar la verificación de los errores viola otro principio de diseño, la confiabilidad (el
aseguramiento de que un programa no se comportará de forma inesperada ni desastrosa en
tiempo de ejecución).
Lo conciso de la sintaxis así como evitar detalles innecesarios como la declaración de variables
también se consideran factores importantes en este tipo de eficiencia. Desde este punto de vista
Lisp y Prolog son lenguajes ideales.
Esto puede comprometer otros principios del lenguaje como son la legibilidad, la eficiencia de
ejecución y la confiabilidad.
5
LENGUAJES DE PROGRAMACIÓN
Página 6 de 78
Autor: informatica_uned@[Link]
Un programa que no sea confiable genera muchos costos adicionales ocasionados por la
necesidad de aislar o eliminar el comportamiento erróneo, tiempo adicional de pruebas etc... En el
sentido de la ingeniería del software la eficiencia con la que se puede crear software depende
de la legibilidad y la capacidad de darle mantenimiento.
Los ingenieros del software estiman que ocupa mucho más tiempo en eliminar errores y en el
mantenimiento que en la codificación original, por la que la legibilidad y la capacidad de
mantenimiento pueden ser en último término de los mayores problemas de eficiencia.
3.3 Regularidad
Expresa lo bien que están integradas las características del mismo. Una mayor regularidad implica
pocas restricciones no usuales en el uso de constructores particulares, menos interacciones raras
entre dichos constructores y, en general, menos sorpresas en la forma en que se comportan las
características del lenguaje.
Generalidad:
- Pascal tiene funciones y procedimientos anidados y estos se pueden pasar como
parámetros a otros procedimientos, pero sin embargo estos no pueden ser asignados a
variables por lo que los procedimientos carecen de generalidad. C carece de la definición
de procedimientos anidados por lo que no tienen generalidad. Scheme y ML tienes un
constructor de procedimientos y funciones totalmente general.
- Pascal no tiene arreglos de longitud variable por lo que dichos arreglos carecen de
generalidad. C y Ada los tienen.
- En C no se pueden comparar estructuras o arreglos con el operador “==” por lo que el
operador carece de generalidad. En Ada esta restricción ha sido eliminada. Algunos
lenguajes como Haskell permiten la definición de nuevos operadores por lo que se puede
decir que los operadores han alcanzado la generalidad completa.
- En Fortran las constantes nombradas no existen, en Pascal no puede ser expresiones y
en Módula-2 las expresiones de constantes no puede incluir llamadas a funciones. Ada
tiene una herramienta de aclaración de constantes completamente general.
Ortogonalidad:
Esto consiste en que los constructores no pueden comportarse de manera diferente en
contextos diferentes, por lo que las restricciones que dependen del contexto son no
ortogonales:
- En Pascal, las funciones sólo pueden devolver tipos de datos escalares o
apuntadores.
- En C, las variables locales sólo pueden ser declaradas al principio de un bloque.
- En C existe una no ortogonalidad en el paso de parámetros a las funciones, pasa
todos los parámetros por valor excepto los de tipo arreglo.
Algo68 tuvo como meta la ortogonalidad, sus constructores pueden combinarse de todas
las formas significativas.
6
LENGUAJES DE PROGRAMACIÓN
Página 7 de 78
Autor: informatica_uned@[Link]
Hay que hacer notar que el afán por hacer imponer una meta como puedan ser la
generalidad o la ortogonalidad en el diseño puede resultar ser un error. Como ejemplo
tenemos Algo68, que si bien cumple con los criterios de generalidad y ortogonalidad esto
mismo hizo que el lenguaje tuviera una cierta oscuridad y complejidad.
La legibilidad y la confiabilidad pueden verse seriamente comprometidas sin la existencia
de restricciones en el uso de ciertas características.
Si una no regularidad no puede justificarse de forma razonable entonces se puede decir
que es un error de diseño.
Simplicidad.
La simplicidad fue el primer objetivo de Pascal y por lo cual tuvo tanto éxito. En principio la
simplicidad pude parecer fácil de lograr pero en la práctica es algo bastante complicado. No hay
que confundir simplicidad con regularidad, Algo68 es uno de los lenguajes más regulares, pero sin
embargo no es simple. Tener pocos constructores básicos no es simplicidad pero ayuda. Por otro
parte, un lenguaje muy simple puede, de hecho, hacer que su uso sea más complejo.
La sobresimplicidad de Pascal ha hecho que se utilicen más C, C++ y Java, por lo que C podría
considerarse como un intento de mayor éxito hacia la simplicidad.
La sobresimplicidad puede hacer que un lenguaje sea difícil de utilizar, carente de expresividad,
legibilidad o seguridad y sujeto a demasiadas restricciones.
Expresividad.
Es la facilidad con la cual un lenguaje puede expresar procesos y estructuras complejas. Uno de
los adelantos en la expresividad fue la introducción de la recursión en los lenguajes de
programación.
La expresividad pude entrar en conflicto con la simplicidad: Lisp, Prolog y Algo68 son lenguajes de
gran expresividad pero no simples (en parte como resultado de su expresividad).
El existo de la programación orientada a objetos viene determinada en gran parte por su gran
expresividad. Estas características (las de la programación OOP), ayudan sobre manera a los
programadores a escribir sus códigos imitando sus diseños.
A veces la expresividad puede comprometer la legibilidad. El lenguaje C es expresivo pero muchas
de sus expresiones puede resultar difíciles de comprender.
Extensibilidad.
Principio que indica que debería de haber algún mecanismo general que permita al usuario añadir
nuevas características al lenguaje. Podría significar simplemente el añadir nuevos tipos al
lenguaje, añadir nuevas funciones a una biblioteca o también poder añadir nuevas palabras claves
y constructores al traductor mismo. Esto en lenguajes funcionales como LISP, no resulta difícil.
Sin embargo en lenguajes imperativos esto resulta bastante más difícil.
A lo largo de los últimos 10 años la extensibilidad ha pasado a ser de importancia primordial como
una propiedad de los lenguajes. En concreto la simplicidad sin extensibilidad, prácticamente tiene
garantizado el fracaso del lenguaje.
7
LENGUAJES DE PROGRAMACIÓN
Página 8 de 78
Autor: informatica_uned@[Link]
Capacidad de restricción.
Un diseño del lenguaje debería dar la posibilidad de que un programador pudiera programar de
una forma útil empleando un conocimiento mínimo del leguaje y un número mínimo de
constructores. El lenguaje debería de tener la capacidad de definir subconjuntos del lenguaje.
Esto produce dos beneficios: No es necesario que un programador aprenda todo el lenguaje para
empezar a utilizarlo con efectividad y segundo el traductor podría elegir implementar un
subconjunto. Un aspecto de la capacidad de restricción es la eficiencia; un programa en C++ que
no utiliza el manejo de excepciones no debe ejecutarse más lentamente que un programa
equivalente en C por el simple hecho de que C++ pueda manejar excepciones.
Precisión.
Es la existencia de una definición precisa para el lenguaje, de tal manera que su comportamiento
de los programas pueda ser predecible. Un paso para lograr la precisión es la publicación de un
manual o informe del lenguaje por parte del diseñador.
Independencia de la máquina.
El método primordial para lograr la independencia de la máquina es el uso de datos predefinidos
que no involucran detalles de asignación de memoria o de la arquitectura de máquina.
Desafortunadamente, este tipo de datos no pueden nunca estar totalmente libres de problemas
de la máquina. Las constantes definidas por la implementación en las bibliotecas estándares de C
son un ejemplo de una manera útil de aislar las dependencias de la máquina.
Seguridad.
Este principio se basa en minimizar los errores de programación y permitir que dichos errores
sean detectados e informados. La seguridad está íntimamente relacionada con la confiabilidad y
con la precisión. Este es el principio que condujo a los diseñadores del lenguaje a introducir los
tipos, la verificación de tipos y las declaraciones de variables en los lenguajes de programación.
Con esto se puede comprometer tanto la expresividad como lo conciso del lenguaje. Carga al
programador con la tarea de poner tantas cosas cómo sea posible en el código real. Por otra
parte, en aplicaciones industriales, comerciales y de defensa, regularmente se presenta una
demanda de incluso mayor seguridad.
El problema real es la forma en el que un lenguaje debe generarse para que sea seguro y aún así
permitan un máximo de expresividad y generalidad.
8
LENGUAJES DE PROGRAMACIÓN
Página 9 de 78
Autor: informatica_uned@[Link]
4 Sintaxis
La sintaxis es la estructura de un lenguaje. Aunque la semántica sigue describiéndose en ingles,
uno de los más grandes adelantos en los lenguajes de programación es el desarrollo de un
sistema formal para describir la sintaxis. Las formas Backus Naur – BNF- se están utilizando de
manera frecuente en la definición de muchos lenguajes de programación incluyendo Java y Ada.
Existen 3 formas de representar BNF: BNF original, BNF extendido (EBNF) y los diagramas
sintácticos.
9
LENGUAJES DE PROGRAMACIÓN
Página 10 de 78
Autor: informatica_uned@[Link]
Consiste en un conjunto de reglas gramaticales, las cuales están formadas de un lado izquierdo
(un solo nombre de estructura) y a continuación el meta símbolo “” (a veces se reemplaza
por “::=”), seguido de un lado derecho formado por una secuencia de elementos que pueden ser
símbolos u otros nombres de estructuras. Las cursivas sirven para distinguir los nombres de las
estructuras de las palabras reales, es decir, de los tokens que pudieran aparecer en el lenguaje.
La barra vertical “|” es también un metasímbolo y significa “o”. Algunas veces un metasímbolo es
un símbolo real en un lenguaje, en cuyo caso, es recomendable entrecomillar el símbolo para
distinguirlo del metasímbolo. Para indicar que una oración debe estar seguida por algún tipo de
marcador final, se hace mediante el signo $.
Se tomará como problema semántico y no sintáctico todo aquello que no pueda ser expresado
con una gramática libre de contexto.
Ejemplo:
(1) oración -> frase-sustantiva frase-verbal
(2) frase-sustantiva -> artículo sustantivo
(3) artículo -> a | the
(4) sustantivo -> girl | dog
(5) frase-verbal -> verbo frase-sustantiva
(6) verbo -> sees | pets
10
LENGUAJES DE PROGRAMACIÓN
Página 11 de 78
Autor: informatica_uned@[Link]
La sintaxis establece una estructura pero no un significado. Pero el significado de una oración (o
un programa) tiene que estar relacionado con su sintaxis.
El proceso de asignar la semántica de una construcción a su estructura sintáctica se conoce como
semántica dirigida por la sintaxis. Por lo que se deberá construir la sintaxis de manera que
refleje lo mejor posible su semántica.
No todas las terminales y no terminales pudieran ser necesarios para determinar totalmente la
estructura sintáctica de una expresión o de una oración. Es estos casos los árboles suelen estar
condensados y se conocen como árboles de sintaxis abstracta o árboles sintácticos puesto
que abstraen la estructura esencial del árbol de análisis sintáctico. (Ver ejemplo Pág. 82).
Para el programador los árboles de sintaxis abstractas no son importantes (algunas veces la
sintaxis ordinaria se distingue de la sintaxis abstracta por el nombre de sintaxis concreta). No
ocurre así con los diseñadores del lenguaje y para los autores de traductores, ya que es la sintaxis
abstracta y no la concreta la que expresa la estructura esencial del lenguaje.
Dos derivaciones diferentes pueden conducir al mismo árbol sintáctico. Sin embargo diferentes
derivaciones pueden producir diferentes árboles de análisis gramatical y sus subsecuentes árboles
abstractos. Una gramática para la cual sean posibles dos árboles diferentes para un mismo
análisis sintáctico se dice que es ambigua.
Ejem.
expr -> expr + expr | expr * expr | (expr) | número
número -> número dígito | dígito
dígito -> 0 | 1 | 2 | 3 | …..
11
LENGUAJES DE PROGRAMACIÓN
Página 12 de 78
Autor: informatica_uned@[Link]
Una gramática para que sea útil no puede ser ambigua, por lo que si lo fuera habría que aplicarle
alguna regla para eliminar dicha ambigüedad.
La forma más habitual de revisar este tipo de gramáticas es escribiendo una nueva regla
gramatical (llamada un “término”) que establece una cascada de precedencia. Para el caso de la
gramática anterior:
EBNF surge para dotar al BNF de una notación especial para el tipo de reglas gramaticales que
expresan con mayor claridad la naturaleza repetitiva de su estructura.
En esta notación las llaves “{}” indican cero o más repeticiones de algo:
Número -> dígito {dígito}
Expr -> término {+término}
En este tipo de notación (Backus-Naur extendida) las llaves se han convertido en nuevos meta
símbolos. Esta notación oculta su asociatividad por la izquierda la cual es generada por la
recursividad por la izquierda.
Las reglas recursivas por la derecha no se pueden representar mediante llaves por lo que no será
por lo que a través de EBNF no se podrán representar directamente los árboles sintácticos o los
árboles de análisis sintáctico, por lo que utilizaremos siempre la notación BNF para escribir árboles
de análisis sintáctico.
Para indicar que una estructura tiene una parte opcional utilizaremos los corchetes “[]”:
Como ejemplo:
También los operadores asociativos por la derecha (binarios) pueden describirse utilizando
estos nuevos metasímbolos:
12
LENGUAJES DE PROGRAMACIÓN
Página 13 de 78
Autor: informatica_uned@[Link]
Una gramática escrita en forma BNF, EBNF o diagrama sintáctico describe las cadenas de tokens
que sintácticamente son correctas en el lenguaje de programación. Por lo que de manera implícita
quedan reflejadas las acciones que debe realizar el analizador sintáctico para analizar de forma
correcta una cadena de tokens.
- Analizadores sintácticos de abajo arriba: Intenta hacer coincidir una entrada con los
lados derechos de las reglas gramaticales, Cuando se encuentra con una coincidencia el
lado derecho es sustituido (reducido) por el no Terminal de la izquierda. Su denominación
(abajo arriba) viene dada por el hecho de que construyen derivaciones y árboles
sintácticos de las hojas hacia la raíz (también llamados de desplazamiento-reducción).
El analizador de abajo arriba es más poderoso que el otro por lo que es más utilizado
generalmente por los generadores de analizadores sintácticos (o compiladores de
compiladores). Un generador ampliamente utilizado es el YACC o su versión libre Birson.
- Otro método más antiguo de general analizadores a partir de su gramática, que resulta
muy efectivo es el análisis sintáctico por descenso recursivo. Básicamente opera
convirtiendo los no terminales en procedimientos mutuamente recursivos, cuyas acciones
están basadas en los lados derechos de los BNF. En estos procedimientos los lados
derechos se interpretan de la siguiente manera: los tokens se hacen coincidir con los
tokens de entrada según se van construyendo y los no terminales se asocian con llamadas
al procedimiento que los representa.
Este tipo de analizadores presenta un problema con las reglas recursivas por la izquierda como la
que corresponde con una expresión:
13
LENGUAJES DE PROGRAMACIÓN
Página 14 de 78
Autor: informatica_uned@[Link]
Si intentamos escribir esta regla como un proceso recursivo descendente se presentan dos
problemas graves:
1- La primera opción de esta regla haría que el proceso se llamara a sí mismo de manera
recursiva y de forma infinita.
2- El segundo problema estriba en que no se sabrá que opción es la correcta hasta que se
viese o no un (+) mucho más adelante en la entrada.
Estos problemas están motivados por la recursividad por la izquierda ya que con la
recursividad por la derecha
En EBNF las llaves representan la eliminación de la recursión por la izquierda, mediante el uso de
un ciclo (ver. 93) y aunque hay mecanismo más generales para resolver la recursión por la
izquierda, en al práctica el simple uso de EBNF es suficiente.
Por lo tanto en situaciones de recursión por la izquierda y en la factorización por la izquierda, las
reglas EBNF o los diagramas sintácticos corresponden con el código de un analizador sintáctico
por descenso recursivo, siendo esta una de las razones de amplia utilización.
14
LENGUAJES DE PROGRAMACIÓN
Página 15 de 78
Autor: informatica_uned@[Link]
A -> α1 | α2 | α3 | … | αn
Para decidir cual elegir, los tokens que inician cada αi tienen que ser distintos. ∩ Primero
(αi) = Ө, donde primero es la función que devuelve el conjunto de tokens que pueden
presentarse al principio de cada αi.
Una gramática libre de contexto típicamente incluye una descripción de los tokens de un lenguaje
al incluir en las reglas gramaticales las cadenas de caracteres que forman los tokens.
Algunas clases típicas de tokens, como las literales o constantes y los identificadores no son por sí
mismos secuencias fijas de caracteres, sino que se elaboran a partir de un conjunto fijo de
caracteres, como los dígitos del 0 al 9.
Estas clases de tokens pueden tener su estructura definida por la gramática, sin embargo, es
posible e incluso deseable utilizar un analizador léxico para reconocer estas estructuras, pues
puede hacerlo mediante una operación repetitiva simple.
Un conflicto entre sintaxis y semántica se presenta cuando los lenguajes requieren que ciertas
cadenas sean identificadores predefinidos (pueden ser modificados) en lugar de palabras
reservadas (cadenas fijas de caracteres no pueden utilizarse como identificadores).
15
LENGUAJES DE PROGRAMACIÓN
Página 16 de 78
Autor: informatica_uned@[Link]
5 Semántica básica.
La especificación de la semántica de un lenguaje de programación es una tarea más difícil que la
especificación de su sintaxis. Como ya se vio en el capítulo 1 existen varias formas de especificar
la semántica de un lenguaje de programación:
El significado de un nombre queda determinado por sus atributos asociados. Tanto las
declaraciones de constantes como las de funciones como las asignaciones tienen sus atributos
asociados.
Ejemplos:
En este caso asocia el atributo “función” al nombre f y los siguientes atributos adicionales:
1.- La cantidad, nombre y tipos de sus parámetros.
2.- El tipo de datos del valor devuelto.
3.- El cuerpo del código a ejecutarse cuando se llama a f.
- X = 2; Asocia el atributo “valor 2” a la variable X.
16
LENGUAJES DE PROGRAMACIÓN
Página 17 de 78
Autor: informatica_uned@[Link]
Los tiempos de ligadura pueden depender del traductor, es decir, para los interpretes se crearán
la mayoría de las ligaduras de forma dinámicas, mientras que para los compilados se crearán la
mayoría de las ligaduras de forma estáticas.
Un atributo estático puede vincularse durante el análisis gramatical o durante el análisis semántico
(tiempo traducción), durante el encadenamiento del programa (tiempo de ligado) o durante la
carga del programa para su ejecución (tiempo de carta).
Todos los tiempos anteriores representan ligaduras estáticas excepto el último que representa
ligaduras dinámicas.
El traductor debe conservar las ligaduras de tal manera que se den significados apropiados
durante la traducción y la ejecución. Esto se lleva a cabo mediante una estructura de datos que
visto de una manera abstracta se puede ver como una función que expresa la ligadura de los
atributos a los nombres. Esta función es parte fundamental de la semántica y se conoce como
tabla de símbolos. Esta función cambiará durante el proceso de traducción y/o ejecución para
reflejar adiciones o eliminaciones de ligaduras.
Entorno
Nombres ---------------------------------> Localizaciones
Mientras que las ligaduras de las localizaciones de almacenamiento con los valores se conoce
como la memoria:
Memoria
Localizaciones ---------------------------> Valores
Entorno
Nombres ---------------------------------> Atributos (incluyendo localizaciones y valores)
17
LENGUAJES DE PROGRAMACIÓN
Página 18 de 78
Autor: informatica_uned@[Link]
A través de la declaraciones puede determinarse las ligaduras ya sea de manera implícita como
explícita.
Int x; con esta declaración se establece de manera explícita el tipo de datos para X, pero su
localización exacta durante la ejecución se vincula de forma implícita.
Las declaraciones también pueden realizarse de manera implícita o explícita. Aquellos lenguajes
que tienen declaración implícita, generalmente tienen reglas convencionales de nombre para
establecer otros atributos.
Las declaraciones que vinculan ciertos atributos se conocen como definiciones, mientras que
aquellas que sólo especifican parcialmente los atributos se conocen simplemente como
declaraciones.
Las declaraciones están asociadas tanto sintácticamente como semánticamente con ciertos
constructores del lenguaje como son el Bloque, los Tipos de datos estructurados y las
clases:
Bloque: Consiste en una secuencia de declaraciones seguidas por una secuencia de enunciados,
y rodeado por marcadores sintácticos como son las llaves o los pares begin-end.
Las declaraciones hechas dentro de un bloque se conocen como locales mientras que las hechas
por fuera se conocen como no locales.
Para terminar las clases también pude reunirse en grupos más grandes como una manera de
organizar los programas: Los paquetes, las tareas de ADA, los paquete de Java, los módulos,
etc.…
Las declaraciones vinculan varios atributos a los nombres, dependiendo del tipo de declaración.
Cada una de estas ligaduras tiene por si misma un atributo que queda determinado por la
posición dentro de la declaración en el programa.
El alcance de un vínculo es la región del programa sobre la cuál se conserva el vínculo. A veces
nos referimos erróneamente al alcance un nombre, pero esto no es cierto pues puede haber
varias declaraciones diferentes sobre el mismo nombre y cada una de ellas con un alcance
distinto.
Se conoce como alcance léxico aquel que comprende el bloque dónde aparece su declaración
asociada. Esta es una regla estándar de alcance de la mayoría de los lenguajes.
Otra regla de alcance (en C) es la conocida como declaración antes de uso, y define el alcance de
una declaración desde el punto justo después de la misma hasta el final del bloque en el que está
localizado.
En el caso de bloques anidados las declaraciones en los bloques anidados toman preferencia
sobre declaraciones anteriores, este caso la declaración global se dice que tiene una apertura en
el alcance dentro del bloque anidado donde se encuentra la otra declaración. En base a esto se
hace una diferenciación entre visibilidad y alcance: la visibilidad hace referencia a aquellas
regiones del programa donde las ligaduras de una declaración son aplicables, en tanto que el
alcance incluye los agujeros en el alcance (dado que las ligaduras siguen existiendo). Se suele
18
LENGUAJES DE PROGRAMACIÓN
Página 19 de 78
Autor: informatica_uned@[Link]
utilizar el operador de resolución de alcance para tener acceso a estas declaraciones ocultas.
(Ejemplo Páginas 124-125).
Una situación especial de alcance sucede con las declaraciones de clases en lenguajes orientados
a objetos, pues este tipo de declaraciones tienen un alcance que se extiende hacia atrás a fin de
incluir toda la clase (suspendiendo la regla de declaración antes de uso).
Las vinculaciones establecidas por las declaraciones se conservan mediante la tabla de símbolos.
La forma en que quedan procesadas las declaraciones en la tabla de símbolos corresponde con el
alcance de cada declaración. Reglas de alcance diferente requieren un comportamiento diferente
en la tabla de símbolos e incluso el uso de estructuras diferentes dentro de la misma.
Ver ejemplos páginas 127-130. Hay que observar que este proceso conserva la información
apropiada de alcance incluyendo los agujeros de alcance. Esta representación de la tabla de
símbolos supone que la tabla procesa las declaraciones de manera estática (antes de su
ejecución) o sea manejada por un compilador, y que las ligaduras de las declaraciones sean todas
estáticas. Pero si estuviera administrada de esta misma forma pero dinámicamente, es decir,
durante la ejecución, entonces las declaraciones se procesan conforme se van encontrando a
través del programa a lo largo de la trayectoria de ejecución. Esto determina una regla de alcance
diferente denominada alcance dinámico. A la regla de alcance léxico anterior se le denomina
alcance estático. Ver ejemplo de alcance dinámico páginas 131-132.
Hay que notar que cada una de las llamadas a un mismo procedimiento a lo largo del programa
puede tener una tabla de símbolos diferentes a su entrada.
Existen varios problemas importantes por los cuales se hace difícil la utilización del alcance
dinámico por la mayoría de los lenguajes:
El principal problema y más importante es que bajo el alcance dinámico cuando se utiliza
un nombre no local en una sentencia, la declaración que se aplica a este nombre no puede
determinarse mediante la simple lectura del programa. Por lo que diferentes ejecuciones
del programa pueden conducir a diferentes resultados, por lo que la semántica de una
función puede cambiar considerablemente conforme avanza la ejecución del programa.
Otro problema serio es que dado que las referencias variables no locales no pueden
predecirse antes de la ejecución tampoco, pueden definirse los tipos de estas variables.
Por este motivo la ligadura estática de los tipos de datos (tipificado estático) y el alcance dinámico
son inherentemente incompatibles.
A pesar de todos esto, el alcance dinámico sigue siendo una opción posible para aquellos
lenguajes muy dinámicos, interpretados cuando no esperamos que los programas sean
19
LENGUAJES DE PROGRAMACIÓN
Página 20 de 78
Autor: informatica_uned@[Link]
(1) Una declaración “struct” realmente contiene una tabla de símbolos locales que es en sí
un atributo.
(2) Esta tabla de símbolos no puede eliminarse hasta que la variable “struct” que la
contenga sea eliminada de la tabla de símbolos global.
Cualquier estructura de alcance que puede ser referenciada directamente en un programa tiene
que tener su propia tabla de símbolos. Esto incluye los alcances nombrados en Ada, las clases, las
estructuras, las clases y los paquetes de Java, etc.… Por lo que una estructura de símbolos más
típica en cualquiera de estos lenguajes es tener una tabla para cada uno de los alcances, que a su
vez tienen que estar anidados con sus propias tablas dentro de las tablas que las encierran. De
nuevo estos pueden tener un estilo basado en pilas. (Ejemplo Página 138).
La sobrecarga se refiere hasta donde un mismo nombre pude utilizarse para referirse a cosas
distintas dentro de un mismo programa y es una faceta importante con respecto a las
declaraciones y a las operaciones con tabla de símbolos. Como ejemplo el símbolo + se refiere
como mínimo a dos operaciones completamente distintas: La adicción de enteros y la adicción en
coma flotante.
Algunos lenguajes como C++ y Ada permiten una amplia sobrecarga tanto de operadores como
de nombres de funciones, Java sólo permite la sobrecarga en los nombres de funciones.
Un traductor consigue determinar entre los distintos usos de un símbolo buscando en los tipos de
datos de los operandos. Para que esto sea posible hay que ampliar el mecanismo de búsqueda
para que no sólo se limite a una búsqueda por nombre sino que también distinga entre nº de
parámetro y el tipo de estos. A este proceso se le conoce como resolución de sobrecarga.
La tabla de símbolos puede determinar la función más apropiada para cada una de las llamadas
(ver ejem. Página 140 5.18) de la información contenida en cada llamada (contexto de
llamado).
Tanto Ada como C++ (pero no Java) permiten la sobrecarga en los operadores incorporados. En
estos casos hay que respetar las propiedades sintácticas del operador; no podemos cambiar su
asociatividad o su precedencia.
20
LENGUAJES DE PROGRAMACIÓN
Página 21 de 78
Autor: informatica_uned@[Link]
Se podría utilizar la sobrecarga para referirnos a cosas de tipos completamente diferentes con el
mismo nombre, si embargo esto resultaría extremadamente confuso, por lo que no está permitido
en la mayoría de los lenguajes.
Necesitamos estudiar el entorno, que mantiene las ligaduras de los nombres con las
localizaciones.
El entorno se puede construir estáticamente, dinámicamente o como una mezcla de ambos.
Entorno estático FORTRAN, dinámico LISP y mezcla C, C++, Ada, Java…
En un lenguaje con estructura de bloques las variables globales se asignan estáticamente. Las
variables locales, se asignan dinámicamente cuando la ejecución llega al bloque en cuestión.
Durante la ejecución, cuando se entra a cada uno de los bloques, las variables declaradas al
principio de cada bloque se asignan, y cuando se sale de cada bloque, estas mismas variables se
desasignan.
En un lenguaje con estructura de bloques y alcance léxico, se puede asociar el mismo nombre con
varias localizaciones diferentes. Debemos por tanto distinguir entre un nombre, una localización
asignada y la declaración que hace que queden vinculadas (Ejemplo Página 146-148).
Un apuntador es un objeto cuyo valor almacenado es una referencia a otro objeto: En C: int *x;
Para permitir la inicialización de los apuntadores que no apuntan a un objeto asignado, C permite
el uso del nombre null: En C: int *x=NULL;
Para que x apunte a un objeto asignado, debemos asignarlo manualmente mediante el uso de
una rutina de asignación. C usa malloc. Por lo que para asignar una nueva variable entera y al
mismo tiempo asignar su localización en el valor de x, se haría en C: x=(int*) malloc(sizeof(int));
Se dice que la variable x se puede desreferenciar utilizando el operador “*”, entonces podemos
asignar valores enteros a *x y referirnos a esos valores como lo haríamos en una variable
ordinaria:
*x=2;
*x también se puede desasignar haciendo una llamada al procedimiento free: free(x);
C++ incorpora new y delete como nombres reservados.
21
LENGUAJES DE PROGRAMACIÓN
Página 22 de 78
Autor: informatica_uned@[Link]
Para permitir la asignación arbitraria y la desasignación utilizando new y delete (o bien malloc y
free) el entorno debe tener un área en la memoria a partir de la cual se pueden asignar las
localizaciones en respuesta a las llamdas de new, y a la cual se pueden devolver las localizaciones
en respuesta a las llamadas de delete. Tradicionalmente esta área se conoce como un montículo o
montón (pero no tiene nada que ver con la estructura de datos montículo). La asignación se
conoce como asignación dinámica.
En una implementación típica del entorno, la pila (para la asignación automática) y el montículo o
montón (para la asignación dinámica) se mantienen en secciones diferentes de la memoria, y las
variables globales también se asignan en un área por separado, estática.
Java permite la asignación, pero no la desasignación bajo el control del programador.
Resumiendo, en un lenguaje con estructura de bloques con asignación de montones, existen tres
tipos de asignación en el entorno: estático (para variables globales), automático (para variables
locales) y dinámico (para asignación de montones). Estas categorías también se conocen como
clases de almacenamiento de la variable.
5.6.1 Variables.
Una variable es un objeto cuyo valor almacenado se puede cambiar durante la ejecución.
También se puede pensar en una variable como completamente especificada por sus atributos,
que incluyen su nombre, su localización, su valor y otros atributos como tipos de datos y
tamaños.
Una representación esquemática se puede dibujar como sigue. A la forma reducida se la conoce
como diagrama cuadro y círculo. La línea que une el nombre con el cuadro de localización se
puede pensar que une el nombre con la localización a través del entorno.
Nombre Valor
Localización
La forma principal en la que una variable cambia su valor es a través del enunciado de
asignación. Hay que distinguir entre la localización y el valor almacenado en dicha localización.
Al valor almacenado en un localización se conoce como valor r, mientras que al la localización se
conoce como valor l.
22
LENGUAJES DE PROGRAMACIÓN
Página 23 de 78
Autor: informatica_uned@[Link]
En los dos casos anteriores se dice que este tipo de asignación como una semántica de
apuntador, y la más usual se conoce como semántica de almacenamiento. Java utiliza la
asignación por compartición para todas las variables de objetos, pero no para los datos simples.
5.6.2 Constantes.
Una constante es una identidad del lenguaje que tiene un valor fijo durante la duración de su
existencia dentro de un programa. Una constante es como una variable, excepto que no tiene
atributo de localización.
Decimos que tiene una semántica de valor en vez de una de almacenamiento como las
variables. Su valor no puede modificarse y su localización no puede ser referenciada de manera
explícita a través de un programa. Una constante es esencialmente el nombre de un valor. Lo
literales son representación de valores (como las secuencia de caracteres “pepe” o el número
42) que hay que diferencia de las constantes.
Las constantes pueden ser estáticas: Aquella cuyo valor se puede computar antes de su la
ejecución, o dinámicas: aquellas cuyo valor sólo puede se computado únicamente durante la
ejecución.
Esta distinción es importante ya que las primeras (tiempo de compilación) pude ser utilizada por
el compilador para mejorar la eficiencia de un programa y no necesita ocupar memoria. Sin
embargo las que se computan en tiempo de carga o dinámica tienen que ser computada ya sea
en el arranque o conforme avanza la ejecución y debe ser almacenada en memoria.
A las primeras nos referiremos como constantes en tiempo de compilación y a las segundas como
constantes estáticas.
También se puede hacer una distinción entre las constantes en general (todas las vistas
anteriormente) y las constantes de manifiesto, que son el nombre de un literal.
23
LENGUAJES DE PROGRAMACIÓN
Página 24 de 78
Autor: informatica_uned@[Link]
Considere el ejemplo en C:
#include <time.h>
int f(int x)
{ const int d=x+1;
return b+c;
}...
5.7.1 Alias
Un alias ocurre cuando el mismo objeto está vinculado a dos nombres diferentes al mismo
tiempo, puede ocurrir de varias maneras:
• La llamada de procedimiento.
• Uso de variables de apuntador.
Ejemplo en C:
int *x,*y;
x= (int*)malloc(sizeof(int));
*x=1;
y=x; /* Despues de esta linea *y e *x se refieren a la misma variable */
*y=2;
printf(“%d\n”,*x); /* Se imprime 2 */
Los alias presentan un problema en el hecho de que causan efectos colaterales potencialmente
dañinos.
Efecto colateral: cualquier cambio en el valor de una variable que persiste más allá de la ejecución
de un enunciado. Los efectos colaterales no son todos dañinos. Los efectos colaterales que son
cambios a variables cuyos nombres no aparecen directamente en el enunciado son
potencialmente dañinos puesto que el efecto colateral no se puede determinar a partir del código
escrito.
El aliado debido a la asignación de apuntadores es difícil de controlar.
24
LENGUAJES DE PROGRAMACIÓN
Página 25 de 78
Autor: informatica_uned@[Link]
Una tercera manera como se pueden crear alias es a través de la asignación por compartición, ya
que la asignación por compartición utiliza apuntadores implícitamente, un ejemplo de ello es Java.
Ejemplo en Java:
class ArrTest
{ public static void main(String[] args)
{ int[] x={1,2,3}; //Se crea array tamaño 3, y con valores x[0]=1..x[2]=3
int[] y=x; //Se define otro arreglo y se inicializa con x
x[0]=42;
[Link](y[0]); //Imprime 42 debido a la asignación por compartición
}
}
Java tiene un mecanismo para clonar explícitamente cualquier objeto, de manera que los alias no
se creen por asignación.
Las referencias pendientes son un segundo problema que se puede presentar con el uso de
apuntadores. Un referencia pendiente es un localización que ha sido desasignada del entorno,
pero a la cual todavía tiene acceso el programa. Otra manera de definirlas es que son objetos que
pueden ser accedidos más allá de su tiempo de vida en el entorno.
Un ejemplo simple en C es el de un apuntador que apunta a un apuntador desasignado:
Otro ejemplo en C serian las que resultan de la desasignación automática de las variables locales
cuando se sale del bloque de la declaración local (esto es debido, a que C tiene & “direccion de”
que permite asignar la localización de cualquier variable a una variable apuntador):
{ int *x;
{ int y;
y=2;
x=&y; /* La variable x contiene la localización de y. *x es un alias de y */
}
/* *x es ahora una referencia pendiente */
}
25
LENGUAJES DE PROGRAMACIÓN
Página 26 de 78
Autor: informatica_uned@[Link]
5.7.3 Basura.
int *x;
...
x=(int*)malloc(sizeof(int));
x=0;
/* la localización asignada *x por la llamada a malloc es ahora basura, ya que ahora x
contiene el apuntador nulo y no existe ninguna manera de tener acceso al objeto
anteriormente asignado */
Otro ejemplo similar ocurre cuando la ejecución sale de la región del programa en el cual x misma
está asignada:
void p(void)
{ int *x;
x=(int*)malloc(sizeof(int));
*x=2;
}
/* Cuando se sale del procedimiento p, la variable x se deasigna y *x ya no es más
accesible para el programa. Una situación similar ocurre con bloques anidados. */
Los sistemas de lenguajes que recuperan automáticamente la basura se dice que llevan a cabo la
recolección de basura.
Los sistemas funcionales fueron pioneros en recolección de basura como LISP
También los sistemas orientados a objetos Smalltalk y Java lo usan automáticamente.
26
LENGUAJES DE PROGRAMACIÓN
Página 27 de 78
Autor: informatica_uned@[Link]
6 Tipos de datos.
La mayoría de los lenguajes incluyen un conjunto de entidades simples de datos, como enteros,
reales y booleanos, así como mecanismos para construir nuevos tipos a partir de los mismos.
Estas abstracciones contribuyen prácticamente a todas las metas del diseño de lenguajes como:
legibilidad, capacidad de escritura, confiabilidad e independencia de la máquina. Sin embargo
estas abstracciones pueden conllevar una serie de problemas como son:
Existe muchas razones para tener alguna forma de verificación de tipos estática (es decir en
tiempo de traducción):
Se ha avanzado mucho en comprender la manera de hacer los tipos estáticos más flexibles y que
al mismo tiempo conserven las propiedades antes descritas, y la mayoría de los lenguajes
modernos utilizan tipos estáticos y a la vez tienen una gran flexibilidad.
Los datos en los programas pueden clasificarse de acuerdo con sus tipos. Todo valor de datos
expresable en un lenguaje de programación tiene implícito un tipo. Por ejemplo, en C el valor -1
es de tipo entero, el 3,2334 es de tipo double...
Un tipo de datos es un conjunto de valores, junto con un conjunto de operaciones sobre dichos
valores y con ciertas propiedades.
27
LENGUAJES DE PROGRAMACIÓN
Página 28 de 78
Autor: informatica_uned@[Link]
Dado un grupo de tipos básicos como int, double y char, todo lenguaje ofrece una diversidad de
maneras para construir tipos más complejos: constructores de tipo, y los tipos creados se
conocen como definidos por el usuario. Ejemplo el arreglo int a[10]
Los nombres para los tipos nuevos se crean utilizando una declaración de tipo.
Los tipos que no poseen nombre se definen como un tipo anónimo. Para darle un nombre a ese
tipo utilizamos en C un typedef:
Durante la verificación de tipos un intérprete debe comparar dos tipos para determinar si se trata
del mismo, para lo que utiliza una serie de reglas: algoritmos de equivalencia de tipo.
Los métodos utilizados para la construcción de tipos, los algoritmos de equivalencia, las reglas de
inferencia y las de corrección de tipos, se conocen de manera colectiva como un sistema de
tipos.
Cuando en un lenguaje todos los errores de tipo se detectan en tiempo de traducción se dice que
es un lenguaje fuertemente tipificado.
Los lenguajes parecidos a Algol (C, Ada, Pascal), incluso los orientados a objetos (C++, Java),
clasifican los tipos de datos de acuerdo con un esquema estándar relativamente básico, con
desviaciones de poca importancia.
Todo lenguaje contiene un conjunto de tipos predefinidos, a partir de los cuales se construyen
todos los demás tipos. Sin embargo, existen tipos simples que no están predefinidos: los tipos
enumerados y tipos de subrango que también son simples.
Los tipos enumerados son conjuntos cuyos elementos se denominan y se listan de manera
explícita.
Ejemplo en C:
Los tipos enumerados en la mayoría de los lenguajes presentan un orden atendiendo al orden en
el cual se listan, y normalmente existe una función predecesora o sucesora. En C si se declara
un tipo enum, los valores son tomados como nombres de enteros y automáticamente se les
asignan los valores 0,1,..., a menos que el programador inicialize los valores enum a otros
enteros.
28
LENGUAJES DE PROGRAMACIÓN
Página 29 de 78
Autor: informatica_uned@[Link]
Ejemplo en C:
#include <stdio.h>
main()
{ enum Color x=Green; /* Ahora x vale 1 */
enum NewColor y=NewBlue; /*Ahora y es en realidad 2*/
....
}
Los tipos subrangos son subconjuntos contiguos de tipos simples especificando por lo memos el
menor y el mayor elemento.
Los lenguajes de la familia C (C, C++ y Java) no tienen tipos subrango, dado que se puede lograr
el mismo efecto manualmente escribiendo los tipos de enteros de tamaño apropiado para ahorrar
almacenamiento y escribiendo verificaciones explícitas para los valores, como en el siguiente
ejemplo de Java:
Sin embargo, los tipos subrango siguen siendo útiles, ya que hacen que este tipo de código se
genere de manera automática.
Típicamente los subrangos se definen dando el primer y el último elemento de otro tipo y se
conocen como tipos ordinales pues tienen un orden discreto en el conjunto. Como tipos
ordinales en cualquier lenguaje tenemos: enteros numéricos, enumeraciones y los subrangos.
Estos siempre tienen operadores de comparación (<=, <>…) y a menudo también tienen
operaciones de sucesor y predecesor. No todos los tipos con operadores de comparación son
ordinales, como ejemplo tenemos los reales que tienen operaciones de comparación pero no
sucesor ni predecesor.
Ya que los tipos de datos son conjuntos, se pueden utilizar las operaciones de conjuntos para
crear nuevos tipos de datos a partir de los existentes. Estas operaciones incluyen: el producto
cartesiano, la unión, el conjunto potencia, el conjunto de función y el subconjunto.
Cuando se aplican estas operaciones de tipo a los tipos se les denomina constructores de
tipos. También existen algunas operaciones de conjuntos que no corresponden con ningún
constructor como por ejemplo la intersección.
Dados los conjuntos U y V, podemos formar el producto cartesiano o cruz formado por todos los
pares ordenados de elementos de U y V.
En muchos lenguajes el constructor de tipos del producto cartesiano está disponible como la
construcción de estructuras o de registros. Por ejemplo en C:
29
LENGUAJES DE PROGRAMACIÓN
Página 30 de 78
Autor: informatica_uned@[Link]
struct IntCharReal
{ int i;
char c;
double r;
}; /* Construye el tipo de producto cartesiano int X char X double */
Existe una diferencia entre un producto cartesiano y una estructura de registro: en un estructura
los componentes tienen nombre, en tanto que en un producto cartesiano se hace referencia a
ellos por su posición.
Algunos lenguajes tienen una forma más pura del tipo estructura de registro, que es en esencia
idéntica al producto cartesiano, donde a menudo se les denomina tuplas (por ejemplo ML).
Un tipo de datos que se encuentra en los lenguajes orientados a objetos, que está relacionado
con las estructuras, es la clase.
Un esquema típico para la asignación para los tipos de producto cartesiano es la asignación
secuencial, según el espacio que requiere cada componente.
6.3.2 Unión.
Al igual que con struct existen nombre para diferenciar los distintos componentes (i y r). Los
nombre son necesarios por que comunican al interprete el tipo con el que deben interpretarse los
bits dentro de la unión.
Estos nombre no deben de confundirse con los discriminante, que es un componente separado,
que indica el tipo de datos que es realmente el valor, a diferencia del tipo que puede pensar que
es. En C, puede imitarse un discriminante de la siguiente forma:
30
LENGUAJES DE PROGRAMACIÓN
Página 31 de 78
Autor: informatica_uned@[Link]
IntOrReal x;
[Link]=IsReal;
[Link].r=2.3;
....
Las uniones pueden resultar útiles para reducir los requerimientos de asignación de memoria para
las estructuras cuando no se necesitan, simultáneamente, diferentes elementos de datos. Esto se
debe a que a las uniones se les asigna un espacio de memoria equivalente al mayor necesario
para cada uno de sus componentes y los valores de cada componente se almacenan en regiones
superpuestas de la memoria.
Las uniones, sin embargo, no son necesarias en lenguajes orientados a objetos, ya que en un
mejor diseño sería utilizar la herencia para representar diferentes requerimientos de datos que no
se superponen. Por lo tanto Java no utiliza uniones, C++ sí utiliza uniones principalmente por
compatibilidad con C.
6.3.3 Subconjuntos
En matemáticas se pueden definir subconjuntos al dar una regla para distinguir sus elementos,
como pos_int = {x|x es un entero y x > 0}. En los lenguajes de programación se puede hacer
algo parecido para definir nuevos tipos que serán subconjuntos de tipos conocidos.
En ocasiones los subconjuntos heredan operaciones de sus tipos padres.
La herencia en los lenguajes orientados a objetos se puede considerar como un mecanismo de
subtipo, en el mismo sentido de compartir operaciones.
El conjunto de todas las funciones f:U->V puede dar lugar a un nuevo tipo de dos formas: como
un tipo arreglo o como un tipo de función. Cuando U es un ordinal, la función puede
considerarse como un arreglo con un tipo de índice U y tipo de componente V. En C, C++ y Java
el conjunto de índices siempre es un rango de enteros positivos que comienzan por 0.
Los arreglos pueden definirse con o sin tamaño, pero para definir una variable de tipo arreglo hay
que asignarle un tamaño ya que los arreglos son asignados estáticamente o en la pila.
typedef int TenIntArray [10]; /*Aqui ya se determina el tamaño de este tipo estaticamente
*/
typedef int IntArray [];
TenIntArray x;
IntArray w={1,2}; /*Aqui se determina el tamaño por la lista de valores iniciales, en
este caso si no se da una lista de inicio de valores, no se sabría su
tamaño y sería incorrecto declararlo como símplemente IntArray w;
*/
int y[5];
int z[]={1,2,3,4};
31
LENGUAJES DE PROGRAMACIÓN
Página 32 de 78
Autor: informatica_uned@[Link]
Sin embargo, C sí permite que arreglos sin tamaño especificado sean parámetros de funciones:
En C y C++ el tamaño del arreglo no forma parte del mismo, por eso en el ejemplo anterior el
tamaño del arreglo tuvo que ser pasado como un parámetro adicional.
class ArrayTest
{
static int array_max (int [] a) //Aqui los [] pueden estar despues del tipo
{
int temp;
temp=a[0];
// El tamaña es parte del arreglo a
for (int i=1; i<[Link]; i++)
...
}
}
public static void main (String args[]) //Tambien se permite que los [] esten asi
{
...
/** Se utiliza el teclado para meter datos y.... **/
int u =[Link]([Link]());
int[] x= new int[u]; // Ubicación de un arreglo dinámico
...
}
Los arreglos multidimensionales también son posibles en C, C++ y Java, ya que se permiten
arreglos de arreglos:
32
LENGUAJES DE PROGRAMACIÓN
Página 33 de 78
Autor: informatica_uned@[Link]
Los arreglos probablemente son los constructores más utilizados ya que su implementación puede
hacerse en forma muy eficiente.
Los lenguajes funcionales por lo general no contienen un tipo arreglo ya que estos están
pensados para la programación imperativa. Usualmente los lenguajes funcionales utilizan listas en
vez de arreglos.
En algunos lenguajes pueden crearse tipos generales de función y procedimiento. Un ejemplo en
C en el que se define un tipo de función de enteros a enteros:
Obsérvese que C exige que definamos las variables, tipos y parámetros de una función utilizando
notación de apuntadores, pero entonces ya no requiere que llevemos a cabo ningún
desreferenciamiento.
La mayoría de los lenguajes orientados a objetos, como Java y Smalltalk, no tienen variables o
parámetros de función; están enforcados a los objetos en vez de a las funciones.
Ejemplo en C de una declaración que construye el tipo de todas las direcciones en que haya
enteros almacenados.
Si x es una variable de tipo IntPtr puede entonces desreferenciarse para asignar por
ejemplo el valor 10 a la localización dada en x: *x=10;
Por supuesto, a x debió asignársele previamente una dirección válida, lo que se suele
llevar a cabo de forma dinámica mediante el uso de la función malloc:
x=(int*)malloc(sizeof(int));
Los apuntadores están implícitos en lenguajes que tienen un gestión automática de memoria.
Esto es el caso de java para el cual todos los objetos son apuntadores implícitos que se asignan
de forma explícita (new) pero son desasignados automáticamente por un recolector de basura.
En ocasiones los lenguajes hacen distinción entre referencias y apuntadores, definiendo como
referencia la dirección de un objeto bajo el control del sistema, que no se puede utilizar como
valor ni operar de forma alguna. Tal vez C++ sea el único lenguaje donde coexisten apuntadores
y referencias. Los apuntadores en Java en realidad son referencias. En C++ los tipos de
referencia se crean con un operador postfijo & (lo cual no debe confundirse con el operador
prefijo de dirección &, que devuelve un apuntador). Ejemplo de C++:
33
LENGUAJES DE PROGRAMACIÓN
Página 34 de 78
Autor: informatica_uned@[Link]
double r = 2.3;
double& s=r; // s es solo una referencia a r , de esta forma comparten memoria
s += 1; // ahora tanto r como s valen 3.3
Este mismo ejemplo puede implementarse por medio de apuntadores (asi tambien se
podria hacer en C):
double r =2.3;
double *p = &r; // p tiene como valor la dirección de r
*p +=1; // Ahora r sería 3.3
Las referencias en C++ son en esencia apuntadores constantes que se desreferencian cada vez
que se usan.
En C, los arreglos apuntan implícitamente a su primer componente. a[2] que sería el tercer
elemento del array a, es tan sólo una abreviatura de a+2, asi que, tambien podemos escribirlo
como 2[a].
Los apuntadores son de gran utilidad en la creación de tipos recursivos: un tipo que se utiliza
así mismo en su declaración. Estos tienen una gran importancia en las estructuras de datos y
algoritmos, ya que corresponden naturalmente a los algoritmos recursivos y representan datos
cuya estructura y tamaño no se conocen de antemano. Dos ejemplos típicos son las listas y los
árboles.
struct CharList
{ char data;
struct CharList next; /* Sería incorrecto, ya que datos de este tipo deberían
contener una cantidad infinita de caracteres */
};
struct CharListNode
{ char data;
struct CharListNode* next;
};
typedef struct CharListNode* CharList; /* Ahora cada elemento individual en
CharListNode tiene un tamaño fijo y pueden
encadenarse para formar una lista de tamaño
arbitrario */
Entorno es cuando la estructura de un tipo de datos requiere que el espacio se asigne en forma
dinámica, éste es el caso de los tipos apuntador, tipos recursivos y los tipos de función general.
En sus formas más generales, estos tipos requieren de entornos completamente dinámicos con
asignación y deasignación automáticas.
34
LENGUAJES DE PROGRAMACIÓN
Página 35 de 78
Autor: informatica_uned@[Link]
6.4.1 C
En C los tipos simples se conocen como tipos básicos, y los tipos que se construyen mediante
constructores de tipos se conocen como tipos derivados.
Tipo C
Básicos Derivados
Integral Flotante
6.4.2 Java
Los tipos simples se llaman primitivos, y los tipos se construyen utilizando constructores de tipos
se llaman tipos de referencia.
Sólo existen tres constructores de tipos en Java: el arreglo, la clase y la interfaz.
Tipo Java
Primitivo Referencia
¿En que casos dos tipos son iguales? Una manera de responder a esta pregunta es comparar los
conjuntos de valores simplemente como conjuntos. Dos conjuntos son iguales si contienen los
mismos valores.
Dos de las formas más habituales de equivalencia de tipos en los lenguajes de programación
actuales son: la equivalencia estructural y la equivalencia de nombre:
35
LENGUAJES DE PROGRAMACIÓN
Página 36 de 78
Autor: informatica_uned@[Link]
La equivalencia estructural viene a decir que dos tipos son iguales si tienen la misma
estructura: están construidos de la misma forma a partir de los mismos tipos simples y en el
mismo orden y con los mismo nombres de variables internas. Es fácil de implementar y aporta
toda la información necesaria para llevar a cabo la verificación de errores y asignación de
almacenamiento. Para verificar la equivalencia estructural, un intérprete puede representar los
tipos como árboles y verificar la equivalencia recursivamente en subárboles.
La equivalencia de nombres se refiere a que dos tipos son iguales sólo si tienen el mismo
nombre. La equivalencia de nombres en su estado más puro es incluso más fácil de implementar
que la estructural, siempre y cuando estemos obligados a dar nombre a todos los tipos. Ada es un
lenguaje que ha implementado un equivalencia de nombres muy pura. C tiene una equivalencia
que está entre la estructural y la de nombres y se puede decir que tiene una equivalencia de
nombre para structs y unions, y estructural para todo lo demás.
struct RecA
{ char x;
int y;
};
typedef struct RecA RecA; /* Define un tipo nuevo llamado RecA, que es un struct RecA*/
struct
{ char x;
int y;
}d;
Java tiene un método relativamente simple para la equivalencia de tipos. Primero no typedef, por
lo que se minimizan los problemas con nombres. Segundo las declaraciones Class e interface
crean implícitamente nuevos nombres de tipos y para estos tipos se utiliza la equivalencia de
nombres. La única complicación es que los arreglos emplean equivalencia estructural con reglas
especiales para establecer la equivalencia del tipo base.
La verificación de tipos es el proceso que sigue el interprete para verificar que todas la
construcciones en un programa tengan sentido en términos de los tipos de constantes, variables,
procedimientos y otras entidades. Involucra al algoritmo de verificación de tipos para expresiones
y asignaciones, y este pude modificar el algoritmo de equivalencias al contexto.
36
LENGUAJES DE PROGRAMACIÓN
Página 37 de 78
Autor: informatica_uned@[Link]
En la verificación estática, los tipos de expresiones y de datos se extraen del texto del programa y
el intérprete lleva a cabo la comprobación antes de la ejecución, por lo que estos lenguajes deben
de tener tipificado estático.
Ejemplo 2: El dialecto Scheme de Lisp es un lenguaje con tipificado dinámico, pero los
tipos se verifican en forma rigurosa: Todos los errores de tipo provocan la terminación del
programa.
Ejemplo 3: Ada es un lenguaje con tipificado fuerte y todos los errores de tipo generan
mensaje de error en la compilación, pero sin embargo, incluso en Ada, ciertos errores,
como los de rango en subíndice de arreglos, no puede detectarse antes de la ejecución.
Una parte esencial en la verificación de tipos es la inferencia de tipos, en la que los tipos de las
expresiones se determinan a través de las subexpresiones que la componen.
La inferencia de tipos y las reglas de corrección a menudo son las partes más complejas en la
semántica de un lenguaje.
A menudo es necesario relajar las reglas de corrección de tipos de manera que los tipos de dos
componentes no sean precisamente iguales, según el algoritmo de equivalencia de tipos. Dos
tipos diferentes que incluso pueden ser correctos cuando se combinan en cierta forma se conocen
como tipos compatibles.
Igual que para la compatibilidad ordinaria, la compatibilidad de asignación pude ampliarse para
casos en los que ambos lados son de diferente tipo.
En C y en Java, todos los tipos numéricos son compatibles y las conversiones se llevan a cabo de
forma que se conserva tanta información como sea posible.
Los tipos de las entidades básicas como las constantes o las variables pueden no establecerse
explícitamente en una declaración. En estos casos el intérprete debe de inferir el tipo ya sea a
través del contexto o a partir de alguna regla estándar. Estos tipos se conocen como implícitos.
En todos los lenguajes los literales son el ejemplo más claro de entidades tipificadas
implícitamente.
37
LENGUAJES DE PROGRAMACIÓN
Página 38 de 78
Autor: informatica_uned@[Link]
Los tipos pueden superponerse cuando dos tipos contienen valores en común. Normalmente es
preferible que los tipos sean conjuntos no conexos.
Sin embargo, imponer esta restricción de forma arbitraria sería demasiado limitante, y eliminaría
una de las características principales de la programación orientada a objetos: la capacidad de
crear subtipos mediante la herencia que refina los tipos existentes, al tiempo que conservan su
pertenencia en el tipo más general.
Por ejemplo, en Java un entero pequeño pudiera ser un “short”, un “int” o un “long”. En los los
tipos “unsigned int” e “int” contienen una superposición sustancial.
Los tipos tienen operaciones que usualmente están definidas de forma implícita. A menudo estas
operaciones son compartidas entre varios tipos, o tienen el mismo nombre que otras operaciones
que pueden ser diferentes (por ejemplo, Operador + puede ser la suma de reales o enteros, o
bien la unión de conjuntos). Se considera que estos operadores están sobrecargados. En este
caso el intérprete debe decidir a partir de los tipos de los operandos, a que operación se refiere.
En Java no existe ninguna clase de operación aritmética en tipos enteros que no sean int o long.
En todos los lenguajes de programación actuales, existe la necesidad bajo ciertas circunstancias
de convertir un tipo a otro. Esta conversión de tipos puede incluirse en el sistema, de forma que
las conversiones se lleven a acabo de forma automática. Ejemplo en C:
int x = 3;
….
x= 2.3 + x / 2;
Al final de este código x sigue valiendo 3, que 3.3 es truncado por el proceso de conversión. En
este ejemplo el intérprete ejecutó dos conversiones implícitas a veces conocidas como
coacciones. La conversión de int a doble es de extensión, mientras que al revés sería de
restricción.
La conversión implícita puede debilitar la verificación de tipos de forma que no se detecten
errores lo que pone en peligro el tipificado fuerte y la confiabilidad del lenguaje de programación.
En C++ se utiliza llamando a funciones, con el valor que desea convertirse como argumento del
parámetro de la función que convierte al tipo deseado:
x= int(2.3 + double(x/2));
La ventaja de utilizar conversiones forzadas es que las conversiones que se llevan a cabo se
documentan en forma precisa dentro del código, y existe menor probabilidad de comportamientos
inesperados.
38
LENGUAJES DE PROGRAMACIÓN
Página 39 de 78
Autor: informatica_uned@[Link]
En algunos lenguajes está prohibida la conversión implícita a favor de la explicita, la cual favorece
la documentación de la conversión minimizando los riesgos de comportamientos extraños y facilita
la sobrecarga. Como ejemplo de estos lenguajes tenemos a Ada. Un paso intermedio es permitir
la conversión implícita siempre que no involucre corrupción de los datos, en este sentido Java sólo
permite conversión por extensión.
Los lenguajes orientados a objetos tienen requerimientos especiales para la conversión, ya que la
herencia puede interpretarse como un mecanismo de subtipificación y en algunos casos es
necesario hacer conversiones de subtipos a supertipos y viceversa.
Una alternativa a las conversiones forzadas es tener funciones predefinidas o de biblioteca, que
lleven a cabo las conversiones. Por ejemplo, en Java la clase Integer que está en la biblioteca
[Link] contiene funciones de conversión toString.
La mayoría de los lenguajes de tipificado estático exigen que en todos los nombres de cada
declaración se dé información explícita sobre los tipos.
Es posible aplicar una forma de inferencia de tipos para determinar los tipos de los nombres en
una declaración sin dar explícitamente estos tipos. Un intérprete puede obtener información sobre
los usos de un nombre, e inferir del conjunto de todos los usos el tipo probable, que es el tipo
más general en el cual todos los usos son correctos. (Ejemplo Página 214-216)
El polimorfismo paramétrico implícito está bien para definir funciones polimórficas, pero no nos
ayuda si deseamos definir estructuras de datos polimórficos, para ello debemos escribir en forma
explícita mediante la sintaxis apropiada.
En C++ y Java los constructores siempre tienen el mismo nombre que su clase y tipo asociados.
C++ es un ejemplo de lenguaje polifórmico paramétrico explícito y usa para ello las plantillas que
pueden usarse ya sea con funciones o con constructores de tipos class o struct.
39
LENGUAJES DE PROGRAMACIÓN
Página 40 de 78
Autor: informatica_uned@[Link]
7.1 Expresiones.
Las expresiones básicas son los literales (constantes manifiestas) y los identificadores. Las
expresiones más complejas se elaboran en forma recursiva a partir de las básicas mediante la
aplicación de operadores y funciones, lo que a veces involucra símbolos de agrupamiento como
los paréntesis.
Los operadores pueden tomar uno o más operandos (unarios, binarios, etc.…). Se pueden
escribir con notación infija, postfija y prefija que corresponde con un recorrido en orden,
postorden y preorden del árbol sintáctico de la expresión respectivamente. Las formas prefijas y
postfijas tienen la ventaja de no necesitar paréntesis para expresar el orden en que se aplican los
operadores. La asociatividad de operadores también queda implícita con las notaciones prefija y
postfija sin la necesidad de reglas.
Muchos lenguajes hacen distinción entre operadores y funciones. Los operadores si son
binarios se escriben en forma infija con reglas de asociatividad y precedencia específicas. Las
funciones que pueden ser predefinidas o definidas por el usuario se escriben en forma prefija y
los argumentos se consideran como parámetros o argumentos reales.
Todos los lenguajes tienen reglas para evaluar las expresiones. Una de las más utilizadas es la
evaluación de orden aplicativo o evaluación estricta la cual consiste en evaluar primero los
operandos y luego aplicarle los operadores. Corresponde con una evaluación de abajo arriba de
los nodos del árbol sintáctico que representa a la expresión.
En C una asignación (la quintaesencia del efecto colateral) es una expresión no un enunciado, no
sólo asigna un valor sino que devuelve este copiado como un resultado.
x = (y = z).
40
LENGUAJES DE PROGRAMACIÓN
Página 41 de 78
Autor: informatica_uned@[Link]
Un operador de secuencia es aquel que permite que se combinen varias expresiones en una
sola y que se evalúen secuencialmente.
La evaluación de una expresión puede llevarse a cabo incluso sin evaluar todas las
subexpresiones. Un ejemplo interesante son las expresiones booleanas o lógicas.
Muchos lenguajes disponen de expresiones que imitan enunciados de control pero devuelven
valores (sentencias if then else y las expresiones Case). En estos casos al igual que con las
expresiones booleanas, ciertas subexpresiones pueden no ser evaluadas. Las expresiones
booleanas de cortocircuito pueden expresar en forma de expresiones if. Los operadores booleanos
de corto circuito y de if son un caso especial de operadores que difieren la operación de sus
operandos, evaluación diferida o no estricta.
La transparencia referencial permite que se utilice un forma muy sólida de evaluación diferida,
evaluación de orden normal, lo que significa que en una expresión los operadores (o
funciones) comienzan a evaluarse antes de que sus operandos sean evaluados, y estos son
evaluados sólo si son necesarios para el cálculo del valor de la operación. La evaluación de orden
normal se conoce como evaluación perezosa en el lenguaje funcional Haskell.
La forma más típica del control estructurado es la ejecución de un grupo de enunciados sólo bajo
ciertas condiciones. Esto incluye llevar a cabo una prueba booleana o lógica antes de entrar a una
secuencia de enunciados.
Enunciado if es la forma más común.
El enunciado if cauteloso, es un tipo de case o de switch
If B1 -> S1
| B2 -> S2
| B3 -> S3
.......
| Bn -> Sn
fi >
Las Bi son expresiones booleanas conocidas como guardas y las Si son secuencias de enunciados.
Si uno de los Bi se evalúa como cierto, la secuencia correspondiente es ejecutada, pero si más de
un Bi es verdadero, se selecciona solo una de las Si. Si ningún Bi es verdadero ocurre un error.
41
LENGUAJES DE PROGRAMACIÓN
Página 42 de 78
Autor: informatica_uned@[Link]
Enunciado puede ser un solo enunciado o una secuencia de enunciados encerrados entre
corchetes.
Esta forma de if (también existe en Java y Pascal) presenta un problema: es ambiguo: el
enunciado
Esta ambigüedad se conoce como el problema del else ambiguo. C resuelve el problema mediante
una regla para eliminar la ambigüedad: el else se asociará con el if anterior más cercano que no
tenga ya una parte else. A esta regla también se le conoce como la regla del anidamiento más
cercano.
El problema del else ambiguo es de diseño de lenguaje, nos obliga a establecer una nueva regla
para describir algo que es esencialmente una característica sintáctica y dificulta al lector
interpretar el enunciado if, viola el criterio de legibilidad del diseño.
Es posible escribir reglas BNF que especifiquen con precisión la asociación (Java).
Además de esta regla, se puede solucionar el problema del else ambiguo empleando una palabra
clave enmarcadora (End If,…) con lo que también se elimina la necesidad de utilizar corchetes.
También se pude utilizar el “Else If” compactado “elseif” que proporciona una nueva secuencia de
enunciados en el mismo nivel.
En Java la condición siempre debe tener tipo booleano. C no tiene tipo booleano por lo que en C y
C++ la expresión de control puede ser de tipo entero o de tipo puntero. El valor resultante se
compara entonces con 0 (el apuntador nulo, en el caso del tipo apuntador); una comparación
desigual es equivalente a verdadero, la comparación igual es equivalente a falso.
El enunciado case (switch en Java, C y C++) fue creado como una variante del If Cauteloso,
donde cada guarda en vez de ser una expresión booleana, representa un valor ordinal
seleccionado por una expresión ordinal.
Los valores de los casos pueden ser literales o expresiones como en C. Si el valor de la expresión
de control no estuviera en algún caso, el control se transfiere a la opción “default” si existe, en
caso contrario la ejecución seguiría con la primera sentencia que sigue al bloque switch.
Ejemplo en C:
switch (x-1)
{ case 0:
y = 0;
z = 2;
break;
case 1:
case 2: z = 10;
break;
default:
break;
}
42
LENGUAJES DE PROGRAMACIÓN
Página 43 de 78
Autor: informatica_uned@[Link]
do B1 -> S1
| B2 -> S2
| B3 -> S3
| B4 -> S4
…
| Bn -> Sn
od
Las formas básica de la construcción de ciclo, que en esencia es un do cauteloso con solo una
guardia (eliminando así el no determinismo), es el ciclo while de C, C++ y Java: while (e) S
La mayoría de los lenguajes cuentan con un enunciado alterno, que asegura que el código del
ciclo se ejecute por lo menos una vez. En C y en Java este enunciado es el do-while: do S while
(e)
Las construcciones while y do tienen la propiedad de que la terminación del ciclo se especifica
explícitamente solo al principio (while) o al final (do).
A menudo también es conveniente salir del ciclo en uno o más puntos intermedios. Por esta razón
C al igual que Java, incluye dos opciones: break para salir por completo de un bucle y continue
que se salta el resto del cuerpo del bucle.
Un caso especial, muy común de la construcción de ciclos es la construcción en C, C++ y Java:
for ( e1 ; e2 ; e3) S;
e1 es el inicializador, e2 es la condición y e3 es la actualización.
Tanto en C++, Java o C el inicializador puede contener declaraciones de forma que el índice de
un ciclo puede definirse dentro del ciclo.
A menudo los lenguajes incluyen esta forma de ciclo porque puede optimizarse más
efectivamente que otras construcciones de ciclo. Restricciones típicas de este tipo de bucle:
A pesar de que el GOTO sigue siendo el elemento principal en algunos lenguajes como Fortran y
Basic, a medida que se fue introduciendo la programación estructurada en el diseño de la mayoría
de los lenguajes, el uso de esta etiqueta se ha sido cada vez más cuestionado (acusado de
generar código espagueti ilegible). Si bien ciertos programadores defienden su uso restringido a
ciertos casos, lo cierto es que lenguajes como Java han prescindido por completo de la etiqueta,
pero sin embargo incluye alternativas importantes: La devolución del control a un nivel de
anidamiento externo, los breaks etiquetados (severamente restringidos) y el enunciado continue.
43
LENGUAJES DE PROGRAMACIÓN
Página 44 de 78
Autor: informatica_uned@[Link]
Hasta ahora todos los mecanismos de control han sido explícitos, pero existen situaciones donde
la transferencia de control es implícita, como puede ser el control de condiciones de error y otros
eventos no usuales durante la ejecución de un programa, esta situación es denominada manejo
de excepciones.
Una excepción es cualquier evento inesperado o poco frecuente. En los lenguajes interpretados
también incluyen errores estáticos, como los de sintaxis y tipo, esto no ocurre así en los lenguajes
compilados ya que el programa que los contiene no pude ser ejecutado.
Pero las excepciones no solo se limitan a los errores: una excepción puede ser cualquier evento
no usual, como el fallo en la entrada de datos o una pausa. Un manejador de excepciones es un
procedimiento o secuencia de código diseñado para ejecutarse cuando se pone de manifiesto una
excepción en particular. Se dice que un manejador de excepciones maneja o atrapa excepciones.
Los errores que el programa no puede atrapar se les denominan excepciones asíncronas y
suelen estar producidas a un nivel demasiado bajo, por lo que es el sistema operativo el que tiene
que entrar en acción para evitar algún fallo catastrófico. Las excepciones síncronas, son
aquellas que el programa puede atrapar y suelen ocurrir en respuesta a errores del programa. Las
excepciones definidas por el usuario sólo pueden ser síncronas.
If (y==0)
handleError(“Denominador del radio es cero”);
else
radio=x/y;
7.5.1 Excepciones.
Una excepción está definida por lo general como un objeto de datos el cual puede estar
predefinido o definido por el usuario.
En C++ no existe un tipo especial para las excepciones por lo que no hay una palabra reservada
para tal fin. En vez de ello cualquier tipo estructurado puede servir (struc o class). Normalmente
se querrá agregar cierta información a estas excepciones, lo que se puede lograr añadiendo
nuevos elementos a estas estructuras.
Las excepciones por lo general obedecen a las mismas reglas de alcance que el resto de
declaraciones. Ya que las excepciones ocurren en tiempo de ejecución puede ocurrir que un
excepción se salga del alcance de una declaración en particular, para que esto no ocurra sería
conveniente declarar la excepciones en forma global, de esta manera no se presentarán
problemas de alcance.
La mayoría de los lenguajes proveen de un conjunto de tipos de excepciones predefinidas.
44
LENGUAJES DE PROGRAMACIÓN
Página 45 de 78
Autor: informatica_uned@[Link]
En C++ los manejadores de excepciones están asociados al try-catch, que pueden aparecer en
cualquier parte donde pueda haber un enunciado. Normalmente el alcance de los manejadores se
limita al enunciado/expresión a los que está adjunto. Si una excepción llega a un manejador de
esta forma definido, este reemplaza a todos los que haya en otro lugar, incluyendo los
predefinidos.
Los manejadores predefinidos usualmente se limitan a imprimir un mensaje de error mínimo, que
indica el tipo de excepción y quizás cierta información sobre el lugar del programa donde ocurrío,
y después terminará el programa.
try
{ // para realizar algún proceso
}
catch (Trouble t)
{ // manejar el problema 1
}
catch (Big_Trouble b)
{ // manejar el problema 2
}
catch (…)
{ // manejar todas las excepciones restantes
}
7.5.3 Control.
Normalmente una excepción predefinida puede ser puesta de manifiesto automáticamente por el
sistema o bien de manera manual ambas en tiempo de ejecución, sin embargo las excepciones
definidas por el programador sólo pueden ser puestas de manifiesto manualmente por el
programa.
Las excepciones definidas por el usuario, sólo pueden ser puestas de manifiesto manualmente por
el programa.
C++ utiliza la palabra reservada throw y un objeto de excepción para poner de manifiesto una
excepción.
Cuando se pone de manifiesto una excepción, por lo general se abandona el cálculo que se esté
haciendo y el sistema en tiempo de ejecución comienza a buscar un manejador. Si no se
encuentra un manejador, se consulta la sección de manejador del bloque siguiente, y así
sucesivamente (a este proceso se le conoce como propagación de excepción). Este proceso
continúa hasta que se encuentre un manejador o bien se salga del programa principal.
Una vez encontrado el manejador se tienen varias opciones respecto en donde se debe continuar
la ejecución:
45
LENGUAJES DE PROGRAMACIÓN
Página 46 de 78
Autor: informatica_uned@[Link]
Hay que tener el cuenta que el manejo de excepciones supone una carga sustancial en el tiempo
de ejecución de un programa. Por esta razón es recomendable no abusar del uso de las
excepciones para implementar situaciones de control ordinarias, que podrían ser reemplazadas
por pruebas simples.
Una especificación de excepción es una lista de excepciones que se añade a la declaración de una
función para garantizar que ésta sólo devuelva las excepciones que están en la lista y ninguna
otra más. La sintaxis en C++ es:
46
LENGUAJES DE PROGRAMACIÓN
Página 47 de 78
Autor: informatica_uned@[Link]
En C y C++, todos los procedimientos implícitamente son funciones; aquellas que no devuelven
valores se declaran como void, las que devuelven algún valor se declaran del tipo del valor
devuelto.
En algunos lenguajes sólo existen funciones, los lenguajes funcionales son un ejemplo.
En el caso de los procedimientos cuando un procedimiento (A) llama a otro (B), este último no
tiene acceso a las variables locales de A, si no a las variables definidas en el ambiente local. Esto
es debido a que el ambiente local es el ambiente definidor de B (o ambiente estático), en
tanto que el ambiente definidor de A se conoce como ambiente llamador de B (o ambiente
dinámico). Para bloques que no son procedimientos el ambiente definidor y el ambiente
llamador son el mismo. Por el contrario un procedimiento tiene ambiente invocador y definición
distintos. Por lo que un procedimiento puede tener cualquier número de ambientes invocadores y
un solo ambiente definidor.
47
LENGUAJES DE PROGRAMACIÓN
Página 48 de 78
Autor: informatica_uned@[Link]
El alcance léxico permite a un bloque tener acceso a las variables del bloque que le rodea que no
están declaradas en sus propias declaraciones. Por el contrario un procedimiento sólo puede
comunicarse con su bloque de definición a través de las variables no locales. No tiene manera de
acceder a las variables en su ambiente invocador. El método de comunicación de un
procedimiento con su ambiente invocador es a través de parámetros. Los parámetros son
conocidos como parámetros formales, en tanto que los argumentos son llamados parámetros
actuales.
Cuando un procedimiento sólo depende de sus parámetros y de características fijas del lenguaje
se dice que está en una forma cerrada ya que no contiene dependencias no locales. En caso de
no hacer esto necesitaríamos lo que se denomina cerradura, la cual consiste en el código del
procedimiento junto con una representación del ambiente que la rodea, y todo esto para poder
resolver referencias no locales.
Es el mecanismo más común para paso de parámetros. En este mecanismo los argumentos son
expresiones que se evalúan en el momento de la llamada y sus valores se convierten en los
valores de los parámetros (como si fueran constantes) durante la ejecución del procedimiento. Se
puede interpretar como el reemplazo de todos lo parámetros en el cuerpo del procedimiento por
los valores de los argumentos.
Este mecanismo es el único disponible en los lenguajes funcionales, C y Java, y por omisión el de
C++. Los parámetros se consideran como variables locales del procedimiento, con valores
iniciales dados por los valores de los argumentos en la llamada, por lo que en C y en Java a los
parámetros de valor se les pueden asignar valores, igual que en el caso de las variables locales.
Paso por valor no implica que no puedan ocurrir cambios fuera del procedimiento mediante el uso
de parámetros. En el caso de que el parámetro fuese un tipo apuntador o de referencia, el valor
es una dirección y puede utilizarse para cambiar la memoria fuera del procedimiento.
Por ejemplo, esta función en C cambia definitivamente el valor del entero al cual el parámetro p
apunta:
Otro ejemplo en C seria cuando un parámetro es un valor de arreglo (que son implícitamente
apuntadores que apuntan a su primer elemento) y puede siempre cambiarse los valores
almacenados en ese arreglo:
48
LENGUAJES DE PROGRAMACIÓN
Página 49 de 78
Autor: informatica_uned@[Link]
En Java por ejemplo todos los objetos son apuntadores, por lo que cualquier parámetro de objeto
pude ser utilizado para cambiar sus datos:
Con este mecanismo el argumento debe de ser en principio un variable con una memoria
asignada, por lo que en vez de pasar el valor de la variable se le pasaría la ubicación de la
variable. El parámetro sería en realidad un alias del argumento, cualquier cambio que se haga
sobre el parámetro quedará reflejado en el argumento. En C++ se hace poniendo el símbolo & a
continuación del tipo de datos:
C puede lograr el mismo efecto de paso por referencia, pasando una ubicación o refeferencia de
manera explícita como apuntador (C utiliza & antes del nombre de una variable para indicar la
ubicación de esa variable y el * para desreferenciar a esa variable).
Un problema adicional con este mecanismo es la respuesta del lenguaje a los argumentos de
referencia que no son variables (no está declarados previamente).
Este mecanismo es similar al paso por referencia, con la diferencia que en este caso el
parámetro no es un alias del argumento: el valor del argumento es copiado en la llamada al
procedimiento y utilizado por este, y cuando el procedimiento termina el valor final del parámetro
se copia de regreso a la ubicación del argumento (por lo que el valor del argumento no cambia
durante la ejecución del procedimiento). Este mecanismo es conocido a veces como copia al
entrar – copia al salir, o como copia-restauración.
49
LENGUAJES DE PROGRAMACIÓN
Página 50 de 78
Autor: informatica_uned@[Link]
main()
{ int a=1;
p(a,a);
...
}
La idea de este mecanismo es que no se evalúa el argumento hasta su uso real (como parámetro)
en el procedimiento llamado. Por lo que el nombre del argumento (o su representación textual en
el punto de la llamada) reemplaza al nombre del parámetro al cual corresponde. (Ejemplo Página
293).
Los adelantos en la evaluación retardada (diferida) en los lenguajes puramente funcionales como
Haskell han aumentado el interés en este mecanismo, y vale la pena comprenderlo como base
para otros mecanismos de evaluación retardada (diferida) como la evaluación perezosa más
eficiente.
En los lenguajes con tipificación fuerte, la verificación de tipos en las llamadas a los
procedimientos debe de comprobar que los tipos y número de argumentos con cuerda con los
parámetros.
En caso del paso por referencia, por lo general, los argumentos deben de tener los mismos tipos
que los parámetros, sin embargo en el paso por valor la verificación de tipos puede relajarse y
contempla la compatibilidad de asignación, permitiéndose conversiones como las que hace C,
C++ y Java.
El ambiente para un lenguaje estructurado en bloquear con un alcance léxico puede mantenerse
de manera parecida a una pila, creando un registro de activación en la pila de ambiente al entrar
el bloque y liberándolo cuando este sale
En un lenguaje como Fortran donde toda la asignación de memoria puede llevarse a cabo en
tiempo de carga, y las localizaciones de todas las variables quedan fija durante toda la ejecución
del programa. Las definiciones de funciones y procedimientos no pueden ser anidados y además
no se permite la recursión, por lo que toda la información de una función un rutina pude
asignarse estáticamente. Cada procedimiento tiene un registro de activación fijo.
50
LENGUAJES DE PROGRAMACIÓN
Página 51 de 78
Autor: informatica_uned@[Link]
La forma de un registro de activación para un programa de este tipo quedaría como sigue:
Área COMMON
Registro de activación de programa principal
Etcétera
Dirección de retorno
Espacio temporal para evaluación de la expresión
Hay que hacer notar que todas las referencias a variables no locales deben ser relativas al área
COMMON global, y no es necesaria una cerradura para la resolución de esas referencias.
Los campos en cada uno de los registros de activación deben contener la información:
Enlace de control
Dirección de retorno
Parámetros pasados
Variables locales
Temporales
51
LENGUAJES DE PROGRAMACIÓN
Página 52 de 78
Autor: informatica_uned@[Link]
Ejemplo en C:
void p(void)
{...}
void q(void)
{p();}
main()
{q();}
Al principio de la ejecución del programa existe sólo un registro de activación para main y
el ep apunta ahí.
Después de la llamada a q, se ha agregado a la pila un registro de activación para q, y el
ep ahora apunta al registro de activación de q; pero q ha almacenado el ep anterior (el
que apuntaba al registro de main) en forma de su enlace de control.
Cuando se llama a p desde el interior de q, se agrega un nuevo registro de activación para
p. Con lo que el ep apunta al registro de activación de p, pero p ha almacenado el ep
anterior (el que apuntaba al registro de q) en forma de su enlace de control.
Así, cuando sale p, su registro de activación es devuelto a la memoria libre, y ep pasa a
apuntar al registro de activación de q (tal y como estaba guardado en el enlace de control
de p). De manera similar, cuando sale q, se restablece ep para que apunte al ambiente
original del programa principal.
Cada variable local puede ser ubicada en la misma posición en el registro de activación con
relación al principio del mismo. Esta posición se conoce como desplazamiento de la variable
local. Puede localizarse cada variable local a partir de la ubicación marcada por ep y su
desplazamiento.
Existen situaciones en las que un compilador no puede detectar este error de manera estática.
Ocurre una situación severa, si el diseñador del lenguaje desea extender la expresividad y la
flexibilidad del lenguaje al permitir que los procedimientos puedan ser creados dinámicamente, es
decir, permitiendo la devolución de procedimientos a partir de otros procedimientos. Esto ocurre
en los lenguajes funcionales e incluso en lenguajes orientados a objetos. En un lenguaje de este
tipo no puede utilizarse un ambiente basado en pilas.
52
LENGUAJES DE PROGRAMACIÓN
Página 53 de 78
Autor: informatica_uned@[Link]
Los lenguajes con necesidades significativas de almacenamiento en el montón, como Java, están
mejor al dejar el almacenamiento dinámico fuera de la pila a un administrador de la memoria que
incluya recolección automática de basura.
Se podría intentar resolver este problema, no desasignando ninguna memoria una vez que ésta
ha sido asignada. Este método tiene dos ventajas: es correcto y fácil de implementar y puede
funcionar para programas pequeños, pero los lenguajes funcionales y los orientados a objetos
tienen la propiedad de que se asignan dinámicamente grandes cantidades de memoria.
Este método ha sido utilizado en conjunto con sistemas de memoria virtual.
53
LENGUAJES DE PROGRAMACIÓN
Página 54 de 78
Autor: informatica_uned@[Link]
Las operaciones de poner de manifiesto y de manejar las excepciones son similares a las llamadas
de procedimientos, y pueden implementarse de forma similar.
También tienen importantes diferencias:
1. No puede crearse una activación en la pila en tiempo de ejecución para representar que se
pone de manifiesto una excepción.
2. Debe localizarse un manejador y “llamar” dinámicamente, en vez de estáticamente.
3. Las acciones de un manejador se basan en el tipo de la excepción, más que en el valor de
la excepción.
Una vez resueltas esas diferencias, la idea básica es recolectar todo el código de manejador
agregado a un bloque en particular, formando un solo manejador implementado como un solo
enunciado switch que esté basado en el tipo del parámetro de excepción recibido, con una caso
por omisión que saca la pila del manejador, y si es necesario, sacando la pila en tiempo de
ejecución antes de volver a poner de manifiesto la misma excepción.
El principal problema es que el mantenimiento de la pila del manejador genera una penalización
potencial significativa en tiempo de ejecución; por lo tanto, tiene mucho más sentido evitar el uso
de excepciones para el flujo de control rutinario, guardándolos para eventos verdaderamente
necesarios.
54
LENGUAJES DE PROGRAMACIÓN
Página 55 de 78
Autor: informatica_uned@[Link]
1- Un método para definir un tipo de datos y a la vez las operaciones sobre este.
2- Un procedimiento para reunir en un solo sitio los detalles de implementación y las
operaciones, así como permitir su restricción.
Un tipo que satisfaga parte o la totalidad de estos criterios se le denomina un tipo de datos
abstracto (o TDA).
Los dos criterios antes mencionados promueven tres metas del diseño que originalmente habían
justificado la utilización de los tipos de datos como medio de ayuda: Capacidad de modificación,
capacidad de reutilización y seguridad.
Otra alternativa para los criterios 1 y 2 para algunos autores es la ocultación y el encapsulado.
El encapsulado significa reunir en una sola localización todas las definiciones relacionadas con
un tipo de datos, y restringiendo el uso del tipo a las operaciones definidas en dicha localización.
La ocultación se refiere a la separación de las definiciones de los detalles de implementación, y
la supresión de dichos detalles en el uso del tipo.
En este tema utilizaremos los criterios 1 y 2 para definir los TDA.
Los mecanismos de tipos de datos abstractos no proporcionan el nivel de control activo que se
espera en la verdadera programación orientada a objetos. La idea de los TDA es de hecho
independiente del paradigma del lenguaje (función, imperativo, orientado a objetos) utilizado para
su implementación.
Un problema adicional es que un mecanismo de TDA a menudo se conoce como un concepto algo
más general, denominado módulo, y que es una colección de servicios que pueden o no incluir
un tipo o tipos de datos.
Una especificación general de un tipo de datos necesita incluir el nombre del tipo y los nombres
de las operaciones, incluyendo una definición de sus parámetros y valores devueltos. Esta es la
especificación sintáctica de un tipo de datos abstracto (conocido también como signatura del
tipo).
Para una especificación independiente del lenguaje sería apropiado utilizar la notación de
funciones para las operaciones del tipo de datos: f: X -> Y (X es el dominio e Y es el rango).
Como ejemplo, los números complejos podrían tener la siguiente signatura:
55
LENGUAJES DE PROGRAMACIÓN
Página 56 de 78
Autor: informatica_uned@[Link]
operaciones:
createstk: stack //constructor
push: stack X element --> stack //constructor
pop: stack --> stack //selector
top: stack --> element //selector
emptystk: stack --> boolean //predicado
variables:
s: stack; x: element
axiomas:
emptystk(createstk)=true
emptystk(push(s,x))=false
top(createstk)=error
top(push(s,x))=x
pop(createstk)=error
pop(push(s,x))=s
Algunos lenguajes tienen un mecanismo específico para expresar los TDA. Dicho mecanismo
deberá tener alguna forma de separar la especificación o signatura del TDA de su
implementación. Dicho mecanismo también debe garantizar que cualquier código fuera de la
definición del TDA no puede utilizar detalles de la implementación, pero puede operar sobre un
valor del tipo definido sólo a través de las operaciones proveídas.
56
LENGUAJES DE PROGRAMACIÓN
Página 57 de 78
Autor: informatica_uned@[Link]
9.2.2 Módulos.
Como proveedores de servicios, los módulos pueden exportar cualquier combinación de tipos de
datos, procedimientos, variables y constantes. Debido a que los módulos tienen interfaces
explícitas (públicas) e implementaciones separadas (privadas), son los mecanismos ideales para
proveer servicios de compilación y de biblioteca independientes dentro de un ambiente de
desarrollo del software. En Java se liga la estructura de los módulos a problemas independientes
de compilación.
C no tiene mecanismos modulares como tales, pero si tiene características de compilación por
separado y de control de nombres, que pueden utilizarse para imitar módulos de una forma
razonablemente efectiva. Pero la efectividad de este mecanismo depende totalmente de reglas
convencionales, ya que ni los compiladores de C ni los enlazadores estándar hacen obligatoria
ninguna de las reglas de protección normalmente asociadas con el módulo o con mecanismos
TDA.
C++ permite un mejor control sobre el acceso al tipo de datos. También ofrece un mejor control
sobre los nombres y el acceso a los mismos a través del mecanismo espacio de nombre. Pero
C++ no ofrece características adicionales que pudieran mejorar el uso de la compilación por
separado para simular módulos.
También Java tiene un mecanismo parecido al espacio de nombres, llamado paquete, los
paquetes se construyen como grupos de clases [Link] archivo compilado por
separado en un programa Java solamente puede tener una clase pública, y esta clase puede
colocarse en una declaración de paquete como la primera declaración en el archivo del código
fuente:
package paqueteEjemplo;
Todos los nombres en un paquete Java pueden ser importados utilizando un asterisco después del
nombre del paquete:
import paqueteEjemplo.*;
57
LENGUAJES DE PROGRAMACIÓN
Página 58 de 78
Autor: informatica_uned@[Link]
9.5 Módulos en ML
ML también tiene un servicio de utilería de módulo más general, que consiste en tres
mecanismos: signaturas, estructuras y functores.
9.7 Problemas que se presentan con los mecanismos de tipos de datos abstractos
En esta sección se catalogan algunos de los inconvenientes principales de los mecanismos TDA.
Muchos aunque no todos de estos inconvenientes han sido corregidos en los lenguajes orientados
a objetos:
58
LENGUAJES DE PROGRAMACIÓN
Página 59 de 78
Autor: informatica_uned@[Link]
Los lenguajes de programación orientada a objetos encaran tres problemas de diseño del
software: la necesidad de volver a utilizar componentes de software tanto como sea posible, la
necesidad de modificar el comportamiento de los programas mediante cambios mínimos al código
existente, y la necesidad de conservar la independencia de los diferentes componentes.
Existen 5 maneras básicas para modificar un componente de software de modo que pueda
reutilizarse: extensión, restricción, redefinición, abstracción y “polimorfización”.
1. Extensión de los datos y/o las operaciones. Ejemplo: se define una ventana en la
pantalla como un rectángulo especificado por sus cuatro esquinas, con operaciones:
trasladar, redimensionar…, una ventana de texto puede definirse como una ventana con
algún texto agregado para su despliegue.
2. Restricciones de los datos y/o de las operaciones. Esta operación es lo contrario que la
extensión, si por ejemplo está disponible una cola de dos puntas, puede obtenerse una
normal restringiendo las operaciones.
3. Redefinición de una o más operaciones. Incluso si las operaciones de un nuevo elemento
se conservan esencialmente igual, pude ser necesario redefinir alguna de las mismas para
que acepte un nuevo comportamiento. En algunos casos, la estructura básica de cada
aplicación es tan similar a otras que los desarrolladores han empezado a utilizar
estructuras de aplicación que ofrecen los servicios básicos en forma orientada a objetos y
son utilizados por estos en forma de redefinición y reutilización. Como ejemplo tenemos el
juego de herramientas para ventanas en Java (swing) y las Microsoft fundations classes en
C++.
4. Abstracción, o la reunión de operaciones similares de dos componentes diferentes en
uno nuevo. Por ejemplo, un circulo y un rectángulo son objetos que tienen una posición y
que pueden trasladarse y desplegarse.
5. Polimorfización o la extensión del tipo de datos a los cuales pueden aplicarse las
operaciones.
El diseño para la reutilización no es el único objetivo de los lenguajes orientados a objetos otro es
la restricción del acceso a los detalles internos de los componentes del software.
• Listar de manera explícita todas las operaciones y los datos accesibles para los clientes en
una lista de exportación.
• Listar aquellas propiedades de un componente que son inaccesibles en una parte privada.
• Exportación implícita, como en el caso de nombres de C++.
Puede ser necesario que los clientes declaren explícitamente el uso que hagan de un componente
mediante un enunciado import (Java) o using (C++).
Aquí denominaremos al mecanismo de protección de los detalles como mecanismo de
protección en vez de encapsulado o mecanismos de ocultación que pueden parecer poco claros.
59
LENGUAJES DE PROGRAMACIÓN
Página 60 de 78
Autor: informatica_uned@[Link]
Creando un patrón para los métodos y el estado se pueden declarar los objetos. A este patrón se
le llama Clase. Esta es esencialmente un tipo de datos. Se dice que un objeto es una instancia
de una clase.
Las variables de locales que representan el estado de un objeto se conoce como variables de
instancia.
....
Complex z,w;
...
z= new Complex();
w=new Complex(-1,1);
Utilizando el punto pueden invocarse métodos de la clase (después de haber sido asignada).
z= [Link](w);
60
LENGUAJES DE PROGRAMACIÓN
Página 61 de 78
Autor: informatica_uned@[Link]
Sin recolección de basura automática, resulta difícil evitar serias fugas de espacio. Por esta razón
la mayoría de los lenguajes orientados a objetos requieren de recolección automática de la
basura.
Naturalmente una clase pude referirse a sí misma en su definición y un objeto puede contener
otro objeto de su propia clase como una variable de instancia. Una característica especial que
facilita lo anterior en Java es la palabra clave This para hacer referencia a la instancia presente.
Los conceptos de clase y objeto pueden visualizarse como la generalización de la idea de tipo de
registro o estructura y de variable, respectivamente.
10.3 Herencia.
En java, B hereda todas las variables de instancia y los métodos de A. A la clase B se la denomina
subclase de A y a A superclase de B.
Un B es una extensión de la clase A, y todas las operaciones aplicables a A también son aplicables
a B.
Las definiciones de clase en Java (y en la mayoría de los lenguajes OO) también son definiciones
de tipo, por lo que B se puede definir como un subtipo de A, y a A como supertipo de B
(obedecen al principio de subtipo: Un objeto de un subtipo puede ser utilizado en cualquier
sitio donde sea correcto un objeto de supertipo.).
La herencia que responde al principio de subtipo expresa la relación es-un: si A es una subclase
de B, entonces todos los objetos pertenecientes a A también pertenecen a B, es decir, cada A “es-
un” B.
En Java todos los objetos obedecen al principio de subtipo, pero en otros lenguajes (sobre todo
en C++) la herencia se define de forma más general, ya que es posible restringir el acceso a
datos y métodos de una superclase, por lo que el principio de subtipo desaparece.
En Java por definición todas las clases son de manera implícita una extensión de la clase Object.
Los métodos Equals y ToString son ejemplos de métodos comunes a todos los objetos en Java. El
comportamiento por omisión para estos métodos no son generalmente lo que deseamos, pero
afortunadamente la herencia no sólo permite la extensión de las operaciones si no que a demás
permite modificar o hacer caso omiso del comportamiento de los métodos en las subclases.
Un método abstracto, es un método que está siempre disponible para la superclase, pero que se
la dan diferentes implementaciones para las diferentes subclases. A estos métodos abstractos se
les conoce como diferidos, y una clase que tiene métodos diferidos se conoce como clase diferida.
La ventaja de definir métodos abstractos o diferidos no es sólo para consolidad el código, sino
también para asegurarnos que cualquier subclase tenga un método determinado.
61
LENGUAJES DE PROGRAMACIÓN
Página 62 de 78
Autor: informatica_uned@[Link]
Se puede relajar la protección de “center” de forma que quede accesible dentro de las
subclases, pero no por lo clientes; y esto se puede lograr declarando a esa variable como
protected.
62
LENGUAJES DE PROGRAMACIÓN
Página 63 de 78
Autor: informatica_uned@[Link]
En un lenguaje orientado a objetos, una de las características principales que distinguen a las
clases de los módulos o paquetes en lenguajes como Ada o ML es la naturaleza dinámica de las
clases en contraste con la naturaleza estática de los módulos.
Dependiendo del lenguaje del que se trate esta asignación dinámica puede quedar en manos del
programador o bien estar totalmente automatizada. Java es un lenguaje híbrido en este sentido
ya que la asignación (y la inicialización) de los objetos queda bajo el control del programador a
través de la expresión new, sin que exista un delete correspondiente. En su lugar, las
asignaciones de los objetos son recuperados bien cuando salen de su alcance (al estilo basado en
pilas), o mediante alguna forma de recolección de basura.
Los métodos también pueden variar dinámicamente. También puede presentarse una ligadura
dinámica cuando se redefine un método en una clase derivada.
Es posible ofrecer a la vez, en un lenguaje orientado a objetos, la ligadura estática y dinámica de
los métodos (C++).
En la mayoría de los lenguajes orientados a objetos “puros”, como Java y SmallTalk, todos los
métodos son “Virtuales”, es decir, tienen ligadura dinámica. De ser necesario es posible obtener
una ligadura estática aplicando la palabra clave “super”. En Java también es posible obtener la
ligadura estática al llamar a los métodos static, final o private.
10.5 C++
C++ también fue diseñado para ser un lenguaje eficiente y práctico, se convirtió en el principal
lenguaje orientado a objetos de principios de la década de 1990.
C++ contiene declaraciones de clase y de objetos similares a los de Java.
Las variables de instancia se conocen como miembros de datos y los métodos como funciones
miembro.
A las subclases se les llama clases derivadas y a las superclases clases base.
Una diferencia fundamental entre C++ y la mayoría de los demás lenguajes orientados a objetos,
incluyendo Java, es que los objetos no son automáticamente apuntadores o referencias.
A excepción de las ligaduras dinámicas y de la herencia de las funciones miembro, el tipo de datos
class en C++ es en esencia idéntico al tipo de datos struct. La herencia en C++ se indica
mediante “:” después del nombre de la subclase, y antes del nombre de la clase padre (con public
generalmente delante del nombre de la clase padre).
Al igual que en Java, en C++ se incluyen tres niveles de protección para los miembros de clase:
público, privado y protegido.
class A
{ // Una clase de C++
public:
// Aqui todos los miembros son públicos
63
LENGUAJES DE PROGRAMACIÓN
Página 64 de 78
Autor: informatica_uned@[Link]
protected:
//Aquí todos los miembros están protegidos
private:
//Aqui todos los miembros son privados
};
En C++ la inicialización de los objetos se efectúa como en Java mediante constructores que
tienen el mismo nombre que el de la clase.
C++ no tiene un recolector de basura incorporado, por lo que C++ también tiene destructores
que al igual que los constructores utilizan el mismo nombre de la clase, pero están antecedidos
por el símbolo ~. Los destructores son llamados automáticamente al desasignar un objeto (ya sea
por haberse salido del alcance o por el uso del operador delete).
Una declaración de clase en C++ no siempre contiene el código de implementación para todas las
funciones miembro. Estas últimas pueden ser implemetadas con un procedimiento fuera de la
declaración utilizando el operador de resolución de alcance indicado por “::” después del
nombre de clase.
class Queue
{
public:
Queue() {...}; //Constructor
void dequeue(); //Función sin su implementación
~Queue() //Destructor
{ while (¡empty())
{ delete... ;}
};
protected:
...
};
En C++ se incluye la ligadura dinámica de las funciones miembro como una opción, aunque no es
lo preestablecido. Solamente los métodos definidos utilizando la palabra clave virtual son
candidatos para ligadura dinámica. Si deseamos redefinir una función area para un cuadrado
como una derivación de un rectángulo, lo haríamos en C++:
class Rectangle
{
public:
virtual double area()
{return length * width;};
...
private:
double length, width;
};
64
LENGUAJES DE PROGRAMACIÓN
Página 65 de 78
Autor: informatica_uned@[Link]
public:
double area()
//Redefine dinámicamente el área del rectángulo
{ return width * width; };
...
};
Si lo que queremos es crear una clase abstracta con un método abstracto para asegurarnos que
se redefine en todas las subclases que heredan esa clase abstracta, en C++ se consigue mediante
el uso de una declaración virtual pura:
La herencia múltiple en C++ generalmente crea copias por separado de cada clase en una
trayectoria de herencia.
Por ejemplo las declaraciones:
class A {...};
class B: public A {...};
class C: public A {...};
class D: public B, public C {...};
proporcionan cualquier objeto de la clase D con dos copias por separado de objetos de la
clase A y crea la siguiente gráfica de herencia, lo que se llama herencia repetida o
múltiple:
A A
^ ^
| |
| |
B C
^ ^
\ /
\ /
D
Para obtener sólo una copia de una A en la clase D, debe declararse la herencia usando la palabra
65
LENGUAJES DE PROGRAMACIÓN
Página 66 de 78
Autor: informatica_uned@[Link]
clave virtual:
class A {...};
class B: virtual public A {...};
class C: virtual public A {...};
class D: public B, public C {...};
A
^
/ \
/ \
B C
^ ^
\ /
\ /
D
10.6 Smalltalk
De todos los lenguajes orientados a objetos Smalltalk ofrece el procedimiento más completo y
consistente con relación al paradigma orientado a objetos.
En Smalltalk, prácticamente toda entidad del lenguaje es un objeto, incluyendo las constantes, los
números y los caracteres. Smalltalk está orientado a objetos de manera pura.
Fue creado como un sistema completo para una estación de trabajo personal. La interfaz de
usuario también era una novedad en cuanto a que incluía sistemas de ventanas con menús y un
ratón.
Como lenguaje es interactivo y está orientado dinámicamente. Todas las clases, objetos y
métodos se crean por interacción con el sistema, utilizando ventanas y plantillas de pantalla
provistas por el sistema.
La jerarquía de clases es un árbol, ya que Smalltalk-80 sólo tiene herencia simple. La clase Object
es la raíz de la jerarquía de clases, igual que ocurre en Java.
Los nombres de las clases deben comenzar por mayúscula, y los nombres de los objetos y
métodos con minúsculas.
66
LENGUAJES DE PROGRAMACIÓN
Página 67 de 78
Autor: informatica_uned@[Link]
Las características orientas a objetos representan capacidades dinámicas más que estáticas (como
la ligadura dinámica de los métodos con los objetos), por lo que un aspecto del diseño de los
lenguajes orientados a objetos es introducir características de modo que se reduzca la
penalización en tiempo de ejecución debido a la flexibilidad adicional.
En la sección de C++ ya se vio una característica que promueve la eficiencia, las funciones en
línea: aquellas funciones miembro cuya implementación estén incluidas en la clase, son
automáticamente funciones en línea. Con esto se evita la penalización del la llamada a la función
en tiempo de ejecución.
Esta sección analiza los problemas de diseño del lenguaje y no al diseño de programas.
Hay varias formas en las que las clases se introducen en un lenguaje con tipos:
Las clases proporcionan un mecanismo versátil para organizar código que también abarca algunas
características propias de los tipos de datos abstractos: proporcionan un mecanismo para la
especificación de interfaces, para la localización de código que se aplica a colecciones específicas
de datos (los métodos) y para la protección de la implementación a través del uso de datos
privados. Pero desafortunadamente las clases no permiten una diferenciación clara entre la
interface y la implementación.
Otro problema es con el espacio de nombre: ocasionalmente se puede usar una clase como un
espacio de nombres o un almacén para un almacén de servicios de utilería. Las clases no función
bien como espacios de nombres o módulos estáticos. Por esta razón algunos lenguajes oo
incluyen mecanismos de módulos independientes al sistema de objetos (C++ y Java, con los
paquetes).
En ocasiones los mecanismos orientados a objetos están relacionados más de cerca con una
estructura de módulo (Ada95).
67
LENGUAJES DE PROGRAMACIÓN
Página 68 de 78
Autor: informatica_uned@[Link]
Con la mezcla de herencia y sobrecarga se presenta el problema conocido como doble despacho
(double-dispatch) o multidespacho (multi-dispatch): Consiste básicamente en que no se toma
en consideración métodos binarios (o de orden n) que pudieran necesitar sobrecarga en la
membresía de clases basados en uno o dos parámetros. Como ejemplo tenemos la clase Complex
que define el método “Complex add(Complex Y)” que lo que hace es sumar dos números
complejos: entonces [Link] y sumaría dos números complejos. Si y fuera un double, esto se podría
solucionar sobrecargando add, pero si x fuera un double esto no sería posible. Un solución sería el
sobrecargar la operación + (esto se puede hacer en C++), pero daría una solución poco clara, ya
que dividiría las operaciones con números complejos en una definición de clase y un número
indefinido de definiciones de funciones independientes. Se han hecho intentos para solucionar
este problema vía los llamados multimétodos: métodos que pueden pertenecer a más de una
clase y cuyo despacho de sobrecarga pude basarse en la membresía de clase de varios
parámetros. Uno de los lenguajes que con más éxito ha llevado el multimétodo a sido Dylan (de
la mano de Appel), pero por desgracia, con el auge de java a quedado algo relegado.
Aunque la sobrecarga y la herencia son conceptos a priori, y salvando el problema del despacho
múltiple, relacionados y aparentemente bien acoplados, se da el problema de polimorfismo
paramétrico, que es más difícil de integrar.
Los objetos se implementan por lo general de la misma manera que las estructuras de registro en
C o en Ada., con variables de instancia que representan los campos de datos en la estructura.
Un objeto de una subclase puede asignarse como una extensión del objeto de datos precedente,
con las nuevas variables de instancias con espacio asignado al final del registro
Es posible implementar los métodos como funciones normales, sin embargo, estos tienen dos
diferencias fundamentales: La primera es que un objeto puede tener acceso directo a los datos
del objeto presentes en la clase. El segundo problema es más significativo y se presenta con
ligadura dinámica de los métodos durante la ejecución, ya que estos dependen de la membresía
de clase del objeto actual cuando se efectúa la llamada.
68
LENGUAJES DE PROGRAMACIÓN
Página 69 de 78
Autor: informatica_uned@[Link]
En una implementación de objetos clásica como la vista anteriormente sólo se le asigna espacio a
cada objeto para las variables de instancias, no para los métodos. Pero cuando se usan ligaduras
dinámicas se presentan problemas con dicha implementación. Una solución sería conservar todos
los métodos ligados dinámicamente como campos adicionales en las estructuras asignadas a cada
objeto. El problema con lo anterior es que todos los objetos tienen que mantener una lista con
todos los métodos virtuales disponibles en ese momento, y dicha lista podría resultar demasiado
grande.
Una solución alternativa sería mantener una tabla de métodos “virtuales” para cada clase en
alguna localización centralizada, como por ejemplo el área de almacenamiento global. A esta tabla
se le conoce como tabla de método virtual (VMT). Hay que hacer notar que ninguna de estas
soluciones de implementación requiere que durante la ejecución se mantenga una gráfica de
herencia.
Java por su parte no tiene rutinas específicas para la desasignación explícita, por lo que requiere
el uso de un recolector de basura para devolver el almacenamiento ya no accesible al montículo.
Esta característica genera una carga de ejecución y una complejidad adicional en tiempo de
ejecución significativas.
Además del orden de inicialización pueden aparecer problemas relacionados con la existencia de
diferentes constructores para una misma clase. Por ejemplo, en Java cuando no existe un
constructor predeterminado para una superclase, debe de proporcionarse una llamada explícita
del constructor en todos los constructores de las subclases.
69
LENGUAJES DE PROGRAMACIÓN
Página 70 de 78
Autor: informatica_uned@[Link]
Existen varias razones por las cuales nunca han llegado a tener la popularidad de otros lenguajes
(imperativos u orientados a objetos):
Si bien los lenguajes funcionales tienen también poderosos mecanismos para controlar la
complejidad y la estructuración de código, pero son más abstractos y matemáticos.
Y = f(x)
70
LENGUAJES DE PROGRAMACIÓN
Página 71 de 78
Autor: informatica_uned@[Link]
1. Todos los procedimientos son funciones y distinguen claramente los valores de entrada
(parámetros) de los de salida (resultados).
2. No existen variables ni asignaciones. Las variables han sido reemplazadas por los
parámetros.
3. El valor de la función sólo depende de los valores de los parámetros.
4. Las funciones son valores de primera clase.
Las técnicas de programación funcional pueden ser ampliamente utilizadas con un lenguaje
imperativo como el C o Pascal. El motivo por el cual estas técnicas están siendo cada vez más
utilizadas son las mismas por las cuales estos lenguajes están siendo cada vez más usados: La
claridad de su semántica y la claridad resultante de sus programas.
Unos de los requisitos básicos para poder aplicar estas técnicas en cualquier lenguaje son la
posibilidad de la recursión y un mecanismo adecuado de funciones.
Los lenguajes imperativos tienen una serie de restricciones que impiden que estos interpreten
todos los programas en este estilo (funcional):
1. Los valores estructurados como los arreglos o los registros no pueden ser valores
devueltos de las funciones.
2. No existe forma de construir un valor de un tipo estructurado de forma directa.
3. Las funciones no son valores de primera clase, por lo que no es posible escribir
funciones de orden superior.
71
LENGUAJES DE PROGRAMACIÓN
Página 72 de 78
Autor: informatica_uned@[Link]
En Scheme todos los programas y los datos son expresiones, y éstas son de dos variedades:
átomos y listas.
Los átomos son como las constantes y los identificadores de un lenguaje imperativo: incluyen
números, cadenas, nombres, funciones y algunos constructores.
Una lista es simplemente una secuencia de expresiones separadas por espacios y rodeadas por
paréntesis.
Como los programas en Scheme son expresiones y los programas necesitan ser ejecutados o
evaluados, la semántica de Scheme está dada por una regla de evaluación para las expresiones:
ML es un lenguaje de programación funcional muy distinto de las versiones de LISP, tiene una
sintaxis más parecida a Algol, que trata de evitar e uso de demasiados paréntesis.
Es de tipificado fuerte, por lo que el tipo de cada operación se determina antes de la ejecución,
verificándose los tipos para ser consistentes.
El lenguaje es más seguro y pueden detectarse más errores antes de la ejecución, y también
permite el polimorfismo paramétrico.
La palabra reservada fun en ML, introduce una declaración de función. El identificador que sigue
inmediatamente después de fun es el nombre de la función, y después siguen los nombres de los
parámetros, hasta llegar al signo igual. Después del signo igual aparece el cuerpo de la función.
En un lenguaje con una regla de evaluación de orden aplicativo, como son Scheme y ML todos
los parámetros a las funciones definidas por el usuario se evalúan en el momento de la llamada,
aunque no sea necesario hacerlo.
72
LENGUAJES DE PROGRAMACIÓN
Página 73 de 78
Autor: informatica_uned@[Link]
En el caso de la función if, no es solo un caso de simple utilidad, sino de necesidad: para que la
expresión (if a b c) en Scheme tenga la semántica apropiada, la evaluación de b y de c debe
retrasarse hasta que se conozca el resultado de a, y con base en lo anterior se evalúa b o c, pero
nunca ambas.
Scheme y ML deben distinguir entre dos clases de funciones predefinidas aquellas que utilizan la
evaluación estándar y aquellas que no lo hacen. Esto compromete la uniformidad y la extensión
de estos lenguajes.
La sintaxis de Haskell es muy similar a ML, aunque con algunas diferencias notables. Haskell
reduce la sintaxis a un mínimo absoluto utilizando indicios internos del programa, incluyendo la
regla de disposición que utiliza la sangría y el formato de los renglones para resolver
ambigüedades.
73
LENGUAJES DE PROGRAMACIÓN
Página 74 de 78
Autor: informatica_uned@[Link]
Los enunciados lógicos, o por lo menos un forma restringida de ellos, pueden considerarse como
un lenguaje de programación y ejecutarse en una computadora, si existe un sistema de
interpretación lo suficientemente sofisticado para ello. Trabajando sobre este enunciado se llegó
al lenguaje Prolog, el cual logró una fama instantánea cuando el gobierno japonés lo utilizó para
el proyecto de quinta generación, para el desarrollo de sistemas de cómputo basados en técnicas
de razonamiento y comprensión de lenguaje humano. Pero tras el abandono del proyecto Prolog
ha ido perdiendo difusión a excepción del área de la comprensión del lenguaje natural y sistemas
expertos.
El cálculo de predicados de primer orden clasifica las distintas pares de estos enunciados de la
siguiente forma:
74
LENGUAJES DE PROGRAMACIÓN
Página 75 de 78
Autor: informatica_uned@[Link]
La resolución es una regla de inferencia para cláusulas Horn que tiene una eficiencia especial. Si
tenemos dos cláusulas Horn, y podemos parear la cabeza de la primera cláusula Horn con uno de
los enunciados en el cuerpo de la segunda cláusula, entonces pueden utilizarse la primer cláusula
para reemplazar su cabeza en la segunda utilizando su cuerpo.
La unificación consiste en hacer coincidir enunciados con variables a través de establecer las
variables iguales a los términos. Las variables que establecen iguales se dice que están
instanciadas. Para que esto se implante con efectividad hace falta un algoritmo para la
unificación.
Para lograr una ejecución eficiente, un sistema de programación lógica debe aplicar un algoritmo
fijo que especifique:
Los sistemas de programación lógica que usan cláusulas Horn y resolución con ordenes
preespecificados para (1) y para (2) violan el principio básico que dichos sistemas pretenden
lograr: que un programador se preocupe de la lógica misma, pudiendo ignorar el control.
Prolog es el lenguaje de programación lógica más utilizado. Prolog utiliza cláusulas Horn e
implementa la resolución utilizando una estrategia “primero en profundidad” estrictamente lineal y
un algoritmo de unificación.
Prolog utiliza casi una notación idéntica a la desarrollada anteriormente para las cláusulas Horn,
excepto que la flecha de implicación “←” se reemplaza con dos puntos seguidos de un guión :-
Las variables se escriben con mayúsculas y las constantes y los nombres con minúsculas.
Las estructuras básicas de datos son términos como padre(X,Z), o bien sucesor(sucesor(0)).
Prolog también incluye listas como una estructura básica de datos.
Ejecución en Prolog.
Existen compiladores para Prolog, pero la mayoría de los sistemas se ejecutan como intérpretes.
Un programa Prolog incluye un juego de cláusulas Horn en la sintaxis de Prolog que generalmente
se toma de un archivo y se almacena en una base de datos de cláusulas mantenida
dinámicamente.
75
LENGUAJES DE PROGRAMACIÓN
Página 76 de 78
Autor: informatica_uned@[Link]
Aritmética.
Unificación.
Estrategia de búsqueda.
Prolog aplica la resolución de una forma estrictamente lineal, reemplazando metas de izquierda a
derecha y considerando las cláusulas en la base de datos en orden descendente, de arriba abajo.
Para que Prolog ejecute ciclos y búsquedas repetitivas podemos utilizar búsqueda primero en
profundidad con retroceso. Lo que debemos lograr es forzar el retroceso incluso cuando se haya
encontrado una solución.
El algoritmo de unificación de Prolog es de hecho erróneo: al unificar una variable con un término,
Prolog no verifica que esta variable ya esté presente en el término en el cual está siendo
instanciada.
Todos los sistemas de programación lógica tienen la propiedad básica de que algo que no pueda
ser probado como verdadero tiene que ser falso, a esto se le conoce como “suposición del
mundo cerrado”.
76
LENGUAJES DE PROGRAMACIÓN
Página 77 de 78
Autor: informatica_uned@[Link]
¿De que manera se puede implementar el operador not en la programación lógica?, la respuesta
sencilla es que la meta not(x) tiene éxito siempre que la meta x fracase. Esto es lo que quiere
decir “negación como fracaso”.
El hecho de que añadir información a un sistema puede reducir el conjunto de cosas que pueden
ser probadas se conoce como razonamiento no monótono, y es una consecuencia de la
suposición del mundo cerrado.
No todas las expresiones lógicas pueden representarse con cláusulas Horn, en concreto aquellas
que involucran cuantificadores posiblemente no podrán expresarse en formato de cláusulas Horn.
Los programas Prolog también tienen información de control implícita que hacen que pueden
hacer que fracasen los programas.
Lo ideal sería que los sistemas de programación lógicos aceptaran la definición matemática de
una propiedad y este encuentre un algoritmo para calcularlo. Pero tanto en Prolog como en
cualquier otro sistema de programación lógica no sólo se le suministra las especificaciones de
nuestros programas si no también, hay que facilitar los mecanismos de control algorítmico.
Al especificar algoritmos secuenciales, por ejemplo ordenar, Prolog no se diferencia tanto como
podría pensarse de la programación imperativa.
Existe una ventaja de poder especificar un propiedad para su ejecución, incluso si no es aceptable
como algoritmo para el cálculo de dicha propiedad: nos permite probar si la especificación es
correcta.
Podrían mejorarse los programas que utilizan este tipo de cálculo si se eliminaran los requisitos de
instanciación y el operador “is”. Existe un buen número de lenguajes que hacen exactamente eso,
a estos lenguajes se les conoce como lenguajes de programación lógica con restricciones.
77
LENGUAJES DE PROGRAMACIÓN
Página 78 de 78
Autor: informatica_uned@[Link]
Prolog y otros sistemas de cláusulas Horn no aceptan las ecuaciones como reglas, por lo que una
especificación algebraica no puede ser interpretada directamente en Prolog. Sería recomendable
si las especificaciones basadas en ecuaciones pedieran escribirse directamente en un lenguaje de
programación lógica. Esto es lo que ha propiciado el desarrollo y estudio de los lenguajes de
programación de lógica basada en ecuaciones.
78