0% encontró este documento útil (0 votos)
81 vistas60 páginas

Libro Talleres de Programacion II

El documento habla sobre el uso de funciones en C para resolver problemas de manera modular. Explica que las funciones permiten dividir un problema principal en subproblemas más pequeños. Describe tres tipos de variables en C - locales, globales y estáticas - y su alcance. También explica cómo definir funciones propias indicando el tipo de dato, nombre, parámetros y cuerpo de la función.

Cargado por

Danilo Paguay
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
81 vistas60 páginas

Libro Talleres de Programacion II

El documento habla sobre el uso de funciones en C para resolver problemas de manera modular. Explica que las funciones permiten dividir un problema principal en subproblemas más pequeños. Describe tres tipos de variables en C - locales, globales y estáticas - y su alcance. También explica cómo definir funciones propias indicando el tipo de dato, nombre, parámetros y cuerpo de la función.

Cargado por

Danilo Paguay
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 60

1

TALLER DE PROGRAMACIÓN III


I NGENIERÍA DE SISTEMAS
UNIVERSIDAD DE N ARIÑO

FUNCIONES
Para resolver problemas complejos y/o de gran tamaño es conveniente utilizar el concepto de
reducción de problemas. De esta forma, el problema se descompone en subproblemas, los cuales a
su vez pueden descomponerse en subproblemas, y así continuar hasta que el problema original queda
reducido a un conjunto de actividades básicas, que no se pueden o no conviene volver a
descomponer.

En el lenguaje de programación C la solución de un problema se expresa por medio de un programa,


la solución de un subproblema, por medio de una función. El uso de funciones tiene múltiples ventajas:
facilitan la lectura y escritura de los programas, permiten el trabajo en paralelo, permiten que el código
de la función se escriba solamente una vez y se utilice tantas veces como sea necesario, facilitando
el mantenimiento de los programas.

De esta manera, un programa en C está constituido por un programa principal y un conjunto de


funciones. El programa principal constará de pocas líneas, las cuales pueden ser llamadas a funciones.

VARIABLES LOCALES, GLOBALES Y ESTÁTICAS


Las variables son objetos que pueden cambiar su valor durante la ejecución de un programa. En el
lenguaje de programación C se puede distinguir entre tres tipos de variables: locales, globales y
estáticas.

Las variables locales son objetos definidos tanto en el programa principal como en las funciones y su
alcance está limitado solamente al lugar donde están definidas.

Las variables globales son objetos definidos antes del inicio del programa principal y su alcance es muy
amplio, ya que tiene influencia tanto en el programa principal como en todas las funciones.

Las variables estáticas son similares a las locales, pero conservan su valor durante la ejecución del
programa.

Crear funciones: Para crear funciones propias, éstas deben definirse en el código. Los pasos para definir
una función son:

• Crear una línea en la que se indica el


nombre de la función, el tipo de datos
que devuelve dicha función y los
parámetros que acepta.

• Los parámetros son los datos que


necesita la función para trabajar Indicar
las variables locales a la función

• Indicar las instrucciones de la función

• Si es preciso, indicar el valor que devuelve


2

tipo nombreDeLaFunción(parámetros){
definiciones
instrucciones
}
La primera línea de la sintaxis anterior es lo que se llama el prototipo o signatura de la función. Es la
definición formal de la función.

• tipo. Sirve para elegir el tipo de datos que devuelve la función. Toda función puede obtener un
resultado. Eso se realiza mediante la instrucción return. El tipo puede ser: int, char, long, float,
double,.... y también void. Éste último se utiliza si la función no devuelve ningún valor (a estas
funciones se las suele llamar procedimientos).

• nombreDeLafunción. El identificador de la función. Debe cumplir las reglas ya comentadas en


temas anteriores correspondientes al nombre de los identificadores.

• parámetros. Su uso es opcional, hay funciones sin parámetros. Los parámetros son una serie de
valores que la función puede requerir para poder ejecutar su trabajo. En realidad es una lista de
variables y los tipos de las mismas. Son variables cuya existencia está ligada a la función
3

Funciones sin paso de parámetros: Son aquellas que no reciben parámetros o valores, ya que éstos se
solicitan dentro de la función, luego se realizan las instrucciones (cálculos u operaciones) y
normalmente se imprime el resultado.

Ejemplo 1:
#include <iostream>
#include <stdlib.h>
#include <conio.h>

using namespace std;

void funcion_x( ){
cout <<"Programacion Modular\n\n";
cout <<"Las funciones son importantes\n";
}
int main(){
funcion_x( );
cout <<"y nos ayudan a programar con facilidad\n";
system("PAUSE");
return 0;
}

Ejemplo 2: Haga el programa principal y dos funciones sin paso de parámetros: a) sumar dos números
enteros y b) multiplicar dos números enteros.

using namespace std;


int n1,n2,res;
void sumar(){
res=n1+n2;
cout<<n1<<"+"<<n2<<"="<<res<<endl;
}
void producto(){
res=n1*n2;
cout<<n1<<"*"<<n2<<"="<<res<<endl;
}
int main(){
cout<<"Valor del primer numero "; cin>>n1;
cout<<"Valor del segundo numero "; cin>>n2;
sumar();
producto();
system("PAUSE");
return 0;
}
4

Funciones con paso de parámetros: Estas funciones son las más utilizadas en la programación ya que
pueden recibir uno o más valores llamados parámetros y regresan un solo valor de tipo entero, real o
carácter. Los parámetros o valores son enviados del programa principal o de otra función. Dentro de
la función se realizan solamente las instrucciones (cálculos u operaciones). Es importante revisar que el
tipo de dato que regresará la función sea del mismo tipo que el valor declarado en el encabezado
de la misma.

Parámetros de una función: También son llamados argumentos y se corresponden con una serie de
valores que se especifican en la llamada a la función, o en la declaración de la misma, de los que
depende el resultado de la función; dichos valores nos permiten la comunicación entre dos funciones

Ejemplo 3: Crear un programa para calcular el precio de un producto basándose en el precio base
del mismo y el impuesto aplicable

//declaración
float precio(float base, float impuesto);
int main(){
float importe = 2.5;
float tasa = 0.07;
float valor= precio(importe, tasa)
cout<<"El precio a pagar es: $\n"<<valor<<endl;
system("PAUSE");
return 0;
}

//definición
float precio(float base, float impuesto) {
float calculo;
calculo = base + (base * impuesto);
return calculo;
}

El ejemplo anterior se compone de dos funciones, la función requerida main y la función creada por
el usuario precio, que calcula el precio de un producto tomando como parámetros su precio base y
el impuesto aplicable. La función precio calcula el precio de un producto sumándole el impuesto
correspondiente al precio base y devuelve el valor calculado mediante la sentencia return.

Una función en C sólo puede devolver un valor. Para devolver dicho valor, se utiliza la palabra
reservada return cuya sintaxis es la siguiente:

return <expresión>;
Donde <expresión> puede ser cualquier tipo de dato salvo un array o una función. Además, el valor
de la expresión debe coincidir con el tipo de dato declarado en el prototipo de la función.
5

Ejemplo 4: Realizar un programa calcula el cubo de los números del 1 al 5 utilizando una función
definida por el usuario.

int cubo(int base);


int main(){
int i;
for(i=1; i<=5; i++){
cout<<"El cubo del número "<< i <<" es "<< cubo(i) <<endl;
}
system("PAUSE");
return 0;
}
int cubo(int base){
int potencia;
potencia = pow(base,3);// * base * base;
return potencia;
}

Paso de Parámetros

Las funciones pueden recibir datos como lo hemos observado, pero existen dos formas de enviar los
datos hacia una función por valor y por referencia, las cuales modifican en diferente forma el
comportamiento del programa.

Por Valor: El paso por valor envía una copia de los parámetros a la función por lo tanto los cambios
que se hagan en ella no son tomados en cuenta dentro de la función main()

void sumar_valor(int numero); /* prototipo de la función */


int main(void){
int numero = 57; /* definimos numero con valor de 57*/
sumar_valor(numero); /* enviamos numero a la función */
cout<<"Valor de numero dentro de main() es " <<numero <<endl;
/* podemos notar que el valor de numero se modifica
* sólo dentro de la función sumar_valor pero en la principal
* número sigue valiendo 57 */
system("PAUSE");
return 0;
}

void sumar_valor(int numero){


numero++; /* le sumamos 1 al numero */
/* el valor de número recibido se aumenta en 1
* y se modifica dentro de la función sumar_valor() */
cout<<"Valor de numero dentro sumar_valor() es: "<< numero <<endl;
}
6

Ejemplo 5: Función para calcular el factorial de un número

float factorial(int num){


int i;
float f=1.0;
for(i=num; i>1; i--)
f = f*i;
return (f);
}
La función factorial recibe como parámetro un valor de tipo entero que almacena en el parámetro
formal num y retorna un valor de tipo float correspondiente al factorial de num. El cuerpo de la función
está formado por la sentencia for, que calcula el factorial de num y lo almacena en la variable local
f. La sentencia return retorna el valor de f. Las variables locales i y f de la función factorial, al igual que
el parámetro formal num, se mantienen vivos mientras se ejecuta la función. Una vez finalizada la
ejecución de la función, estas variables y el parámetro desaparecen, y ya no se pueden manipular.

Por Referencia: El paso por referencia se hace utilizando apuntadores. Se envía la dirección de
memoria de la variable, por lo tanto los cambios que haga la función si afectan el valor de la variable.
Ejemplo:

void sumar_referencia(int *numero); /* prototipo de la función */

int main(void){
int numero = 57; /* definimos numero con valor de 57*/
sumar_referencia(&numero); /* enviamos numero a la función */
cout<<"\nValor de numero dentro de main() es: " << numero <<endl;
/* podemos notar que el valor de numero se modifica
* y que ahora dentro de main() también se ha modificado
* aunque la función no haya retornado ningún valor. */
system("PAUSE");
return 0;
}

void sumar_referencia(int *numero){


*numero += 1; /* le sumamos 1 al numero */
/* el valor de numero recibido se aumenta en 1
* y se modifica dentro de la función */
cout<<"\nValor de numero dentro sumar_referencia() es: " << *numero;
}
7

Prototipo de una función

En la estructura de un programa, las definiciones de funciones de usuario pueden ir antes o después


del main. En nuestro caso se escribirán siempre después del main y se utilizarán prototipos de funciones
para informar al compilador de la existencia de éstas.

En concreto, los prototipos de las funciones sirven para indicar al compilador la cantidad de
parámetros formales, el tipo de dato de cada parámetro formal y el tipo del resultado de la función.
De esta forma, el compilador puede comprobar si la función es llamada correctamente dentro del
programa, sin conocer la definición completa de esta. El prototipo de una función contiene
únicamente la cabecera de la función y el carácter punto y coma. En la estructura del programa, los
prototipos de las funciones se colocan justo antes del main y después de la definición de los tipos de
datos

La sintaxis general del prototipo de una función es la siguiente:

tipo_result nombre_función(tipo1 pformal1,…, tipon pformaln);

donde el tipo_result, el nombre_función y la lista de parámetros (tipo1 pformal1, ..., tipon pformaln) son
los mismos que los indicados en la definición de la función.

Ejemplos:

A continuación, se muestran algunos ejemplos de prototipos de funciones y, para cada uno de ellos,
se describe brevemente lo que realiza la función.

void mostrar_tablas();

Esta función muestra en pantalla las tablas de multiplicar desde la tabla del 2 hasta la tabla del 10. La
función mostrar_tablas no tiene parámetros formales y no devuelve ningún resultado.

float factorial(int num);

Esta función recibe como parámetro un valor de tipo entero, que almacena en el parámetro formal
num, y retorna el valor real correspondiente al factorial de num.

char mayuscula(char c);

Esta función recibe como parámetro un carácter, que almacena en el parámetro formal c, y retorna
el carácter correspondiente al carácter almacenado en c en letra mayúscula.

double vol_cili (float r, float h);

Esta función recibe dos valores reales, que almacena en los parámetros formales r y h, que representan,
respectivamente, el radio (r) y la altura (h) de un cilindro. La función vol_cili retorna el valor real
correspondiente al volumen del cilindro para los parámetros recibidos.
8

La siguiente tabla muestra el prototipo y la llamada de cada una de las funciones anteriores, e indica
en cada caso el tipo de función que representa: función con parámetros, función sin parámetros,
función que no retorna valor o función que retorna valor.

