Académique Documents
Professionnel Documents
Culture Documents
DEPARTAMENTO DE ELECTRONICA
ELO320 Estructuras de Datos y Algoritmos
8/4/2010
1.Areas de Memoria
Los datos de los programas en C se almacenan en uno de las tres areas de memoria que
el programador tiene a su disposición: memoria estática, heap y stack:
• La zona de memoria estática es para datos que no cambian de tamaño, permite
almacenar variables globales que son persistentes durante la ejecución de un
programa.
• El heap permite almacenar variables adquiridas dinámicamente (via funciones
como malloc o calloc) durante la ejecución de un programa.
• El stack permite almacenar argumentos y variables locales durante la ejecución de
las funciones en las que están definidas.
El compilador asigna un espacio determinado para las variables y genera las referencias
para acceder a las variables del stack y de la zona estática. El tamaño de las variables del
stack y de la zona estática no puede cambiarse durante la ejecución del programa, es
asignado en forma estática.
Cuando se carga un programa a ejecutar en memoria desde el disco duro solamente los
datos de la zona estatica son creados. El heap y el stack son creados dinamicamente
durante la ejecución del programa.
1.1Memoria estática
La zona estática de memoria permite almacenar variables globales y de tamaño
estático. Si se encuentra una variable definida afuera de las funciones (incluyendo
main) en un archivo fuente de código (de tipo .c o .cc), se la considera global, el
compilador les asigna un espacio determinado y genera las referencias para accesarlas en
la zona estática.
El tamaño de las variables no puede cambiarse durante la ejecución del programa,
es asignado en forma estática.
El tiempo de vida de las variables de la zona estática es la duración del programa.
Estas variables son visibles para todas las funciones que estén definidas después de
ellas en el archivo en que se definan.
1.2 Heap
El heap es un area de memoria dinámica que el programa pide al sistema operativo
utilizando llamadas (funciones) como malloc y calloc.
Esta memoria se maneja via punteros y es la responsabilidad del mismo proceso el liberar
la memoria (utilizando free) después de su uso o puede ocurrir un escape de memoria
(memory leak).
Si ocurre un memory leak este va a durar mientras el proceso siga corriendo ya que al
terminar de correr el sistema operativo libera toda la memoria del heap que fue pedida.
1.3 Stack
El stack se utiliza para almacenar las variables denominadas automáticas, ellas existen
durante la ejecución de la función que las referencia. Los argumentos y variables
locales, son asignados y desasignados en forma dinámica durante la ejecución de las
funciones; pero en forma automática por código generado por el compilador, el
programador no tiene responsabilidad en ese proceso.
Esta organización permite direccionar eficientemente variables que serán usadas
frecuentemente; a la vez posibilita ahorrar espacio de direccionamiento ya que se
puede reutilizar el espacio de memoria dedicado a la función cuando ésta termina; y
también posibilita el diseño de funciones recursivas y reentrantes, asociando un espacio
diferente para las variables por cada invocación de la función.
Es importante notar que varias funciones pueden emplear los mismos nombres para las
variables locales y argumentos y esto no provoca confusión; existe independencia
temporal de las variables de una función.
Si se emplea una global, con igual nombre que una local, dentro de la función se ve
la local; y fuera existe la global. No pueden comunicarse los valores de variables locales
de una función a otra. Obviamente como convención para tener mejor legibilidad en
nuestros es mejor tener no usar los mismos nombres a las variables globales y locales.
Cada función al ser invocada crea un frame en el stack o registro de activación, en el
cual se almacenan los valores de los argumentos y de las variables locales.
Los valores de los argumentos son escritos en memoria, antes de dar inicio al código
asociado a la función. Es responsabilidad de la función escribir valores iniciales a las
variables locales, antes de que éstas sean utilizadas; es decir, que aparezcan en
expresiones para lectura.
Si se precede con la palabra static a una variable local en una función, ésta variable
también es ubicada en la zona estática, y existe durante la ejecución del programa; no
desaparece al terminar la ejecución de la función, y conserva su valor.
Si se precede a una variable con la palabra extern se permite el acceso a una variable
definida en un archivo fuente externo.
Preceder a una variable con el termino register indica que en lo posible se debería utilizar
un registro para almacenar esta variable. El uso de registros es un tipo de optimización,
tipicamente los compiladores modernos automáticamente utilizan optimizaciones como
esta.
2.Uso de memoria de funciones
2.1 Una función con variables locales
La siguiente función, tiene variables locales, argumentos y retorno de tipo entero. Si una
variable se define fuera de las funciones, es global y se le asigna espacio en zona estática.
int función1(int arg1, int arg2)
{ int local1; /* local1 no inicializado*/
int local2=5;
local1=arg1 + arg2 + local2;
return( local1+ 3);
}
int x=7;
void main(void)
{
/*Todavia no existen las variables: local1, local2, arg1 y arg2. */
x = función1(4, 8);
/* Ya no existen los argumentos y variables locales de función1( ) */
}
Se ilustra el espacio de memoria asignado a las variables, después de
invocada función1 y justo después de la definición de la variable local2. La
variable local1, no está iniciada y puede contener cualquier valor.
Zona de Memoria Estatica Stack
x= 7 local1 = ?
local2 = 5 frame de
arg1 = 4 funcion1
arg2 = 8 (activa)
Al salir de función1:
Zona de Memoria Estatica Stack
x= 20
2.2 Visión lógica del stack
Los diagramas anteriores describen lógicamente el espacio asignado en la
memoria para las variables estáticas y automáticas. Cada compilador
implementa físicamente la creación de estos espacios de manera diferente;
en el frame de la función se suelen almacenar: la dirección de retorno, los
valores de los registros que la función no debe alterar; además del espacio
para locales y argumentos.
int* plocal(void)
{
int local;
return(&local); // retorna puntero a local
}
A su vez esta organización implica crear una copia, lo cual puede ser muy costoso en
tiempo si el tipo del argumento tiene un gran tamaño en bytes. Para solucionar el
problema de la copia, se puede pasar una referencia a un argumento de grandes
dimensiones, esto se logra pasando el valor de un puntero (se copia el valor del puntero,
el cual ocupa normalmente el tamaño de un entero).
2.5 Ejemplo: Una función f que invoca a otra función g
Se tienen las siguientes definiciones de las funciones.
local1 c = 16
local2 d = 5 frame de f
arg1 a = 5
arg2 b = 6
Justo antes de salir de g:
local1 c = 16
local2 d = 5 frame de f
arg1 a = 5
arg2 b = 6
Después de salir de g:
Muchas veces los diseños recursivos requieren más espacio de memoria (especialmente
stack) y ocupan mayor tiempo en ejecutarse. Sin embargo generan diseños simples
cuando las estructuras de datos quedan naturalmente definidas en términos recursivos. Es
el caso de los árboles y de algunas funciones matemáticas.
Cada vez que se activa una invocación de una función recursiva, se crea espacio para
sus variables automáticas en un frame; es decir cada una tiene sus propias variables. El
cambio de las locales de una invocación no afecta a las locales de otra invocación que
esté pendiente (que tenga aún su frame en el stack). Las diferentes encarnaciones de las
funciones se comunican los resultados a través de los retornos.
Si existe una forma recursiva de resolver un problema, entonces existe también una forma
iterativa de hacerlo; y viceversa.
Consideremos la función matemática factorial, que tradicionalmente está definida en
forma recursiva (a través de sí misma).
factorial( 0 ) = 1
factorial( n ) = n * factorial( n-1 )
La condición para detener la recursión, el caso base, es que factorial de cero es uno.
También se puede detener con factorial(1) = 1.
3.2 Iterativo
En un procedimiento iterativo, también denominado bottom-up, se parte de la base
conocida y se construye la solución paso a paso, hasta llegar al caso final.
int x;
x= f(3, 5);
Donde a y b deben ser variables visibles o dentro del alcance de la función e inicializadas
si son variables locales. Se evalúan las expresiones y se copian los valores resultantes
en el espacio de memoria del frame de la función.
El único valor retornado por la función es copiado en el espacio asignado a x.
En un caso más general, el resultado de la función puede formar parte de una expresión,
cuyo tipo debe ser compatible con el de la variable a la cual se asigna dicha expresión:
x = f(a, b) + c
4.3 Variables Globales
4.4 Punteros
El concepto de puntero permite pasar la referencia a una variable que existe fuera de
la función, eliminando, de esta forma, la copia de variables de grandes dimensiones, ya
que sólo es preciso copiar el valor del puntero.
En este ejemplo los resultados deseados son la suma y la resta de dos variables o valores.
c = f(3, 4, pd);
Árbol binario.
El arbol binario es una estructura que puede ocupar grandes dimensiones. Se tienen los
siguientes tipos definidos según:
Tipicamente para arboles binarios, la convención es que para cada nodo los valores de los
nodos que están vinculados por la derecha son mayores que los valores de los nodos que
están conectados por la izquierda. Los nodos que no tienen descendientes deben tener
valores nulos en los punteros left y right.
4.7 Ejemplo: Argumento usando un Árbol binario.
Se desea diseñar una función que encuentre el nodo con mayor valor. No es razonable
pasar como argumento el árbol completo a la función, por el espacio requerido en el
stack, y porque sería necesario además copiar la estructura completa.
Entonces podemos pasar un puntero a un nodo como argumento, esto sólo requiere crear
el espacio y copiar el valor de un puntero (normalmente el tamaño ocupado por un
entero). El retorno de la función, será un puntero al nodo que tenga el mayor valor.
También vamos a utilizar variables de tipo puntero a nodo, una apunta a un nodo
denominado raíz, el otro es una variable auxiliar (e.g. pn).
pnodo busca_max(pnodo T)
{
if (T != NULL)
while (T->right != NULL) T = T->right; /* Busca por la
derecha */
return(T);
}
Con la definición anterior el string almacenado en el arreglo puede tener un largo máximo
de cinco caracteres. El nombre del arreglo str, es un puntero constante que apunta al
primer carácter del string.
destino
cp fuente
El punto y coma se considera una acción nula, tambien se podria usar continue.
5.4 lvalue y rvalue
El operador de postincremento opera sobre un left value (lval++). Un lvalue es un
identificador o expresión que está relacionado con un objeto que puede ser accesado
y cambiado en la memoria. Un rvalue que es el dato leído que se almacena en la
locación especificada por el lvalue.
+ - Left to Right
== != Left to Right
Binary operator: & Left to Right
Binary operator: ^ Left to Right
Binary operator: | Left to Right
&& Left to Right
|| Left to Right
Ejemplo:
Usando el siguiente código se desea incrementar el contenido de arreglo[0].
main()
{
int arreglo[10]; // crear arreglo
int *pa = arreglo; // asignar pa para que apunte a arreglo
arreglo[0]=1; // insertar 1 en arreglo[0]
*pa++; // referenciar arreglo[0] e incrementar en 1
printf("*pa=%d \n",*pa);
}
Al ejecutarse:
$:~/code/algo-ds/puntero$ puntero
*pa=2
El contenido del bloque debe ser inicializado antes de ser usado, ya que inicialmente
contiene los valores que estaban previamente almacenados en el bloque del heap. Suele
emplearse el operador sizeof(tipo o nombre_de_variable) que retorna un valor entero sin
signo con el número de bytes con que se representa el tipo o la variable, para calcular el
tamaño del bloque que debe solicitarse.
void free(void * puntero)
Free libera el bloque apuntado por puntero y lo devuelve al heap. El valor del puntero
debe haber sido obtenido a través de malloc; produce errores difíciles de depurar invocar
a free con un puntero no devuelto por malloc. Después de ejecutar free, el programador
no puede volver a referenciar datos a través de ese puntero; debería asignarle NULL, para
prevenir errores.
Ejemplo:
void UsaArregloDinámico(unsigned int size)
{
int * pArreglo;
int i;
int i, j;
int **t = malloc(r * sizeof(int *)); /*crea arreglo de punteros a
enteros */
assert(t !=NULL);
assert(t[i] !=NULL);
}
t[i][j] = val;
return t;
}
void BorreMatriz(int ** p, int r)
{
int i;
int **t = p;
for (i = 0; i < r; i++)
}
El siguiente segmento ilustra el uso de las funciones:
int **m;
}
void LiberaNodo( pnodo pn)
{
free( pn); //Libera el espacio }
Ejemplo de uso:
pnodo root=NULL;
root=CreaNodo(5); //se pega el nodo a la raíz
LiberaNodo(root);
7. Referencias
1- Kernighan, B., Ritchie, D., The C Programming Language, Prentice Hall, 1988
2- Robert Sedgewick, "Algorithms in C", (third edition), Addison-Wesley, 2001
2- Apuntes del Prof. Leopoldo Silva Bijit, curso EDA-320, UTFSM