Programación (PRG) PR´ACTICA 9. Estructuras de datos

Anuncio
Programación (PRG)
PRÁCTICA 9. Estructuras de datos
Facultad de Informática
Departamento de Sistemas Informáticos y Computación
Universidad Politécnica de Valencia
Curso 2002/2003
1.
Introducción
El objetivo de esta práctica es aprender a utilizar y a implementar algunas de las estructuras de datos lineales estudiadas en clase. Para ello, se
implementarán en C una serie de estructuras, utilizando memoria dinámica.
Una de las dificultades que habitualmente se encuentra cuando se está aprendiendo C es la utilización de los punteros. En esta práctica se mostrarán las
operaciones básicas de manejo de punteros y memoria dinámica.
Las estructuras de datos de un programa son tan importantes como las
instrucciones que las utilizan. Seleccionar inicialmente una buena estructura
de datos es crucial para solucionar un problema de manera eficiente.
2.
2.1.
Punteros
Punteros en C
Un puntero es una dirección de memoria. Una variable de tipo puntero
es una variable que almacena una dirección. Las variables puntero tienen
asociado un tipo, que indica el tipo de datos de la zona de memoria donde
apunta.
Gráficamente, los punteros se suelen representar por flechas, que parten de la variable de tipo puntero, y llegan a la zona de memoria a donde
apuntan. La Figura 1 muestra un ejemplo de una variable puntero a entero.
Cuando se trabaja con punteros se ha de tener en mente que hay dos
zonas de memoria implicadas. Una es la que contiene la dirección de memoria
a la que apunta el puntero (la variable p, que contiene el valor 0x0012F en
la Figura 1). La otra es la zona de memoria que empieza en la dirección
que indica el puntero (la zona de memoria que empieza en 0x0012F). Para
declarar la variable p de la figura, se utiliza la siguiente sintaxis:
1
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
Memoria del ordenador
p
0x0012F
0x0012F
123
Figura 1: Representación gráfica de un puntero a entero
int *p;
Para declarar punteros a otro tipo de datos, únicamente habrı́a que modificar el tipo que aparece a la izquierda del *:
double *a;
float *b;
char *c,*d;
struct cliente *e;
Nótese que, si se desea declarar dos o más variables de tipo puntero en
la misma lı́nea, habrá que anteponer siempre el asterisco al nombre de cada
variable. Todas las variables puntero, independientemente del tipo de datos
al que apunten, ocupan 4 bytes en la arquitectura Intel Pentium. El tamaño
de la zona a la que apuntan viene dado por el tipo de datos asociado, por
ejemplo, un puntero a double apunta a una zona de 8 bytes.
En C se puede definir un tipo puntero genérico como void *, pero no
se puede trabajar con ellos directamente, ya que el tamaño de la zona de
memoria a la que apuntan es indeterminado.
Justo después de declarar una variable de tipo puntero, su valor está indeterminado, por lo que no resulta aconsejable ni leer ni escribir en la zona
de memoria a la que apunta. Se dice que el puntero no está inicializado, o
que es un puntero loco. Gráficamente, se puede representar como se muestra
en la Figura 2.
Si se intenta leer o escribir en la zona de memoria donde apunta un
puntero no inicializado, en la mayorı́a de las ocasiones el sistema operativo
Página 2 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
Figura 2: Representación gráfica de un puntero no inicializado
intervendrá, terminando el programa y mostrando un mensaje del tipo:
Segmentation fault
ó
Violación de segmento
Cero patatero. Acceder a un puntero no inicializado es la falta más grave
en la que se puede incurrir trabajando con punteros.
En la cabecera stdio.h está definida la constante NULL. Dicha constante
es una dirección de memoria especial, que se suele utilizar para inicializar
los punteros a un valor conocido (aunque no accesible). Posteriormente se
explicará la utilidad de dicha constante. Cuando se asigna a un puntero el
valor NULL, del siguiente modo:
p=NULL;
se dice que p es un puntero nulo. A todos los efectos, intentar leer o escribir
donde apunta un puntero nulo es equivalente a acceder a donde apunta un
puntero no inicializado.
2.2.
Operadores de punteros
Cuando se trabaja con punteros se pueden utilizar dos operadores especiales: uno para calcular la dirección en memoria de cualquier variable, y
otro para leer o escribir en la dirección a la que apunta un puntero.
El operador & (léase ampersand ) devuelve la dirección de la variable a
la que precede. En el siguiente ejemplo:
int x, *p;
x=100;
p = &x;
p contendrá a partir de ese momento la dirección de memoria asociada a la
variable x. Una vez que se tiene una variable puntero, para acceder al valor
apuntado por ella, se usará el operador *. Siguiendo el ejemplo anterior:
Página 3 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
if (*p==x) printf("iguales\n");
else printf("distintos\n");
En la Figura 3 se muestran gráficamente las distintas operaciones que se
pueden hacer sobre variables de tipo puntero.
double *p,q=7,*r;
p=&q;
p
r
p
r
7.0
q
7.0
q
*p=10;
p
r
10.0
q
r=p;
p
r
10.0
q
p=NULL;
p
r
10.0
q
Figura 3: Operaciones con punteros
Ejercicio. Escribe un programa en el que se declaren 3 variables de tipo
float y una variable tipo puntero. Asigna a las variables float los valores
3.0, 6.0 y 9.0. Imprime el valor de cada una de ellas por pantalla, pero utilizando el puntero, es decir, tendrás que utilizar 3 llamadas como la siguiente:
printf("%f\n",*p);
¿Cuándo * y cuándo &? Siempre que te surjan dudas al utilizar los operadores de punteros, recuerda que, en cualquier asignación, el tipo de datos
de la expresión que aparece a la derecha del signo = debe coincidir con el
tipo de datos de la variable que aparece a la izquierda. Por ejemplo:
double a, *b;
[...]
a=b; // MAL!
Como a es de tipo double, lo que aparece a la derecha debe ser double.
Para convertir un puntero a double en un double, hay que escribir un *
delante:
a=*b;
De esta forma, se le está asignando a a el valor al que apunta b.
Página 4 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
Ejercicio. Indica si las siguientes expresiones son sintácticamente correctas.
Explica lo que hace en el caso de que sea correcta, y corrige la expresión en
caso de que sea incorrecta.
int *a,b,*c;
a=b;
a=c;
b=a;
2.3.
Memoria dinámica
Hasta el momento, la única forma que se ha visto para hacer que un
puntero apunte a una zona de memoria válida, es hacer que apunte a la
zona de memoria de otra variable. Todas las variables que se han declarado
hasta ahora se dice que son estáticas, porque es el compilador el que calcula
la memoria que necesitan. Además, en cuanto se invoca a la función que
contiene la variable, se le reserva memoria automáticamente en el registro
de activación correspondiente.
Hay una forma de posponer la asignación de memoria. Mediante el uso
de memoria dinámica, se puede reservar espacio para una variable en tiempo
de ejecución. Dicha zona de memoria podrá tener cualquier tamaño, y no es
necesario conocerlo en tiempo de compilación. La zona de memoria dinámica
de un programa C se debe ver como una zona, separada de la pila de registros
de activación, en la que se puede reservar y liberar segmentos de memoria
(véase la Figura 4).
Zona de memoria
dinámica (montículo o
heap)
Pila de registros
de activación
func2(a:
)
aux=
func1(a:
, b:
)
main()
res=
Figura 4: Pila de registros de activación y heap
Para el manejo de la memoria dinámica de C hay principalmente tres
Página 5 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
funciones, definidas en el fichero de cabecera stdlib.h.
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void free(void *ptr);
La función malloc reserva una zona de memoria de size bytes (el tipo
size_t es un sinónimo de unsigned long). Para calcular el tamaño en
bytes de cualquier tipo (o variable), se puede utilizar el operador sizeof.
Por ejemplo:
tam=sizeof(double);
La llamada a malloc devuelve, en caso de que haya memoria disponible,
un puntero a la primera posición de la zona recién reservada, en forma de un
puntero de tipo void. Para convertirlo al tipo con el que se desea trabajar,
habrá que utilizar un cast. En el caso de que no haya memoria libre suficiente,
la llamada a malloc devolverá el valor NULL. Por ello, después de llamar a
malloc, siempre habrá que comprobar que ha devuelto un puntero válido.
En el siguiente ejemplo se reserva espacio para una variable de tipo double,
se le asigna un valor y se imprime por pantalla:
double *pd;
pd=(double *)malloc(sizeof(double));
if (pd==NULL) exit(-1);
*pd=34.5;
printf("%lf",*pd);
La zona de memoria recién creada por malloc no se inicializa, y por lo
tanto contendrá un valor indefinido. Si se desea que la zona de memoria
recién creada se escriba con ceros, se debe llamar a la función calloc, que
se estudiará más adelante.
Una vez que se ha terminado de utilizar una zona de memoria dinámica es
necesario liberarla, para que pueda ser utilizada posteriormente. La función
que se encarga de liberar zonas de memoria reservadas con malloc es free,
y únicamente hay que pasarle el puntero a liberar:
free(pd);
La función free no pone el puntero que libera a nulo. En la Figura 5
se muestra gráficamente los distintos estados por los que pasa un puntero
trabajando con memoria dinámica.
Página 6 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
Figura 5: Etapas en la vida de un puntero
2.4.
Punteros y vectores
La forma general de declarar un vector en C es la siguiente:
tipo nombre_variable[tama~
no];
Hay dos formas principales de declarar un vector, por ejemplo, de caracteres:
char a[10];
char *a;
La primera forma tiene reservada una zona de memoria de tamaño 10
caracteres. La segunda declara un puntero sin inicializar. En C, apenas hay
distinción entre los vectores y los punteros. De hecho, si se utiliza el nombre
del vector sin corchetes, se tiene un puntero al primer elemento del mismo.
Cuando se pasa un vector a una función, en realidad lo que se está pasando
es un puntero al primer elemento. Por ello, si se modifica un elemento del
vector dentro de la función, cuando acabe la misma, el vector original se
habrá modificado.
Hay una tercera forma de declarar un vector sin dar las dimensiones
explı́citamente, pero enumerando los datos del mismo. De esta forma los
vectores se pueden inicializar en tiempo de compilación:
char a[]="Esto es una cadena";
int b[]={1,2,3,4,5};
En ambos casos, el compilador calculará el tamaño exacto necesario para cada vector. Si se desea reservar espacio para un vector en tiempo de
Página 7 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
ejecución, se puede utilizar la función malloc vista anteriormente:
char *a;
int *b;
a=(char *)malloc(sizeof(char)*10);
if (a==NULL) return -1; /* Comprobación de que hay espacio */
b=(int *)malloc(sizeof(int)*5);
if (b==NULL) return -1;
En el ejemplo se está reservando espacio para un vector de 10 caracteres,
y otro de 5 enteros. A la hora de reservar espacio para cadenas de caracteres,
hay que recordar que el carácter de fin de cadena (ASCII 0) también se debe
almacenar.
Ejercicio. Haz un programa que muestre por pantalla una serie de números,
de la forma que se indica a continuación:
10 32 87 93 1
10 32 87 93
10 32 87
10 32
10
Es el usuario el que debe indicar la cantidad de números a mostrar. Los
números se generarán aleatoriamente entre 1 y 100. Utiliza para ello un
vector del tamaño exacto que indique el usuario.
Para declarar una matriz bidimensional, se dispone de las siguientes posibilidades:
char a[3][4];
char *b[3];
char **c;
/* Tiene toda la memoria asignada */
/* Tiene parte de la memoria asignada */
/* No tiene memoria asignada */
Para reservar memoria a la matriz c, se deben seguir los siguientes pasos:
c=(char **)malloc(sizeof(char *)*3);
c[0]=(char *)malloc(sizeof(char)*4);
c[1]=(char *)malloc(sizeof(char)*4);
c[2]=(char *)malloc(sizeof(char)*4);
Como se ha dicho antes, el uso de vectores y punteros es intercambiable.
Por lo tanto, se puede acceder a los elementos de un vector por medio de
Página 8 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
a
DSIC
PRÁCTICA 9. Estructuras de datos
0,0 0,1 0,2 0,3
1,0 1,1 1,2 1,3
2,0 2,1 2,2 2,3
c
b
Figura 6: Representación de matrices bidimensionales
punteros. De igual forma, se puede indexar una variable que se ha declarado
como puntero. Ası́, para recorrer un vector de 10 elementos, se puede hacer:
int a[10],i;
for (i=0;i<10;i++)
printf("%d ",a[i]);
o bien,
int a[10],i,*p;
p=a;
for (i=0;i<10;i++)
{
printf("%d ",*p);
p++;
}
En el caso de matrices bidimensionales:
d[1][2]
≡
*(d[1]+2)
≡
*(*(d+1)+2)
En C se permite aritmética de punteros (incrementos, decrementos, sumas y restas), y se hacen con respecto al tipo al que apunte el puntero. Por
ejemplo:
char *a;
int *b;
double *c;
Página 9 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
a++; /* Apunta al siguiente byte en memoria (sizeof(char)=1)*/
b++; /* Apunta cuatro bytes más allá (sizeof(int)=4) */
c=c+10; /* Apunta 80 bytes más allá (sizeof(double)=8) */
Por último, si se desea que los elementos del vector estén inicializados
a cero, se deberá utilizar la función calloc para reservar memoria, del siguiente modo:
int *vector;
vector=(int *)calloc(10,sizeof(int));
if (vector==NULL) exit(-1);
[...]
free(vector);
2.5.
Punteros y estructuras
Las estructuras permiten agrupar un conjunto de variables de distinto
tipo bajo una misma entidad. Son equivalentes a los registros de Pascal. La
sintaxis de la definición de las estructuras es la siguiente:
struct [nombre_estructura]
{
tipo nombre_campo;
tipo nombre_campo;
...
} [variables_de_estructura];
Los dos ejemplos siguientes son equivalentes. Definen el tipo estructura
y declaran una variable de ese tipo estructura, y otra de tipo puntero a
estructura:
struct s1 {
char *c1;
int c2;
float c3[10];
};
struct s1 v1,*v2;
struct {
char *c1;
int c2;
float c3[10];
} v1,*v2;
Mientras que en el primer ejemplo se ha declarado un nuevo tipo utilizable en el resto del programa (struct s1), en el segundo, al no darle un
Página 10 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
nombre al tipo, sólo se podrán utilizar las dos variables declaradas. Para acceder a los campos de una variable tipo estructura, se usan los operadores .
o -> (si la variable es de tipo estructura, o bien de tipo puntero a estructura,
respectivamente). Para los ejemplos anteriores:
v1.c2
v1.c3[3]
v2->c2
v2->c3[3]
/* también se puede usar (*v2).c2 */
/*
(*v2).c3[3]
*/
¡Cuidado!. Al declarar una variable puntero a estructura, inicialmente
apunta a una zona de memoria no válida, como pasa con cualquier puntero.
De la misma forma que antes, hay que hacer que apunte a una zona válida,
tomando la dirección de otra variable tipo estructura, o bien reservándole
espacio con malloc.
2.6.
Paso de parámetros a funciones
El paso de parámetros a funciones en C siempre se realiza por valor, es
decir, las funciones reciben copias de los valores originales. De esta forma,
cuando una función cambia el valor de uno de sus argumentos, la variable
que originalmente tenı́a el valor continúa intacta. Por ejemplo:
void machaca(int a) {
a=3;
}
...
int entero;
entero=0;
machaca(entero);
entero=?
La llamada a la función machaca crea una copia temporal de la variable
entero, que es la que se pasa a la función. A todos los efectos, los argumentos de una función se pueden ver como variables locales (ver Figura 7).
Por ello, cuando una función acaba, se eliminan de la memoria tanto las
variables locales como los argumentos, junto a los valores que tuvieran en
ese momento.
De esta forma, C ofrece tres posibilidades para sacar información de una
función:
Página 11 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
0
entero
0
void machaca(int a)
{
a=3;
}
Figura 7: No se modifica la variable original
Variables globales (no aconsejada).
Valor de retorno de la función.
Argumentos pasados por referencia.
En el caso de querer devolver más de un dato, no se podrá utilizar el
valor de retorno de la función. Para devolver más de un valor, se pueden utilizar parámetros pasados por referencia. Pasar un argumento por referencia
implica que la función puede modificar el contenido de la variable original.
Para ello, debe conocer dónde se encuentra en memoria. La solución es pasar
a la función un puntero a la variable original (ver Figura 8). En el ejemplo
anterior:
entero
0
void machaca(int *a)
{
*a=3;
}
Figura 8: Sı́ se modifica la variable original
void machaca(int *a) {
*a=3;
}
...
int entero;
entero=0;
machaca(&entero);
entero=3
Página 12 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
Ejercicio. Implementa una función que, dado el radio de un cı́rculo, devuelva su diámetro, su área y su perı́metro.
3.
Implementación de una lista
A continuación se implementarán algunas operaciones de una lista. La
lista está compuesta por nodos enlazados, sin centinela, como muestra la
Figura 9.
Lista l;
Esto
es
una
lista
Figura 9: Esquema de una lista de palabras
A continuación se muestra la definición del tipo de datos Nodo, y del tipo
de datos Lista:
typedef struct t_nodo {
char *info;
struct t_nodo *sig;
} Nodo;
typedef Nodo *Lista;
La operación más común que se realiza con las estructuras de datos de
tipo lista suele ser el recorrido. Para recorrer una lista, casi siempre se sigue
el siguiente esquema (se asume que el puntero al primer elemento de la lista
se almacena en la variable lst:
Nodo *aux;
aux=lst; /* aux apunta al primer nodo de la lista */
while (aux!=NULL) /* mientras que aux sea válido...*/
{
/* PROCESAR EL NODO aux */
aux=aux->sig; /* aux apunta al siguiente nodo */
}
Dentro del while habrá que procesar cada nodo mediante el puntero
aux. Por ejemplo, para contar el número de caracteres total almacenados en
la lista, se podrı́a utilizar la siguiente función:
Página 13 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
int CuentaCaracteres(Lista l) {
Nodo *aux;
int contador;
contador=0;
aux=l;
while (aux!=NULL) {
contador+=strlen(aux->info);
aux=aux->sig;
}
return contador;
}
Ejercicio. El esquema anterior, ¿funciona en todos los casos? ¿Funciona en
el caso de que la lista esté vacı́a? Justifica la respuesta.
A continuación se muestran las funciones a implementar:
Constructores
Lista ListaVacia(void);
int InsertarPrimero(Lista *l, char *palabra);
int InsertarUltimo(Lista *l,char *palabra);
Consulta
int EstaVacia(Lista l);
void MostrarLista(Lista l);
int LongitudLista(Lista l);
int ConsultarPalabra(Lista l, int pos, char *palabra);
Borrado
int BorrarPrimero(Lista *l);
int BorrarUltimo(Lista *l);
3.1.
Constructores
Una vez que se hayan implementado las funciones anteriores, se podrá construir una lista del siguiente modo:
Lista *lst;
lst=ListaVacia();
InsertarPrimero(&lst, "pepito");
InsertarPrimero(&lst, "juanito");
InsertarPrimero(&lst, "andres");
Página 14 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
Ejercicio. Construye una librerı́a para la implementación de listas de palabras. Para ello, crea dos ficheros, llamados lista_pal.c y lista_pal.h. En
el primero introducirás el código para el manejo de listas, tal y como lo vayas
desarrollando (los apartados siguientes te guiarán). En lista_pal.h escribe
la definición de los tipos Nodo y Lista vistos en el Apartado 3, y las cabeceras de las funciones de la librerı́a. El siguiente programa te permitirá probar
la librerı́a; escrı́belo en un fichero llamado prueba_lista.c:
#include "lista_pal.h"
int main(void) {
Lista lst;
lst=ListaVacia();
InsertarUltimo(&lst,"es");
InsertarUltimo(&lst,"una");
InsertarUltimo(&lst,"prueba");
InsertarPrimero(&lst,"esto");
printf("Lista: \n");
MostrarLista(lst);
return 0;
}
Para compilar el programa, una vez que tengas la librerı́a o, al menos, las
funciones que se utilizan, debes ejecutar:
gcc -o prueba_lista prueba_lista.c lista_pal.c -Wall
3.1.1.
ListaVacia
La cabecera de la función es:
Lista ListaVacia(void);
Esta función devuelve una lista vacı́a. Una lista vacı́a se representa por medio
del puntero nulo.
3.1.2.
InsertarPrimero
La cabecera es:
Página 15 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
int InsertarPrimero(Lista *l, char *palabra);
Esta función inserta una copia del elemento apuntado por palabra en
la primera posición de la lista. En el caso de que pueda acabar la operación,
la función devolverá cero. En caso de error, devuelve -1.
Para ayudar a la inserción de elementos en una lista, implementa la
siguiente función:
Nodo *CrearNodo(char *palabra);
Dicha función construye un nodo que contiene la palabra que recibe en
el parámetro. Para ello:
1.
Reserva espacio para un nodo.
2.
Crea una copia de la cadena que se le pasa (en el argumento palabra)
mediante la función strdup. Dicha función está definida en string.h,
y su cabecera es: char *strdup(char *s).
3.
Almacena la copia en el campo info de la estructura creada.
4.
Pon el campo sig del nodo a nulo.
5.
Devuelve el nodo.
Para insertar un elemento en la primera posición de una lista, hay que
seguir los pasos mostrados en la Figura 10:
1.
Crear un nuevo nodo con la función CrearNodo.
2.
Hacer que el campo sig del nuevo nodo apunte al elemento que estaba
en la primera posición.
3.
Hacer que la cabeza de la lista sea el nuevo nodo.
lst
3
aux
2
1
Figura 10: Pasos para insertar un elemento en cabeza
Página 16 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
3.1.3.
DSIC
PRÁCTICA 9. Estructuras de datos
InsertarUltimo
La cabecera de la función es:
int InsertarUltimo(Lista *l,char *palabra);
Esta función debe encontrar el último elemento de la lista, crear un nuevo
nodo con la palabra correspondiente (con la función CrearNodo) y hacer que
el siguiente al último elemento sea el nuevo nodo. Devuelve cero si ha podido
realizar la operación, y -1 en caso de error.
3.2.
Consulta
Las funciones que se van a construir a continuación permiten acceder a
la información albergada en la lista.
3.2.1.
EstaVacia
La cabecera de la función es:
int EstaVacia(Lista l);
Devuelve un cero en el caso de que la lista tenga algún elemento, y un
uno en caso contrario.
3.2.2.
MostrarLista
Este procedimiento muestra por pantalla el contenido de la lista. La
cabecera es la que aparece a continuación:
void MostrarLista(Lista l);
Ejercicio. Cuando hayas implementado las funciones anteriores ya puedes
resolver el ejercicio de la página 15. Una vez que hayas probado que las
funciones implementadas funcionan, continúa implementando las funciones
siguientes y modificando el programa principal para probarlas.
3.2.3.
LongitudLista
Esta función devuelve el número de palabras de la lista. La cabecera de
la función es:
int LongitudLista(Lista l);
Página 17 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
3.2.4.
DSIC
PRÁCTICA 9. Estructuras de datos
ConsultarPalabra
Esta función permite recuperar las palabras almacenadas en los nodos
de la lista. Para ello, se deberá indicar la posición del nodo a consultar, y
una variable de tipo cadena donde copiar la palabra. El perfil de la función
es el siguiente:
int ConsultarPalabra(Lista l, int pos, char *palabra);
Si la posición existe, y la función puede copiar el resultado en el argumento palabra, entonces devolverá un cero. Si la palabra no existe, devolverá un
-1. Por ejemplo, una llamada a esta función para recuperar la tercera palabra
de la lista serı́a la siguiente:
char resultado[200];
Lista lst;
[...]
if (ConsultarPalabra(lst,3, resultado))
printf("No existe esa palabra\n");
else
printf("La palabra de la posición 3 es %s\n",resultado);
[...]
3.3.
Borrado
Por último, las funciones que se presentan a continuación permitirán
eliminar los elementos de la lista.
3.3.1.
BorrarPrimero
Esta función borra el primer nodo de la lista. El perfil de la función es
el siguiente:
void BorrarPrimero(Lista *l);
En la Figura 11 se pueden ver los pasos para desenlazar el primer nodo
de la lista. Después de actualizar los punteros, sólo queda liberar el campo
info del nodo, y luego el propio nodo.
En el caso de que la lista sólo tuviera un elemento, ésta deberá quedar
vacı́a.
Página 18 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
2
aux
1
Figura 11: Pasos para borrar el elemento en cabeza
3.4.
BorrarUltimo
Esta función borra el último nodo de la lista. El perfil de la función es
el siguiente:
void BorrarUltimo(Lista *l);
En el caso de que la lista sólo tuviera un elemento, ésta se deberá quedar
vacı́a.
4.
Implementación de una pila
Una vez que se tienen implementadas las operaciones de manejo de listas,
es muy sencillo implementar una pila a partir de dichas operaciones. Una
pila es una estructura de almacenamiento que únicamente permite el acceso
al último elemento apilado. En la realidad hay múltiples ejemplos de pilas.
Por ejemplo, una serie de cajas colocadas una encima de otra son una pila.
Sólo se puede acceder a una caja cuando no tiene ninguna otra encima, y
sólo se puede añadir una caja encima de todas las demás (no se puede poner
una caja debajo de otra).
Las dos operaciones principales de manejo de pilas son la de apilar (push
en inglés) y desapilar (pop).
A continuación se muestra el fichero de cabecera pila.h, donde se indican las funciones que se deben implementar en otro fichero, que se llamará pila.c:
#include "lista.h"
typedef Lista Pila;
Pila PilaVacia();
int EstaPilaVacia(Pila p);
int Apilar(Pila *p, char *palabra);
int Desapilar(Pila *p);
int ConsultarCima(Pila p, char *palabra);
Página 19 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
En el fichero anterior se puede ver la definición del tipo de datos Pila.
Dicho tipo se ha definido como un sinónimo de Lista, por lo que se podrán utilizar las operaciones implementadas anteriormente. Una pila vacı́a
se representará exactamente igual que una lista vacı́a.
A continuación se enumeran las operaciones que debes implementar.
4.1.
PilaVacia
Esta función devuelve una pila vacı́a. La cabecera de la función es:
Pila PilaVacia();
4.2.
EstaPilaVacia
Esta función devuelve un uno si la pila está vacı́a, y un cero en otro caso.
El perfil es:
int EstaPilaVacia(Pila p);
4.3.
Apilar
Esta función permite insertar elementos en la cima de la pila. Se le debe
pasar la palabra a insertar. El perfil de la función es el siguiente:
int Apilar(Pila *p, char *palabra);
Si la llamada se puede completar, la función devuelve cero y si ha ocurrido un error (por ejemplo, si no hay memoria suficiente), devolverá -1.
4.4.
Desapilar
Esta función saca de la pila el elemento que está en la cima. Si únicamente
habı́a un elemento en la pila, la pila quedará vacı́a. La cabecera de la función
es:
int Desapilar(Pila *p);
Devuelve cero si ha podido sacar el elemento de la cima, y -1 si la pila
estaba vacı́a.
4.5.
ConsultarCima
Por último, esta función devuelve una copia de la palabra que está en la
cima de la pila. Una llamada a esta función no modifica el estado de la pila,
por lo que el elemento que estaba en la cima sigue estando allı́. La cabecera
de la función es la siguiente:
Página 20 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
int ConsultarCima(Pila p, char *palabra);
Devuelve cero en caso de que haya podido devolver la palabra, y -1 si la
pila estaba vacı́a.
Ejercicio. Una vez que hayas implementado las funciones anteriores, haz
un programa que las utilice para probar que funcionan correctamente.
5.
Ejercicios propuestos
1. Construye las funciones int BorrarElemento(Lista *l, int pos)
y void BorrarLista(Lista *l) que borran el elemento de la posición
pos, y todos los elementos de la lista, respectivamente. En el caso de
que en la primera función se borre el último elemento de la lista, ésta
deberá quedar vacı́a. La segunda función siempre deja la lista vacı́a.
2.
Escribe un programa que lea desde teclado (o desde la entrada estándar)
una serie de frases, hasta encontrar un carácter EOF. Luego las deberá escribir por pantalla, pero empezando por la última frase leı́da y
acabando por la primera.
3. Implementa una cola de palabras utilizando las funciones de la lista
implementadas en la práctica. Las funciones a implementar se llaman:
ColaVacia, EstaColaVacia, Encolar, Desencolar y CosultarCabeza
A.
Compilación de programas con múltiples ficheros fuente
Uno de los principios de la programación modular es dividir un problema
complejo en subproblemas más sencillos, que se puedan implementar más
fácilmente por separado. Esta división se puede hacer tanto a nivel de subrutinas (procedimientos y funciones) como de módulos (grupos de subrutinas).
Los compiladores de C ayudan a implementar este tipo de diseño permitiendo que un programa ejecutable proceda de uno o más ficheros fuente. Como
se comentó en la Práctica 3, es posible compilar un programa de este tipo
con una orden como:
gcc -o prg fich1.c fich2.c fich3.c
El inconveniente de esta forma de compilación es que siempre se compilan
todos los ficheros, aunque no se hayan modificado. En el ejemplo anterior, si
el programador modifica el fichero fich3.c y ese cambio no afecta al resto de
ficheros, éstos no tendrı́an por qué compilarse de nuevo. Para poder compilar
únicamente los ficheros modificados, se pueden utilizar los ficheros objeto,
Página 21 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
que son ficheros que contienen el código compilado de un archivo fuente.
Los ficheros objeto no son directamente ejecutables, ya que normalmente no
tienen una función main. Las siguientes órdenes generan, respectivamente,
los ficheros fich1.o, fich2.o y fich3.o:
gcc -c fich1.c
gcc -c fich2.c
gcc -c fich3.c
Para generar el programa final habrı́a que ejecutar:
gcc -o prg fich1.o fich2.o fich3.o
Dicha orden lo que hace es enlazar el código objeto de cada fichero y las
librerı́as de ejecución necesarias para construir el archivo ejecutable, que se
llamará prg.
Si el programador modifica el fichero fich3.c, pero ese cambio no afecta
a los demás ficheros, para construir el programa podrá ejecutar la orden:
gcc -o prg fich1.o fich2.o fich3.c
Dicha orden compilará únicamente el código del fichero fich3.c y utilizará el código objeto de los otros dos ficheros para construir el programa
ejecutable final.
El comando make está pensado para facilitar el mantenimiento y regeneración de grupos de programas. Para su utilización, el programador debe
crear un fichero, que normalmente se llama makefile, en el que se definen las dependencias entre unos ficheros y otros. Las dependencias indican
qué ficheros se deben recompilar si cambia uno dado. Si, por ejemplo hay dos
ficheros independientes entre sı́, y se modifica uno de ellos, el otro no tiene
por qué recompilarse. Por ello, cada vez que se ejecuta el comando make
se examina el fichero makefile para determinar las dependencias entre los
ficheros que forman el programa. Con esta información, el comando make
recompilará aquellos ficheros fuente que han sido modificados con posterioridad a la generación de su código objeto y, a su vez, los que dependen de
éstos.
La utilización del comando make tiene, pues, dos consecuencias:
en cada momento, únicamente se recompila lo que es estrictamente
necesario y
se evita el error (muy frecuente) de olvidarse de recompilar algún fichero fuente que haya sido modificado o que dependa de alguno que
haya sido modificado, además de ejecutar la compilación en el orden
correcto.
Página 22 de 23
Prácticas PRG. Facultad de Informática
Curso 2002/2003
DSIC
PRÁCTICA 9. Estructuras de datos
Como ejemplo, supongamos que tenemos creado el fichero makefile,
cuyo contenido es:
prg:
modulo.o prg.o
gcc -o prg prg.o modulo.o
modulo.o: modulo.c modulo.h
gcc -c modulo.c
prg.o: prg.c modulo.h
gcc -c prg.c
Nótese que las lı́neas que indican las órdenes a ejecutar (en el ejemplo
anterior, las que empiezan por gcc) deben comenzar con un tabulador.
Cuando el usuario ejecuta el comando make, se genera el ejecutable prg,
que depende de los ficheros modulo.o y prg.o. Si existen y están actualizados
dichos ficheros objeto, entonces se ejecuta la orden:
gcc -o prg prg.o modulo.o
Si no existen los ficheros objeto, o no están actualizados, se compilarán
teniendo en cuenta el resto del fichero makefile. Por ejemplo, el fichero
modulo.o depende de sus ficheros fuente (modulo.c y modulo.h) y por lo
tanto, si se han modificado o no existe modulo.o, se ejecutará la orden correspondiente para generarlo. Si, como es normal, el programa principal incluye
el fichero de cabecera modulo.h para conocer el perfil de las funciones exportadas por modulo.c, entonces se debe hacer explı́cita esa dependencia, como
muestra el fichero makefile anterior. Por lo tanto, si se modifica el fichero
modulo.h, la próxima vez que se ejecute el comando make, se recompilarán
todos los ficheros.
Se puede definir una variable nom_var dentro de un fichero makefile
y después utilizar su contenido con $(nom_var). Por ejemplo, el fichero
anterior se podrı́a escribir:
nom = prg
$(nom): modulo.o $(nom).o
gcc -o $(nom) $(nom).o modulo.o
modulo.o: modulo.c modulo.h
gcc -c modulo.c
$(nom).o: $(nom).c modulo.h
gcc -c $(nom).c
Página 23 de 23
Descargar