Tipo de Función Prototipo de la función Llamada a la función

No retorna valor y void mostrar_tablas(); int main(){


no tiene mostrar_tablas();
parámetros return 0;
}
Retorna valor y float factorial(int num); int main(){
tiene parámetros int n=5;
float fac;
f ac= factorial(n);
return 0;
}
Retorna valor char mayuscula(char c); int main() {
char may,min='e';
y tiene parámetro may = mayuscula(min);
return 0;
}
Retorna valor double vol_cili(float r, int main(){
float h); float r=3.4, h=5.3;
y tiene parámetros
double vol;
vol = vol_cili(r,h);
return 0;
}

Obsérvese que en la tabla en la llamada a la función (v. tercera columna):

− En las funciones que retornan un resultado, este se asigna a una variable del mismo tipo que el que
retorna la función.

− Las variables que aparecen en las expresiones de los parámetros reales han de estar inicializadas
antes de realizar la llamada a la función.
9

Ejercicios

1. Realizar una función que reciba dos parámetros de tipo entero y retorne el mayor de ellos

2. Realizar una función que retorne el valor absoluto de un número enviado como parámetro

3. Desarrollar una función que determine si el número enviado es par o impar. Debe retornar 1 si es
par, 0 si es impar.

4. Desarrollar una función que reciba 3 valores como parámetros y retorne el promedio de los
números enviados.

5. Escribir una función llamada "cantidad" que reciba como argumento un número entero y un string
y que luego diga si el string tiene la misma cantidad de letras que el número entero ingresado.
Escribir un programa que pruebe la función.

6. Desarrollar un programa en c que permita obtener el perímetro y área de una figura de 4 lados y
diga si es un cuadrado o un rectángulo. Esto mediante funciones.

7. Escribir una función llamada "edades" que reciba 2 edades y decir cuál de ellas es la mayor y
cuanto le falta a la menor para llegar a la edad mayor.
10

La sentencia return

Una función finaliza su ejecución cuando termina su secuencia de sentencias o cuando ejecuta un
sentencia return. Esta sentencia también es la encargada de devolver el valor de retorno de la función.

La sintaxis de la sentencia return es la siguiente: return expresión;

La expresión debe ser un valor del mismo tipo de datos que devuelve la función. La sentencia return
puede estar situada en cualquier lugar de la secuencia de instrucciones de la función. Cuando se
ejecuta dicha sentencia ocurren dos cosas:

1. La función finaliza su ejecución. El control del programa regresa a la función que haya
realizado la llamada a esa función. Si la función donde se encuentra la sentencia return es la
función main, finalizará la ejecución del programa.
2. Se devuelve el valor de la expresión a la función que realizó la llamada.

Ejemplo 6

Se necesita realizar un programa que simplifique un fraccionario, dividiendo numerador y


denominador por su máximo común divisor, también debe determinar si la fracción es propia (en este
caso de debe representar gráficamente) o impropia (se debe mostrar la conversión de la fracción a
número mixto).

int mcd(int n,int d);


bool propia(int n, int d);
void mixta(int n, int d);
void grafico(int n, int d);
int main(int argc, char *argv[])
{
int num,den,res;
gotoxy(3,3);cout<<"Simplificacion de fracciones";
gotoxy(3,5);cout<<"Ingrese el numerador ==> "; cin>>num;
gotoxy(3,6);cout<<"Ingrese el denominador ==> "; cin>>den;
res=mcd(num,den);
gotoxy(3,7);cout<<num<<"/"<<den<<"="<<num/res<<"/"<<den/res;
gotoxy(3,8);
if(propia(num,den)){
cout<<"la fraccion es impropia";
mixta(num,den);
}
else{
cout <<"la fraccion es propia";
grafico(num,den);
}
gotoxy(3,24);system("PAUSE");
return 0;
}
11

