Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Introducción.
int x;
x=2;
Asocia el nuevo atributo “valor 2” a la variable “x”. Y, en el caso que “y” sea una
variable apuntador declarado como
int * y;
el enunciado C++
y=new int;
asigna memoria para una variable entera (esto es, asocia un atributo de localización
a la misma) y asigna esta localización a *y, es decir, asocia un nuevo atributo de
valor a y.
int x;
el valor entero se vincula en forma estática al nombre “x”. Por otra parte, la
asignación “x=2;” se vincula el número ”2” en forma dinámica con “x” cuando el
enunciado de asignación se ejecuta. Y el enunciado en C++
y = new int;
Los nombres pueden vincularse con atributos aún antes del tiempo de traducción.
Algunos identificadores predefinidos como el tipo de dato integer o la constante
maxint tienen especificados sus atributos mediante la especificación del lenguaje y
mediante la implementación. La definición del lenguaje especifica que el tipo del
dato integer tiene valores consistentes de un subconjunto de enteros y que maxint
es una constante, en tanto la implementación especifica el valor de maxint y el rango
real del tipo de dato integer.
Esta función cambiara Conforme avanza la traducción y/o ejecución para reflejar
adiciones o eliminaciones de ligaduras dentro del programa qué se está traduciendo
y/o Ejecutando.
Existe una diferencia fundamental entre la forma en la que se mantiene una tabla
de símbolos a través de un intérprete y la forma en la que la mantiene un compilador.
Un compilador puede, por definición, procesar únicamente atributos estáticos, ya
que el programa no será ejecutado sino hasta después de qué se complete la
compilación.
Las declaraciones son un método primordial para establecer las ligaduras. Las
ligaduras pueden determinarse mediante una declaración, ya sea de manera
implícita o de manera explícita. Por ejemplo, La declaración en C
int x;
int x=0;
double f (int);
especifica únicamente el tipo de dato de la función f ( por ejemplo, los tipos de sus
parámetros y el valor de retorno), pero no especifica el código para implementar f.
void p (void){
double r,z; // el bloque de p
…
{ int x,y; //otro bloque anidado
…
}
…
}
Establece dos bloques, uno de los cuales representa el cuerpo de p y el otro está
Anidado dentro del cuerpo de p.
Las declaraciones asociadas con un bloque específico se conocen cómo locales,
en cambio declaraciones en bloque que rodean se conocen cómo no locales. Por
ejemplo, En la definición anterior del procedimiento p, Las variables r y z son locales
con respecto a p, Pero no son locales desde el interior del segundo bloque anidado
en p.
De manera similar, En lenguajes orientados a objetos, La clase es una fuente
importante de declaraciones – de hecho, lenguajes orientados a objetos Cómo Java
La clase es la única declaración que no necesita estar en el interior de otra
declaración de clase, siendo entonces la función principal de declaraciones.
Finalmente, Las declaraciones pueden también reunirse en grupos más grandes
como una manera de organizar los programas y de obtener un comportamiento
especial. Los paquetes encajan en esta categoría.
Las declaraciones vinculan varios atributos a los nombres despendiendo del tipo de
declaración. Cada una estás ligaduras tiene por sí misma un atributo que queda
determinado por la posición dentro de la declaración en el programa y por Las reglas
del lenguaje correspondientes a la vinculación. El alcance de un vínculo es la
región del programa sobre la cual se conserva el vínculo.
En lenguajes estructurados por bloques dónde los bloques pueden anidarse, el
alcance de un vínculo queda limitado al bloque en el cual aparece su declaración
asociada. Estas reglas de alcance se conocen cómo alcance léxico ya que siguen
la estructura de los bloques Conforme aparece en el código escrito. Se trata de la
regla estándar de alcance en la mayoría de los lenguajes. Es la figura 7.1 se da un
ejemplo simple de alcance en C.
int x;
void p( ){
char y;
…
}
void q( ){
…
}
main( ){
…
}
Figura 7.1. Un programa C simple mostrando el alcance.
En la figura 7.1, las declaraciones de la variable “x” y los procedimientos “p”. “q” y
main son globales. Las declaraciones de “y”, “z” y “w”, por otra parte, están
asociadas con los bloques de procedimiento p, q y main, respectivamente. Son
locales con respecto a estas funciones y sus declaraciones son solamente válidas
para dichas funciones. C tiene la regla adicional de que el alcance de una
declaración se inicia en el punto de la declaración misma (ésta es la regla conocida
como declaración antes de uso), de tal manera que podemos enunciar la siguiente
regla básica de alcance en C: el alcance de una declaración se extiende en el punto
justo después de la misma hasta el fin del bloque en el que está localizado.
Una característica de la estructura de bloques es que las declaraciones en los
bloques anidados toman precedencia sobre declaraciones anteriores. Veamos el
ejemplo de la figura 7.2.
int x;
void p( ){
char x;
x=’a’;
…
}
main( ){
x=2;
…
}
Fig. 7.2
La declaración de x en p tiene precedencia sobre la declaración global de x en el
cuerpo de p. Por lo tanto, el entero global de x no puede ser accesado desde dentro
de p. La declaración global de x se dice que tiene una apertura en el alcance dentro
de p. Por esta razón, a veces se hace una distinción lo que es el alcance y lo que
es la visibilidad de una declaración: la visibilidad incluye únicamente aquellas
regiones de un programa donde las ligaduras de una declaración son aplicables, en
tanto que el alcance incluye a los agujeros en le alcance (dado que las ligaduras
existen pero están ocultas a la vista). En C++ el operador de resolución de
alcance :: (doble dos puntos) puede utilizarse para tener acceso a estas
declaraciones ocultas (siempre y cuando sean globales);
int x;
void p( ){
char x=’a’; //asignación a variable local
::x=42; //asignación a variable global
…
}
main( ){
x=2; //asignación a variable global.
…
}
int factorial(int x)
//el alcance del factorial inicia aquí
{
//el factorial se puede llamar aquí
…
}
Por otra parte, las declaraciones de variables recursivas generalmente no tienen
sentido.
TABLA DE SIMBOLOS.
1) int x;
2) char y;
3) void p(){
4) double x;
5) …
6) { int y[10];
7) …
8) }
9) …
10) }
11) void q( ){
12) int y;
13) …
14) }
15) main(){
16) char x;
17) …
18) }
Figura 7.3. Programa en C demostrando la estructura de la tabla de símbolos
Los nombres dentro de este programa son x, y, p, q y main, pero “x” y “y” están
asociados con tres declaraciones diferentes y con alcances distintos.
Inmediatamente después del procedimiento de la declaración de variable al principio
del cuerpo de “p” (línea 5 de la figura 7.3), la tabla de símbolos puede representarse
como se ve en la figura 7.4 (Dado que en C todas las declaraciones de función son
globales, no indicamos esto de manera explícita en los atributos de la figura 7.4, ni
en las figuras subsecuentes).
Una vez introducido q (línea 13), la tabla de símbolos se convierte como en la figura
7.7.
1. include <stdio.h>
2. int x=1;
3. char y=’a’;
4. void p( ){
5. double x=2.5;
6. printf(“%c”,y);
7. { int y[10]
8. }
9. }
10. void q( ){
11. { int y=42;
12. printf(“%d\n”,x);
13. p();
14. }
15. main( ){
16. char x=’b’;
17. q( );
18. return 0;
19. }
Figura 7.10. Programa en C.
Sin embargo, utilizando el alcance dinámico, las referencias no locales hacia “y” y
hacia “x” en el interior de p( ) y de q( ), respectivamente, pueden modificarse,
dependiendo de la trayectoria de ejecución. En este programa, el enunciado printf
en la línea 12 de la figura 7.10 es alcanzado con la tabla de símbolos como en la
figura 7.12 (con una nueva declaración de “x” como el carácter ‘b’ en el interior del
main) y, por lo tanto, el enunciado printf imprimirá este carácter interpretado como
un entero (debido al formato %d), siendo el valor ASCII 98. Segundo, la referencia
printf hacia “y” en el interior de p( ) (línea 6 de la figura 7.10 es ahora entero con
valor 42 definido dentro de q( ) según se muestra en la figura 7.13. Este valor ahora
es interpretado como carácter (ASCII 42= ‘*’), y el programa imprimirá
92
*
Este ejemplo muestra los problemas que hace difícil el alcance dinámico y es la
razón por la cual pocos lenguajes lo usan. El primer problema es que, bajo el
alcance dinámico, cuando un nombre no local es utilizado en una expresión o un
enunciado, la declaración que se aplica a ese nombre no puede determinarse
mediante la simple lectura del programa. En vez de ello el programa deberá ser
ejecutado. Diferentes ejecuciones producirán resultados diferentes y la semántica
de una función puede cambiar conforme avanza la ejecución. Ya que la referencia
a variables locales no puede predecirse antes de la ejecución. De esta forma, la
ligadura estática de los tipos de datos (tipificado estático) y el alcance dinámico son
inherentemente incompatibles.
El alcance dinámico es una buena opción para lenguajes dinámicos interpretados,
por lo que, lenguajes como APL, Snobol, Perl, Lisp han tenido alcance dinámico
Independientemente de la cuestión del alcance léxico en contraste con el dinámico,
existe una complejidad adicional significativa que todas las estructuras y el
comportamiento de la tabla de símbolos que todavía no se ha analizado. La tabla
de símbolos única para un programa, como se ha estudiado hasta este momento,
es adecuada para lenguajes simples como C o Pascal con un requisito de
declaraciones estática antes del uso, y donde los alcances no pueden ser
ingresados de nuevo una vez que se ha salido de ellos durante el procedimiento
mediante el traductor.
Incluso esta descripción no cubre todas las situaciones de C o Pascal. Por ejemplo,
la declaración “struct” en C tal como se observa en la figura 7.14.
1. struct{
2. int a;
3. int b;
4. double c;
5. } x= {1,’a’,2.5};
6. void p( ){
7. struct{
8. double a;
9. int b;
10. char c;
11. } y={1.2,2,’b’};
12. printf(“%d, %c, %g\n”,x.a,x.b,x.c);
13. printf(“%f, %d, %c\n”,y.a,y.b,y.c);
14. }
15. main( ){
16. p( );
17. return 0;
18. }
Figura. 7.14. Ejemplo de código ilustrando el alcance de las declaraciones locales
con “struct” de C.
Está claro que cada una de las declaraciones struct en éste código (líneas 1-5 y 7-
11 en la figura 7.14) debe contener declaraciones adicionales de los campos de
datos dentro de cada struct, y que éstas deben ser accesibles (utilizando la notación
punto de la selección de miembros) siempre y cuando las mismas variables struct
(x,y) estén en el alcance. Esto quiere decir dos cosas: 1. Una declaración struct
realmente contiene una tabla de símbolos local y es en sí un atributo (que contiene
las declaraciones miembro), y 2. Esta tabla de símbolos local no puede eliminarse
hasta que la variable tipo struct misma sea eliminada de la tabla de símbolos.
“global” del programa.
Por lo que dentro de p( ) (línea 12 de la figura 7.14) la tabla de símbolos por el
código arriba citado puede verse como se muestra en la figura 7.15.
Es importante estudiar el entorno que mantiene las ligaduras de los nombres con
las localizaciones. Se presentarán los fundamentos de los entornos sin funciones,
como una comparación con la tabla de símbolos.
Dependiendo del lenguaje, el entorno puede construirse como estático (en tiempo
de carga), dinámico (en tiempo de ejecución), o una mezcla de ambos. Un lenguaje
que utiliza un entorno completamente estático es FORTRAN donde todas las
localizaciones están vinculadas estáticamente. Un lenguaje que utiliza un entorno
totalmente dinámico es Lisp donde todas las localizaciones se vinculan durante la
ejecución. C, C++, Java, Ada y otros lenguajes estilo Algol se quedan en medio.
En un lenguaje compilado, los nombres de las constantes y de los tipos de datos
pueden representar puramente cantidades en tiempo de compilación que no tienen
existencia en el tiempo de carga o en el tiempo de ejecución. Por ejemplo, la
declaración constante global en C
const int Max=10;
puede ser utilizado por un compilador para reemplazar todos los usos de Max por
el valor de 10. El nombre Max nunca se asigna a localización y de hecho desaparece
completamente del programa cuando está en ejecución.
Las declaraciones se utilizan para construir el entorno, así como la tabla de
símbolos. En un compilador, las declaraciones son utilizadas para indicar el código
de asignación que el compilador tiene que generar conforme se procesa la
declaración. En un intérprete, se combinan la tabla de símbolos y el entorno, por lo
que la ligadura de atributos por una declaración incluye la ligadura de las
localizaciones.
Típicamente, en un lenguaje con estructura de bloques las variables globales se
asignan estáticamente, en vista de que su significado es fijo a lo largo del programa.
Las variables locales, sin embargo, se asignan dinámicamente cuando la ejecución
llega al bloque en cuestión. El entorno en un lenguaje por bloques vincula las
localizaciones a las variables locales en forma de pila. Observar el programa en C
de la figura 7.16.
1. A: { int x;
2. char y;
3. …
4. B: { double x;
5. int a;
6. …
7. } //fin de B.
8. C: { char y;
9. int b;
10. …
11. D: { int x;
12. Double y;
13. …
14. }//fin de D.
15. …
16. }//fin de C.
17. …
18. }//fin de A.
Fig. 7.16. Programa en C con bloques anidados para demostrar el entorno.
Durante la ejecución de este código, cuando se entra a cada uno de estos bloques,
las variables declaradas al principio de cada bloque se asignan y cuando se sale de
cada bloque, estas mismas variables se desasignan. Si vemos el entorno como una
estructura lineal de localizaciones de almacenamiento, con las localizaciones
asignadas a partir de la parte superior en orden descendente, entonces el entorno
de la línea 3 de la figura 7.16 tiene la apariencia que sigue (ignorando el tamaño de
la variable asignada):
Y el entorno después de la entrada en B:
Se observa que las variables del bloque C están ahora asignadas al mismo espacio
previamente asignado a las variables “x” y “a” del bloque B. Esto es correcto, dado
que se está fuera del alcance de esas variables.
Void p( ){
Int x;
Double y;
…
}
Durante la ejecución esta declaración por sí misma no disparará una ejecución del
bloque de p( ) y las variables “x” y “y” de p( ) no serán asignadas en el punto de las
declaraciones. Más bien, se asignarán en el punto de la llamada de p( ). Por lo que,
cada vez que se invoca a p( ) resulta en una región de la memoria asignada en el
entorno. A cada llamada a p( ) se refiere como una activación de p( ) y la región
correspondiente de la memoria asignada como un registro de activación. Por
ejemplo, en el siguiente programa se realiza una llamada recursiva a la misma
función 4 veces, por lo que cada llamada tendrá su propio registro de activación
void main( ){
int x=4;
recursivo(x);
}
void recursivo(int x){
int y;
if (x>0)
recursivo(x-1)
}
Queda claro que un lenguaje estructurado por bloques y alcance léxico, se puede
asociar el mismo nombre con varias locaciones diferentes (aunque sólo una de ellas
pueda ser accedida en cualquier momento). Por lo que se debe de distinguir entre
un nombre con locaciones diferentes. Debemos por lo tanto distinguir entre nombre,
localización asignada y declaración que hace que queden vinculadas. Se llama la
localización asignada un objeto. Esto es, un objeto es un área de almacenamiento
asignada en el entorno como resultado del procesamiento de una declaración. De
acuerdo a esta definición, las variables y los procedimientos son objetos en C, pero
las constantes y los tipos de datos no lo son (dado que las declaraciones de
constantes y de tipos de datos no dan como resultado una asignación de
almacenamiento). El tiempo de vida o extensión de un objeto es la duración de su
asignación en el entorno. Observando la figura 7.16, en el caso de bloques previos,
la declaración del entero “x” en el bloque A define un objeto cuya duración se
extiende a lo largo del bloque B, aun cuando la declaración tiene un agujero de
alcance en B, y el objeto no es accesible desde el interior de B. De manera similar,
es posible que ocurra lo inverso: un objeto puede ser accesible más allá de un
tiempo de vida.
Cuando están disponibles los apuntadores en un lenguaje, resulta necesaria una
extensión de la estructura del entorno. Un apuntador es un objeto cuyo valor
almacenado es una referencia a otro objeto.
En C, el procedimiento de la declaración
int * x;
por parte del entorno genera la asignación de una variable apuntador “x”, pero no la
asignación de un objeto hacia el cual apunta “x”. Ciertamente, “x” puede tener un
valor no definido, que podría ser cualquier localidad arbitraria de la memoria. Para
permitir la inicialización de los apuntadores que no apuntan a un objeto asignado, y
para permitir que un programa determine si una variable apuntador apunta a una
memoria asignada, C permite el uso del entero 0 como una dirección que no puede
ser la dirección de cualquier objeto asignado, y varias bibliotecas de C le dan el
nombre de NULL a 0, por lo que uno puede escribir en C
int * x=NULL;
Con esta inicialización, es posible probar a “x” para ver si apunta a un objeto
asignado
If (x!=NULL) *x=2;
Para que “x” apunte a un objeto asignado, se debe asignar manualmente mediante
el uso de una rutina de asignación. En C se tiene el módulo de biblioteca “stdlib”
donde se tienen varias funciones para asignar y desasignar memoria. La más usada
es “malloc” (memory allocation) y la función “free”.
x=(int*)malloc(sizeof(int));
asigna una nueva variable entera y al mismo tiempo su locación en el valor “x”. La
función “malloc” devuelve la localización que asigna y debe darsele el tamaño de
los datos al cual asigna espacio. Esto se puede dar en forma independiente de la
implementación utilizando la función incorporada “sizeof”, a la cual se le da el tipo
de dato y retorna su tamaño en bytes. La función “malloc” debe adecuarse al tipo
de datos de la variable cuyos resultados le son asignados al colocar el tipo de datos
(antes de la función), dado que la dirección que devuelve es un apuntador anónimo
(un void * en C).
Una vez asignada “x” a la división de una variable entera asignada, esta nueva
variable entera se puede accesar mediante el uso de la expresión “*x”. Se dice que
la variable “x” se puede desreferenciar utilizando el operador unitario “*”. Entonces
se pueden asignar valores enteros a “*x” y referirnos a valores como se haría en
una variable ordinaria, como en
*x=2;
printf(“%d”,*x);
*x también se puede desasignar utilizando el procedimiento free
Free(x);
En este caso se está liberando la memoria del elemento referenciado *x, no la
memoria de la variable “x”.
Para permitir la asignación y la desasignación arbitraria en tiempo de ejecución
utilizando malloc y free, el entorno debe tener un área en la memoria a partir de la
cual se puede asignar las localizaciones en respuesta a las llamadas malloc, y la
devolución de la localización de memoria con la instrucción free. Tradicionalmente
ésta área de memoria se conoce como montículo o montón. La asignación en el
montón se conoce como asignación dinámica, aún cuando la asignación de
variables locales es dinámica. Para distinguir entre estas dos formas de asignación
dinámica, a veces se conoce como basada en pilas o automática a la asignación
de variables locales ya que ocurre de manera automática bajo el control del sistema
en tiempo de ejecución. En cambio, los apuntadores se ejecutan de manera manual
bajo el control del programador.
En una implementación típica del entorno, la pila (por la asignación automática) y el
montículo (por la asignación dinámica) se mantienen en secciones diferentes de la
memoria, y las variables globales también se mantienen en otra sección de la
memoria. Aunque estas tres secciones se podrían colocar en cualquier parte de la
memoria, una estrategia común es colocar las tres adyacentes, en primer término,
las variables globales, la pila en segundo término y el montículo al final, creciendo
el montículo y la pila en direcciones opuestas para evitar colisiones pila/montículo
en caso de que no existan límites fijos entre ambos. Esto se observa en la figura
7.17.
Se observa que a pesar de que el montículo aparentemente crece en una sola
dirección, el almacenamiento se puede liberar en cualquier parte del área asignada
del montículo, y necesita ser reutilizable, por lo que un protocolo de pila simple no
funciona para el montículo.
En muchos lenguajes se requiere que la desasignación de la memoria del montículo
sea realizada en forma automática, contrario a la asignación que es controlada por
el usuario.
Área estática
(Global)
Pila
No asignada
Monticulo