Libro Talleres de Programacion II
Libro Talleres de Programacion II
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.
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:
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).
• 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>
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.
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.
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()
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:
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;
}
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
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.
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.
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.
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.
− 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 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
}
Ejemplo 7
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
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
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
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:
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:
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:
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í:
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).
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 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
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:
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
int main(){
for (int n = 0; n < 4; n++)
cout << "3 a la potencia " << n << " es " << potencia(3, n) << endl;
system("PAUSE");
return 0;
}
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.
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:
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
• 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:
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.
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
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.
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:
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:
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.
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;
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
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
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.
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.
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 ] .
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.
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.
Registros o Estructuras
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:
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:
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
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.
};
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:
struct discos{
char titulo[30];
char artista[25];
int canciones;
float precio;
char fecha[8];
}cd1,cd2,cd3;
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
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’};
La asignación de datos a estructuras también se puede hacer mediante el operador punto así:
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.
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.
#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;
};
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;
};
system("PAUSE");
return 0;
}
37
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
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).
struct datos{
int matricula;
char nombre[30];
float cal[5];
};
using namespace std;
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.
Por lo general, un registro (es decir, struct en C) está compuesto de varios campos conocidos como
miembros.
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:
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.
#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”;
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
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.
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:
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
archivo = fopen(“cuentas.dat”,”a+”);
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
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.
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.
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
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
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.
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
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
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.
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.
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.
#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;
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
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;
}