int mcd(int n, int d){


int r;
r=n % d;
while(r!=0){
n=d;
d=r;
r=n % d;
}
return d;
}
bool propia(int n, int d){
return n>d;
}
void mixta(int n, int d){

void grafico(int n, int d){

}
Ejemplo 7

Crear un programa que permita realizar los siguientes cálculos matemáticos

1. Múltiplos
2. Sumatoria
3. Factorial
4. Primo
5. Terminar

El programa debe tener una función llamada menú que retorne la opción elegida por el usuario

En la opción 1 se debe solicitar al usuario 2 números enteros y se debe determinar si el segundo es


múltiplo del primero. La función retornará 0 si se cumple la condición, 1 en otro caso

La opción 2 retorna la sumatoria de los números desde 1 hasta N. El valor de N debe ser ingresado por
el usuario

La opción 3 retorna el valor del factorial de un número N ingresado por teclado

La opción 4 permite determinar si un número es primo. La función debe retornar 0 si el número a


comprobar es primo, 1 en otro caso.

No está permitido declarar variables globales. Todos los valores ingresados por el usuario deben ser de
tipo entero mayores o iguales a 1.
12

R ECURSIÓN
Al escribir una función para realizar una tarea, una técnica básica de diseño consiste en dividir la tarea
en subtareas. En ocasiones, al menos una de las subtareas es un ejemplo más pequeño de la misma
tarea. Por ejemplo, si la tarea consiste en buscar cierto valor en un arreglo, podríamos dividirla en la
subtarea de examinar la primera mitad del arreglo y la subtarea de examinar la segunda mitad del
arreglo. Las subtareas de examinar las mitades del arreglo son versiones “más pequeñas” de la tarea
original. Siempre que una subtarea es una versión más pequeña de la tarea original a realizar, se puede
resolver la tarea original usando una función recursiva. Se requiere algo de práctica para
descomponer fácilmente los problemas de esta manera, pero una vez que aprende la técnica puede
ser una de las formas más fáciles de diseñar un algoritmo, y en última instancia una función C.

En C una definición de función puede contener una llamada a la función que se está definiendo. En
tales casos se dice que la función es recursiva.

Ejemplo 8

Diseñar una función recursiva que escriba números en la pantalla colocando los dígitos verticalmente,
de modo que 1984, por ejemplo se vea así:

1
9
8
4

La función que se va a desarrollar tiene la siguiente signatura:

void escribir_vertical(int n)

Para desarrollar esta función se debe tener en cuenta el caso más sencillo que se presentaría cuando
el número sólo tiene un dígito, en este caso, simplemente se escribe el número. A pesar de ser tan
sencillo, este caso es importante, y no debemos olvidarlo:

Caso sencillo: Si n<10, escribe el número n en pantalla

Consideremos ahora el caso más representativo en el que el número a escribir consiste en dos o
más dígitos. Supongamos que queremos escribir el número 1234 verticalmente, de modo que el
resultado sea:

Una forma de descomponer esta tarea en dos subtareas es la siguiente:

1. Despliega todos los dígitos menos el último así:

2. Despliega el último dígito, que en este ejemplo es 4


13

La subtarea 1 es una versión más pequeña de la tarea original, así que podemos implementar esta
subtarea con una llamada recursiva. La subtarea 2 es el caso sencillo antes mencionado. Así pues, el
pseudocódigo que se muestra a continuación es un bosquejo de nuestro algoritmo para la función
escribir_vertical con parámetro n:

if (n<10){
cout<<n<<endl;
}
else{
//n tiene dos o más dígitos
escribir_vertical(el número n sin su último dígito);
cout<< el último dígito de n <<endl;
}
Para convertir este algoritmo en el código de una función C, lo único que necesitamos es traducir los
siguientes dos elementos del algoritmo a expresiones C: el número n sin su último dígito y el
último dígito de n.

Estas expresiones se pueden traducir fácilmente a expresiones C usando los operadores de división
entera / y % como se muestra a continuación:

n/10 //el número n sin su último dígito


n%10 //el último dígito de n
Por ejemplo, 1234/10 da 123, y 1234%10 da 4.

Varios factores influyeron en nuestra selección de las dos subtareas que usamos en este algoritmo. Uno
fue que era fácil calcular el argumento de la llamada recursiva a escribir_vertical (en negritas) que
usamos en el algoritmo. El número n sin su último dígito se calcula fácilmente con n/10. Como
alternativa, podríamos haber intentado dividir las subtareas así:

1. Despliega el primer dígito de n.


2. Despliega el número n sin su primer dígito.

Ésta es una descomposición de la tarea en subtareas perfectamente válida, y se puede implementar


de manera recursiva. Sin embargo, es difícil calcular el resultado de eliminar el primer dígito de un
número; en cambio, es fácil calcular el resultado de eliminar el último dígito de un número.

Otra razón para elegir esta descomposición es que una de las dos subtareas no requiere una llamada
recursiva. Una buena definición de una función recursiva siempre incluye al menos un caso que no
implica una función recursiva (además de uno o más casos que sí implican al menos una llamada
recursiva).

void escribir_vertical(int n);

int main(){
cout << "escribir_vertical(3):" << endl;
escribir_vertical(3);
cout << "escribir_vertical(12):" << endl;
escribir_vertical(12);
cout << "escribir_vertical(123):" << endl;
14

escribir_vertical(123);
system("PAUSE");
return 0;
}
//////////////////////////////////////////////////////
void escribir_vertical(int n){
if (n < 10) {
cout << n << endl;
}
else{
//n tiene dos o más dígitos:
escribir_vertical(n/10);
cout << (n%10) << endl;
}
}
Puesto que 123 no es menor que 10, la expresión lógica de la instrucción if-else es false, y se ejecuta la
parte else. Sin embargo, la parte else inicia con la siguiente llamada de función:

escribir_vertical(n/10);
que (dado que n es igual a 123) es la llamada

escribir_vertical(123/10);
que equivale a:

escribir_vertical(12);
Cuando la ejecución llega a esta llamada recursiva, el cálculo de la función en curso se coloca en
animación suspendida y se ejecuta esta llamada recursiva. Cuando termine esta llamada recursiva, la
ejecución del cálculo suspendido regresará a este punto y el cálculo continuará a partir de aquí.

La llamada recursiva

escribir_vertical(12);
se maneja igual que cualquier otra llamada de función. El argumento 12 sustituye al parámetro n y se
ejecuta el cuerpo de la función. Después de que 12 sustituye a n, hay dos cálculos pendientes, uno
suspendido y uno activo, como se muestra a continuación:
15

Puesto que 12 no es menor que 10, la expresión booleana de la instrucción if-else es false y se ejecuta
la parte else. Sin embargo, como ya vimos, dicha parte inicia con una llamada recursiva. El argumento
para esta llamada recursiva es n/10, que en este caso equivale a 12/10. Por tanto, este segundo
cálculo de la función escribir_vertical quedará suspendido y se ejecutará la siguiente llamada
recursiva:

escribir_vertical(12/10);
que equivale a:

escribir_vertical(1);
En este punto hay dos cálculos suspendidos que esperan para reanudarse y el computador comienza
a ejecutar la nueva llamada recursiva, que se maneja igual que las anteriores. El argumento 1 sustituye
al parámetro n y se ejecuta el cuerpo de la función. En este punto, el cálculo tiene el siguiente aspecto:

Cuando el cuerpo de la función se ejecuta esta vez, sucede algo distinto. Puesto que 1 es menor que
10, la expresión booleana de la instrucción if-else es true, y se ejecuta la instrucción que está antes del
else. Ése no es más que una instrucción cout que escribe el argumento 1 en la pantalla, así que la
llamada escribir_vertical(1) despliega 1 en la pantalla y termina sin una llamada recursiva.

Cuando termina la llamada escribir_vertical(1), el cálculo suspendido que estaba esperando a


que terminara continúa donde se quedó, y la situación es la siguiente:
16

Cuando este cálculo suspendido se reanuda, ejecuta una instrucción cout que despliega el valor
12%10, que es 2. Con eso termina el cálculo, pero todavía hay otro cálculo suspendido que espera
para reanudarse. Cuando se reanuda ese último cálculo, la situación es:

Al reanudarse este último cálculo suspendido, se despliega el valor 123%10, que es 3, y con ello termina
la ejecución de la llamada de función original. Y, efectivamente, se han desplegado en la pantalla los
dígitos 1, 2 y 3, uno por línea, en ese orden.

La definición de la función escribir_vertical usa recursión. Sin embargo, no hicimos nada nuevo o
diferente al evaluar la llamada de función escribir_vertical(123); la tratamos igual que
cualquiera de las llamadas de funciones que se trabajó anteriormente. Simplemente sustituimos el
argumento 123 por el parámetro n y luego ejecutamos el código del cuerpo de la definición de
función. Cuando llegamos a la llamada recursiva escribir_vertical(123/10); simplemente
repetimos el proceso una vez más.

El computador sigue la pista a las llamadas recursivas de la siguiente manera. Cuando se invoca una
función, el computador inserta los argumentos en lugar de los parámetros y comienza a ejecutar el
código. Si encuentra una llamada recursiva, detiene temporalmente el cálculo. Hace esto porque
necesita conocer el resultado de la llamada recursiva para poder continuar. El computador guarda
toda la información que necesita para continuar el cálculo más adelante, y procede a evaluar la
llamada recursiva. Cuando ésta se completa, el computador regresa para terminar el cálculo exterior.

El lenguaje C no restringe el uso de llamadas recursivas en las definiciones de funciones. Sin embargo,
para que una definición recursiva sea útil, debe diseñarse de modo que cualquier llamada a la función
termine finalmente con algún fragmento de código que no dependa de la recursión. La función podría
llamarse a sí misma, y esa llamada recursiva podría llamar a la función otra vez. El proceso podría
repetirse cualquier cantidad de veces. Sin embargo, el proceso sólo terminará si en algún momento
una de las llamadas recursivas puede devolver un valor sin depender de la recursión. El bosquejo
general de una definición de función recursiva útil es el siguiente:

• Uno o más casos en los que la función realiza su cometido usando llamadas recursivas para
efectuar una o más versiones más pequeñas de la tarea.
• Uno o más casos en los que la función efectúa su tarea sin usar llamadas recursivas. Estos casos
sin llamadas recursivas se llaman casos base o casos de paro.
17

Ejercicio: ¿Qué salidas produce el siguiente programa?

void barra(int n);


int main(){
barra(3);
system("PAUSE");
return 0;
}
///////////////////////////////////////////////////////
void barra(int n){
if (n == 1){
cout << "Hurra\n";
}
else{
cout << "Hip ";
barra(n - 1);
}
}

Ejemplo 9

Anteriormente se ha utilizado la función predefinida pow que calcula potencias. Por ejemplo,

pow(2.0, 3.0) devuelve 2.03.0, así que lo siguiente asigna 8.0 a la variable x:

double x = pow(2.0, 3.0);


La función pow recibe dos argumentos de tipo double y devuelve un valor de tipo double. A
continuación se presentará una definición recursiva de una función que es similar pero que opera con
el tipo int, en lugar de double. Esta nueva función se llama potencia. Por ejemplo, lo que sigue asigna
a y el valor 8, ya que 23 es 8:

int y = potencia(2, 3);


Nuestra principal razón para definir la función potencia es tener otro ejemplo de función recursiva,
pero hay situaciones en las que potencia es preferible a pow. La función pow devuelve valores de tipo
double, que son cantidades aproximadas. La función potencia devuelve valores de tipo int, que son
cantidades exactas. En algunas situaciones podría necesitarse la exactitud adicional que ofrece la
función potencia.

La definición de la función potencia se basa en la siguiente fórmula:

xn es igual a xn_1 * x
Si traducimos esta fórmula a C dirá que el valor devuelto por potencia(x, n) debe ser igual al valor de
la expresión

potencia(x, n _ 1)*x
La definición de la función potencia dada en el ejemplo sí devuelve este valor para potencia(x, n),
siempre que n > 0.
18

El caso en el que n es igual a 0 es el caso de paro. Si n es 0, entonces potencia(x, n) simplemente


devuelve 1 (porque x0 es 1).

int potencia(int x, int n);

int main(){
for (int n = 0; n < 4; n++)
cout << "3 a la potencia " << n << " es " << potencia(3, n) << endl;

system("PAUSE");
return 0;
}

int potencia(int x, int n){


if (n < 0){
cout << "Argumento de potencia no valido.\n";
exit(1);
}
if (n > 0)
return ( potencia(x, n - 1)*x );
else // n == 0
return (1);
}
Veamos qué sucede cuando se invoca la función potencia con algunos valores de muestra.
Consideremos primero la sencilla expresión:

potencia(2, 0)
Cuando se invoca la función, el valor de x se establece a 2, el valor de n se establece a 0, y se ejecuta
el código del cuerpo de la definición de la función. Puesto que el valor de n es válido, se ejecuta la
instrucción if-else. Puesto que este valor de n no es mayor que 0, se usa la instrucción return que sigue
al else, y la llamada de función devuelve 1. Así, lo que sigue hará el valor de y igual a 1.

int y = potencia(2, 0);


Veamos ahora un ejemplo que implica una llamada recursiva. Consideremos la expresión

potencia(2, 1)
Cuando se invoca la función, el valor de x se establece a 2, el valor de n se establece a 1, y se ejecuta
el código del cuerpo de la definición de la función. Puesto que este valor de n es mayor que 0, se usa
la siguiente instrucción return para determinar el valor devuelto:

return ( potencia(x, n -1)*x );


que en este caso equivale a

return ( potencia(2, 0)*2 );


En este momento se suspende el cálculo de potencia(2, 1), se coloca en la pila una copia de este
cálculo suspendido, y el computador inicia una nueva llamada de función para calcular el valor de
potencia(2, 0). Como ya vimos, el valor de potencia(2, 0) es 1. Después de determinar el valor de
19

potencia(2, 0), el computadora sustituye la expresión potencia(2, 0) por su valor de 1 y reanuda el


cálculo suspendido. Dicho cálculo determina el valor final de potencia(2, 1) a partir de la instrucción
return anterior así:

potencia(2, 0)*2 es 1*2 que es 2


y así el valor final devuelto por potencia(2, 1) es 2. Entonces, lo que sigue establecerá el valor de z igual
a 2:

int z = potencia(2, 1);


Si el segundo argumento es un número grande se producirá una cadena larga de llamadas recursivas.
Por ejemplo, considere la instrucción: cout << potencia(2, 3);

El valor de potencia(2, 3) se calcula así:

potencia(2, 3) es potencia(2, 2)*2


potencia(2, 2) es potencia(2, 1)*2
potencia(2, 1) es potencia(2, 0)*2
potencia(2, 0) es 1 (caso de paro)
Cuando el computador llega al caso de paro potencia(2, 0), hay tres cálculos suspendidos.

Después de calcular el valor devuelto para el caso de paro, el computador reanuda el cálculo que se
suspendió más recientemente para determinar el valor de potencia(2, 1). Después, el computadora
completa todos los demás cálculos suspendidos, insertando cada valor calculado en el siguiente
cálculo suspendido hasta llegar al cálculo de la llamada original potencia(2, 3) y completarlo. Los
detalles del cálculo se ilustran en la siguiente figura
20

En resumen se puede decir

• Una función recursiva debe terminar y para eso existen los casos base.
• Los casos base, se deben definir explícitamente, y no deben incluir llamadas recursivas. Ya que
estos serán lo que rompan la recursión.
• Una mala elección del caso base, puede hacer que la función recursiva nunca termine.
• Se debe plantear bien, la forma de hacer las llamadas recursivas, de tal forma que solucione
el problema.
• Si el árbol de recursión es muy grande, se debe tener cuidado, porque el tiempo de ejecución,
puede ser demasiado largo.

Riesgos de la recursividad

El riesgo al usar recursividad, es no tener una manera de salir del paso recursivo, esto es peligroso
porque se hacen llamadas a la misma función, lo que significa una entrada en la pila donde se
almacenan los estados generales del programa.

Para decidir hacer un programa recursivo se deben de tener al menos dos cosas muy claras:

1. El paso base: Esta es la clave para terminar la recursión, es cuando deja de hacer llamadas a
la función recursiva y hace evaluaciones devolviendo los resultados. Además se debe asegurar
de que es posible entrar a este paso.

2. El paso recursivo: Es la parte de la definición que hace llamadas a esa misma función y que es
la causante de las inserciones en la pila, almacenando en cada una de las llamadas,
información del programa, del estado de sus variables locales y globales.

Frecuentemente tanto el paso base como el paso recursivo, se encuentran en una sentencia
condicional if, pero por supuesto que es posible usar cualquier otra sentencia de control, dependiendo
de las necesidades particulares del problema.
21

APUNTADORES
Un apuntador es la dirección en memoria de una variable. Recuerde que la memoria de un
computador se divide en posiciones de memoria numeradas (llamadas bytes), y que las variables se
implementan como una sucesión de posiciones de memoria adyacentes. Recuerde también que en
ocasiones el sistema C++ utiliza estas direcciones de memoria como nombres de las variables. Si una
variable se implementa como tres posiciones de memoria, la dirección de la primera de esas
posiciones a veces se usa como nombre para esa variable. Por ejemplo, cuando la variable se usa
como argumento de llamada por referencia, es esta dirección, no el nombre del identificador de la
variable, lo que se pasa a la función invocadora.

Una dirección que se utiliza para nombrar una variable de este modo (dando la dirección de memoria
donde la variable inicia) se llama apuntador porque podemos pensar que la dirección “apunta” a la
variable. La dirección “apunta” a la variable porque la identifica diciendo dónde está, en lugar de
decir qué nombre tiene. Digamos que una variable está en la posición número 1007; podemos
referirnos a ella diciendo “es la variable que está allá, en la posición 1007”.

V ARIABLES DE APUNTADOR
Un apuntador se puede guardar en una variable. Sin embargo, aunque un apuntador es una dirección
de memoria y una dirección de memoria es un número, no podemos guardar un apuntador en una
variable de tipo int o double. Una variable que va a contener un apuntador se debe declarar como de
tipo apuntador. Por ejemplo, lo que sigue declara p como una variable de apuntador que puede
contener un apuntador que apunta a una variable de tipo double:

double *p;
La variable p puede contener apuntadores a variables de tipo double, pero normalmente no puede
contener un apuntador a una variable de algún otro tipo, como int o char. Cada tipo de variable
requiere un tipo de apuntador distinto.

En general, si queremos declarar una variable que pueda contener apuntadores a otras variables de
un tipo específico, declaramos las variables de apuntador igual que declaramos una variable ordinaria
de ese tipo, pero colocamos un asterisco antes del nombre de la variable. Por ejemplo, lo siguiente
declara las variables p1 y p2 de modo que puedan contener apuntadores a variables de tipo int;
también se declaran dos variables ordinarias v1 y v2 de tipo int:

int *p1, *p2, v1, v2;


Es necesario que haya un asterisco antes de cada una de las variables de apuntador. Si omitimos el
segundo asterisco en la declaración anterior, p2 no será una variable de apuntador; será una variable
ordinaria de tipo int. El asterisco es el mismo símbolo que hemos estado usando para la multiplicación,
pero en este contexto tiene un significado totalmente distinto.

Una variable que puede contener apuntadores a otras variables de tipo Nombre_de_Tipo se declara
del mismo modo que una variable de tipo Nombre_de_Tipo, excepto que se antepone un asterisco al
nombre de la variable.

Sintaxis: Nombre_de_Tipo *Nombre_de_Variable1, *Nombre_de_Variable2, ...;

Cuando tratamos apuntadores y variables de apuntador, normalmente hablamos de apuntar en lugar


de hablar de direcciones. Cuando una variable de apuntador, como p1, contiene la dirección de una
variable, como v1, decimos que dicha variable apunta a la variable v1 o es un apuntador a la variable
v1.
22

Las variables de apuntador, como p1 y p2 en nuestro ejemplo de declaración, pueden contener


apuntadores a variables como v1 y v2. Podemos usar el operador & para determinar la dirección de
una variable, y luego podemos asignar esa dirección a una variable de apuntador. Por ejemplo, lo
siguiente asigna a la variable p1 un apuntador que apunta a la variable v1:

p1 = &v1;
Ahora tenemos dos formas de referirnos a v1: podemos llamarla v1 o “la variable a la que p1 apunta”.
En C++ la forma de decir “la variable a la que p1 apunta” es *p1. Éste es el mismo asterisco que usamos
al declarar p1, pero ahora tiene otro significado. Cuando el asterisco se usa de esta manera se le
conoce como operador de desreferenciación, y decimos que la variable de apuntador está
desreferenciada.

Si armamos todas estas piezas podemos obtener algunos resultados sorprendentes. Consideremos el
siguiente código:

v1 = 0;
p1 = &v1;
*p1 = 42;
cout << v1 << endl;
cout << *p1 << endl;
Este código despliega lo siguiente en la pantalla:

42

42

En tanto p1 contenga un apuntador que apunte a v1, entonces v1 y *p1 se referirán a la misma variable.
Así pues, si asignamos 42 a *p1, también estamos asignando 42 a v1.

El símbolo & que usamos para obtener la dirección de una variable es el mismo símbolo que usamos
en una declaración de función para especificar un parámetro de llamada por referencia. Esto no es
una coincidencia. Recuerde que un argumento de llamada por referencia se implementa dando la
dirección del argumento a la función invocadora. Así pues, estos dos usos del símbolo & son
básicamente el mismo. Sin embargo, hay ciertas diferencias pequeñas en la forma de uso, así que los
consideraremos dos usos distintos (aunque íntimamente relacionados) del símbolo &.

int main(){
int var=1;
int *ptr;
ptr=&var;
cout<<"Acceso directo al contenido de var "<<var<<endl;
cout<<"Acceso indirecto al contenido de var "<<*ptr<<endl;
cout<<"Direccion de la variable var "<<&var<<endl;
cout<<"Direccion de la variable var "<<ptr<<endl;
system("PAUSE");
return 0;
}
23

LOS OPERADORES * Y &


El operador * antepuesto a una variable de apuntador produce la variable a la que apunta. Cuando
se usa de esta forma, el operador * se llama operador de desreferenciación.

El operador & antepuesto a una variable ordinaria produce la dirección de esa variable; es decir,
produce un apuntador que apunta a la variable. El operador & se llama simplemente operador de
dirección de.

Por ejemplo, consideremos las declaraciones

double *p, v;
Lo siguiente establece a p de modo que apunte a la variable v:

p= &v;
*p produce la variable a la que apunta p, así que después de la asignación anterior *p y v se refieren
a la misma variable. Por ejemplo, lo siguiente establece el valor de v a 9.99, aunque nunca se usa
explícitamente el nombre v:

*p = 9.99;

Podemos asignar el valor de una variable de apuntador a otra variable de apuntador. Esto copia una
dirección de una variable de apuntador a otra. Por ejemplo, si p1 todavía está apuntando a v1, lo que
siguiente establecerá el valor de p2 de modo que también apunte a v1:

p2 = p1;
Siempre que no hayamos modificado el valor de v1, lo siguiente también desplegará 42 en la pantalla:

cout << *p2;


Asegúrese de no confundir

p1 = p2; con *p1 = *p2;


Cuando añadimos el asterisco, no estamos tratando con los de apuntadores p1 y p2, sino con las
variables a las que estos apuntan. Esto se ilustra en la siguiente figura.
24

Puesto que podemos usar un apuntador para referirnos a una variable, el programa puede manipular
variables aunque éstas carezcan de identificadores que las nombren. Podemos usar el operador new
para crear variables sin identificadores que sean sus nombres.

Hacemos referencia a estas variables sin nombre mediante apuntadores. Por ejemplo, lo siguiente crea
una nueva variable de tipo int y asigna a la variable de apuntador p1 la dirección de esta nueva
variable (es decir, p1 apunta a esta nueva variable sin nombre):

p1 = new int;
Podemos usar *p1 para referirnos a esta nueva variable (es decir, la variable a la que p1 apunta).
Podemos hacer con esta variable sin nombre lo mismo que con cualquier otra variable de tipo int. Por
ejemplo, lo que sigue lee un valor de tipo int del teclado y lo coloca en la variable sin nombre, suma 7
al valor y luego despliega el nuevo valor en la pantalla:

cin >> *p1;


*p1 = *p1 + 7;
cout << *p1;
El operador new produce una nueva
variable sin nombre y devuelve un
apuntador que apunta a esta nueva
variable. Especificamos el tipo de la nueva
variable escribiendo el nombre del tipo
después del operador new. Las variables
que se crean con el operador new se
llaman variables dinámicas porque se
crean y se destruyen mientras el programa
se está ejecutando. El programa del que se
observa a continuación demuestra algunas
operaciones sencillas con apuntadores y
variables dinámicas.

int main(){
int *p1, *p2;
p1 = new int;
*p1=42;
p2=p1;
cout << "*p1 ==>"<<*p1<<endl;
cout << "*p2 ==>"<<*p2<<endl;
*p2=53;
cout << "*p1 ==>"<<*p1<<endl;
cout << "*p2 ==>"<<*p2<<endl;
p1=new int;
*p1=88;
cout << "*p1 ==>"<<*p1<<endl;
cout << "*p2 ==>"<<*p2<<endl;
system("PAUSE");
return 0;
}
25

En la figura, las variables se representan como cuadritos y el valor de la variable se escribe dentro del
cuadrito. No hemos mostrado las direcciones numéricas reales en las variables de apuntadores. Los
números reales no son importantes. Lo que es importante es que el número es la dirección de una
variable dada. Así pues, en lugar de usar el número real de la dirección, sólo hemos indicado la
dirección con una flecha que apunta a la variable que tiene esa dirección. Por ejemplo, en la
ilustración (b), p1 contiene la dirección de una variable en la que se escribió un signo de interrogación.

Uso de variables de apuntador con =

Si p1 y p2 son variables de apuntador, la instrucción

p1 = p2;
modificará p1 de modo que apunte a lo mismo que p2.

El operador new

El operador new crea una nueva variable dinámica del tipo que se especifica y devuelve un
apuntador que apunta a esta nueva variable. Por ejemplo, lo que sigue crea una variable dinámica
nueva del tipo MiTipo y deja a la variable del apuntador p apuntando a esa nueva variable.

MiTipo *p;
p = new MiTipo;

Administración de memoria básica

Se reserva un área especial de la memoria, llamada almacén libre para las variables dinámicas.Toda
variable dinámica nueva creada por un programa consume parte de la memoria del almacén libre.
Si nuestro programa crea demasiadas variables dinámicas, consumirá toda la memoria del almacén
libre. Si esto sucede, todas las llamadas subsecuentes a new fracasarán.

El tamaño del almacén libre varía de una implementación de C++ a otra. Por lo regular es grande y es
poco probable que un programa modesto use toda la memoria del almacén libre. No obstante,
incluso en los programas modestos es recomendable reciclar la memoria del almacén libre que ya no
se necesite. Si nuestro programa ya no necesita una variable dinámica dada, la memoria ocupada
por esa variable puede ser reciclada.

El operador delete elimina una variable dinámica y devuelve al almacén libre la memoria que
ocupaba, para poder reutilizarla. Supongamos que p es una variable de apuntador que está
apuntando a una variable dinámica. Lo que sigue destruye la variable dinámica a la que p apunta y
devuelve al almacén libre la memoria ocupada por esa variable dinámica:

delete p;
Después de la llamada a delete, el valor de p es indefinido y p será tratada como una variable no
inicializada.

El operador delete

El operador delete elimina una variable dinámica y devuelve al almacén libre la memoria que esa
variable ocupaba. Esa memoria puede entonces reutilizarse para crear nuevas variables dinámicas.
Por ejemplo, lo que sigue elimina la variable dinámica a la que apunta la variable de apuntador p:
26

delete p;
Después de una llamada a delete, el valor de la variable de apuntador (p en nuestro ejemplo) no está
definido.

Ejercicios

1. ¿Qué salidas produce el siguiente código?

int main(){
int *p1, *p2;
p1 = new int;
p2 = new int;
*p1 = 10;
*p2 = 20;
cout << *p1 << " " << *p2 << endl;
p1 = p2;
cout << *p1 << " " << *p2 << endl;
*p1 = 30;
cout << *p1 << " " << *p2 << endl;
system("PAUSE");
return 0;
}

2. ¿Cómo cambiarían las salidas si se sustituyera *p1 = 30; con lo siguiente? *p2 = 30;
3. ¿Qué salidas produce el siguiente código?

int main(){
int *p1 , *p2;
p1 = new int;
p2 = new int;
*p1 = 10;
*p2 = 20;
cout << *p1 << " " << *p2 << endl;
*p1 = *p2; cout << *p1 << " " << *p2 << endl;
*p1 = 30;
cout << *p1 << " " << *p2 << endl;
system("PAUSE");
return 0;
}
27

Comparación entre paso de parámetros por valor y por referencia

int cuboPorValor( int n ); // prototipo


int main() {
int numero = 5; // inicializa numero
cout<< "El valor original de numero es "<< numero ;
// pasa numero por valor a cuboPorValor
numero = cuboPorValor( numero );
cout<<"\nEl nuevo valor de numero es "<< numero<<endl ;
system("PAUSE");
return 0;
}
int cuboPorValor( int n ) {
return n*n*n;
// eleva al cubo la variable local n y devuelve el resultado
}
28

void cuboPorReferencia( int *ptrN ); // prototipo


int main() {
int numero = 5; // inicializa numero
cout<< "El valor original de numero es "<<numero ;
cuboPorReferencia( &numero );
cout<< "\nEl nuevo valor de numero es "<< numero <<endl;
return 0;
}
// calcula el cubo de *ptrN; modifica la variable numero en main
void cuboPorReferencia( int *ptrN ) {
*ptrN = *ptrN * *ptrN * *ptrN; // cubo de *ptrN
}
29

Expresiones con apuntadores y aritmética de apuntadores

Los apuntadores son operandos válidos dentro de las expresiones aritméticas, expresiones de
asignación y expresiones de comparación. Sin embargo, por lo general no todos los operadores
utilizados son válidos con el uso de las variables de apuntadores. Esta sección describe los operadores
que pueden tener apuntadores como operandos, y cómo se utilizan estos operadores.

Se puede realizar un conjunto limitado de operaciones con los apuntadores. Un apuntador se puede
incrementar (++) o decrementar (--), se puede sumar un entero a un apuntador (+ o +=), se puede
restar un entero a un apuntador (- o -=) y se puede restar un apuntador a otro.

Suponga que el arreglo int v[5] ya está definido


y que su primer elemento se encuentra en la
ubicación 3000 de memoria. Suponga que el
apuntador ptrV se inicializa para apuntar a v[0],
es decir, el valor de ptrV es 3000. La figura ilustra
esta situación para una máquina con enteros de
4 bytes. Observe que ptrV puede inicializarse
para que apunte al arreglo v con cualquiera de
las instrucciones

ptrV = v;
ptrV = &v[0];

Nota: La mayoría de las computadoras actuales tienen enteros de 2 y 4 bytes. Algunas de las máquinas
más nuevas utilizan enteros de 8 bytes. Debido a que los resultados de la aritmética de apuntadores
dependen del tamaño de los objetos al que apunta el apuntador, la aritmética de apuntadores
depende de la máquina.

En la aritmética convencional, 3000 + 2 da como


resultado 3002. Por lo general, éste no es el caso en
la aritmética de apuntadores. Cuando se suma o se
resta un entero a un apuntador, el apuntador no
aumenta o disminuye por dicho entero, sino por el
número de veces del tamaño del objeto al que hace
referencia el apuntador.

El número de bytes depende del tipo de datos del


objeto. Por ejemplo, la instrucción

ptrV += 2;
producirá 3008 (3000 + 2 * 4), suponiendo que un entero se almacena en 4 bytes de memoria. En el
arreglo v, ptrV ahora apunta a v[ 2 ] .

Si un entero se almacena en 2 bytes de memoria, entonces el cálculo anterior arrojará la dirección de


memoria 3004 (3000 + 2*2). Si el arreglo es de un tipo de dato diferente, la instrucción anterior
incrementará el apuntador el doble del número de bytes necesarios para almacenar un objeto de ese
tipo de dato. Cuando utilizamos la aritmética de apuntadores en un arreglo de caracteres, los
resultados serán consistentes con la aritmética regular, debido a que cada carácter ocupa 1 byte de
longitud.
30

Si ptrV se incrementa a 3016, lo cual apunta a v[ 4 ], la instrucción ptrV -= 4;

establece ptrV de nuevo en 3000, es decir, al principio del arreglo. Si un apuntador se incrementa o se
decrementa en uno, pueden utilizarse los operadores de incremento (++) y decremento(--). La
instrucción ptrV++ incrementa el apuntador para que apunte al elemento siguiente del arreglo; o la
instrucción ptrV--; decrementa el apuntador para que apunte al elemento previo del arreglo.

Las variables apuntador se pueden restar entre sí. Por ejemplo, si ptrV contiene la ubicación 3000, y
ptrV2 contiene la dirección 3008, la instrucción: x = ptrV2 – ptrV;

asignará a x el número de elementos del arreglo ptrV a ptrV2, en este caso 2 (y no 8). La aritmética de
apuntadores no tiene sentido a menos que se realice en un arreglo. No podemos asumir que dos
variables del mismo tipo se almacenan de manera contigua en memoria, a menos que sean
elementos adyacentes de un arreglo.

Un error frecuente de programación es utilizar la aritmética de apuntadores sobre un apuntador que


no hace referencia a un elemento de un arreglo.

Ejercicios

Responda a cada una de las siguientes preguntas. Suponga que los números de punto flotante se
almacenan en 4 bytes de memoria, y que la dirección inicial del arreglo es la ubicación de memoria
1002500. Cada parte del ejercicio debe utilizar los resultados de las partes previas, en donde sea
apropiado.

1. Defina un arreglo de tipo float llamado numeros con 10 elementos, e inicialice


los elementos con los valores 0.0, 1.1, 2.2. . . , 9.9. Suponga que la constante
simbólica TAMANIO se definió como 10.
2. Defina un apuntador, ptrN, que apunte a un objeto de tipo float.
3. Imprima los elementos del arreglo numeros mediante la utilización de una
instrucción for y suponga que la variable entera de control i ya se definió.
4. Escriba dos instrucciones separadas que asignen la dirección inicial del arreglo
numeros a la variable de apuntador ptrN.
5. Imprima los elementos del arreglo numeros mediante la notación
apuntador/desplazamiento con el apuntador ptrN.
6. Suponga que ptrN apunta al inicio del arreglo numeros, ¿A cuál dirección se hace
referencia con ptrN + 8? ¿Cuál valor se almacena en dicha ubicación?
7. Suponga que ptrN apunta a numeros[ 5 ], ¿a cuál dirección se hace referencia
mediante ptrN -= 4? ¿Cuál es el valor almacenado en dicha ubicación?
Para cada uno de los siguientes enunciados, escriba una instrucción que realice la tarea indicada.
Suponga que las variables de punto flotante numero1 y numero2 ya se definieron, y que numero1 se
inicializa en 7.3.

1. Defina la variable ptrF como un apuntador a un objeto de tipo float.


2. Asigne la dirección de la variable numero1 hacia el apuntador ptrF.
3. Imprima el valor del objeto al que apunta ptrF.
4. Asigne a la variable numero2 el valor del objeto al que apunta ptrF.
5. Imprima el valor de numero2.
6. Imprima la dirección de numero1.
7. Imprima la dirección almacenada en ptrF. ¿El valor impreso es el mismo que el de numero1?
31

Registros o Estructuras

Cuando estudiamos arreglos anteriormente, observamos que representan un tipo de datos


estructurado y permiten resolver un gran número de problemas en forma efectiva. Definimos a los
arreglos como una colección finita, homogénea y ordenada de elementos. Ahora estudiaremos un
tipo de datos estructurados que se distingue fundamentalmente de los arreglos porque sus elementos
pueden ser heterogéneos, es decir, pueden pertenecer —aunque no necesariamente— a tipos de
datos diferentes.

Las estructuras, conocidas generalmente con el nombre de registros, representan un tipo de datos
estructurado. Se utilizan tanto para resolver problemas que involucran tipos de datos estructurados,
heterogéneos, como para almacenar información en archivos —como veremos más adelante. Las
estructuras tienen varios componentes, cada uno de los cuales puede constituir a su vez un tipo de
datos simple o estructurado. Sin embargo, los componentes del nivel más bajo de un tipo estructurado,
siempre son tipos de datos simples. Formalmente definimos a una estructura de la siguiente manera:

“Una estructura es una colección de elementos finita y heterogénea.”

Finita porque se puede determinar el número de componentes y heterogénea porque todos los
elementos pueden ser de tipos de datos diferentes.

A los datos del registro se les denomina campos, elementos o miembros. Los campos de un registro
pueden ser todos de diferentes tipos. Cada campo se identifica por un nombre único (el identificador
de campo). Una estructura está formada por variables que tienen relación entre sí.

Consideremos que por cada alumno de una universidad debemos almacenar la siguiente
información:

• Número de registro del alumno (entero).


• Nombre del alumno (cadena de caracteres).
• Carrera del alumno (cadena de caracteres).
• Promedio del alumno (real).
• Dirección del alumno (cadena de caracteres).

La estructura de datos adecuada para almacenar esta información es la estructura. Cabe aclarar que
no es posible utilizar un arreglo para resolver este problema, porque sus componentes deben ser del
mismo tipo de datos. En la figura se muestra la representación gráfica de este ejemplo.

Alumno
Número Nombre Carrera Promedio Dirección
7893 Ignacio Contreras Comunicación Social 4.3 Calle 100 #4-58

Declaración de una estructura

Una estructura define una plantilla con la que posteriormente se puede declarar una variable. Una de
las características de las estructuras es que hay que definirlas antes de usarlas en la declaración de
variables. En la definición no se declara ni se reserva memoria para ninguna variable, sólo se construye
una estructura con determinadas características, para después poder declarar una variable de ese
32

tipo. Por lo tanto al ser una estructura un tipo de dato definido por el usuario, se debe declarar antes
de que se pueda utilizar.

El formato de la declaración es:


struct <nombre de la estructura>{
<tipo de dato elemento 1> <nombre elemento 1>;
<tipo de dato elemento 2> <nombre elemento 2>;
...
<tipo de dato elemento n> <nombre elemento n>;

};
struct alumno{ /* Declaración de la estructura. */
int numero;
char nombre[20];
char carrera[20]; /* Campos de la estructura. */
float promedio;
char direccion[20];
}; // Observar que la declaración de una estructura termina con punto y coma
Definición de variables de estructuras

Al declarar una estructura se especifica el nombre y el formato de una estructura de datos, pero no
reserva almacenamiento en memoria. Por consiguiente, cada definición de variable para una
estructura dada crea un área de memoria en donde los datos se almacenan de acuerdo al formato
estructurado declarado. Las variables de estructuras se pueden definir de dos formas:

1. Listándolas inmediatamente después de la llave de cierro de la declaración de la estructura

struct discos{
char titulo[30];
char artista[25];
int canciones;
float precio;
char fecha[8];
}cd1,cd2,cd3;

2. Listando el nombre de la estructura seguida por las variables correspondientes en cualquier


lugar del programa antes de utilizarlas

discos cd1,cd2,cd3;

Una definición de tipo struct crea un tipo de datos nuevo. De la misma forma como usamos el tipo short
o float para declarar una variable, podemos usar este nuevo tipo de datos para declarar una variable
de tipo struct.
33

Asignación de valores a los campos de una estructura

Una vez declarada una estructura, pueden asignárseles valores iniciales a sus campos. Para ello se dan
los valores iniciales escribiendo entre llaves los valores de sus campos en el mismo orden en que se
declararon éstos. Pero ahora cada dato puede tener un tipo diferente.

struct fecha{
int dia;
char mes[10];
int anio;
};
fecha fec_ant = {15,”Abril”,2008};

struct Persona{
char nomb[40];
char genero;
int edad;
}usuario = {“Rosa Flores”, ‘F’};

Como no se proporciona valor para la edad, ésta se inicializa en cero

La asignación de datos a estructuras también se puede hacer mediante el operador punto así:

<nombre variable estructura> . <nombre elemento> = dato;


Ejemplo:

cd1.titulo=”Cancioncitas”;
cd1.precio=10500;
cd1.canciones=8;
El operador punto proporciona el camino directo al elemento correspondiente. Los datos que se
almacenan en un elemento dado deben ser del mismo tipo que el tipo declarado para ese elemento.

Para introducir la información en la estructura mediante el teclado, basta con emplear una sentencia
de entrada (scanf o cin) utilizando el operador punto

scanf("%d",&cd1.canciones);
cin>>cd1.canciones;

Cada miembro o campo de un registro puede usarse como una variable cualquiera.

Recuperación de información de una estructura

Se recupera información de una estructura utilizando el operador de asignación o una sentencia printf
o cout, igual que antes utilizando el operador punto para acceder al elemento de la estructura
deseado.

printf("El número de canciones del cd es %d",cd1.canciones);


cout<<"El número de canciones del cd es ”<<cd1.canciones<<endl;
34

Ejemplo 8: Capturar la información básica de un empleado y mostrar su salario

#include <C:\formato.h>
#define n 3
using namespace std;
void titulos();
void captura();
void proceso();
void escribir();
struct reg{
char nom[20];
int nh;
float vh;
float sm;
};
reg ficha; int i;

int main(){
textbackground(1); textcolor(WHITE);
for (i=0;i<n;i++) {
clrscr();
titulos();
captura();
proceso();
escribir();
gotoxy(10,22);system("PAUSE");
}
return 0;
}
void titulos(){
centrar("+++ Datos empleados +++",3);
cuadro(8,5,18,7,15); cuadro(8,8,18,10,15);
cuadro(8,11,18,13,15); cuadro(20,5,50,7,15);
cuadro(20,8,50,10,15); cuadro(20,11,50,13,15);
}
void captura(){
gotoxy(10,6);cout<<"Nombre"; fflush(stdin);
gotoxy(22,6);gets(ficha.nom);
gotoxy(10,9);cout<<"horas"; gotoxy(22,9);cin>>ficha.nh;
gotoxy(10,12);cout<<"Valor"; gotoxy(22,12);cin>>ficha.vh;
}
void proceso(){
ficha.sm=ficha.nh*ficha.vh;
}
35

void escribir(){
textcolor(14);
gotoxy(20,15);cout<<"El salario de "<< ficha.nom <<" es $" <<ficha.sm;
textcolor(15);
}

Ejercicio 1: Crear un programa que permita registrar la información de los artículos de un almacén
(código (int), descripción (char 30), precio de compra (float), cantidad actual (int)), calcular el precio
de venta que es el 35% más del precio de compra y mostrar ese valor en pantalla.

Ejercicio 2: En una biblioteca necesitan una aplicación que permita controlar los préstamos de los libros
existentes. De cada libro se almacena el código, cantidad existente, cantidad prestada, título del libro
y autor.
36

Estructuras jerárquicas

En ocasiones es razonable tener estructuras cuyos miembros son a su vez estructuras más pequeñas.
Por ejemplo, un tipo de estructura llamado InfoPersona en el que podríamos almacenar la estatura,
peso y fecha de nacimiento de una persona se podría definir como sigue:

struct Fecha{
int dia;
int mes;
int anio;
};
struct InfoPersona{
double estatura;//en centímetros
int peso;//en kilogramos
Fecha fec_nac;
};

Una variable de estructura de tipo InfoPersona se declara de la forma acostumbrada:

InfoPersona persona1;

Ejemplo 9: Utilizando la estructura InfoPersona registrar y mostrar los datos de una persona

struct Fecha{
int dia;
int mes;
int anio;
};
struct InfoPersona{
double estatura;
int peso;
Fecha fec_nac;
};

void capturaDatos(InfoPersona &persona1);


void mostrarDatos(InfoPersona &persona1);
int main(){
InfoPersona persona;
clrscr();
capturaDatos(persona);
system("PAUSE");
mostrarDatos(persona);

system("PAUSE");
return 0;
}
37

void capturaDatos(InfoPersona &persona1){


cout<<"Registro de datos personales"<<endl<<endl;
cout<<"Estatura cm:";cin>>persona1.estatura;
cout<<"Peso kg :";cin>>persona1.peso;
cout<<endl;
cout<<"Fecha de nacimiento"<<endl;
cout<<"Dia :";cin>>persona1.fec_nac.dia;
cout<<"Mes :";cin>>persona1.fec_nac.mes;
cout<<"Anio :";cin>>persona1.fec_nac.anio;
}
void mostrarDatos(InfoPersona &persona1){
clrscr();
cout<<"Los datos registrados son"<<endl<<endl;
cout<<"Estatura "<<persona1.estatura<<"cms"<<endl;
cout<<"Peso "<<persona1.peso<<"kg"<<endl<<endl;
cout<<"Fecha de nacimiento"<<endl<<endl;
cout<<"Dia "<<persona1.fec_nac.dia<<endl;
cout<<"Mes "<<persona1.fec_nac.mes<<endl;
cout<<"Anio "<<persona1.fec_nac.anio<<endl;
}

Arrays de Estructuras

Se puede crear un array de estructuras tal como se crea un array de otros tipos. Los arrays de
estructuras son idóneos para almacenar un archivo completo de empleados, un archivo de inventario,
o cualquier otro conjunto de datos que se adapte a un formato de estructura. Mientras que los arrays
proporcionan un medio práctico de almacenar diversos valores del mismo tipo, los arrays de
estructuras le permiten almacenar juntos diversos valores de diferentes tipos, agrupados como
estructuras.

En un arreglo de estructuras cada elemento es un registro por lo tanto a cada registro se le reconoce
con un índice. Además cada campo o elemento de registro se lo reconoce por el nombre del registro
y el nombre del campo. En este caso el nombre del registro será el elemento del arreglo acompañado
del nombre del campo. Ejemplo cd1[0].titulo;
38

Ejemplo 10: Almacenar en un arreglo los datos básicos de un empleado (nombre, número de horas
trabajadas y valor de la hora). Después de la captura de datos se debe mostrar en pantalla el reporte
general de todos los empleados con sus datos incluido su salario

#define n 3
using namespace std;
struct reg{
char nom[20];
int nh;
float vh;
float sm;
};
reg emp[n];
int i;
void titulos(){
gotoxy(10,8); cout<<"Nombre ";
gotoxy(10,10);cout<<"Horas ";
gotoxy(10,12);cout<<"Valor ";
}
////////////////////////////////////////////
void captura(){
fflush(stdin);
gotoxy(20,8);gets(emp[i].nom);
gotoxy(20,10);cin >>emp[i].nh;
gotoxy(20,12);cin >>emp[i].vh;
}
////////////////////////////////////////////
void proceso() {
emp[i].sm=emp[i].nh*emp[i].vh;
}
////////////////////////////////////
void escribir(){
int fila=4;
textbackground(7);
textcolor(0);
clrscr();
gotoxy(5,2);cout<<"+++ Datos empleados +++";
gotoxy(5,3);cout<<" Nombre ";
gotoxy(35,3);cout<<" Horas ";
gotoxy(45,3);cout<<" Valor ";
gotoxy(60,3);cout<<" Salario ";
for(i=0;i<n;i++){
fila++;
gotoxy(5,fila); cout<<emp[i].nom;
39

gotoxy(35,fila);cout<<emp[i].nh;
gotoxy(45,fila);cout<<emp[i].vh;
gotoxy(60,fila);cout<<emp[i].sm;
}
}
//////////////////////////////////
int main(){
clrscr();
for(i=0;i<n;i++) {
clrscr();
gotoxy(5,2);cout<<"+++ Datos empleados +++";
titulos();
captura();
proceso();
gotoxy(24,24);
system("PAUSE");
}
escribir();
gotoxy(24,24);
system("PAUSE");
return 0;
}
Ejercicio 3: Crear un programa que permita registrar la información de 100 artículos de un almacén
(código (int), descripción (char 30), precio de compra (float), cantidad actual (int)), calcular el precio
de venta que es el 35% más del precio de compra y mostrar ese valor en pantalla.

Ejercicio 2: En una biblioteca necesitan una aplicación que permita controlar los préstamos de los 500
libros existentes. De cada libro se almacena el código, cantidad existente, cantidad prestada, título
del libro y autor. La aplicación debe permitir registrar los libros y mostrar el reporte general de
existencias.
40

Estructuras con arreglos

Existen numerosos casos en la vida real en los que para resolver un problema de manera eficiente
necesitamos utilizar estructuras combinadas con arreglos. Observemos el siguiente ejemplo, en el que
uno de los campos de la estructura es a su vez otro arreglo.

Ejemplo 10: En una escuela almacenan la información de sus alumnos utilizando arreglos
unidimensionales. La siguiente información de cada alumno se guarda en una estructura:

• Código (entero).
• Nombre y apellido (cadena de caracteres).
• Promedios de las materias (arreglo unidimensional de reales).

Escribe un programa en C que obtenga lo siguiente:

1. El código y el promedio de cada alumno.


2. El código de los alumnos cuya calificación en la tercera materia sea mayor a 4.
3. El promedio general de la materia 4.

struct datos{
int matricula;
char nombre[30];
float cal[5];
};
using namespace std;

void menu(int *opc);


void lectura(datos est[], int t);
void promedio(datos est[], int t);
void mayor(datos est[], int t);
void mat4(datos est[], int t);

int main(){
datos arreglo[50];
int tam,opc;
do{
cout<<"Ingrese el tamaño del arreglo:";
cin>>tam;
}while (tam > 50 || tam < 1);
do{
clrscr();
menu(&opc);
switch(opc){
case 1:{
lectura(arreglo, tam);
break;
}
41

case 2:{
promedio(arreglo, tam);
break;
}
case 3:{
mayor(arreglo, tam);
break;
}
case 4:{
mat4(arreglo, tam);
break;
}
}
system("pause");
}while(opc!=5);
return 0;
}
/////////////////////////////////////////////////////////
void menu(int *opc){
cout<<"Registro de estudiantes y calificaciones"<<endl;
cout<<"1. Ingresar datos"<<endl;
cout<<"2. Promedio individual"<<endl;
cout<<"3. Notas mayores"<<endl;
cout<<"4. Promedio materia"<<endl;
cout<<"5. Terminar"<<endl;
cin>>*opc;

}
////////////////////////////////////////
void lectura(datos est[], int t){
int i, j;
for (i=0; i<t; i++){
clrscr();
cout<<"Ingrese los datos del alumno "<< i+1<<endl;
cout<<"Ingrese el codigo del alumno: ";
cin>>est[i].matricula;
cout<<"Ingrese el nombre del alumno:";
fflush(stdin); gets(est[i].nombre);
for (j=0; j<5; j++){
cout<<"Ingrese la calificacion "<<j+1<< " del alumno "<<i+1<<endl;
cin>>est[i].cal[j];
}
}
}
42

/////////////////////////////////////////////////
void promedio(datos est[], int t){
int i, j;
float sum, pro;
for (i=0; i<t; i++){
clrscr();
cout<<"\ncodigo del alumno: "<<est[i].matricula;
sum = 0.0;
for (j=0; j<5; j++)
sum+= est[i].cal[j];
pro = sum / 5;
cout<<"\t\tPromedio: "<< pro<<endl;
}
}
/////////////////////////////////////
void mayor(datos est[], int t){
int i;
cout<<"\nAlumnos con calificación en la tercera materia > 4"<<endl;
for (i=0; i<t; i++){
if (est[i].cal[2] > 4)
cout<<"\nCodigo del alumno: "<< est[i].matricula<<endl;
}
}
///////////////////////////////////////////
void mat4(datos est[], int t){
int i;
float pro, sum = 0.0;
for (i=0; i<t; i++)
sum+= est[i].cal[3];
pro = sum / t;
cout<<"\n\nPromedio de la materia 4: "<<pro<<endl;
}
43

A RCHIVOS SECUENCIALES
Los datos que se han trabajado hasta el momento han residido en la memoria principal (RAM). Sin
embargo, las grandes cantidades de datos se almacenan normalmente en un dispositivo de memoria
secundaria. Estas colecciones de datos se conocen como archivos. Los archivos se utilizan para lograr
la persistencia de los datos; es decir, la retención permanente de grandes cantidades de datos.

Existe una estrecha relación entre la memoria principal, el microprocesador y los dispositivos de
almacenamiento secundario ya que el procesamiento que realiza un computador es tarea absoluta
del procesador en conjunto con la memoria principal; es decir, los dispositivos de almacenamiento
secundario (discos duros, CDs, memorias, etc.) no procesan datos, sólo los almacenan. En estos
dispositivos sólo se reflejan los datos previamente procesados y funcionan exclusivamente como una
bodega. Esto repercute de manera significativa al momento de utilizar archivos, ya que para hacerle
modificaciones a los datos de un registro previamente almacenado es necesario primero “cargarlo”
en la memoria principal, es decir, localizar el registro en el archivo y leerlo para colocar sus datos en la
memoria RAM, ahí modificarlo y
posteriormente grabarlo en la misma
posición en la que se encontraba, sin
embargo estas operaciones no se
realizan directamente, sino a través
de la unidad aritmética-lógica, la
unidad de control y los registros del
procesador.

En el fondo, todos los elementos de


datos procesados por los
computadores se reducen a
combinaciones de ceros y unos. Esto
ocurre debido a que es simple y
económico el construir dispositivos
electrónicos que puedan asumir dos
estados estables: un estado
representa al 0 y el otro estado
representa al 1.

Por lo general, un registro (es decir, struct en C) está compuesto de varios campos conocidos como
miembros.

Pedro Antonio Gómez Luisa Andrea Ramírez


Registro Registro

Un archivo es un conjunto de datos estructurados en una colección de entidades elementales o


básicas denominadas registros que son de igual tipo y constan a su vez de diferentes entidades
de nivel más bajos denominadas campos.
44

Un registro es un grupo de campos relacionados. Un archivo es un grupo de registros relacionados. Por


ejemplo, el archivo de nómina de una empresa pequeña podría tener solamente 22 registros, mientras
que el de una empresa grande puede tener miles de registros.

Creación de un archivo de acceso secuencial

C ve a cada archivo como una secuencia de bytes. Cada archivo termina ya sea con un marcador
de fin de archivo o un número de byte específico, registrado en una estructura de datos administrativa
que mantiene el sistema.

En los archivos secuenciales, como su nombre lo indica, los registros se graban en secuencia o
consecutivamente y deben accesarse de ese mismo modo, es decir, conforme se van insertando
nuevos registros, éstos se guardan al final del último registro almacenado; por lo tanto, cuando se
desea consultar un registro grabado previamente es necesario recorrer completamente el archivo
leyendo cada registro y comparándolo con el que se busca. En este tipo de archivo se utiliza una
marca invisible que el sistema operativo coloca al final de los archivos: EOF (End of File), la cual sirve
para identificar dónde termina el archivo.

Los archivos secuenciales tienen algunas características que hay que tener en cuenta:

1. La escritura de nuevos datos siempre se hace al final del archivo.


2. Para leer una zona concreta del archivo hay que avanzar siempre, si la zona está antes de la
zona actual de lectura, será necesario "rebobinar" el archivo.

Los archivos secuenciales son más sencillos de manejar, ya que requieren menos funciones, además
son más rápidos, el punto de lectura y escritura está siempre determinado.

Declaración del nombre del archivo

Para realizar programas de manejo de archivos en Lenguaje C se requiere el encabezado “Standard


I/O” y se necesita incluirlo de la siguiente forma:

#include <stdio.h>
45

Además es necesario declarar una variable de tipo FILE que opere como el apuntador a la estructura
del archivo (nombre), esto se logra con la siguiente línea:

FILE *alias;
Es importante señalar que antes de trabajar con un archivo debemos abrirlo y cuando terminamos de
trabajar con él debemos cerrarlo por seguridad de la información que se haya almacenado. En el
lenguaje C un archivo básicamente se abre y cierra de la siguiente forma

/* El siguiente conjunto de instrucciones muestra la sintaxis para abrir y cerrar un Archivo en el lenguaje
de programación C. */

...
FILE *apuntador_archivo;
apuntador_archivo = fopen (nombre_archivo, “tipo_archivo”);
if (apuntador_archivo != NULL) {
proceso; /* trabajo con el archivo. */
fclose(apuntador_archivo);
}
else
cout<<“No se puede abrir el archivo”;

La instrucción: apuntador_archivo = fopen (nombre_archivo, “tipo-archivo”);

Permite abrir un archivo llamado nombre_archivo que puede ser una variable de tipo cadena de
caracteres, o bien, una constante sin extensión o con extensión txt para realizar actividades de
tipo_archivo. Observa que la función fopen tiene dos argumentos: el nombre del archivo y el tipo de
archivo, que puede ser de lectura, escritura, etc. En la siguiente tabla se muestran los diferentes tipos
de archivos

Tipo de Archivo Explicación


r Se abre un archivo sólo para lectura.
Se abre un archivo sólo para escritura. Si el archivo ya existe, el apuntador se
w
coloca al inicio y sobrescribe, destruyendo al archivo anterior.
Se abre un archivo para agregar nuevos datos al final. Si el archivo no existe,
a
crea uno nuevo.
Se abre un archivo para realizar modificaciones. Permite leer y escribir. El archivo
r+
tiene que existir.
Se abre un archivo para leer y escribir. Si el archivo existe, el apuntador se coloca
w+
al inicio, sobrescribe y destruye el archivo anterior.
Se abre un archivo para lectura y para incorporar nuevos datos al final. Si el
a+
archivo no existe, se crea uno nuevo.

La función fopen() cuando realiza el trabajo de abrir un archivo, regresa la dirección física donde crea
o graba el archivo en disco. El primer parámetro o argumento en esta función es el nombre con el que
se reconoce físicamente el archivo.

La siguiente instrucción: if (apuntador_archivo != NULL)

Permite evaluar el contenido del apuntador. Si éste es igual a NULL, implica que el archivo no se pudo
abrir, en cuyo caso es conveniente escribir un mensaje para notificar esta situación. Por otra parte, si
46

el contenido del apuntador es distinto de NULL entonces se comienza a trabajar sobre el archivo. Por
último, la instrucción:

fclose (apuntador_archivo); se utiliza para cerrar el archivo.

Con apuntadores se manejan dos operadores diferentes que son (* y &) el asterisco como ya se indicó
se usa para crear una variable apuntador, es decir variables que almacenarán direcciones físicas de
algún lugar de la memoria del computador. El operador & (ampersand) se usa para accesar a los
datos

Ejemplo: Utilizando un archivo secuencial almacenar la información de las cuentas de los clientes de
un banco, por cada cuenta se guarda su código de cuenta, nombre del titula y saldo inicial.

El programa debe permitir registrar, buscar (por código y saldo), modificar (todos los registros, un solo
registro), eliminar un registro y listar los datos de las cuentas

#include <stdio.h>
#include <string.h>
struct reg{
char codigo[15];
char titular[30];
int saldo;
} cuenta;
using namespace std;

FILE *archivo;
Lo primero que se crea es una variable de tipo puntero o apuntador a un archivo a disco (instrucción
FILE y debe ser en MAYUSCULAS) llamada archivo. Las variables apuntadores son tipos especiales de
variables que tienen la capacidad de almacenar no datos, pero si direcciones ya sean de la memoria
del computador o como en este caso de una dirección física del disco. En C++ una variable apuntador
se declara anteponiendo un asterisco antes del nombre.

En el programa se está creando una variable apuntador con el nombre de “archivo” que almacenará
la dirección física del archivo en disco.

void insertar(){
clrscr();
gotoxy(30,6); cout<<"Ingrese los datos de la cuenta ";
gotoxy(30,8); cout<<"Codigo :";cin>>cuenta.codigo;
fflush(stdin);
gotoxy(30,9); cout<<"Titular :";gets(cuenta.titular);
gotoxy(30,10);cout<<"Saldo Inicial :";cin>>cuenta.saldo;
archivo = fopen("cuentas.dat","a+");
fwrite(&cuenta,sizeof(cuenta),1,archivo);
fclose(archivo);
gotoxy(30,12);cout<<"Cuenta Registrada Exitosamente";
}
47

Como segundo paso se abre el archivo con la instrucción fopen():

archivo = fopen(“cuentas.dat”,”a+”);

A continuación la instrucción fwrite(&alumno, sizeof(alumno),1,archivo); se encarga de


“trasladar” los datos desde la memoria hacia el disco o el almacenamiento permanente

Utiliza 4 parámetros que son:

1) Primer parámetro: fwrite(&cuenta) debe conocer que datos va a almacenar en disco, aquí se le
está indicando que es el dato que se tiene en la dirección de memoria donde está el registro
“cuenta”.
2) Segundo parámetro: fwrite((&cuenta, sizeof(cuenta)), debe conocer cuántos bytes de
información debe grabar, se usa sizeof() que regresa el tamaño del registro.
3) Tercer parámetro: fwrite((&cuenta, sizeof(cuenta),1), necesita conocer también cuantas
estructuras o registros a la vez debe grabar por lo general es un solo registro.
4) Cuarto parámetro: fwrite(&cuenta, sizeof(cuenta),1,archivo) se debe conocer exactamente en
qué clúster, sector y byte exacto del disco duro debe grabar el registro, se debe usar la variable
archivo que ya tiene esa dirección física del archivo en disco.

