Objetivos - Universidad Nacional de Colombia

Anuncio
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
MODULO 06
OBJETIVOS
Con el desarrollo de este módulo se pretende:
-
Introducir el uso de los punteros como una potente herramienta para el manejo de
estructuras dinámicas en memoria y hacer ver los peligros que puede traer un manejo
inadecuado de estos
-
Introducir conceptos afines como la Indirección y accesos complejos.
-
Definir e ilustrar el funcionamiento del Heap.
-
Profundizar en el concepto de recursión, dar elementos para comprender su
funcionamiento. Mostrar los casos en los que resulta ventajosa e ilustrar como hace el
sistema uso del stack para implementarla.
-
Presentar ejemplos que ayuden a la comprensión del tema, dando elementos al estudiante
para seguir algoritmos recursivos.
CONTENIDO
1
1.1
1.1.1
1.2
1.2.1
1.2.2
1.3
1.3.1
Punteros
Direcciones de variables
Operador de referencia
Uso de los punteros
Operador de Indirección
Paso de argumentos por referencia
Aritmética de punteros
Arreglos
2
2.1
2.2
Memoria dinámica
Heap
Uso de la memoria dinámica
3
Recursión
A
Referencias
B
Taller
02-2003
1
3004597 – Estructura de Datos – Modulo 06
1
Universidad Nacional de Colombia – Sede Medellín
PUNTEROS
Los punteros son variables muy especiales: toman como valores direcciones a otras variables. La
importancia de los punteros radica en su estrecha relación con el manejo de arreglos y
memoria dinámica, sin embargo su utilidad va mucho más allá, lo que los convierte en una de las
más potentes características de C/C++. Un uso adecuado de los punteros permite al programador
codificar programas extremadamente eficientes y potentes, sin embargo un uso equivocado de
estos conduce casi siempre a errores de manejo de memoria muy difíciles de encontrar y
que pueden traer consecuencias catastróficas.
1.1 DIRECCIONES DE VARIABLES
En el módulo anterior se estudió como la memoria está dividida en un conjunto de posiciones de
memoria, cada una de las cuales puede guardar un byte (8 bits). Cada variable que es declarada,
tiene asociado un espacio de memoria, es decir, un conjunto de posiciones de memoria en las
cuales se puede guardar un valor correspondiente al tipo de la variable. Cada espacio de
memoria tiene una dirección de memoria que permite hacer referencia a él. A la dirección de
la primera posición de memoria asociada a una variable se le llamará dirección de la variable.
Así, al usar el nombre de una variable, realmente nos estamos refiriendo al valor almacenado en
el espacio de memoria asociado a dicha variable.
1.1.1
Operador de referencia
C/C++ posee un mecanismo para obtener la dirección de una variable, conocido como el
operador de referencia. Para usarlo se debe seguir la sintaxis:
dest = &nombre_variable;
// dest es una variable que almacena
// direcciones, es decir, un puntero.
El operador de referencia devuelve la dirección de la variable que le precede, así que su resultado
debe ser asignado a alguna variable en la cual pueda guardarse una dirección de memoria (es
decir, a un puntero).
Figura 1.1: Direcciones de variables
Dirección
Nombre
de la
variable
[0001]
A
50
Espacio de memoria asociado
a la variable entera A. La
dirección de la variable A
inicia en 0001, el contenido de
la variable A es 50. ¡Cuidado!
no se debe confundir el valor
de una variable con su
dirección.
Ejemplo 1.1: valor y dirección
02-2003
2
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
El siguiente fragmento de código muestra la diferencia entre el valor y la dirección de una variable
usando el operador de referencia. Primero se crea la variable entera A, se le asigna un valor de 50
y se muestra en pantalla su dirección y a continuación su valor (50).
int A;
A=50;
cout<<"La dirección de A es: "<< &A<<" El valor de A es: "<<A;
//Esto imprimirá algo como:
// La dirección de A es: 0x0012FF7C El valor de A es: 50
1.2 USO DE LOS PUNTEROS
Un puntero es una variable en la cual se almacenan direcciones de memoria, es decir en un
puntero se pueden guardar direcciones de variables. Cuando a un puntero se le asigna la
dirección de una variable se dice que el puntero apunta a dicha variable.
Para declarar un puntero a una variable de un tipo dado se usa la siguiente sintaxis:
tipo_var
*nombre_puntero;
Que ordena al compilador: “declare una variable de tipo puntero llamada nombre_puntero que
guardará direcciones de variables de tipo tipo_var”.
Es muy importante tener en cuenta que al declarar un puntero este quedará apuntando a algún
lugar indeterminado de la memoria hasta que se le asigne la dirección de alguna variable válida del
programa, por tanto, nunca debe utilizarse un puntero antes de inicializarlo.
Figura 1.2: Punteros
int A=50;
int *P;
[0001]
P=&A;
A
50
[0002]
P
0001
Se declara un puntero (a entero) P, se le
asigna la dirección del entero A, por
tanto P queda apuntando a A.
1.2.1
Operador de indirección
02-2003
3
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
El operador de indirección permite acceder a la variable cuya dirección se encuentra guardada en
un puntero. Es decir, nos permite manipular la variable a la que apunta el puntero (ya sea para
acceder al valor almacenado en la variable o para modificarlo). El operador de Indirección usa
la siguiente sintaxis:
*nombre_puntero
Que puede usarse en cualquier lugar del código en el que sea pertinente usar un nombre de
variable.
Ejemplo 1.2: uso de punteros
En el siguiente código se crea la variable entera A con un valor inicial de 50, luego se declara el
puntero P y se le asigna la dirección de A (P queda apuntando a A). A continuación se usa el
operador de Indirección para acceder al valor de A y luego para cambiar el valor de A a 100.
Nótese que la expresión *P es completamente equivalente a usar el nombre de variable A.
int A=50;
int *P;
P=&A;
1.2.2
cout<< *P;
*P=100;
// Imprime 50
cout<< *P;
cout<< A;
// Imprime 100
//Imprime 100
Paso de argumentos por referencia
En el módulo anterior se estudió el paso de argumentos a funciones por valor y como devolver
resultados, allí se vio que las funciones operan sobre una copia local de los argumentos y en
consecuencia al terminar la función no se habrá modificado ninguna de las variables pasadas como
argumentos (a menos que se le haya asignado el valor de retorno de la función a alguna de ellas).
En algunas funciones se necesita retornar más de un valor; para hacer esto existe otro método de
paso de argumentos llamado paso de argumentos por referencia, con este método la función
recibe las direcciones de las variables sobre las cuales se va a operar.
Cuando una variable se pasa por referencia a una función, será modificada a medida que la
función opere sobre ella. Esto se debe a que la función utiliza la dirección de sus argumentos
para escribir y leer directamente sobre ellos en vez de crear copias locales con sus valores. El
siguiente ejemplo ilustra este método.
Ejemplo 1.3: paso de argumentos por referencia
La función definida a continuación recibe la dirección de dos enteros y recibe un entero por valor,
los dos primeros enteros se suman y el resultado se asigna a cada uno de ellos y al tercer entero,
02-2003
4
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
que posteriormente es retornado. Al declarar la función, deben indicarse que argumentos se
pasarán por referencia, esto se hace escribiendo * justo antes del nombre del argumento y
después de su tipo.
Ya que la función recibió las direcciones de los dos primeros argumentos (a y b), es necesario usar
el operador de Indirección (*) dentro de la función para acceder a su contenido.
int sumap(int *a, int *b, int c)
{
c = *a + *b;
*a = c;
*b = c;
/*la función recibe 2 direcciones de
enteros y el valor de un entero*/
/*Dentro de la función, a y b son dos
variables locales de tipo puntero a
int*/
return c;
}
Ahora, veamos como se invoca la función y como se le asignan los argumentos. Al invocar la
función sumap deben entregársele como parámetros dos direcciones a variables entero y un
entero. Para entregar las direcciones de las variables a y b se usa el operador de referencia &, c
se pasa por valor.
void main()
{
int a=3;
int b=2;
int c=0;
int r=0;
r=sumap( &a, &b, c );
cout<<”Valores
cout<<”a=”<<a;
cout<<”b=”<<b;
cout<<”c=”<<c;
cout<<”r=”<<r;
finales: \n“;
// Imprime 5
// Imprime 5
// Imprime 0
// Imprime 5
}
Al final, la función imprimirá a = 5, b = 5, c = 0, r = 5. Esto indica que a y b fueron modificados
directamente por la función (ya que se pasaron por referencia), mientras que c no fue
modificado (ya que se pasó por valor), finalmente r fue modificado al asignársele el valor retornado
por la función.
1.3 ARITMÉTICA DE PUNTEROS
En C/C++ pueden llevarse a cabo dos operaciones aritméticas sobre un puntero: adición y
sustracción (sólo de números enteros), estas operaciones se utilizan para cambiar el lugar de la
memoria al que apunta el puntero. Cada vez que se le suma/resta un número n a un puntero la
dirección contenida en este se aumenta/disminuye en n*(tamaño) donde tamaño es el número de
02-2003
5
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
bytes que ocupa una variable del tipo asociado al puntero, esto significa que la aritmética de
punteros tiene en cuenta el tamaño de la variable apuntada.
En C/C++ puede obtenerse el tamaño en bytes de las variables de un tipo dado usando la función
sizeof(tipo) que recibe como argumento el nombre del tipo de dato y devuelve un entero (el
tamaño en bytes que ocupan las variables de dicho tipo).
Las operaciones sobre punteros deben usarse con mucha precaución, ya que al cambiar
indiscriminadamente los lugares de la memoria a los que se apunta se pueden sobrescribir
accidentalmente otros datos importantes del programa ocasionando errores al momento de la
ejecución.
Ejemplo 1.4: aritmética de punteros
Supongamos que se crea una variable entera a cuya dirección es 0001 y un puntero a entero
llamado p. Si se asigna a p la dirección de a, entonces p contendrá la dirección 0001 (apunta a a).
Ahora, si se suma 1 a p, la dirección que contenía se aumentará en 2, (debido a que el tamaño en
bytes de un entero es 2) es decir, apuntará a la posición de memoria 0003. Esto es peligroso, ya
que p quedó apuntando a un lugar de memoria cuyo contenido es desconocido.
void main()
{
int a=654;
int *p;
/*Luego de sumar 1 a p, este apuntará
dos posiciones de memoria adelante de la
dirección de a*/
p=&a;
p++;
cout<< *p << "y" << *(p-1); // Imprimió: 1245120 y 654
}
Figura 1.3: aritmética de punteros
a
Dirección de a (a
ocupa 2 bytes)
p
[0001] [0002]
[0003]
[0004]
654
?
?
[0045]
...
?
Antes de ejecutar p++
Después de ejecutar p++
1.2.3
Arreglos
Como se vio en módulos anteriores, los arreglos son un conjunto de espacios de memoria de igual
tamaño ubicados uno al lado del otro, al crear un arreglo de n posiciones en realidad se están
declarando n variables del tipo dado localizadas en posiciones de memoria contiguas. ¿Pero cómo
02-2003
6
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
se accede a esas variables? en C/C++ esto se hace usando un puntero al primer elemento del
arreglo y el operador [n].
El identificador (o nombre) de un arreglo siempre se refiere a la dirección del primer elemento, el
operador [n] precedido del nombre de un arreglo es equivalente a sumar n al puntero a la primera
posición del arreglo.
Supongamos que hemos creado un arreglo de 10 enteros llamado A con la instrucción int A[10],
siempre que se utilice el identificador A, se estará haciendo referencia a la dirección del primer
elemento del arreglo. Para acceder al elemento 5 del arreglo se escribe A[5] que significa
exactamente lo mismo que *(A+5).
Para crear un puntero a un arreglo, simplemente se declara una variable puntero al tipo de
los elementos del arreglo y se le asigna la dirección al primer elemento (es decir, el nombre del
arreglo). Este puntero puede ser acompañado de + i para acceder al elemento i del arreglo (ver
figura 1.4)
Siempre se debe tener precaución de no sobrepasar los límites del arreglo, ya que al hacerlo
pueden modificarse datos del programa alterando su funcionamiento, C/C++ no reportan este
tipo de errores al compilar y por lo general son muy difíciles de encontrar una vez se han
cometido.
Figura 1.4: arreglos y punteros
0
A
Dirección
de p1
p1
1
2
3
[0001]
[0003]
[0005]
[0007]
50
50
50
50
[0023]
0001
[0345]
p2
0003
int *p1;
int *p2;
int *p3;
p1=A;
p2=&p1[1];
p3=p1+3;
//Dir. ele 1
//Dir. ele 2
//Dir. ele 4
[0090]
p3
0007
Con respecto a la figura 1.4, nótese que las siguientes expresiones son equivalentes:
(p1 +3) , (A + 3) y (&p1[3]); todas ellas proporcionan la dirección del cuarto elemento del
arreglo.
Ejemplo 1.5: la verdad acerca de [n]
02-2003
7
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
En el siguiente código se recorre un arreglo sin usar el operador [n], para acceder a los elementos
del arreglo se usa aritmética de punteros, que es la manera como se implementa este operador.
void main()
{
int A[]={2, 5, 87, 21, 34}; //No es necesario especificar el
//tamaño del arreglo
int *p;
p=A;
for(int i=0; i<5; i++)
{
cout<<"Elemento "<<i<<"="<<*p<<"\n";
p++;
}
}
2
MEMORIA DINÁMICA
Hasta ahora todos los programas que se han desarrollado tienen un conjunto de variables conocido
que se declaran al escribir el programa. Pero existen programas en los que no es posible conocer
de antemano (antes de la ejecución) qué cantidad de memoria será necesaria, incluso puede ser
necesario crear y destruir variables en plena ejecución, esto es muy común en los programas de
estructuras de datos que necesitan insertar y eliminar elementos continuamente. La memoria
dinámica permite a los programas pedir y liberar espacio de memoria durante su ejecución a
medida que lo necesiten.
2.1 HEAP
Al igual que el stack y el segmento de datos, el Heap o montículo es una zona de memoria con una
función muy especial. También se conoce como pila de variables dinámicas, ya que se encarga
de almacenar las variables dinámicas (espacios de memoria creados durante la ejecución de un
programa).
Es importante comprender que las variables asignadas dinámicamente (que residen en el Heap)
no están sujetas a las reglas de duración vistas en el módulo anterior. Estas variables nunca
salen de ámbito, por lo que una vez que se asigna memoria sobre el Heap se es responsable de
liberarla cuando ya no se necesite.
2.2 USO DE LA MEMORIA DINÁMICA
En C, la gestión de memoria dinámica se hace mediante las funciones malloc() y free(), que crean
y liberan espacio de memoria en el Heap. Estas dos funciones se encuentran en la librería stdlib.h,
por tanto, todo programa que las use debe tener esta librería en uno de sus includes.
02-2003
8
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
La función malloc() reserva un espacio de memoria de tamaño dado en el Heap y retorna un
puntero a dicho espacio, si no había memoria suficiente, retorna NULL; malloc() utiliza la
siguiente sintaxis:
puntero = ( tipo_de_variable * ) malloc ( numero_de_bytes );
malloc() no es consciente del tipo de variable que se almacenará en el espacio que reservó,
retorna una dirección de memoria sin tipo. La expresión ( tipo_de_variable * ) se conoce como
typecast y se utiliza para asignarle tipo a la dirección retornada por malloc(), así, a puntero se le
asignará la dirección de una variable de tipo tipo_de_variable (este tipo debe coincidir con el
tipo del puntero).
Ejemplo 1.6: creación de un arreglo usando malloc()
La función crear_arreglo reserva espacio en memoria para n variables enteras, es decir, crea un
arreglo de n enteros, inicializa en 1 todas sus posiciones y retorna su dirección.
Para indicarle a malloc() el número de bytes que debe reservar, se usa sizeof(int) que retorna el
tamaño en bytes de una variable entera; como se quiere crear un arreglo de n enteros, debemos
indicar a malloc() que reserve n*sizeof(int) bytes.
int *crear_arreglo(int n)
{
int *p;
p=(int *)malloc( n*sizeof(int) );
for(int i=0; i<n; i++){
p[i]=1;
}
return p;
}
Figura 2.1: Mapa del Heap ejemplo 1.6
Heap
void main(void)
{ int *a;
a=crear_arreglo(4);
}
[0089]
a
02-2003
0001
Dirección
de a
[0001]
[0003]
[0005]
[0007]
0
0
0
0
9
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
Si no se libera el espacio reservado que ya no es necesario, se corre el riesgo de agotar la zona de
memoria heap. La función free() libera un espacio de memoria reservado mediante malloc(),
no retorna nada. Utiliza la siguiente sintaxis:
free( direccion );
direccion, el único argumento de free(), es precisamente la dirección de memoria del espacio que
se quiere liberar. Esta dirección debe ser alguna retornada anteriormente en un llamado a
malloc().
C++ incluye la función new() que reemplaza a malloc() y es más fácil de usar. Al igual que
malloc(), new() reserva un espacio de memoria determinado en el Heap:
puntero= new tipo_de_dato [n];
new() retorna la dirección del espacio de memoria reservado, nótese que ya no es necesario
usar el typecast, new() lo ejecuta automáticamente. tipo_de_dato es el tipo de las variables que
se desean almacenar en el espacio reservado y [n] indica que se reserven n de estas variables.
Ejemplo 1.7: creación de un arreglo usando new()
La función crear_arreglo_new() hace exactamente lo mismo que la función crear_arreglo() del
ejemplo 1.6, pero usa new() en vez de malloc().
int *crear_arreglo_new(int n)
{
int *p;
p= new int[n];
for(int i=0; i<n; i++)
p[i]=1;
return p;
}
3 RECURSIÓN
Un algoritmo es recursivo si se encuentra definido en términos de sí mismo. Es posible que una
función se llame a sí misma, basta para ello con invocar la función y pasarle los argumentos
correspondientes. A este tipo de función la denominamos “recursiva”.
Ejemplo 3.1: Función recursiva
La función factorial es uno de los ejemplos clásicos de función recursiva, ya que el factorial de un
número puede definirse como el producto entre el número y el factorial del número anterior:
02-2003
10
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
N!=N*(N-1)!
La recursión resulta muy útil en ciertos problemas, en especial estructuras de datos recursivas
como los árboles, de hecho, la recursión simplifica enormemente algunos algoritmos que por
naturaleza son recursivos y es esta su mayor virtud. La desventaja de la recursión es la gran
cantidad de espacio que se ocupa en memoria debido al llamado continuo de funciones que, como
se vio, se acumulan en el stack.
En una función recursiva se deben considerar dos casos. En el caso trivial se da la respuesta sin
necesidad de hacer más llamadas recursivas, a este caso se le denomina salida de la recursión y
es necesario que exista por lo menos uno de estos casos dentro de toda función recursiva
(de no existir el programa podría no terminar)
En el segundo caso, para poder dar una respuesta, es necesario esperar hasta obtener el
resultado de una o varias llamadas recursivas. A este caso se le conoce como avance de la
recursión y su presencia también es propia de toda función recursiva.
Ejemplo 3.2: función factorial recursiva
En este ejemplo se presenta la implementación recursiva de la función factorial de un número
entero n.
int factorial(int n)
{
Caso de
salida de la recursión
if(n==0)
return 1;
Avance de recursión
else
return (n*factorial(n-1));
}
Figura 3.1: función factorial recursiva
void main(void)
{ int a;
a=factorial(3);
}
a
6
Factorial(3)
2
Factorial(2)
1
Valor retornado
Factorial(1)
Llamadas
recursivas
a la
función
factorial
1
02-2003
Factorial(0)
11
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
Para seguir la ejecución de una función recursiva, resulta interesante diagramar un mapa del stack
a medida que la función se va llamando a sí misma. Como se vio en el módulo anterior, cada vez
que se invoca una función se apila una entrada en el stack que contiene copias de los
argumentos y las variables locales de la función. Las funciones recursivas crean una serie de
entradas en el stack correspondientes a cada invocación que hacen de sí mismas, cuando se llega
a la función que ejecuta la salida de recursión comienzan a retornar y desapilarse una tras otra
las funciones invocadas recursivamente en secuencia. Este proceso se muestra en el ejemplo 1.8.
Ejemplo 1.8: mapa de memoria para la función factorial
A continuación se muestra paso a paso el mapa del stack para el programa listado, que invoca la
función factorial(3).
void main()
{
int a;
a=factorial(3);
}
-
Paso 1: Función main()
El programa inicia en la función main(), para la cual se crea una entrada en el stack, dentro de esta
función, se declara una variable entera llamada a.
-
Paso 2: Primer llamado a factorial()
Dentro de la función main() se invoca a factorial(3), por tanto se crea una entrada en el stack para
el primer llamado a la función factorial con su respectivo argumento, llamado n.
02-2003
12
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
Figura 3.2: pasos 1 y 2
Paso 1
S
T
A
C
K
-
Paso 2
S
T
A
C
K
factorial(3)
n: 3
main()
main()
a: ¿
a: ¿
Paso 3: Llamada recursiva a factorial()
Dentro de factorial(3) se invoca recursivamente a la función factorial(), esta vez con argumento n-1,
cuyo valor en este paso es 2. Se apila una nueva entrada en el stack para esta función.
Nótese que esta segunda función factorial() que se apila tiene su propio espacio en la pila al igual
que las variables n's son independientes la una de la otra.
-
Paso 4: Segunda llamada recursiva a factorial()
Similar al paso anterior, pero esta vez el argumento dado a factorial(), n-1, vale 1.
02-2003
13
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
Figura 3.3: pasos 3 y 4
Paso 3
Paso 4
factorial(1)
n: 1
S
T
A
C
K
-
factorial(2)
n: 2
S
T
A
C
K
factorial(2)
n: 2
factorial(3)
factorial(3)
n: 3
n: 3
main()
main()
a: ¿
a: ¿
Paso 5: Última llamada recursiva a factorial()
Dentro de factorial(1) se llama de nuevo a factorial() recursivamente, se le pasa como argumento
n-1 que en este paso vale 0.
Figura 3.4: paso 5
factorial(0)
n: 0
S
T
A
C
K
Como se cumple la condición de
salida de recursión (n=0),
factorial(0) retorna 1 a la función
que la invocó (factorial(1)).
factorial(1)
n: 1
factorial(2)
n: 2
factorial(3)
n: 3
main()
a: ¿
02-2003
14
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
Paso 6: salida de recursión
-
En la función factorial(0), se cumple la condición de la salida de recursión: n=0, por tanto,
factorial(0) retorna 1 a la función factorial(1).
Figura 3.5: paso 6
factorial(1)
n: 1
S
T
A
C
K
factorial(2)
n: 2
La función factorial(1) recibe el
valor retornado por factorial(0) y lo
multiplica por su valor de n, es
decir 1*1, posteriormente retorna
este producto. El proceso se
repite para cada llamado de la
función factorial hasta llegar a
factorial(3), que retorna el
resultado final de la operación a la
variable a de la función main().
factorial(3)
n: 3
main()
a: ¿
-
Paso 7: retorno de todas las funciones factorial
A su vez, factorial(1) retorna el valor que le fue entregado por factorial(0) multiplicado por el valor
de su variable n (que en este caso es 1). Este proceso se repite para cada uno de los llamados
hechos a factorial, hasta que por último, factorial(3) retorna el resultado final del proceso recursivo
a la variable a de la función main().
02-2003
15
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
Figura 3.6: paso 7
S
T
A
C
K
main()
a: 6
02-2003
La variable a contiene el
valor de 3!, es decir, 6.
Las demás funciones
factorial() han sido
desapiladas
16
3004597 – Estructura de Datos – Modulo 06
Universidad Nacional de Colombia – Sede Medellín
ANEXOS
A
REFERENCIAS
Villalobos Jorge, Quintero Alejandro, Otero Mario. Estructuras de datos. Ediciones Uniandes.
Stubbs & Webre. Data structures.
B
TALLER
(Basado en Villalobos, Quintero, Otero. Estructuras de datos, Capítulo 0)
1. Escriba una función que ordene los elementos de un arreglo de menor a mayor, haga que
la función reciba como argumento un puntero con la dirección del primer elemento del
arreglo y el tamaño de este. Se prohíbe el uso del operador [n].
2. Cree una clase sencilla, y luego declare una función que reserve memoria para un arreglo
de n elementos de dicha clase, la función debe retornar la dirección del espacio de
memoria reservado. Use malloc().
3. Cree una función no recursiva que calcule el factorial de un número. Compárela con la
función factorial presentada en la sección 3. ¿Cuál considera mejor?. ¿Cuál es más fácil de
escribir?. ¿Cuál ocupa menos espacio en memoria durante su ejecución?.
4. Desarrolle un procedimiento recursivo que imprima. En orden inverso, una cadena de
caracteres sin alterarla.
C CREDITOS
Editor: PhD. Fernando Arango
Colaboradores: Ing. Edwin Hincapié, Francisco Moreno MS.C, Santiago Londoño, Alberto
Jiménez, Juan Carlos Hernández, Carlos Andrés Giraldo, Diego Figueroa.
02-2003
17
Descargar