/////////////////////////////////////////
void listar() {
clrscr();
archivo = fopen("cuentas.dat","r");
gotoxy(30,2);cout<<"Informe general\n";
while(fread(&cuenta,sizeof(cuenta),1,archivo)==1){
cout<<"\nCodigo cuenta= "<<cuenta.codigo;
cout<<"\nTitular = "<<cuenta.titular;
cout<<"\nSaldo Inicial = "<<cuenta.saldo;
cout<<"\n======================================================";
}
fclose(archivo);
}
Para listar se abre el archivo como lectura para poder mostrar el listado de los alumnos registrados

while(fread(&cuenta,sizeof(cuenta),1,archivo)==1)

Se usa fread() con los mismos cuatro parámetros, fread(), regresa la cantidad de registros que leyó del
disco, por eso el ciclo while se convierte en falso cuando fread() regresa cero y esto indica que se llegó
al fin del archivo.

/////////////////////////////////////////
bool buscar(char clave[15]){
archivo = fopen("cuentas.dat","r");
while(fread(&cuenta,sizeof(cuenta),1,archivo)==1){
if ( strcmp(clave,cuenta.codigo)==0){
gotoxy(20,18);cout<<"***** Datos Encontrados *****";
gotoxy(20,19);cout<<" <==> Codigo = "<<cuenta.codigo;
48

gotoxy(20,20);cout<<" <==> Nombre = "<<cuenta.titular;


gotoxy(20,21);cout<<" <==> Saldo Inicial = "<<cuenta.saldo;
fclose(archivo);
return true;
}
}
fclose(archivo);
return false;
}/////////////////////////////////////////
Condiciones de búsqueda

En muchas ocasiones es necesario obtener información acerca de un subconjunto de renglones del


archivo. En este caso se listarán únicamente las cuentas que tengan un saldo mayor al ingresado por
el usuario

void saldo (int sal_aux){


clrscr();
gotoxy(10,4);cout<<"SALDO MAYOR O IGUAL QUE >= : "<<sal_aux<<endl;
archivo = fopen("cuentas.dat","r");
while(fread(&cuenta,sizeof(cuenta),1,archivo)==1) {
if (cuenta.saldo >= sal_aux){
cout<<" <==> Codigo "<<cuenta.codigo;
cout<<" <==> Nombre "<<cuenta.titular;
cout<<" <==> Edad "<< cuenta.saldo<<endl;
}
}
fclose(archivo);
}
/////////////////////////////////////////
Modificación de datos

Se puede realizar operaciones o procesos con los campos de los registros en el archivo secuencial, lo
único importante a considerar es que los campos del registro son en cierta medida igual que variables
normales y por tanto se pueden procesar de manera normal, por lo tanto se puede cambiar los datos
y colocar nuevos valores reemplazando los anteriores

Para modificar se deben utilizar dos punteros de tipo FILE el primero apunta al archivo original y el
segundo a un archivo auxiliar. El modo de apertura del archivo original es r+, es decir, permite la
escritura de datos pero no se pueden agregar nuevos registros, solamente se permite modificar los
existentes.

El archivo auxiliar se abre con ”w”, este modo de apertura crear el archivo temporal.dat si no existe, si
ya existiera se sobreescriben los datos existentes

void modificarSaldos(){
FILE *arch2;
FILE *arch1;
arch1 = fopen("cuentas.dat","r+");
49

arch2 = fopen("temporal.dat","w");
while(fread(&cuenta,sizeof(cuenta),1,arch1)==1){
cuenta.saldo= cuenta.saldo +50;
fwrite(&cuenta,sizeof(cuenta),1,arch2);
}

fclose(arch1);
fclose(arch2);
// removiendo y renombrando archivos
// recordar que directorios y archivos de datos no más de 8.3 letras
remove("cuentas.dat");
rename("temporal.dat","cuentas.dat");
gotoxy(20,18);cout<<"TODAS las CUENTAS se incrementaron en $50";
}

Edición de registros

Significa cambiar el contenido de algunos de los campos o columnas por nueva información o para
corregir algún error de captura original. La solución es similar al ejemplo anterior, se ocupan los dos
archivos el original y el temporal y ya sea que se modifique uno o todos los registros.

void modificarUno(char aux[15]){


FILE *arch1;
FILE *arch2;
clrscr();
arch1 = fopen("cuentas.dat","r");
arch2 = fopen("temporal.dat","a");

while(fread(&cuenta,sizeof(cuenta),1,arch1)==1){
if ( strcmp(aux,cuenta.codigo)==0){
fflush(stdin);
cout<<"Nuevo nombre :";gets(cuenta.titular);
cout<<"Nuevo saldo :";cin>>cuenta.saldo;
fwrite(&cuenta,sizeof(cuenta),1,arch2);
}
else
fwrite(&cuenta,sizeof(cuenta),1,arch2);
}
fclose(arch1);
fclose(arch2);
remove("cuentas.dat ");
rename("temporal.dat","cuentas.dat");
gotoxy(20,18);cout<<"*** Cuenta Modificada ***";
}
50

Eliminación de registros

Es el proceso por medio del cual algunos registros del archivo son eliminados permanentemente, para
realizar este proceso su utiliza el siguiente algoritmo: Se usan dos archivos, el archivo original y un
archivo temporal, se lee el registro del archivo original y si no es el registro a eliminar entonces se
almacena en el archivo temporal, cuando se termina de procesar todo el archivo original, el archivo
temporal solo contendrá todos los registros que no se quisieron eliminar, ya con estos dos archivo se
procede a borrar el archivo original y a renombrar el archivo temporal como nuevo archivo original.

void eliminar(char aux[15]){


FILE *arch1;
FILE *arch2;
arch1 = fopen("cuentas.dat","r");
arch2 = fopen("temporal.dat","a");
while(fread(&cuenta,sizeof(cuenta),1,arch1)==1){
if (strcmp(aux,cuenta.codigo)!=0)
fwrite(&cuenta,sizeof(cuenta),1,arch2);
}
fclose(arch1);
fclose(arch2);
remove("cuentas.dat");
rename("temporal.dat","cuentas.dat");
gotoxy(20,18);cout<<"*** Cuenta eliminada *** ";
}
/////////////////////////////////////////

int main() {
int opc;
bool esta=false;
int saldoaux=0;
char codaux[15];
do{
textbackground(1);
clrscr();
menu(&opc);
switch(opc){
case 1: { insertar();
break;
}
case 2: { gotoxy(20,17);cout<<"Codigo buscar:";
cin>>codaux;
esta=buscar(codaux);
if (!esta){
gotoxy(20,18);cout<<"Codigo no registrado";
}
break;
}
51

case 3: { gotoxy(20,17);cout<<"Listar Saldo mayor a $";


cin>>saldoaux;
saldo(saldoaux);
break;
}
case 4: { modificarSaldos();
break;
}
case 5: { gotoxy(20,17);cout<<"Codigo a modificar "; cin>>codaux;
modificarUno(codaux);
}
case 6: { gotoxy(20,17);cout<<"Codigo de la cuenta a eliminar: ";
cin>>codaux;
eliminar(codaux);

break;
}
case 7: { listar();

break;
}

}
gotoxy(24,24);
system("PAUSE");
}while(opc!=8);
return 0;
}
////////////////////////////////
void menu(int *opc){
gotoxy(20,4);cout<<"Registro de Cuentas bancarias";
gotoxy(20,6);cout<<"1. Registrar cuenta";
gotoxy(20,7);cout<<"2. Buscar por codigo";
gotoxy(20,8);cout<<"3. Buscar por saldo";
gotoxy(20,9);cout<<"4. Modificar todos los saldos";
gotoxy(20,10);cout<<"5. Modificar datos cuenta";
gotoxy(20,11);cout<<"6. Eliminar datos cuenta";
gotoxy(20,12);cout<<"7. Listado General";
gotoxy(20,13);cout<<"8. Finalizar";
gotoxy(20,15);cout<<"Ingrese su opcion";
do{
gotoxy(40,15);clreol(); cin>>*opc;
}while(*opc<1 || *opc>8);
}
52

Archivos directos (relativos, de acceso directo o aleatorio)

A diferencia de los archivos secuenciales, en los archivos de acceso directo no es necesario recorrerlo
completamente para acceder a un registro en particular, sino se puede colocar el apuntador interno
del archivo directamente en el registro deseado, permitiendo con esto mayor rapidez de acceso.
Cabe destacar que para reposicionar este apuntador se utiliza el comando SEEK indicándole la
dirección del registro que se desea.

Direcciones lógicas y direcciones físicas

El lenguaje C utiliza direcciones físicas para los archivos, esto es, que el direccionamiento consiste en
el espacio ocupado por los datos en el archivo(calculado en bytes) no en el renglón al que se asignó
dicho registro. Por ejemplo se tiene un archivo llamado “PRODUCTOS.DAT” que almacena registros con
el formato mostrado en la figura. Dicho registro tiene cinco campos, cada uno con un tipo de dato
determinado, un tamaño específico y que al sumarlos se obtiene el espacio ocupado por cada
registro con este formato

Cálculo de direcciones físicas

Para poder reposicionar el apuntador de un archivo en un registro específico es necesario calcular su


dirección física correspondiente de acuerdo al espacio ocupado por sus registros predecesores.

Archivo Productos.Dat
Dir. Dir.
No_prod Descrip Cantidad Precio Garantia
Lógica Física
0 0 0 Camisa 100 30000 N
1 39 1 Pantalón 50 45000 N
3 78 3 Televisor 20 350000 S
4 117 4 Zapatos 40 50000 S
5 156
int char[30] int float char

De esta forma, para convertir direcciones lógicas en direcciones físicas se utiliza la siguiente fórmula

long int dir_fisica, dir_logica;


dir_fisica=dir_logica*sizeof(Registro);
53

Reposicionar el apuntador mediante fseek()

Para comprender la operación de esta función es necesario estar relacionado con el apuntador del
archivo, el cual indica la posición dentro del archivo. Cuando se abre un archivo en modo de sólo
lectura, el apuntador del archivo se posiciona al inicio del mismo y cuando un archivo se abre en
modo escritura se posiciona al final, sin embargo, se puede reposicionar este apuntador del archivo
mediante la función fseek. También es importante mencionar que cada vez que se realiza una
operación de lectura o de escritura de cualquier tipo de datos (caracter, cadena, estructura, etc.), el
apuntador del archivo se mueve al final de dicho dato, de tal forma que está posicionado en el
siguiente, por lo que es muy importante asegurarse que se encuentre en la posición deseada antes de
realizar cualquier operación.

La función fseek permite reposicionar el apuntador del archivo en la dirección física deseada
mediante tres argumentos: el puntero del archivo, la dirección física (en bytes) y el punto de referencia.
Para poder reposicionar el apuntador del archivo es necesario que éste se encuentre abierto y se le
haya asignado el archivo correspondiente, también es necesario calcular la dirección física donde se
desea colocar e indicarle el punto de referencia de donde se partirá en el conteo de la cantidad de
bytes indicado por la dirección física, los cuales pueden ser desde el principio, desde donde se
encuentre el apuntador del archivo y desde el final.

fseek (puntero, dir_fisica, punto de referencia);

Conocer la posición del apuntador del archivo con la función ftell()

Se usa la función ftell para conocer la posición actual del apuntador de un archivo abierto. La posición
se expresa en bytes (dirección física) contados desde el principio del archivo.

long int dir_fisica;


dir_fisica=ftell(puntero del archivo);

Colocar el apuntador del archivo al principio con la función rewind()

Se usa la función rewind para colocar el apuntador del archivo al principio de un archivo abierto sin
necesidad de usar la función fseek. Basta con enviar el nombre del puntero del archivo como
argumento.

rewind(puntero del archivo);


54

#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <iomanip>
struct registro{
int no_prod;
char des[20];
int can;
float pre;
char act;
};
registro prod;
FILE *a_prod;

using namespace std;

void mostrar(registro aux){


cout<<setfill('.');
cout<<endl;
cout<<"Codigo"<<setw(10)<<aux.no_prod<<setw(20)<<"Descripcion"<<setw(20)<<aux.des;
cout<<endl;
cout<<"Cantidad"<<setw(10)<<aux.can<<setw(16)<<"Precio $"<<setw(25)<<aux.pre;
cout<<"\n--------------------------------------------------------------------";
}

void InsertaProducto(){
system("cls");
system("color 1B");
cout<<"\nRegistrar Productos\n";
a_prod = fopen("productos.dat", "ab+");
if (a_prod==NULL)
cout<<"\nNo se puede abrir el archivo !!!\n";
else{
rewind (a_prod);//ubica el puntero al inicio del archivo
fseek(a_prod,0,SEEK_END);
prod.no_prod=(ftell(a_prod)/sizeof(prod))+1;
cout<<"\nProducto No.=> "<<prod.no_prod <<" <=\n";
cout<<"\nDescripcion ==> ";cin>>prod.des;
cout<<"\nCantidad ==> ";cin>>prod.can;
cout<<"\nPrecio ==> ";cin>>prod.pre;
prod.act='s';
fwrite(&prod,sizeof(prod),1,a_prod);
cout<<"\nDatos almacenados exitosamente\n";
fclose(a_prod);
}
}
55

bool buscar(int aux){


system("cls");
system("color 1D");
cout<<"\nConsultar Productos"<<endl;
a_prod = fopen("productos.dat", "rb");
// Intenta abrir el archivo productos.dat en modo de solo lectura
if (a_prod==NULL){
cout<<"\nNo se puede abrir el archivo !!!";
return false;
}
else{
rewind(a_prod);
int dir=(aux-1)*sizeof(prod); // Calculo de la dir. fisica
fseek(a_prod,dir,SEEK_SET); //Posicionar el apuntador del archivo
// Lee el "registro", de tamano=sizeof(prod) del archivo "a_prod"
fread(&prod,sizeof(prod),1,a_prod);
if(prod.no_prod==aux && prod.act=='s'){
mostrar(prod);
fclose(a_prod);
return true;
}
}
return false;
}

void modificar(int aux){


system("cls");
system("color 1E");
cout<<"\nModificar Productos\n";
a_prod = fopen("productos.dat", "rb+");// Intenta abrir el archivo productos.dat
// en modo de lectura y escritura
if (a_prod==NULL)
cout<<"\nNo se puede abrir el archivo !!!\n";
else {
int dir=(aux-1)*sizeof(prod); // Calculo de la dir. fisica
fseek(a_prod,dir,SEEK_SET); //Posicionar el apuntador del archivo
fread(&prod,sizeof(prod),1,a_prod);
if(prod.no_prod==aux && prod.act=='s'){
cout<<"\nDatos Actuales";
mostrar(prod);
cout<<"\nIngrese nuevos datos\n";
cout<<"\nProducto No.=> "<<prod.no_prod <<" <=";
cout<<"\nDescripcion ==> ";fflush(stdin);gets(prod.des);
cout<<"\nCantidad ==> ";cin>>prod.can;
cout<<"\nPrecio ==>";cin>>prod.pre;
prod.act='s';
56

// Es necesario reposicionar el apuntador del archivo al principio del


// registro que desea modificar, ya que al leer un registro, el
// apuntador se posiciona en el registro siguiente
// La funcion ftell(a_prod) devuelve la posicion donde se encuentra el
// apuntador
fseek(a_prod,dir,SEEK_SET); //Posicionar el apuntador del archivo
fwrite(&prod,sizeof(struct registro),1,a_prod);
// Graba el registro con los nuevos campos
cout<<"\nDatos modificados exitosamente\n";
}
else
cout<<"\nNo existe ese Producto!!!\n";
fclose(a_prod);
}
}
void eliminar(int aux){
system("cls");
system("color 1A");
cout<<"\nEliminar Productos\n";
a_prod = fopen("productos.dat", "rb+");// Intenta abrir el archivo productos.dat
// en modo de lectura y escritura
if (a_prod==NULL)
cout<<"\nNo se puede abrir el archivo !!!\n";
else{
int dir=(aux-1)*sizeof(prod); // Calculo de la dir. fisica
fseek(a_prod,dir,SEEK_SET); //Posicionar el apuntador del archivo
fread(&prod,sizeof(prod),1,a_prod);
if(prod.no_prod==aux && prod.act=='s'){
mostrar(prod);
prod.act='n';
fseek(a_prod,dir,SEEK_SET); //Posicionar el apuntador del archivo
fwrite(&prod,sizeof(struct registro),1,a_prod);
cout<<"\nDatos eliminados exitosamente\n";
fclose(a_prod);
}//fin if comparación
else
cout<<"\nNo existe ese producto !!!\n";
fclose(a_prod);
}//fin else abrir archivo
}//fin eliminar
void listar(){
system("cls");
system("color 1E");
int muestra=0;
cout<<"\nListado General Productos\n";
a_prod = fopen("productos.dat", "rb");
57

if (a_prod==NULL)
cout<<"\nNo se puede abrir el archivo !!!\n";
else{
while(fread(&prod,sizeof(prod),1,a_prod)==1){
if(prod.act=='s'){
mostrar(prod);
muestra++;
if(muestra==5){
cout<<endl;;
system("pause");
system("cls");
muestra=0;
}
} //fin if
}//fin while
}//fin else
}
void opcionesListar(char op){
switch(op){
case 'P':{
rewind(a_prod);
fread(&prod,sizeof(prod),1,a_prod);
mostrar(prod);
break;
}
case 'S':{
fread(&prod,sizeof(prod),1,a_prod);
if (!feof( a_prod ) )
mostrar(prod);
else
cout<<"\nFin de archivo\n";
break;
}
case 'A':{
long int pos;
if (feof( a_prod ) ){
fseek(a_prod,0,SEEK_END);
pos = ftell(a_prod) - sizeof(prod);
}
else
pos = ftell(a_prod) - (2 * sizeof(prod));
if ( pos >= 0 ){
fseek(a_prod,pos,SEEK_SET);
fread(&prod,sizeof(prod),1,a_prod);
mostrar(prod);
}
58

else{
cout<<"Inicio del archivo";
rewind(a_prod);
}
break;
}
case 'U':{
rewind(a_prod) ;
fseek(a_prod,-sizeof(prod),SEEK_END);
fread(&prod,sizeof(prod),1,a_prod);
mostrar(prod);
break;
}
case 'T':{
listar();
break;
}
}//fin switch
if (prod.act=='n')
cout<<"\nProducto inactivo\n";
cout<<endl;
system("pause");
}

int menu(){
system("cls");
system ("color 17");
int res;
cout<<"(1)Registrar";
cout<<" (2)Consultar";
cout<<" (3)Modificar";
cout<<" (4)Eliminar ";
cout<<" (5)Listar ";
cout<<" (6)Salir";
do{
cout<<"==> "; cin>>res;
} while(res<1 || res>6);
return res;
}

char menuListar(){
char lis;
system("cls");
system("color 1A");
cout<<"\nOpciones de listado\n";
cout<<"(P)rimero";
59

cout<<" (S)iguiente";
cout<<" (A)nterior";
cout<<" (U)ltimo";
cout<<" (T)odos";
cout<<" (R)etornar";
do{
cout<<"==> "; cin>>lis;
lis=toupper(lis);
} while(lis!='P' && lis!='S'&& lis!='A'&& lis!='U' && lis!='T' && lis!='R');
return lis;
}
int main()
{
int opc; bool esta=false;
int codaux; char resL;
do{
opc= menu();
switch(opc){
case 1:{ InsertaProducto();
break;
}
case 2:{ cout<<"Codigo buscar:";
cin>>codaux;
esta=buscar(codaux);
if (esta==false)
cout<<"Producto no encontrado";
break;
}
case 3:{ cout<<"Codigo a modificar:";
cin>>codaux;
modificar(codaux);
break;
}
case 4:{ cout<<"Codigo a eliminar:";
cin>>codaux;
eliminar(codaux);
break;
}
case 5:{ a_prod = fopen("productos.dat", "rb");
if (a_prod==NULL)
cout<<"No se puede abrir el archivo !!!";
else
do{
resL=menuListar();
opcionesListar(resL);
}while(resL!='R');
60

fclose(a_prod);
break;
}
case 6:{
cout<<"Fin de la aplicacion";
break;
}
}//fin switch
cout<<endl;
system("pause");
}while (opc!=6);
return 0;
}

También podría gustarte