Professional Documents
Culture Documents
en
Lenguajes
Estructurados
Ciclo
de
Desarrollo de Aplicaciones Informáticas
© A.L.I. D.A.I. Programación en lenguajes estructurados. Página: i
INTRODUCCIÓN.
El contenido organizador debe comprender y aglutinar todas las capacidades que pretendemos
que desarrolle el alumno y que vienen expresadas como capacidades terminales del módulo en el
Real Decreto que define el título.
Analizando las capacidades terminales del módulo, teniendo en cuenta el perfil profesional
del título y considerando las responsabilidades asignadas a la función de administración en el
entorno de los sistemas, hemos optado por el siguiente enunciado para el contenido organizador:
Elaborar, adaptar y probar programas para mejorar la explotación del sistema y las aplicaciones.
OBJETIVOS.
1.- Introducción.
El propósito fundamental de éste módulo es la resolución de problemas mediante un ordenador.
Los objetivos que se pretender conseguir son los siguientes:
· Diseño del algoritmo: describe la secuencia ordenada de pasos que conducen a la solución de
un problema dado.
· Expresar el algoritmo como un programa en un lenguaje de programación adecuado: C, C++,
Pascal, etc..
· Validación y ejecución del programa por el ordenador: comprobar si el programa resuelve o no
el problema planteado y es válido para todo el conjunto de datos posibles.
PROBLEMA -> DISEÑO DEL ALGORITMO -> PROGRAMA DE ORDENADOR -> EJECUCIÓN
Se pueden citar infinidad de ejemplos de algoritmos de la vida cotidiana, por ejemplo el manual
de instalación de un televisor, manual de uso de una radio, manual de reparación de una bicicleta,
pasos para la instalación de un juego en el ordenador, etc. En definitiva, un algoritmo es cualquier
manual de instrucciones que describa paso a paso las acciones que debemos realizar para conseguir
una meta final.
· Un algoritmo debe ser preciso. Debe indicar el orden correcto de la realización de cada paso.
Cada uno de estos pasos debe realizarse en el orden en que se indican para poder solucionar el
problema. No debemos encender el ordenador sin haberlo conectado previamente a la red
eléctrica o no debemos tirar de la palanca de cambio de marcha del coche sin haber pisado
antes el embrague por que los resultados son imprevisibles.
· Un algoritmo debe estar bien definido. Si un algoritmo se sigue dos veces se debe obtener el
mismo resultado cada vez; en caso de no obtener el mismo resultado quiere decir que no está
bien diseñado y, por tanto, hay que corregir su diseño. Por ejemplo, el camino que seguimos
todos los días para ir a clase nos conduce al mismo sitio, está bien definido. Esto no implica
que el camino (algoritmo) para cumplir el objetivo sea único; puede haber, de hecho hay,
distintos caminos para llegar al mismo lugar.
· Un algoritmo debe ser finito. Si un algoritmo se sigue, este debe terminar en algún momento;
o sea, debe tener un número finito de pasos. Podemos plantearnos una pregunta: ¿Llegamos o
no llegamos a clase?
1) ¿Cuales y cuantas son las entradas? Son las premisas (datos) que proporciona el enunciado
del problema. Por ejemplo, para calcular la velocidad a la que se desplaza un cuerpo pueden
darnos la siguiente descripción: calcular la velocidad a la que corre un atleta los cien metros
lisos si tarda en realizarlos diez segundos. Las entradas que nos proporciona el enunciado son
cien metros y diez segundos.
2) ¿Cuales y cuantas son las salidas? Son los resultados (datos) que nos pide el enunciado. En el
ejemplo anterior nos indica que debemos calcular la velocidad de desplazamiento.
3) ¿Cual es el método que produce las salidas a partir de las entradas? Habitualmente, para
resolver cualquier problema matemático (y cualquier problema en general) combinamos
adecuadamente las premisas proporcionadas por el enunciado para obtener los resultados. El
método es, entonces, el conjunto de operaciones realizadas con los datos de entrada para poder
ofrecer una solución, unos resultados. En el ejemplo, el método consiste en la aplicación de la
fórmula para calcular la velocidad (v= s/t).
Ejemplos:
Las entradas son la Base y la Altura. El enunciado no indica cuales son las medidas de los
lados del rectángulo; somos nosotros quienes debemos proporcionar los valores numéricos para esas
entradas.
Normalmente los pasos diseñados en un primer esbozo son incompletos, con muy pocos pasos;
posteriormente se desarrolla otro, basado en el anterior, más completo, con mayor número de pasos y
mucho más específicos. Esta técnica de desarrollo de algoritmos con diferentes niveles de complejidad
se denomina "refinamiento del algoritmo".
Ejemplos:
Subproblema Refinamiento
introducir la BASE introducir la BASE
Subproblema Refinamiento
Dar coeficientes introducir A
Introducir B
Introducir C
Obtener raices x1=-b+sqrt(b*b-4*a*c)/(2*a)
X2=-b-sqrt(b*b-4*a*c)/(2*a)
¿Errores?
Sacar resultados Visualizar x1
Visualizar x2
¿Errores?
Tras haber realizado la división del problema en subproblemas, hay que representar el
algoritmo con una determinada herramienta de programación como es el diagrama de flujo o el
pseudocódigo.
· Deben estar seguidas de alguna secuencia definida de pasos hasta que se obtenga un resultado
diferente.
· Sólo puede ejecutarse una operación a la vez.
ir al cine
comprar la entrada
ver la película
regresar a casa
Como puede observarse, el algoritmo es muy simple; consta de cuatro acciones que se realizan
una a continuación de otra y en un orden estricto, de forma que una acción no se realiza hasta que no
ha sido realizada la anterior: sin embargo, este algoritmo, se puede descomponer en pasos más simples
siguiendo el método de refinamiento sucesivo. Así, en un primer refinamiento, el algoritmo anterior se
puede escribir de la siguiente forma:
Inicio.
ver la cartelera del cine
si no proyectan "Casablanca"
entonces decidir otra actividad
· Hay palabras reservadas que describen las estructuras de control fundamentales y procesos de
toma de decisión. Incluyen los conceptos de selección (si_entonces_sino) y los de repetición
(mientras_hacer, repetir_hasta).
· Se ha empleado la identación (sangrado o justificación) para la escritura del algoritmo, es decir,
las acciones que van dentro de las estructuras fundamentales se han desplazado a la derecha.
inicio
caminar hasta llegar a la primera fila
repetir
comparar número fila con número billete
si no son iguales entonces
pasar a la siguiente fila
hasta que se localice la fila correcta
mientras número asiento no igual número billete hacer
avanzar en fila al siguiente asiento
sentarse en el asiento
fin.
Como puede observarse, podemos seguir aplicando el método de refinamiento sucesivo sobre
el algoritmo, todo depende de la imaginación del programador. Otro ejemplo de algoritmo es el que
responde a "cambiar una rueda de un coche", prodría ser el siguiente:
inicio
si el gato del coche está averiado entonces
llamar a la estación de servicio
sino poner el gato en su alojamiento
repetir
aflojar los tornillos de las ruedas
hasta que todos los tornillos estén flojos
repetir
levantar el gato
hasta que la rueda pueda girar
Inicio Fin
Entrada / Salida
Proceso. Se utiliza siempre que los datos son manipulados o procesados. Los dos tipos
generales de operaciones de proceso son operaciones aritméticas y operaciones de transferencia de
datos.
PROCESO
Decisión. Indica operaciones lógicas entre los datos y en función del resultado determina cual
de los distintos caminos debe seguir el programa.
Conectores. Se usan para unir partes lejanas de un diagrama de flujo; por ejemplo cuando un
algoritmo no cabe en una página y debemos unir acciones de una a otra página.
Ejemplos:
1.- Cálculo del área de un triángulo.
2.- Cálculo del área y de la circunferencia de un círculo.
5.2.- Pseudocódigo.
El pseudocódigo es un lenguaje de especificación de algoritmos. El uso de tal lenguaje hace el
paso de codificación final (traducción a un lenguaje de programación) relativamente fácil.
Nació como un lenguaje similar al inglés y era un medio para representar básicamente las
estructuras de control de programación estructurada. Se considera un primer borrador, dado que ha de
traducirse posteriormente a un lenguaje de programación. Su ventaja es que el programador se puede
concentrar en la lógica y en las estructuras de control y no preocuparse de las reglas de un lenguaje
específico.
El uso del pseudocódigo se ha extendido ampliamente con términos en español como inicio,
fin, leer, escribir, visualizar, repetir_hasta, si_entonces_sino, mientras_finmientras, etc. Usar esta
terminología en español facilita enormemente el aprendizaje y uso diario de la programación.
Ejemplos:
1.- Cálculo del área de un triangulo.
2.- Cálculo del área y de la circunferencia de un círculo.
· Lenguaje máquina
· Lenguaje de bajo nivel
· Lenguajes de alto nivel
Las instrucciones en lenguaje máquina dependen del hardware del ordenador y, por tanto,
diferirán de un ordenador a otro.
Un programa escrito en este lenguaje no puede ser ejecutado directamente por el ordenador,
sino que necesita una fase de traducción a lenguaje máquina.
Las ventajas e inconvenientes son los mismos que los del lenguaje máquina, aunque más
relajados.
Ventajas:
· Formación corta.
· La escritura de programas se basa en reglas sintácticas similares a los lenguajes
humanos (READ, PRINT, ...).
· Fácil modificación y puesta a punto.
· Reducción del coste de los programas.
· Transportabilidad.
Inconvenientes:
· Incremento del tiempo de puesta a punto de los programas al necesitarse diferentes
traducciones del programa.
· No se aprovechan los recursos internos de la máquina que se explotan mejor en los
lenguajes máquina y ensambladores.
· Aumento de la ocupación de memoria.
· El tiempo de ejecución de los programas es mucho mayor.
Al igual que en los lenguajes ensambladores, los programas escritos en lenguajes de alto nivel
deben ser traducidos a lenguaje máquina.
Una vez diseñado el lenguaje de alto nivel, como la máquina no reconoce mas que el lenguaje
máquina, es necesario convertir los programas escritos en el primero al segundo. Esta conversión
puede ser realizada de dos formas diferentes: compilación e interpretación.
6.5.1.- Compiladores.
Los compiladores son programas que toman cada instrucción del lenguaje de alto nivel y la
convierten en una secuencia equivalente de instrucciones del lenguaje máquina. El resultado es un
nuevo programa escrito en lenguaje máquina inteligible por misma. Esta técnica se denomina
traducción y al programa compilador.
6.5.2.- Intérpretes.
Los intérpretes son programas que toman programas escritos en lenguaje de alto nivel como
datos de entrada y los lleva a cabo examinando cada instrucción por turno y ejecutando directamente la
secuencia equivalente de instrucciones en lenguaje máquina. A diferencia del anterior, no requiere la
Para realizar la conversión del algoritmo en programa se deben sustituir las palabras reservadas
en español por sus homónimos en inglés, y las operaciones/instrucciones indicadas en el lenguaje
natural expresarlas en el lenguaje de programación correspondiente.
Una vez obtenido el programa ejecutable, ya se puede ejecutar con solo teclear su nombre. Si
no existen errores de ejecución se obtendrán los resultados, en otro caso volvemos a corregir sobre el
programa fuente y realizamos los pasos anteriores.
La depuración es el proceso de encontrar los errores y corregirlos. Se pueden dar tres tipos de
errores:
· Errores de compilación: Se producen por el uso incorrecto de las reglas del lenguaje de
programación.
· Errores de ejecución: producidos por instrucciones que el ordenador puede comprender pero no
ejecutar. Ejemplo, división por cero o raíz negativa.
· Errores lógicos: se producen en la lógica del programa y la fuente del error suele ser el
algoritmo. Son los más frecuentes y los más difíciles de detectar.
Ejercicios.
Ejemplos de datos numéricos enteros son: 5, 7, 23, 123, 214, 0, -8, -45, -98 etc.
· Numérico real: es un subconjunto de los números reales. Tienen parte decimal (y punto
decimal) y también pueden ser positivos y negativos.
Ejemplos de datos numéricos reales son: -0.234, -67.0, 87.34, 3.1416, etc.
Una cadena de caracteres es una sucesión de caracteres que se encuentran delimitados por una
comilla (apóstrofe) o dobles comillas. Su longitud es el número de caracteres que están encerrados
entre las comillas.
Ejemplos de datos de tipo alfanumérico son: 'Pepe', "Luís", '1 de Enero de 1993', etc.
ejemplo, cuando se pide determinar si un valor entero es par, la respuesta es verdadera o falsa según
sea par o impar.
2.1.- Identificador.
En un ordenador los datos necesitan ser manejados mediante herramientas que permitan
almacenarlos en memoria, leerlos, operar con ellos, etc. En los lenguajes de alto nivel se necesitan
nombres para identificar los objetos que se deseen manipular: variables, constantes, procedimientos,
etc. Los identificadores son los nombres que se designan para dicho propósito. Estos permiten elegir
nombres significativos que sugieran lo que representan. Los identificadores se construyen de acuerdo a
las reglas de sintaxis del lenguaje específico. Estas básicamente son:
2.2.- Constante.
Es una partida de datos (objetos) que permanecen sin cambios durante todo el desarrollo del
algoritmo o durante toda la ejecución del programa. Existen constantes lógicas, enteras, reales, carácter
y cadena.
2.3.- Variable
Es un objeto o partida de datos cuyo valor puede cambiar durante el desarrollo del algoritmo o
ejecución del programa. Dependiendo del lenguaje, hay diferentes tipos de variables, pero
generalmente son enteras, reales, carácter, lógicas y cadena. Una variable que es de un cierto tipo
puede tomar valores únicamente de ese tipo. Una variable carácter, por ejemplo, puede tomar como
valor sólo caracteres, mientras una variable entera puede tomar sólo valores enteros.
Para que una variable esté bien definida, hay que especificar: Nombre, tipo de dato y valor
inicial que toma. Los nombres de variables elegidos deben ser significativos, como pueden ser los
casos siguientes:
3.- Expresiones.
Las expresiones son combinaciones de constantes, variables, símbolos de operación, paréntesis
y nombres de funciones especiales.
Las mismas ideas son utilizadas en notación matemática tradicional. Por ejemplo:
a + (b + 3) + c
Aquí los paréntesis indican el orden de cálculo y c representa la función raíz cuadrada de 'c'.
Cada expresión toma un valor que se determina tomando los valores de las variables y
constantes implicadas y la ejecución de las operaciones indicadas. Una expresión consta de operandos
y operadores. Según sea el tipo de objetos que manipulan, se clasifican en:
+ Suma
- Resta
* Multiplicación
/ División
**, ↑ Potencia
div División entera
mod Módulo (resto de la división)
Expresión Resultado
(3+2)*5 25
6-2 36
40 div 6 6
23 mod 2 1
La aplicación de los operadores de relación es evidente. Por ejemplo si 'A=6' y 'B=5' entonces
'A > B' es verdadero, mientras '(A-2) < (B-4)' es falso.
Para realizar comparaciones de datos tipo carácter, se requiere una secuencia de ordenación de
los caracteres, similar al orden creciente o decreciente. Esta ordenación suele ser alfabética, tanto
mayúsculas como minúsculas, considerándolas de modo independiente, pero si se consideran
caracteres mixtos, se debe recurrir a un código normalizado como es el ASCII.
· Los valores de los caracteres que representan a los dígitos están en su orden natural. Esto es
'0'<'1'<'2'<...<'9'.
· Las letras mayúsculas 'A' a 'Z' siguen el orden alfabético. ('A'<'B'<'C'<...<'Z').
· Las letras minúsculas están en el mismo orden alfabético. ('a'<'b'<'c'<...<'z').
- Operadores lógicos: los operadores lógicos o booleanos básicos son NOT (no, negación),
AND (y, intersección) y OR (o, unión). La definición de los operadores lógicos se resume en las
siguientes tablas, denominadas tablas de verdad.
A NO A
FALSO CIERTO
CIERTO FALSO
A B AYB
FALSO FALSO FALSO
FALSO CIERTO FALSO
CIERTO FALSO FALSO
CIERTO CIERTO CIERTO
AND o Y un operador binario, afecta a dos operandos. La expresión formada es cierta cuando
ambos operandos son ciertos al mismo tiempo. Por ejemplo: es de día y hace sol. 5>3 y 5>4.
A B AOB
FALSO FALSO FALSO
FALSO CIERTO CIERTO
CIERTO FALSO CIERTO
CIERTO CIERTO CIERTO
OR u O es un operador binario. La expresión que forma es cierta cuando al menos uno de sus
operandos es cierto. Por ejemplo: es de día o es de noche.
Ejercicios.
a) 4 * 3 + 5 17
b) 7 * 10 - 15 mod 3 * 4 + 9 79
c) (7 * (10 - 5) mod 3) * 4 + 9 17
d) - 4 * 7 + 2 ** 3 / 4 - 5 -31
5(x + y)
x2 + y2
x
y(z + w)
x+ y
w
n+
a
- b ± b2 - 4ac
x=
2a
Valores: a = 3, b = 6, c = 4
d) (a + 5) * 3 / 2 * b - b 66
Valores: a = 3, b = 6
Entorno:
Nombre Tipo Valor inicial
Lista de objetos Tipo de los objetos
Cuando se declaran en una misma sección variables y constantes, estas se situarán delante de la
variables. Ejemplos:
Entorno:
Nombre Tipo Valor inicial
AREA N. Real
BASE N. Entero
ALTURA N. Entero
Entorno:
Nombre Tipo Valor inicial
PI N. Real 3.14159
AREA_CIRCULO N. Real
LONG_CIRCUNF N. Real
Usada para darle el valor que le corresponda a una variable. La operación de asignación se
representa con el símbolo u operador ““ (flecha con sentido a la izquierda). Esta instrucción es
conocida como sentencia de asignación. El formato general de su uso es
IDENTIFICADOR EXPRESIÓN
Ejemplo:
El valor inicial del objeto denominado 'PRECIO' es el siguiente:
1253 PRECIO
PRECIO 8312
8312 PRECIO
a) Evalúa la expresión situada a la derecha del operador de asignación, y obtiene, por tanto, un
valor de un tipo de dato específico.
b) El valor obtenido se almacena en la variable cuyo identificador está a la izquierda del
operador de asignación, sustituyendo el valor que tenía anteriormente.
Ejemplo:
x (- b + (b * b - 4 * a * c) ** (1 / 2)) / (2 * a)
Las sentencias de asignación se clasifican según el tipo de dato de las expresiones. Por tanto,
pueden ser aritméticas, lógicas y de carácter. Ejemplos válidos de sentencias de asignación en
pseudocódigo son las siguientes:
a4
b 25
ca*b
nombre 'José' + 'Luis’
continuar verdadero
fin a > b and continuar
En ocasiones, los cálculos realizados por el ordenador requieren de entrada de datos externos
para realizar las acciones necesarias para obtener los resultados. Consiste en darle al ordenador un dato
(valor) desde el dispositivo estándar de entrada (teclado) y este lo almacenará en memoria principal en
el objeto cuyo identificador aparece en la propia instrucción.
Introducir Objeto/Lista_de_Objetos
donde “Introducir” es una palabra reservada que indica el tipo de instrucción a realizar y
“Objeto/Lista_de_Objetos” es el conjunto de elementos (objetos) donde se van a depositar en memoria
principal los datos leídos. Si hay más de un elemento, se separan por comas.
Ejemplo:
Introducir BASE, ALTURA
Si damos los valores 5 y 6 desde teclado, el objeto “BASE” tomará el valor 5 y el objeto
“ALTURA” el valor 6.
La sentencia de entrada de datos suele (debe, según los casos) ir precedida de una sentencia de
salida de datos (se verá a continuación) que nos identifique el tipo de entrada que se nos pide por
pantalla.
La sentencia se salida de datos es la forma principal de comunicación del ordenador con las
personas. Consiste en que el ordenador envía datos (información procesada) al dispositivo estándar de
salida (el monitor); estos datos pueden ser:
Visualizar Expresión/Lista_de_Expresiones
donde “Visualizar” es una palabra reservada que indica el tipo de instrucción a realizar y
“Expresión/Lista_de_Expresiones” es el conjunto de informaciones y datos que queremos mostrar. Los
argumentos de la instrucción son los siguientes:
· Objetos: su valor -el contenido almacenado para el objeto en memoria- se visualizará por
el monitor, no se visualizará el identificador del objeto.
· Valores: datos constantes que se visualizarán por pantalla tal como están definidos en la
propia instrucción. En caso de ser de tipo carácter, irán encerrados entre comillas o
apóstrofes.
· Expresiones: son todo tipo de expresiones posibles de construir. La expresión es evaluada
y únicamente su valor final resultará mostrado por pantalla.
Los tres tipos de argumentos pueden entrar a formar parte de la instrucción de salida, debiendo
ir separados por comas.
Ejemplos:
Visualizar 'El área de rectángulo de base ', BASE , ' y altura ', ALTURA , ' es ', BASE *
ALTURA
Visualizar 'El área de rectángulo es ', AREA
Ejercicios :
1. Programa que calcule y escriba el cuadrado del número 243.
2. Programa que lea un número y escriba su cuadrado.
3. Programa que calcule y escriba el perímetro y el área de un rectángulo cuya base y altura se
leen desde teclado.
4. Algoritmo que calcule la hipotenusa de un triángulo rectángulo cuyos catetos se dan por
teclado.
5. Programa que intercambie los valores de dos variables numéricas.
6. Programa que lee una cantidad menor de 1000 y la desglosa en unidades, decenas y centenas.
7. Programa que lee una cantidad de dinero y la desglosa en las monedas de 1, 5, 25 y 100
pesetas.
Una instrucción de selección o decisión es aquella que controla la ejecución de uno o varios
bloques de instrucciones, dependiendo del cumplimiento o no de alguna condición o valor final de
alguna expresión; es decir, se evalúa una condición o expresión y en función de su resultado se bifurca
a un determinado punto del programa. Las estructuras selectivas se utilizan para tomar decisiones
lógicas. Pueden ser de tres tipos: simples, dobles y múltiples.
Se evalúa una condición y si esta es verdadera se realiza una acción o un bloque de acciones ;
si, por el contrario, es falsa, no se realiza ninguna acción. El formato es el siguiente:
si CONDICIÓN
Instrucción/es
fin si
Esta estructura nos permite elegir entre la realización de dos posibles acciones. Si la condición
es verdadera se realiza una o varias acciones y si es falsa se realiza otro grupo de acciones. La
alternativa simple es un caso particular de la alternativa doble. El formato de la sentencia alternativa
doble es el siguiente:
si CONDICIÓN
Instrucción/es si CONDICION ES CIERTA
si no
Instrucción/es si CONDICION ES FALSA
fin si
La alternativa de decisión múltiple evalúa una expresión que podrá tomar N valores diferentes.
Según se elija uno de estos valores en la condición, se realizará una de las N acciones posibles. Las
distintas opciones deben ser disjuntas entre sí, es decir, sólo puede cumplirse una de ellas ya que los
valores de cada opción no se pueden repetir. El formato es el siguiente:
Evaluar Expresión_Evaluar
Cuando VALOR_1
Instrucción/es 1
Cuando VALOR_2
Instrucción/es 2
Cualquier otro
Instrucción/s N Bloque opcional
Fin evaluar
Para cada valor de la expresión se pueden ejecutar una o varias acciones. Los valores de la
expresión no tienen por qué ser consecutivos pero sí únicos; se pueden considerar rangos de constantes
numéricas o de caracteres (según los lenguajes).
Ejercicios :
1. Decir si un número es positivo, negativo o nulo.
2. Algoritmo que nos diga si un número N es o no múltiplo de 2.
3. Programa que calcula las raíces de la ecuación de segundo grado.
4. Programa que lee una calificación numérica entera comprendida entre 0 y 10 y la transforma en
una calificación alfabética.
5. Programa que toma como entrada el número de un mes y nos indica el número de días que
tiene.
· Decisión: Se sitúa una condición que puede ser verdadera o falsa y se comprueba una vez a
cada paso o iteración del bucle, determinando la continuidad o finalización del mismo. La
condición determina el número de veces que se deben ejecutar las acciones.
· Salida del bucle: Finalización del bucle. A continuación están situadas el resto de las
instrucciones que componen el programa.
Mientras Repetir
En un programa pueden existir varios bucles; estos pueden ser independientes o anidados. Son
anidados cuando están dispuestos de tal modo que unos son interiores a otros.
Es aquella en la que el cuerpo del bucle se repite mientras se cumple la condición. Su formato
es el siguiente :
Mientras CONDICION
Instrucción/es
Fin mientras
Al ser la condición de finalización la primera acción que se realiza del bucle, puede darse el
caso de que el cuerpo del bucle no se ejecute ninguna vez. En ocasiones esta opción es necesaria.
Ejercicio: Visualizar los números menores que N, siendo N un número introducido por teclado.
Se ejecuta hasta que se cumpla una condición determinada que se comprueba al final del bucle.
El formato es el siguiente :
Repetir
Instrucción/es
Hasta CONDICION
Como la última acción que se realiza en el bucle es la evaluación de la condición, el cuerpo del
bucle se realiza siempre, al menos una vez.
Las diferencias principales entre los bucles mientras y repetir son estas:
a) El bucle mientras finaliza cuando la evaluación de la condición da como resultado falso y el
repetir cuando es verdadero.
b) El cuerpo del bucle repetir se realiza una o más veces y el cuerpo del bucle mientras se
ejecuta 0 o más veces.
Controla la ejecución del conjunto de instrucciones que configuran su rango, de tal forma que
estas se ejecutan un número determinado de veces que queda definido en la cabecera del bucle. En ella
se define un identificador de variable que va a actuar como contador asociado y que se denomina
variable de control del bucle (Vc), definiéndose al mismo tiempo su valor inicial (Vi), su valor final
(Vf) y el incremento (In) que esta variable de control va a adquirir en cada repetición. El formato es el
siguiente :
Los valores Vi, Vf e In pueden estar expresados por un valor constante, una variable o una
expresión ; entendemos que si son variables, estas no se modificarán en el cuerpo del bucle. De igual
forma, la Vc tampoco debe ser modificada en el cuerpo del bucle. El número de iteraciones que se
realizarán en una instrucción para viene dado por la expresión :
Vf − Vi
Ni = parte entera +1
In
Instrucción 1 Instrucción 1
Instrucción 2 Instrucción 2
Instrucción 3 Instrucción 3
... ...
Instrucción 9 Instrucción 9
... ...
Salto hacia delante Salto hacia atrás
Bifurcación incondicional:
Se realiza siempre que el flujo del programa pase por la instrucción sin necesidad del
cumplimiento de ninguna condición.
Ir a <etiqueta>
Bifurcación condicional:
La bifurcación depende del cumplimiento de una determinada condición. Si la
condición es cierta se realiza la bifurcación, y si es falsa no se realiza.
Si CONDICION ir a <etiqueta>
Un contador es una variable cuyo valor se incrementa o decrementa en una cantidad constante
en cada iteración. El valor de esta constante no tiene por que ser la unidad; puede ser cualquier valor,
pero en todo el programa debe conservar siempre dicho valor. Antes de usarlo, debemos darle un valor
inicial (0, 1).
3.2.- Acumuladores.
El acumulador o totalizador es una variable cuya misión es almacenar cantidades variables
resultantes de sumas sucesivas. Realiza la misma función que un contador con la diferencia de que el
incremento o decremento de cada suma es una variable en lugar de una constante. Antes de usarlo,
debemos darle un valor inicial.
3.3.- Interruptores.
Son objetos que sólo pueden tomar uno de dos valores posibles CIERTO o FALSO, 1 ó -1,
ETC.) Se usan dándoles un valor inicial y en el lugar que corresponda del algoritmo se niega, de tal
forma que si después de negarlo examinamos su valor, será en cada caso contrario al anterior. Son
usados en casos como los siguientes:
SW = FALSO
SW = NO SW
?
NO SI
· Cabecera de programa
· Declaración de objetos
· Cuerpo del programa
Entorno:
Nombre Tipo Valor inicial
Declaración de los objetos
Inicio
Descripción de las sentencias
Fin Programa.
Ejercicios:
A) Programa EJEMPLO_1
Entorno:
Nombre Tipo
X Nº Entero
Y Nº Entero
Z Nº Entero
Inicio
X <-- 5
Y <-- 20
Z <-- X + Y
Visualizar X, Y, X * Y
X <-- X * X
Visualizar X, Z
Fin Programa
B) Programa EJEMPLO_2
Entorno:
Nombre Tipo V. Inicial
TRES Nº Entero 3
NX Nº Entero
DOBLE Nº Entero
Inicio
NX <-- 25
DOBLE <-- NX * TRES
Visualizar NX
Visualizar DOBLE
Fin Programa
4.- Algoritmo que lee dos números enteros positivos distintos y diga si el mayor es múltiplo del menor.
5.- Algoritmo que calcula las siguientes características de los alumnos de la clase:
a) ¿Cuantos alumnos tienen menos de 18 años?
b) ¿Cuantos alumnos tienen 18 años?
c) ¿Cuantos alumnos tienen 19 años?
d) ¿Cuantos alumnos tienen más de 19 años?
1 1 1 1 1 1
e=∑ = + + + + ...+
i ! 0! 1! 2 ! 3! N!
20.- Un capital C se coloca a un rédito R. ¿Al cabo de cuantos años de duplicará el capital ?
C×R
I=
100
21.- Obtener la raíz cúbica de todos los números leídos por teclado hasta que encontremos el 0.
22.- Programa que obtenga e imprima la lista de los intereses producidos y el capital acumulado
anualmente por un capital inicial C impuesto un rédito R durante N años a interés compuesto. El
capital se incrementa cada año con los intereses producidos en el mismo.
23.- Calcular la sucesión de Fibonacci. Cada término es igual a la suma de los dos anteriores. Por
definición el 1º es 0 y el 2º es 1.
24.- Programa que lee una secuencia de números no nulos, terminada con la introducción de un cero, y
obtiene e imprime el mayor, visualizando un mensaje de si ha aparecido o no un número negativo.
25.- Igual al anterior. Dar el mayor, el menor y el número de veces que ha aparecido cada uno de ellos.
28.- Números perfectos menores que 1000. Un número es perfecto si la suma de sus divisores, excepto
él mismo, es igual a propio número.
29.- Determinar si dos números enteros positivos son amigos. Son amigos si la suma de los divisores
del primero excepto él mismo es igual al segundo y viceversa.
31.- Programa que lee el nombre y sueldo de cien personas y escribe el nombre y sueldo de la persona
que más cobra y de la que menos.
34.- Programa que lee una secuencia de calificaciones numéricas (0..10) que finaliza con el valor –1 y
calcula y escribe la media aritmética, el número y porcentaje de aprobados y de suspensos.
35.- Programa que lee una secuencia de nombres y escribe el número de veces que se repite el primero
de ellos.
4.- SUBPROGRAMAS.
Llamadas
Programa
Subprograma
Retornos
Un subprograma puede realizar las mismas acciones que un programa principal: entrada de
datos, salida de datos, procesos. Sin embargo, un subprograma lo utiliza el programa para un propósito
específico: recibir datos desde el programa y devolverle los resultados. El subprograma ejecuta una
tarea y cuando la finaliza retorna el control al programa, al punto desde donde se realizó la llamada;
esta operación se puede realizar tantas veces como sea necesario y desde cualquier punto del
programa. A su vez, un subprograma también puede realizar llamadas a otros subprogramas,
comportándose de la misma que lo hace que cuando la llamada proviene del programa principal.
Subprograma 2
2.- Funciones.
Matemáticamente una función es una operación que toma uno o más valores llamados
argumentos y produce un valor denominado resultado (valor de la función para los argumentos dados).
Así, por ejemplo
x
f ( x) =
1+ x
2
evaluar la función debe dársele un valor real a ‘x’, denominado parámetro actual, y con él calcular el
resultado.
Cada lenguaje de programación implementa sus propias funciones: cos(x), sin(x). Cada función
se invoca utilizando su nombre en una expresión con los argumentos actuales, los valores reales,
encerrados entre paréntesis.
Existen dos tipos de funciones: las funciones incorporadas en el propio sistema, llamadas
funciones internas, y las definidas por el usuario o funciones externas (usadas cuando las funciones
internas no satisfacen las necesidades de los cálculos).
El programa llama a la función con el nombre de esta en una expresión seguida de una lista de
argumentos y la función, después de realizar los cálculos, devuelve un único valor.
En el cuerpo de la función debe existir una sentencia de asignación -o más de una- en la que se
asigne un valor (el de retorno) al nombre de la función; es decir debe haber una sentencia como esta:
IdentificadorDeFuncion <--- Expresión.
Una llamada a una función implica que se realicen los siguientes pasos:
Ejemplo: Calcular Y= Xn
Función Potencia(x es real, n es entero) es real
Entorno:
I es entero
y es real
Inicio:
y 1
para i de 1 a absoluto(n) hacer
y y*x
fin para
si n<0 entonces
y 1/y
fin si
Potencia= y
Fin de función
· Una vez ejecutada la subrutina, el control pasa a la siguiente sentencia desde la que se
invocó; la función continúa en la misma sentencia.
· La subrutina no tiene asociado ningún valor a su nombre y puede devolver 0, 1 o más
valores en forma de lista de parámetros; la función tiene asociado un valor y sólo puede
retornar un valor.
Los parámetros formales y los parámetros actuales tienen el mismo significado que en las
funciones. Cuando se realiza una llamada a la subrutina, los parámetros formales toman el valor o la
dirección de su correspondiente parámetro actual.
· Una variable local es aquella que está declarada dentro de un subprograma y es distinta de
otras variables con el mismo nombre declaradas en cualquier otro lugar del programa,
subprograma o programa principal.
· Una variable global es aquella que está declarada para el programa completo.
La parte del programa en que está declarada la variable se conoce como ámbito de la variable y
es el fragmento de programa donde puede ser utilizada; es decir, el ámbito es la parte del programa
donde la variable es visible.
El uso de variables locales hace posible la independencia de los subprogramas; esta se consigue
con la comunicación a través de los parámetros entre el subprograma y el programa principal. Para
utilizar un subprograma sólo necesitamos conocer lo que hace, en ningún caso cómo lo hace ni cómo
está programado. Por el contrario, las variables globales tienen la ventaja de compartir información de
diferentes subprogramas sin una correspondiente entrada de lista de parámetros.
· Entradas: proporcionan valores desde el lugar que llama y son utilizados dentro del
subprograma. El subprograma no se comunica con el exterior salvo que utilicemos una función,
que tiene asociado un valor. P.P ---> S.P.
· Salidas: producen los resultados del subprograma. El subprograma se comunica con el exterior
a través de los parámetros. P.P <--- S.P.
· Entradas/Salidas: los parámetros se utilizan tanto para mandar argumentos a un subprograma
como para devolverlos. P.P <---> S.P.
Existen dos métodos principales para pasar los parámetros a subprogramas: paso por valor y
paso por variable o referencia.
Los parámetros formales -locales a la función- reciben como valores iniciales los valores de los
parámetros actuales y con ellos se ejecutan las acciones descritas en el subprograma. Cualquier cambio
que se realice en los parámetros formales (variables locales) no se verá reflejado en el programa
llamante; por tanto, los parámetros son sólo de entrada.
En la cabecera del subprograma no ha habido ningún cambio con respecto a las declaraciones
anteriores: sólo se declaran los parámetros formales con su correspondiente tipo de dato. En la llamada
al subprograma los parámetros actuales son expresiones, ya que sólo importa el valor del argumento,
no el argumento en sí.
En seudocódigo la forma de indicar que un parámetro va a ser pasado por referencia consiste en
anteponerle la palabra “REFERENCIA” o simplemente indicándolo como un comentario.
Los dos métodos se aplican tanto a las funciones como a los procedimientos.
· Una función puede devolver los valores al programa principal como valor asociado a la
función y a través de argumentos por referencia.
· Un procedimiento sólo puede devolver valores por medio de los parámetros por referencia.
Ejercicios:
1.- División de dos números por restas sucesivas. Calcular el cociente y el resto.
5.- Calcular el producto de dos números por el “Algoritmo ruso del producto”. Consiste en duplicar el
primer factor y dividir (cociente entero) por dos el segundo, obteniendo un producto equivalente, salvo
si el segundo factor es impar, en cuyo caso es necesario acumular previamente el primero en donde se
va a obtener el resultado. El proceso finaliza cuando el segundo factor se hace cero.
· Como una tira de caracteres encerrados entre “/*” (comienzo del comentario) y “*/” (fin del
comentario). Pueden ocupar una o más líneas y no se pueden anidar.
· Como una tira de caracteres precedida por “//”. Sólo actúa desde donde se encuentra hasta
el final de la línea.
1.2.- Cabecera.
En ella se sitúan las directivas usadas por el preprocesador de C. Las directivas son palabras
reservadas que comienzan por el carácter ‘#’ y son detectadas por el preprocesador al llamar al
compilador. El preprocesador tiene la función de leer el programa fuente y sustituir las definiciones de
las directrices por los valores correspondientes dentro del programa. Las más habituales son las dos
siguientes :
#define : Usada para la definición de constantes simbólicas. El signo ‘#’ debe ir en la primera
columna seguido de la palabra ‘define’ y a continuación el identificador (habitualmente en
mayúsculas) seguido de su valor.
#define PI 3.14159
#define ANGULORECTO 90
#include <ArchivoCabecera.h>
#include “ArchivoCabecera.h”
#include <stdio.h>
#include “c:\datos\miarch.h”
Si el nombre del archivo cabecera está encerrado entre los signos de mayor y menor, dicho
archivo se buscará en el directorio por omisión en el que se encuentran los archivos cabecera ; si va
encerrado entre comillas, el archivo debe encontrarse en el directorio indicado explícitamente o en el
directorio actual de trabajo.
1.3.- Entorno.
Se definen las variables globales del programa y los prototipos de las funciones, siempre y
cuando, tanto unas como otras, se consideren necesarias. Un formato muy general de definición de
variables, tanto globales como locales, es el siguiente :
La función ‘main()’ es una función especial ya que es la primera que se llama cuando se ejecuta
el programa; indica el comienzo del mismo. Puede encontrase en cualquier lugar del programa aunque
generalmente está situada en la zona superior del mismo (por razones de claridad y estructuración).
En un programa C siempre debe existir esta función, y además única, para que el compilador
sepa donde empezar la ejecución. La llave que cierra la función indica el final del programa. Como
toda función, esta puede recibir parámetros de entrada y retornar un valor de salida, aunque en esta
ocasión se realizará desde y hacia el sistema operativo.
#include <stdio.h>
main()
{ printf(“El primer programa en lenguaje C”) ;
}
· Separadores : usados para separar los diferentes elementos que forman el lenguaje (espacio
en blanco, tabulador, cambio de línea y comentario).
2.2.- Sintaxis de C.
Las reglas generales para escribir programas en C son las siguientes :
· Toda declaración y sentencia simple debe finalizar en un punto y coma ; este es usado
como terminador de la instrucción.
[signed|unsigned] char
int Un entero (16 bits o 32 bits), normalmente del tamaño de los enteros de la
máquina donde se ejecuta.
float
[long] double
enum Una lista de enteros que pueden ser tomados por una variable de ese tipo. Los
valores del tipo enumerado se representan con identificadores, que serán las
constantes del nuevo tipo.
void Usado para la declaración de funciones que no retornan ningún valor o para
declarar punteros a un tipo no especificado.
Existen, además, una serie de cualificadores que se pueden aplicar a los tipos básicos
modificando su rango de valores y/o su tamaño:
short Se aplica al tipo de datos int. Si se usa sólo, se asume que es un tipo de datos int.
long Se aplican a los tipos de datos enteros y reales de doble precisión. Si se usa sólo,
se asume que es un tipo de datos int.
Tanto long como short, pueden proporcionar diferentes tamaños a los tipos de datos.
signed, unsigned Se aplican a los tipos de datos int, char, long y short. Los unsigned son
positivos o 0 y los signed son positivos, negativos o 0. Si se usan sin
ningún tipo de dato, se asume que es int. Por omisión, todos los tipos de
datos son con signo.
2.- Constantes.
Una tira es un vector cuyos elementos son caracteres. El compilador sitúa el carácter nulo (\0)
al final de la cadena para conocer cual es el final de la misma. La memoria requerida para almacenar
una cadena es su longitud más uno (el carácter nulo).
Debemos distinguir entre una contante de tipo carácter y una cadena. La primera representa el
valor numérico del carácter y la segunda es una tira de caracteres finalizada con el carácter nulo.
int paso = 1;
char linea[1000];
Las variables pueden ser inicializadas en su declaración como en el ejemplo anterior. Las
globales las inicializa el compilador a 0 y las locales tendrán un valor indefinido. La declaración de un
objeto puede ir precedida de la palabra 'const' indicando que este objeto es una contante.
const int Dias = 10; (const Dias = 10;)
const char nombre[] = "Antonio"
3.- Operadores.
3.1.- Operadores aritméticos.
Los operadores aritméticos binarios son: +, -, *, / y %. El % (el resto de la división entera) sólo
puede usarse con operandos enteros.
Cuando en una expresión aparecen los operadores lógicos, su evaluación finaliza tan pronto
como se conozca su resultado.
Si N % 2
visualizar N, "es impar" /* N % 2 <> 0 */
si no
visualizar N, "es par" /* N % 2 = 0 */
La característica fundamental de estos dos operadores es que se pueden usar como prefijos
(delante del operando, ++i) o como sufijos (detrás del operando i--). En ambos caso el efecto
producido sobre la variable es el mismo, incremento o decremento de la misma en una unidad; sin
embargo, existe una diferencia: cuando se aplica como prefijo (++i), modifica el valor de la variable
antes de usar su valor y cuando se aplica como sufijo (i--), modifica el valor de la variable después de
usar su valor. Esto significa que en el contexto donde se utilice el valor y no sólo su efecto, ++i e i++
son distintos. En el fragmento de programa siguiente es indiferente usar el operador como prefijo que
como sufijo:
En las siguientes expresiones, depende de que se use como prefijo o sufijo para obtener un
resultado u otro:
int j = 1, i = 10;
char c = 'D';
visualizar ++i /* 11 */
visualizar i++ /* 10 */
visualizar ++i, i++ /* Resultados no deseados 12 10 ( de derecha a izquierda)*/
visualizar i++, ++i /* Resultados no deseados 11 11*/
visualizar ++c /* E */
visualizar c++ /* D */
i = c++ /* i <-- ASCII('D'), c <-- 'E' */
i = ++c /* i <-- ASCII('E'), c <-- 'E' */
i = i++ + j /* Expresión no deseada i <-- 12 */
Variable = Expresión
Además, en las expresiones donde la variable receptora del valor también forma parte de la
expresión, esta se puede simplificar.
es equivalente a
x *= y + 1 ===> x = x * (y + 1)
La mayor parte de los operadores binarios disponen del correspondiente operador op=, donde
op es: +, -, *, /, %, <<, >>, &, ^, |.
Usar sentencias de asignación de esta forma hace que el compilador genere un código
ejecutable mas eficiente y normalmente ayuda a la comprensión de las sentencias.
Si E2 y E3 son de diferente tipo de dato, el tipo del resultado se establece por las reglas de
conversión.
Ejemplos:
z = (a > b) ? a : b;
Si a > b
z=a
si no
z=b
Fin si
si Numero % I = 0
Suma = Suma + I
Fin si
Otra forma de realizar una conversión del tipo de dato es indicando explícitamente la
conversión mediante una construcción denominada cast (casting).
La expresión se convierte al tipo de dato citado mediante las reglas de conversión anteriores.
Operadores Asociatividad
Todo fichero fuente que use funciones de la librería estándar de E/S deberá contener la línea
"#include <stdio.h>" al principio del mismo. El fichero stdio.h define ciertas macros y variables
empleadas por la librería estándar de E/S así como el prototipo de las funciones de E/S.
printf() convierte (da formato) y visualiza sus argumentos en la salida estándar, bajo la
supervisión de la cadena de control o formato. La cadena de control es una constante de tipo cadena
que contiene dos tipos de objetos: caracteres ordinarios, que se visualizan por pantalla, y los comandos
de control de formato o especificadores de conversión, cada uno de los cuales origina una conversión
del siguiente argumento de printf(). Retorna el número de bytes visualizados.
Cada comando de control comienza por el símbolo de tanto por ciento (%) y finaliza en un
carácter de control. El formato del comando de control por orden de escritura es el siguiente:
[ancho] Especificador del ancho del campo. Es opcional. Indica el número mínimo de
caracteres que se deben imprimir; se rellena con blancos o ceros (si se escribe un
0 delante del ancho, que no implica base octal).
[h|l|L] Modificador del tamaño del campo. Es opcional. Este modificador afecta a la
forma en que la función printf() debe interpretar el tipo de dato del argumento
que se escribe a continuación.
h = short int (dioux)
l = Long (dioux)
L = long double (efg)
l = Double (efg). Si es pto. Flotante.
Carácter
de control Carácter de conversión de tipo de dato. Es obligatorio. Indica la forma en que
debe realizarse la conversión del argumento para que sea visualizado por
pantalla. Estos, son los siguientes:
También se usan constantes de tipo carácter no imprimibles en la cadena de control. Estos son:
'\a' Beep.
'\b' Backspace (retroceso).
'\n' Nueva línea.
'\t' Tabulador horizontal.
'\\' Visualiza \.
'\"' Visualiza ".
'\'' Visualiza '.
'\ooo' Valor ASCII octal.
'\xhh' Valor ASCCI hexadecimal.
int main(void)
{
char Cadena[10] = "La casa";
:La casa:
: La casa:
:La casa :
:La c :
:12345:
: 12345:
:000000000012345:
:12345 :
: ff:
:FF :
: 377:
: 65432:
: -104:
:111111.523438:
: 111111.523438:
:111111.523438 :
:111111.52:
:1.111115E+05:
: 111112:
Lee los caracteres desde el teclado, los interpreta de acuerdo con la cadena de control, y
almacena los resultados en los restantes argumentos. Los argumentos deben ser punteros (dirección de
memoria del argumento) que indican donde se debe almacenar el resultado; la función scanf() pasa los
argumentos por referencia, no por valor como lo hace printf(), siendo necesario indicarselo
anteponiendo el carácter & (dirección) a cada argumento, excepto a los de tipo cadena. Retorna el
número de argumento convertidos.
La cadena de control es una constante de tipo cadena que contiene los comandos de control de
formato o especificadores de conversión, cada uno de los cuales origina una conversión del siguiente
argumento de scanf().
Cada comando de control comienza por el símbolo de tanto por ciento (%) y finaliza en un
carácter de control. El formato del comando de control por orden de escritura es el siguiente:
[ancho] Especificador del ancho del campo. Es opcional. Indica el número máximo de
caracteres que se deben leer para el argumento.
[h|l|L] Modificador del tamaño del campo. Es opcional. Este modificador afecta a la
forma en que la función scanf() debe interpretar el tipo de dato del argumento
que se escribe a continuación.
h = short int
l = long int (si Carácter Control integer)
l = double (si Carácter Control pto. flt.)
L = long double
Carácter
de control Carácter de conversión de tipo de dato. Es obligatorio. Indica la forma en que
debe realizarse la conversión del argumento para que sea almacenado en el
argumento correspondiente. Estos, son los siguientes:
Un campo de entrada se define como una cadena de caracteres delimitada por espacio en
blanco (blancos, tabuladores, fin de línea); abarca hasta el primer blanco o tantos caracteres como
indique el tamaño del campo.
Ejemplos:
scanf("%2d %f %*d %2s", &i, &x, nombre); Entrada: 56789 0123 54a72
Asigna 56 a i, 789.0 a x, se salta 0123 y a nombre la tira "54". La próxima llamada a cualquier
rutina de entrada comenzará la exploración en la letra a.
scanf() acaba cuando se agota la tira de control o cuando la entrada no coincide con el carácter
de control. Devuelve como valor el número de elementos de entrada que ha podido reconocer y
asignar. Al llegar al final del fichero se devuelve EOF.
Hay que usar corchetes en la cadena de control; estos permiten que scanf() acepte caracteres
que reemplacen el grupo de caracteres que haya dentro de los corchetes. El contenido de los corchetes
se llama scanset. El scanset puede ser un rango de valores ([a-z]), una lista ([01234]) 0 el signo ^
seguido por un rango o lista para indicar no (negación).
Scanf("%[^0-9] %d", str, &i); Lee caracteres situándolos en str mientras no sean
dígitos.
Espera a que se puse una tecla, siempre que no haya caracteres almacenados en el buffer
esperando para ser leídos) y después devuelve su valor. También envía un eco del carácter pulsado a la
pantalla, por lo que no es necesario inprimirlo.
int getchar(void);
#define LONGITUD 20
char Cadena[LONGITUD + 1];
... gets(Cadena);
printf("%s", gets(Cadena));
puts(Cadena);
1.- Introducción.
Se van a estudiar las sentencias que afectan al control de flujo del programa. Las sentencias
condicionales if y switch permiten alterar el flujo del programa según ciertas condiciones. Las
construcciones de bucle, while, for y do-while, permiten la repetición una serie de operaciones. Puesto
que se han diseñado los bucles para tener un punto de entrada y un punto de salida, se mencionan las
demás sentencias que afectan al flujo del programa -break, continue y goto- pero no se profundiza en
su uso por no formar parte de la programación estructurada.
if ( <condición> )
<Sentencia/s 1>;
[else
<Sentencia/s 2>;]
Esta sentencia se usa para tomar decisiones. Se evalúa la expresión y si esta es cierta (distinta
de cero) se realiza el grupo de <sentencias 1>; si es falsa (igual a cero) no se realiza ninguna acción o,
en el caso de que la clausula else esté presente, se realiza el grupo de <sentencias 2>. Si existe más de
una sentencia en cualquiera de los bloques de acciones, deberá encerrarse entre llaves. La cláusula
‘else’ se asocia con el ‘if’ inmediatamente anterior; si no se quiere así, se deben usar las llaves para
forzar la asociación deseada.
if (n > 0) if (n > 0)
if (a > b) { if (a > b)
z = a; z = a;
else }
z = b; else
z = b;
El sangrado de las sentencias no implica asociación de ningún tipo, sólo se usa para dar una
mayor legibilidad al programa y así evitar la ambigüedad.
if (expresión 1)
<Sentencia/s 1>
else if (expresión 2)
<Sentencia/s 2>
else if (expresión 3)
<Sentencia/s 3>
else
<Sentencia/s N>
Es la forma normal de escribir una decisión múltiple. Las expresiones se evalúan en orden; si
alguna es cierta, se ejecuta la sentencia o bloque de sentencias encerradas entre llaves asociada con
ella. El último else, que es opcional, se realizará si no se cumple ninguna de las condiciones anteriores.
c = getchar();
if (c >= 'a' && c <= 'z')
++CuentaMinusculas;
else if (c >= 'A' && c <= 'Z')
++CuentaMayusculas;
else if (c >= '0' && c <= '9')
++CuentaDigitos;
else
++CuentaOtro;
switch ( <Expresión> )
{
case <Expresión constante 1> :
<Sentencia/s 1>
[break;]
Cada opción debe ser etiquetada con un entero, constante carácter o expresión constante; no
admite expresiones variables. Las opciones deben ser todas diferentes, disjuntas.
Se evalúa la expresión entera y compara su valor con todos los casos. En el caso en que un
valor sea igual a la expresión, se ejecuta el grupo de sentencias asociado a él. El ‘default’, que es
opcional, se ejecuta si no se cumple ninguna de las anteriores; si no existe, no se realiza nada. Después
de la ejecución de un bloque de sentencias, dado que cada ‘case’ se comporta como una etiqueta, la
ejecución continúa en el siguiente case, a no ser que se use la sentencia ‘break’ que fuerza una salida
inmediata del ‘switch’.
Ejemplo
switch (Operando)
{
case MULTIPLY: x *= y; break;
case DIVIDE: x /= y; break;
case ADD: x += y; break;
case SUBTRACT: x -= y; break;
case INCREMENT2: x++; /* Continúa en la línea siguiente */
case INCREMENT1: x++; break;
case EXPONENT:
case ROOT:
case MOD: printf("No se hace nada\n"); break;
default: printf("¡Error!\n"); break;
}
· Asegurarse de que hay condiciones de finalización del bucle para evitar que estos se
ejecuten indefinidamente; se debe modificar el valor de la condición de finalización del
bucle dentro del cuerpo del mismo.
· Comprobar que se realizan correctamente la primera y la última iteración; es fácil que el
bucle realice una iteración de más o de menos.
while ( Condición )
<Sentencia/s>
La sentencia o grupo de sentencias (entre llaves en este caso) se realizará hasta que la
Condición de valor falso. Como lo primero que se hace es evaluar la condición, puede que el cuerpo
del bucle no se realice ninguna vez.
int main(void)
{ int Caracter;
while ((Carácter = getchar()) != '*')
putchar(toupper(Caracter));
}
Ejercicio: Programa que lee una secuencia de números desde teclado y finaliza cuando se
introduce uno mayor que la suma de los dos anteriores. También debe contar el número de entradas.
#include <stdio.h>
int main(void)
{ int Entradas = 3;
int Penul, Ultimo, Numero;
printf("Los tres primeros\n");
scanf("%d %d %d", &Penul, &Ultimo, &Numero)
while (Numero <= Penul + Ultimo);
{ Penul = Ultimo;
Ultimo = Numero;
printf("Nuevo número: ");
scanf("%d", &Numero);
Entradas++;
}
printf("Introducidos %d", Entradas);
}
int main(void)
{
int c;
int CuentaMinusculas = 0;
int CuentaMayusculas = 0;
int CuentaDigitos = 0;
int CuentaOtro = 0;
Funcionamiento:
· Antes de la primera iteración del bucle, se inicializa la/s variable/s de control del bucle
situada/s en la zona de inicialización.
· La sentencia o grupo de sentencias se ejecuta hasta que la condición sea falsa.
· Después de cada iteración se incrementa el contador, así pues, es indiferente usar la notación
prefija o sufija en los operadores de incremento.
Ejemplos:
Suma = 0 ;
for (I = 1; I < Numero; Suma = Numero % I ? Suma : Suma += I, ++I);
La sentencia o grupo de sentencias (entre llaves en este caso) se realizará hasta que la
Condición de valor falso. Como lo último que se hace es evaluar la condición, el cuerpo del bucle se
realiza al menos vez.
Sentencia Break.
Sentencia continue.
Obliga a que se ejecute la siguiente iteración del bucle que la contiene. En los bucles while y
do-while obliga a evaluar la condición inmediatamente; en un for, el control pasa a la etapa de
incremento y posterior evaluación de la condición.
For (I=0; I<100; I++)
{
scanf("%d", &Numero);
If (Numero<0)
Continue;
else
...
}
Sentencia goto.
Goto <Etiqueta>
Etiqueta:
Si está definida como una variable global, indica que la variable sólo es accesible en el
programa fuente en el que está definida, en ningún otro.
Sólo pueden definirse de tipo register las variables locales automáticas y los parámetros
formales de las funciones.
Int suma(register int a)
{ register int Contador = 0;
Contador++
...
}
La posición de memoria 1001 tiene como valor la dirección de memoria 1003, que es la que
contiene el dato, 123; la dirección de memoria 1001 apunta a la 1003.
TipoDeDato* NombreDeVariable;
donde TipoDeDato es el tipo base del puntero y puede ser cualquier tipo válido de C.
NombreDeVariable es un puntero que apunta a un tipo de dato base definido por TipoDeDato.
El tipo base del puntero define el tipo de variables a las que puede apuntar el puntero.
Técnicamente, cualquier tipo de puntero puede apuntar a cualquier lugar de la memoria. Sin embargo,
la aritmética de punteros está hecha en relación a su tipo base, por lo que es importante declarar bien el
puntero.
m = &Cuenta;
pone en la variable “m” la dirección de memoria de la variable “Cuenta”; es decir, “m” recibe la
dirección de “Cuenta”, no el valor de la variable.
Cuenta = 123;
m = &Cuenta;
q = *m;
Cuenta = 123;
m = &Cuenta;
q = *m;
Esta sentencia significa que “q” recibe el valor almacenado en la dirección “m”.
Los dos operadores tienen mayor prioridad que todos los operadores aritméticos excepto los
unarios (+ y -), respecto de los cuales tienen igual precedencia.
Un puntero no puede ser usado antes de que se haya inicializado, antes de que tome la
dirección de una variable; si se usa sin inicializar es posible reescribir zonas de memoria no reservadas
para almacenar determinados valores, lo que implica que el sistema puede funcionar mal.
0xAA00 0xFF00
Puntero Variable
0 10
0xAA00 0xFF00
Puntero Variable
0xFF00 10
0xAA00 0xFF00
Puntero Variable
Ejemplo:
#include <stdio.h>
main()
{ int* puntero;
int numero = 10;
*puntero = numero; // Operación no deseada; puntero no está inicializado.
printf("%d %d\n", numero, *puntero); // 10 10
numero = 20;
printf("%d %d\n", numero, *puntero); // 20 10
*puntero = 30;
printf("%d %d\n", numero, *puntero); // 20 30
puntero = №
printf("%d %d\n", numero, *puntero); // 20 20
numero = 40;
printf("%d %d\n", numero, *puntero); // 40 40
*puntero = 50;
printf("%d %d\n", numero, *puntero); // 50 50
numero = getchar();
}
3.- Funciones.
El lenguaje C no dispone de subrutinas, sólo de funciones; estas se pueden comportar como
cualquiera de los dos tipos de subprogramas.
El tipo de dato de retorno de una función puede ser declarado de tipo void, indicando que la
función no retorna nada. Si no se especifica tipo dato de retorno, el compilador asume que es un tipo
de dato int.
El compilador se encarga de comprobar si los argumentos coinciden en número y tipo con los
del prototipo de la función.
En las dos últimas formas existe comunicación desde el sistema operativo hacia el programa.
Los argumentos de main() significan lo siguiente:
· argc: Número de parámetros que pasa el sistema operativo al programa incluido el nombre
del programa.
Ejemplo:
Llamada desde el S.O.: C:\copiar a: b:
return [<Expresión>];
Expresión es del mismo tipo que el tipo de retorno de la función. Return provoca la salida
inmediata de la función y puede haber cualquier número de ellas en el cuerpo de la función. La
sentencia return no es necesaria cuando la función no retorna ningún valor (tipo void), aunque puede
usarse sin la expresión para finalizar la función (equivalente a break en un bucle).
Por omisión, todos los parámetros de una función en C se pasan por valor, es decir, se pasa una
copia temporal del parámetro. Esto significa que la función no puede modificar el valor de las
variables externas a ella, ya que se le pasa una copia, no el valor original. Un parámetro por valor se
puede entender como una variable local, de modo que dentro de la función puede cambiar su valor
pero los cambios no tienen efecto fuera de ella.
La única comunicación hacia el exterior de una función que pasa todos sus parámetros por
valor es a través del tipo de dato de retorno de una función usando la sentencia return.
Como en muchas ocasiones queremos que los parámetros pasados a la función cambien de
valor o devolver más de un dato, se debe usar el paso de parámetros por referencia. El paso de
parámetros por referencia consiste en pasar la dirección de memoria donde está almacenado el
argumento, no una copia de su valor. De esta forma, el parámetro actual debe ser una dirección de
memoria de una variable y, por tanto, lo que recibe el parámetro formal es esa dirección de memoria.
Cada vez que se cambia de valor ese parámetro en la función, se cambia el contenido de una dirección
de memoria.
La forma que tiene C de pasar por referencia consiste en usar los punteros.
Los parámetros formales se declaran como punteros al tipo de dato base y, en la llamada se les
asigna la dirección de los parámetros actuales.
a = &x y b = &y
y luego modificamos el contenido de los punteros a y b usando para ello el operador de indirección *;
*a y *b, como ya se ha visto, se tratan igual que si fueran variables enteras.
Ejercicios:
ax2 + bx + c = 0
2.- Función que visualice un rectángulo de * en pantalla. El tamaño del mismo se introduce por
teclado; no superior ni inferior a la pantalla.
m
= m! / (n!* (m - n)!)
n
4.- Función que pase un número en base diez a cualquier otra base.
5.- Calcular el número del tarot de una persona a partir de la fecha de su nacimiento. Consiste
en sumar el día, mes y año; del resultado obtenido se suman sus dígitos y así sucesivamente hasta
reducir a un sólo dígito.
6.- Indicar si una fecha es correcta. Un año es bisiesto cuando es múltiplo de cuatro, salvo los
múltiplos de cien a no ser que los sean de cuatrocientos.
7.- Se introduce la hora del día en Horas, Minutos y Segundos. Se desea escribir la hora
correspondiente al siguiente segundo.
Dinámicos Listas
Listas enlazadas
Árboles
Grafos
Los tipos de datos primitivos son aquellos que no están compuestos por otras estructuras de
datos y son los más utilizados en los diferentes lenguajes de programación; son los que hemos
trabajado hasta el momento.
Los tipos de datos estructurados pueden ser organizados en diferentes estructuras de datos:
estáticas y dinámicas. Las estructuras de datos estáticas son aquellas en las que el tamaño ocupado en
memoria se define antes de que el programa se ejecute y no puede modificarse dicho tamaño durante la
ejecución del mismo. Las estructuras de datos dinámicas no tienen limitaciones o restricciones en el
tamaño de memoria ocupada.
Finito: Al ser una estructura de datos interna, su tamaño viene limitado por la capacidad
de la memoria principal o por el propio lenguaje.
Ordenado: El primero, segundo, tercero, ... elemento de un array puede ser identificado, no
es así en las estructuras dinámicas.
El nombre de una tabla es un identificador común para todos sus elementos, distinguiéndose
cada uno de ellos por una lista de índices que complementan a dicho nombre para referenciarlos.
Definiciones:
Las tablas, igual que cualquier otro objeto del programa, se debe definir en su lugar
correspondiente indicando su identificador, su tipo, su longitud y su número de dimensiones.
Los límites del array no se comprueban en tiempo de compilación, por tanto es posible
sobrescribir datos fuera de dichos límites.
Las operaciones que se realicen en el array se harán sobre cada componente individualmente,
no pueden realizarse sobre el array en su conjunto.
Nombre: EDAD.
Componentes: EDAD[0], EDAD[1], EDAD[2],..., EDAD[5].
Índices: Valores numéricos de 0 a 5 que direccionan cada componente.
Dimensión: Una.
Longitud: 6
Tipo: Numérico entero.
EDAD
2 bytes 2 bytes 2 bytes 2 bytes 2 bytes 2 bytes
20 18 31 20 17 21
Elto 1º Elto 2º Elto 3º Elto 4º Elto 5º Elto 6º
Pos. 0 Pos. 1 Pos. 2 Pos. 3 Pos. 4 Pos. 5
TipoDeLasComponentes Identificador[LONGITUD_DIMENSIÓN];
Ejemplo:
int EDAD[6];
EDAD[6] es de tipo Numérico entero.
Cada elemento es un dato que está almacenado en una posición dentro del vector, y las
operaciones que se pueden realizar son las mismas que la usadas con cualquier otra variable, pero se
realizarán sobre cada componente individualmente.
Asignación:
Identificador = Nombre_Tabla[Indice];
Nombre_Tabla[Indice] = Expresión;
Edad[0] = 20;
Edad[1] = 18;
Entrada:
Introducir Nombre_Tabla[Indice]
Scanf("%d", &Edad[0]);
Scanf("%d", &Edad[1]);
Salida:
Visualizar Edad[Indice]
Printf("%d", Edad[0]);
printf("%d", Edad[1]);
Si queremos conocer o simplemente tratar cada una de las posiciones de un array, debemos
realizar un recorrido secuencial del mismo; para ello usaremos la estructura de control 'para' (bucle
for), ya que conocemos el número de componentes que contiene el vector. Un tratamiento secuencial
de una tabla consiste en recorrerla completamente, componente a componente, y realizar las
operaciones necesarias con la componente tratada en cada caso. El tratamiento puede realizarse en
sentido creciente o decreciente:
Creciente:
Para Indice de 0 a TAMANHO
TRATAR Tabla[Indice]
Fin Para
Decreciente:
Para Indice de TAMANHO a 0 con incremento -1
TRATAR Tabla[Indice]
Fin Para
Ejemplos:
void main()
{ int i;
int Edad[MAXIMO];
// Carga el vector
for (i = 0; i < MAXIMO; i++)
{ printf("Elemento %d: ", i+1);
scanf("%d", &Edad[i]);
}
// Visualiza el contenido en sentido creciente
for (i=0; i<MAXIMO; i++)
printf("Elemento %d:\n", Edad[i]);
// Visualiza el contenido en sentido decreciente
for (i=MAXIMO - 1; i>=0; i--)
printf("Elemento %d: %d\n", i, Edad[i]);
}
2.- Cargar un array con las notas de los alumnos de la clase y posteriormente calcular su media.
#define ALUMNOS 30
Float TotalNotas, Medias, Notas[ALUMNOS];
... // Cargar la tabla y demás definiciones de datos y sentencias
for (i = 0; i < ALUMNOS; i++)
TotalNotas += Notas[i];
printf("La media es: %f\n", TotalNotas / ALUMNOS);
Aux = Tabla[i];
Tabla[i] = Tabla[j];
Tabla[j] = Aux;
Un array, igual que cualquier otra variable, debe inicializarse cuando sea necesario y en el
lugar del programa oportuno con los valores que deba almacenar. Hay dos formas de inicializar un
array:
2.- Inicializar el array en la propia definición del mismo. No es necesario indicar el número
de elementos, en los corchetes, pues el propio compilador cuenta el número de valores
que hay entre las llaves, separados por comas, y dará la longitud del vector. Si se
especifica la longitud, habrá que poner tantos valores como marque la misma.
EDAD
17 18 20 22
0 1 2 3 4 5
Las operaciones que se deben realizar desglosadas por pasos sucesivos, son las siguientes:
Edad[5] = Edad[4]
Edad[4] = Edad[3]
Edad[3] = Edad[2]
El resultado es el siguiente:
EDAD
17 18 19 20 22
0 1 2 3 4 5
Debe observarse que si el vector está lleno en su totalidad, la última componente del mismo se
pierde, a no ser que este hecho impida la inserción de la nueva componente.
El array bidimensional o matriz se puede considerar como un vector de vectores. Es, por
consiguiente, un conjunto de elementos, todos del mismo tipo, en el cual el orden de los componentes
es significativo y en el que se necesitan dos índices para poder identificar a cada elemento del array.
Esta referencia a un elemento del array, se realiza de la misma forma que en un vector; el nombre de la
matriz seguido, en este caso, de dos índices encerrados, por separado, entre corchetes.
Una matriz está compuesta de filas y columnas, de forma que el primer índice se corresponde
con las filas y el segundo con las columnas, independientemente del identificador usado para
nombrarlos.
Si se visualiza un vector, se puede considerar como una fila de datos; una matriz como un
grupo de filas.
En la figura siguiente se representa una matriz, MATRIZ, de cuatro filas y seis columnas:
MATRIZ
Fila 0
Fila 1 M[1][3]
Fila 2
Fila 3
Columna 0 Columna 1 Columna 2 Columna 3 Columna 4 Columna 5
Una matriz, por tanto, necesita un valor para cada indice para poder identificar un elemento
individual. La expresión MATRIZ[1][3] accede a la componente situada en la fila 1 y columna 3 (el
primer índice indica la fila y el segundo la columna). Los dos índices son expresiones numéricas
enteras.
Un tratamiento secuencial de una matriz se realiza anidando dos bucles; este puede realizarse
de dos formas distintas: por filas y por columnas.
Por filas: El bucle más externo recorre las filas y el mas interno todas las componentes de
la fila, es decir, el externo se posiciona en una fila de la tabla y el interno recorre
todas sus componentes, todas las columnas de esa fila.
Algoritmo en seudocódigo:
Para F de 1 a FILAS
Para C de 1 a COLUMNAS
Tratar TABLA[F][C]
Fin para
Fin para
Fragmento de programa en C:
For(F = 0; F < FILAS; F++)
For(C = 0; C < COLUMNAS; C++)
Tratar TABLA[F][C];
Por columnas: El bucle más externo recorre las columnas y el mas interno todas las
componentes de la misma.
Algoritmo en seudocódigo:
Para C de 1 a COLUMNAS
Para F de 1 a FILAS
Tratar TABLA[F][C]
Fin para
Fin para
Fragmento de programa en C:
For(C = 0; C < COLUMNAS; C++)
For(F = 0; F < FILAS; F++)
Tratar TABLA[F][C];
Inicialización de matrices
Un array, igual que cualquier otra variable, debe inicializarse cuando sea necesario y en el
lugar del programa oportuno con los valores que deba almacenar. Hay dos formas de inicializar un
array:
2._ Inicializar el array en la propia definición del mismo. Es necesario indicar el número de
elementos de todas las dimensiones excepto de la primera para permitir que el
compilador indexe el array adecuadamente.
// Definición e inicialización de un array
Int Matriz1[][4] = {{0,0,0,0},{0,0,0,0},{0,0,0,0}};
Int Matriz2[2][7] = {0,0,0,0,0,0,0, 0,0,0,0,0,0,0};
El resto de operaciones que pueden realizarse sobre los elementos de una matriz son las
mismas que las que pueden realizarse con los vectores.
Ejemplo:
a) Cargar una matriz de 30 filas y 3 columnas que contiene la nota de cada alumno por
cada módulo del ciclo; las filas corresponden al alumno y las columnas a los módulos.
Calcular e imprimir la nota media de cada alumno y de cada asignatura.
b) Suponiendo que las notas son enteras y cuyo valor varía de 1 a 10, calcular el total de
alumnos para cada nota y asignatura.
Ejercicios.
1. Programa que lee 50 número enteros sobre un vector y obtiene e imprime cuales son el mayor y
el menor número almacenado y cuantas veces se repiten ambos.
2. Con el vector anterior, programa que suma las componentes de índice par y las de índice impar
independientemente.
6. Dado un número entero positivo con un máximo de N dígitos, comprobar es capicúa usando un
vector de N componentes.
7. Programa que lee un vector de N componentes y las rota un lugar a la derecha, teniendo en
cuenta que la última componente pasa a ser la primera.
10. Cargar un vector de N componentes y dividirlo en otros dos de forma que uno contenga las
componentes cuyo valor es primo y el otro con las de valor no primo.
11. Programa que carga una matriz de 5 filas y 10 columnas con números enteros y almacena los
valores máximo y mínimo de cada fila en un array. Visualizar los resultados.
12. Programa que calcula la matriz traspuesta de una matriz de N filas y M columnas.
13. Programa que genera e imprime una matriz unitaria de orden N. Es aquella que tiene todas sus
componentes a 0 excepto las de la diagonal principal que están a 1.
14. Programa que imprime un cuadrado latino de orden N. La primera fila contiene los N primeros
números naturales y la siguientes N - 1 filas contienen la rotación de la fila anterior un lugar a
la derecha.
15. Programa que carga una matriz cuadrada y nos dice si es mágica. Es mágica si la suma
individual de cada una de sus filas, columnas, diagonales principal y secundaria son iguales.
16. Una empresa dispone de las hojas de caja diarias de un año. En ellas se refleja el total en
pesetas de las ventas producidas en el día de su fecha. Se pide realizar un programa en el que se
introduzcan por teclado cada una de las cantidades y calcule:
A) Capital total de ventas producidas en cada mes.
B) Capital total anual.
C) Día y mes que se hizo la mejor caja.
17. Queremos crear una tabla (NxM) que contenga el número de votos de una serie de candidatos
de unas elecciones, estando distribuídos por distritos. Se pide:
A) Número de votos totales.
B) Votos de cada candidatos.
C) Votos de cada distrito.
D) Candidato y distrito más votados y número de votos obtenidos.
19. Genera e imprime un cuadrado mágico de orden N (positivo e impar). Un cuadrado mágico de
orden N es una matriz cuadrada que contiene los números naturales de 1 a N elevado a 2 tal
que coincide la suma de los números de cualesquiera de sus filas, columnas o diagonales
principales. Se construye mediante las siguientes reglas:
Aunque el algoritmo se implementa con un bucle while, puede hacerse con un bucle for.
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#define N 8
#define RANGO 49
#define RREIN 9
int Buscar(int, int *);
void Visualizar(int *);
void Generar(int *);
main()
{ int Tabla[N] = {0, 0, 0, 0, 0, 0, 0, 0};
Generar(Tabla);
Visualizar(Tabla);
getch();
}
void Generar(int *V)
{
int Numeros = 0, Valor;
randomize();
while (Numeros < N - 1)
{ do
{ Valor = random(RANGO) + 1;
}while(Buscar(Valor, V));
V[Numeros++] = Valor;
}
V[N - 1] = random(RREIN) + 1;
}
int Buscar(int Valor, int *V)
{ int i = 0;
while (Valor != V[i] && i < N - 1)
i++;
return V[i] == Valor ? 1 : 0;
}
I = 0;
while (X > Vector[i] && i < N - 1) //En Pascal daría fuera de rango
I++;
if (X == Vector[i])
printf("El valor %d está en la posición %d", X, i);
else
printf("El valor %d no está en el vector", X);
b) Es igual que el anterior pero damos todas la posición que ocupan todas las componentes
iguales a X.
i = 0; Encontrado = FALSO;
// Esta expresión en otro lenguaje de programación puede dar un error de fuera de rango.
While (i < N && X >= Vector[i])
{ If (X == Vector[i])
{ printf("El valor %d está en la posición %d", X, i);
Encontrado = CIERTO;
}
i++;
}
if (!Encontrado)
printf("El valor %d no está en el vector", X);
int LimiteInferior = 0;
int LimiteSuperior = ELTOS - 1;
int ComponenteCentral = LimiteSuperior / 2;
while (Valor != V[ComponenteCentral] && LimiteInferior < LimiteSuperior)
{ if (Valor > V[ComponenteCentral])
LimiteInferior = ComponenteCentral + 1;
else
LimiteSuperior = ComponenteCentral - 1;
ComponenteCentral = (LimiteInferior + LimiteSuperior) / 2;
}
if (Valor == V[ComponenteCentral])
ComponenteCentral;
else
Indicar que no está;
Secuencia inicial: 3 2 4 1 2
Primer paso: 2 3 4 1 2
Segundo paso: 2 3 4 1 2
Tercer paso: 1 2 3 4 2
Cuarto paso: 1 2 2 3 4
Algoritmo:
void InsercionDirecta(int *V)
{ int i, j, Auxiliar;
for (i = 1; i < ELTOS; i++)
{ Auxiliar = V[i];
j = i - 1;
while (V[j] > Auxiliar && j > 0)
{ V[j + 1] = V[j];
j--;
}
if (V[j] > Auxiliar)
{ V[j + 1] = V[j];
V[j] = Auxiliar;
}
else
V[j + 1] = Auxiliar;
}
}
Secuencia inicial: 3 2 4 1 2
Primer paso: 1 2 4 3 2
Segundo paso: 1 2 4 3 2
Tercer paso: 1 2 2 3 4
Cuarto paso: 1 2 2 3 4
void SeleccionDirecta(int *V)
{ int i, j, k, Auxiliar;
for (i = 0; i < ELTOS - 1; i++)
{ k = i;
Auxiliar = V[i];
for (j = i + 1; j < ELTOS; j++)
if (V[j] < Auxiliar)
{ k = j;
Auxiliar = V[j];
}
V[k] = V[i];
V[i] = Auxiliar;
}
}
Secuencia inicial: 3 2 4 1 2
Primer paso: 2 3 1 2 4
Segundo paso: 2 1 2 3 4
Tercer paso: 1 2 2 3 4
Cuarto paso: 1 2 2 3 4
void BurbujaID(int *V)
{ int i, j, Auxiliar;
for (i = ELTOS - 2; i >= 0; i--)
for (j = 0; j <= i; j++)
if (V[j] > V[j + 1])
{ Auxiliar = V[j];
V[j] = V[j + 1];
V[j + 1] = Auxiliar;
}
}
Recorrido de derecha a izquierda: esta versión es igual que la anterior pero desplazando los
elementos menores a la izquierda.
Secuencia inicial: 3 2 4 1 2
Primer paso: 1 3 2 4 2
Segundo paso: 1 2 3 2 4
Tercer paso: 1 2 2 3 4
Cuarto paso: 1 2 2 3 4
void BurbujaDI(int *V)
{ int i, j, Auxiliar;
for (i = 1; i < ELTOS; i++)
for (j = ELTOS - 1; j >= i; j--)
if (V[j - 1] > V[j])
{ Auxiliar = V[j - 1];
V[j - 1] = V[j];
V[j] = Auxiliar;
}
}
El proceso es el siguiente:
· Se compara el elemento situado en la posición dos del array, del cual se pasó el elemento a
TABLA3, con el primero del otro array. El más pequeño se coloca en la posición dos de
TABLA3, y así sucesivamente.
· Llegará un momento en que se hayan pasado todos los elementos de un array a TABLA3,
quedando solamente elementos del otro array. Dichos elementos se pasarán directamente a
TABLA3.
Ejercicios :
2. Programa que carga 100 nombres en un vector y a continuación permite consultas sucesivas
para ver si un nombre, introducido por teclado, se encuentra o no dentro del vector.
3. Cargar dos vectores de N componentes cada uno; el primero almacena nombres de personas y
el segundo sus números de teléfono. Realizar un programa que permita consultas sucesivas de
teléfonos para nombres introducidos por teclado.
4. Igual que el ejercicio anterior. En lugar de dos vectores, una matriz de 2*N.
5. Programa que carga dos vectores de 35 componentes cada uno, almacenando en el primero una
lista de nombres de personas y en el segundo las edades correspondientes a cada una de ellas. A
continuación permite consultas sucesivas de edades para nombres introducidos por teclado.
Suponiendo que el vector de nombres está ordenado ascendentemente, usar el método de
búsqueda binaria.
6. Programa que realiza una ordenación de un vector imprimiendo todos los estados intermedios
del mismo.
7. Programa que clasifica simultáneamente dos vectores numéricos de igual dimensión, el primero
en orden creciente y el segundo en orden decreciente.
8. Programa que carga una matriz alfanumérica de 80*2 conteniendo en cada un nombre de
persona y su número de teléfono. A continuación realiza una clasificación ascendente por orden
alfabético de nombres y, finalmente, imprime la lista ordenada de nombres y teléfonos.
10. Programa que carga un vector numérico de 100 componentes y obtiene e imprime los 10
valores menores y los 10 mayores.
11. Dado un vector numérico de 100 componentes. Programa que clasifica simultáneamente en
orden creciente sus componentes pares y en orden decreciente las impares.
12. Programa que carga una matriz de 100 filas y 3 columnas, con primer apellido, segundo
apellido y nombre de 100 personas, realizando una clasificación alfabética completa e
imprimiendo la lista de nombres clasificada.
13. Programa que realiza la clasificación completa de una matriz numérica en orden creciente (de
izquierda a derecha y de arriba a abajo).
14. Programa que carga dos vectores, V1 y V2, de N componentes cada uno; los ordena
ascendentemente y realiza una mezcla de los dos. El resultado es otro vector V3 ordenado.
La declaración
#define LONGITUD 20
int array[LONGITUD];
define un array de LONGITUD componentes consecutivas de tipo entero (el compilador reserva la
memoria necesaria para poder almacenar todos los datos) denominados array[0], array[1], array[2], ...,
array[LONGITUD - 1]. La notación array[i] significa que el elemento del array se encuentra a i
posiciones del comienzo del mismo. Si declaramos un puntero a un entero como
int *p_array;
entonces podemos asignar la dirección del primer elemento del array a ese puntero
p_array = &array[0]
o bien, dado que el nombre del array es un puntero a la primera componente del mismo, podemos
hacer la asignación
p_array = array
p_array array
Dirección --------->
0 1 2 3 4 5
Si el puntero p_array apunta al primer elemento de array, entonces por definición p_array + 1
apunta al siguiente elemento. En general, p_array - i apunta a i elementos delante de p_array y
p_array + i apunta a i elementos detrás de p_array.
La expresión
x = *(p_array + 1) ;
Esta modificación, incremento o decremento en enteros, del valor del puntero es independiente
del tipo de dato de las componentes del array. Cuando incrementamos en una unidad el puntero al
array (p_array + 1), estamos realizando un salto a la siguiente componente del array, calculado por el
tamaño de los datos almacenados en las componentes; no estamos accediendo a la siguiente posición
de memoria. En definitiva, siempre que modifiquemos un puntero el incremento o decremento se
adecua al tamaño en memoria del objeto apuntado. El tamaño de un objeto se conoce mediante el
operador sizeof(); este retorna el número de bytes de memoria usados por dicho objeto.
Como el compilador convierte toda referencia a un array en un puntero al comienzo del mismo,
también son válidos los acceso al array de la siguiente manera:
&array[i], array + i
p_array + i, &p_array[i]
Como se ve, con todo puntero que apunta a un array puede usarse la indexación de elementos y
con todo array pueden usarse los punteros para acceder a las componentes. Sin embargo, existen
diferencias entre el nombre de un array y un puntero a un array:
· Un puntero es una variable, por lo que se puede modificar su valor con operaciones como
p_array++ o --p_array mientras que el nombre de un array es una constante, no pudiendo
modificar su valor ( no podemos hacer array++, array1 = array2, array1 = &array2, son
sentencias ilegales).
· La declaración de una variable de tipo puntero no lleva implícita la asignación o reserva de
memoria por el compilador con lo que no puede usarse mientras no se realice una asignación a
una zona de memoria. Esta asignación de memoria se realiza como se ha visto antes (p_array =
array o p_array = &array[0]) o bien, mediante la petición directa de memoria.
La siguiente definición declara un array de LONGITUD punteros a enteros; en cada uno de sus
elementos podemos almacenar la dirección de otra variable, tal como la dirección de una variable de
tipo int (dato) o la dirección de un vector de enteros (notas). En definitiva, la longitud del dato al que
apunta cada componente del array de punteros es irrelevante.
main()
{ int *p_array[LONGITUD];
int dato = 20, i;
int notas[ELTOS] = {0,1,2,3,4,5,6,7,8,9};
p_array[0] = &dato;
p_array[1] = notas;
printf("%d\n", *p_array[0]);
for (i=0; i<ELTOS/2; i++)
printf("%d\t", p_array[1][i]);
for (i=ELTOS/2; i<ELTOS; i++)
printf("%d\t", *(p_array[1] + i));
}
p_array dato
---> 20
---> 0 1 2 3 4 5 6 7 8 9
Notas
Se puede ver como una posición de un array de punteros referencia a una variable. La cuestión
es que pasaría si cada elemento de un array apuntara a la dirección de otro puntero. Esto se denomina
doble indirección, que hay que definir como los demás objetos del programa. La declaración
int **puntero_puntero; //Crea una var. ptr. que apunta a otra var. ptr.
indica que se está declarando una variable puntero que almacena la dirección de otro puntero a un
entero.
Puntero Variable
dirección -------------------> Valor
Indirección simple
Desde este punto de vista, un array multidimensional se puede expresar como un array de
punteros. Cada puntero indica el comienzo de un array con un tamaño (N - 1). Por tanto un array
bidimensional se puede definir como un array unidimensional de punteros de la forma:
tipo_dato *identificador[LONGITUD];
tipo_dato identificador[FILAS][COLUMNAS];
array
FF FF + sizeof() FF + sizeof()*2 FF + sizeof()*3
FF + sizeof()*4
NOTA:Los contenidos de las celdas no son reales a excepción de los de p_array que si
almacenan una dirección; el resto, sólo representan la dirección de memoria que
ocupan, no el dato almacenado.
Expresiones propias de arrays de punteros, que también pueden usarse con arrays
bidimensionales normales, son las siguientes:
Lo anterior sólo es válido para arrays bidimensionales definidos con sus longitudes dado que
todas las componentes se almacenan en posiciones consecutivas de memoria reservadas por el
compilador en tiempo de compilación; no es válido para un array de punteros a vectores puesto que
cada vector se almacenará en posiciones de memoria no necesariamente consecutivas asignadas en
tiempo de ejecución, no de compilación.
main()
{ int array[FILAS][COLUMNAS];
int i, j;
#include <stdlib.h>
puntero = void (tipo *) calloc(Numero_Elementos, Tamaño_Elemento);
puntero = void (tipo *) malloc(Tamaño_Total_Necesario);
void free(puntero);
Una vez que no se necesita la memoria reservada con las funciones calloc() y/o malloc() hay
que liberarla, devolvérsela al sistema, con la función free() para un uso posterior.
Es necesario reservar memoria para los punteros antes de hacer uso de ellos.
#include <stdlib.h>
#include <conio.h>
#define FILAS 3
#define COLUMNAS 2
main()
{ int **p_p_array;
int i, j;
El encabezamiento de una función con un vector como parámetro formal puede ser de tres
formas diferentes:
main()
{ int vector[ELEMENTOS]={1,2,3,4,5,6};
visualizar(vector);
}
Genéricos:
Visto del modo anterior, las funciones no son independientes; dependen de la constante
simbólica definida anteriormente (excepto las cadenas, que acaban con el nulo). Para solventar esta
dependencia, sólo hay que pasar a la función un parámetro más: la longitud del array.
Longitud fija:
Son necesarios dos índices para referenciar un elemento de una matriz. Cuando pasamos una
matriz a una función, el compilador necesita conocer, por lo menos, la segunda dimensión de la
misma, no puede contentarse con el puntero al comienzo del primer elemento.
En el cuerpo de la función sólo podemos usar un índice dadas las características del parámetro
formal: puntero a entero.
main()
{ void cargar(int *, int, int);
void visualizar(int *, int, int);
int array[FILAS][COLUMNAS];
cargar(&array[0][0], FILAS, COLUMNAS);
printf("Puntero: array= %d\n", array);
printf("Vector de punteros: &array[0]= %d\n", &array[0]);
visualizar(&array[0][0], FILAS, COLUMNAS);
}
void cargar(int *tabla, int f, int c)
{ int i, j;
for (i=0; i<f; i++)
for (j=0; j<c; j++)
*(tabla + i * COLUMNAS + j) = rand() % 100;
}
void visualizar(int *tabla, int f, int c)
{ int i, j;
for (i=0; i<f; i++)
for (j=0; j<c; j++)
printf("Valor= %d\n", *(tabla + i * c + j));
}
El primer array es de punteros; hay que reservar memoria para el tamaño del puntero.
1.- Introducción
En lenguaje C no existe ningún tipo de dato para definir variables de tipo cadena. El medio
para almacenar esta información es un array de caracteres. Existe una convención de representación de
las cadenas:
· por parte del compilador, para representar las cadenas constantes (escritas entre comillas);
· por un cierto número de funciones, que permiten realizar:
· lecturas o escrituras de cadenas;
· tratamientos como concatenación, copia, comparación, ...
cadena
E l h o t e l . . \0
0 1 2 3 4 5 6 7 8 . N-1
porque cadena es un puntero y "Cadena constante" ha sido traducido por el compilador a un puntero a
la memoria donde se almacena; sin embargo, la siguiente asignación no es válida porque, aunque el
nombre de un array es un puntero al mismo, este es constante y no se puede modificar.
char cadena[LONGITUD + 1];
cadena = "Cadena constante";
Si, en lugar de asignar una cadena constante al puntero, queremos usar una de las funciones de
entrada de datos por teclado o copiar el contenido de otra variable, es necesario reservar la memoria
suficiente para almacenar los datos; como todo puntero, no se reserva memoria.
cadena=(char*)malloc(LONGITUD);
scanf("%s", cadena);
printf("%s", cadena);
En cualquiera de las dos inicializaciones anteriores, con longitud explícita o sin ella, el
compilador añade el carácter nulo al final de la cadena.
strcat(destino, blanco);
strcat(destino, segunda);
printf("%s\n", destino);
}
Es igual que la anterior pero copiando de la segunda cadena sólo el número de caracteres
especificado por maxlon.
strcat(destino, blanco);
strncat(destino, segunda, 4);
printf("%s\n", destino);
Compara dos cadenas de las que recibe la dirección. Retorna un valor entero definido como:
Es igual que la anterior pero comparando sólo el número de caracteres especificado por
maxlon.
Son similares a strcmp y strncmp pero sin tener en cuenta la diferencia entre caracteres
mayúsculas y minúsculas. No incluye los caracteres europeos, tal como la 'Ñ'.
Es igual que la anterior pero copiando sólo el número de caracteres especificado por maxlon. Si
la longitud de la cadena origen es inferior a la longitud máxima, el carácter nulo de fin de cadena se
copia; en caso contrario no se copiará.
char *c1 = "xxxxxxxxxxxxxxxxxxxx";
char c2[20] = "bueno";
char c3[20] = "buenos";
strncpy(c1, c2, 6);
printf("%s", c1); //"bueno";
c1 = "xxxxxxxxxxxxxxxxxxxx";
strncpy(c1, c3, 6);
printf("%s", c1); //"buenosxxxxxxxxxxxxxx";
Busca la primera posición que ocupa un carácter en una cadena. Retorna un puntero a esa
posición o uno NULL.
Busca la última posición que ocupa un carácter en una cadena. Retorna un puntero a esa
posición o uno NULL.
Busca la primera ocurrencia completa de una subcadena en una cadena. Retorna un puntero a
esa posición o uno NULL.
Busca la primera ocurrencia de cualquier carácter de una cadena en otra cadena. Retorna un
puntero a esa posición o uno NULL.
Todas reconocen los caracteres adecuados a su tipo de dato. Ignoran los espacios iniciales,
devuelven el valor cero si el primer carácter es erróneo y la exploración finaliza con el primer carácter
no válido.
Convierten una cadena a número double, long y unsigned long respectivamente. El puntero
siguiente es la dirección del siguiente carácter a tratar. Base es la base numérica en la que se trabaja.
1._ Estudiante
2._ Nombre
3._ Primer apellido
3._ Segundo apellido
3._ Nombre
2._ Fecha nacimiento
3._ Día
3._ Mes
3._ Año
2._ Examen(3)
2._ Final
2._ Calificación
struct [<tipo_estructura>]
{ <tipo1> <identificador_campo_1>;
<tipo2> <identificador_campo_2>;
...
<tipoN> <identificador_campo_N>;
}[<variable_estructura>[,...]];
Los campos de la estructura pueden ser de cualquier tipo de dato permitido en C: arrays,
punteros, tipos simples, otras estructuras, ...
<variable_estructura> y <tipo_estructura> son opcionales, pero uno de los dos, o los dos, debe
aparecer en la declaración. Si en la declaración aparece <tipo_estructura>, estamos creando un tipo de
estructura, siendo necesario crear una variable de ese <tipo_estructura> (si está <variable_estructura>
no es necesario crearla pues se crea en la propia declaración). El formato para crear una variable
estructura es el siguiente:
- Crea un tipo estructura - Crea una variable estructura - Crea un tipo estructura
(s_persona). (persona). (s_persona)
- Crea una variable estructura - No se pueden definir más - Se pueden definir más
(la_persona). variables estructura de ese variables de tipo s_persona.
tipo, salvo en la declaración. Para poder usar el nuevo tipo
- Se pueden definir más hace falta definir al menos
variables de tipo s_persona. una variable.
A los campos de una estructura también se les pueden asignar valores iniciales en la
declaración. Estos valores deben aparecer en el orden en que serán asignados a los correspondientes
campos de la estructura, encerrados entre llaves y separados por comas. Permite inicializaciones
parciales.
struct s_persona v_persona = {"Antonio","Pérez",4321};
La representación en memoria interna es semejante a la del array; los campos que componen la
estructura se sitúan en direcciones contiguas y cada uno ocupa lo que le corresponde en función de su
tipo.
Estudiante
Nombre Fecha Nacimiento Examen Final Calif.
P.Ape. S.Ape. Nombre Día Mes Año 0 1 2 Final Calif.
<variable_estructura> . <identificador_campo>
Para acceder a los campos de la estructura anterior podemos hacerlo asignando valores
constantes con el operador de asignación o dando valores introducidos por la entrada estándar (igual
que cualquier otra variable); también se puede asignar toda la información contenida en una estructura
a otra del mismo tipo en lugar de hacerlo campo a campo.
Ejemplo:
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#define TN 3
struct SPersona
{ char Nombre[15];
char Apelli[30];
int Ntas[TN];
int Expediente;
};
main()
{ struct SPersona UnaPersona, OtraPersona;
int i;
gets(UnaPersona.Nombre);
gets(UnaPersona.Apelli);
for (i=0; i<TN; i++)
scanf("%d", &UnaPersona.Notas[i]);
OtraPersona = UnaPersona;
OtraPersona.Notas[0] /= 2;
puts(OtraPersona.Nombre);
puts(OtraPersona.Apelli);
for (i=0; i<TN; i++)
printf("%d\t", OtraPersona.Notas[i]);
getch();
}
De la misma forma que un array (que es un tipo estructurado de datos) puede ser miembro de
una estructura, otra estructura también puede serlo. Cuando esto ocurre, la declaración de la estructura
interna debe aparecer antes que la declaración de la estructura que la contiene.
Para referirse a un campo de la estructura interna debe realizarse con el operador punto; así
mismo, se debe tener en cuenta que esta es un campo de la estructura externa y, por tanto, para accede
a ella hay que usar también el operador punto.
#include<stdio.h> #include<stdio.h>
#include<stdlib.h> #include<stdlib.h>
#include<conio.h> #include<conio.h>
#define TN 3 #define TN 3
main()
{ int i;
struct Estudiante Alumno, Otro;
struct Nombre vNombre;
struct Fecha vFecha;
gets(Alumno.El_Nombre.Nombre);
gets(Alumno.El_Nombre.Apellido1);
gets(Alumno.El_Nombre.Apellido2);
for (i=0; i<TN; i++)
scanf("%d", &Alumno.Notas[i]);
Otro = Alumno;
Alumno.Fecha_Nacimiento= vFecha; //Registro completo, E.interna
}
Se puede usar cualquiera de las dos formas usadas para la declaración de la estructura; todo
depende de las necesidades futuras que tengamos (definir o no variables de las estructuras internas).
Para acceder a un elemento de un array de estructuras, se debe aplicar el índice sobre el nombre
de la variable estructura seguido del operador punto y el campo de la estructura que queremos
referenciar.
Alumno[3].Expediente = 1234;
Ejercicios/Ejemplos:
1.- Se desea escribir un programa para controlar los pasajes de una línea aérea que tiene un
vuelo a ocho ciudades distintas. Para ello se necesita almacenar la capacidad de cada vuelo y el
número de plazas libres de cada vuelo, así como el número de reservas que quedan en lista de espera.
Diseña y escribe la estructura de datos necesaria.
struct Pasajes
{ char Ciudad[20];
int Capacidad;
int Plazas_Libres;
int Reserva_Espera;
} Vuelo[8];
2.- Diseña y escribe una estructura de datos para almacenar la información del problema
anterior, suponiendo que puede haber varios vuelos a una misma ciudad.
struct Tiempo
{ int Hora, Minutos;
};
struct Vuelo
{ int Capacidad;
int Plazas_Libres;
int Reserva_Espera;
struct Tiempo Hora;
};
struct Pasajes
{ char Ciudad[20];
struct Vuelo DatosVuelo[NV];
} DatosPasaje[8];
3.- Se desea escribir un programa para controlar las ventas (en número y pesetas) de una
agencia de automóviles que tiene en plantilla 10 vendedores y distribuye 5 marcas de coche cada una
con 3 modelos diferentes. Diseña y escribe la estructura de datos necesaria para ello.
struct Venta
{ int Numero;
long Valor;
};
struct Agencia
{ int Codigo_Vendedor;
struct Venta Coches[MARCA][MODELO];
} Vendedores[NUMERO];
4._ Programa que carga el nombre y número de teléfono de varias personas y realiza una
clasificación ascendente por orden alfabético de nombres y, finalmente, imprime la lista ordenada de
nombres y teléfonos.
significa que se reconoce la palabra real como otro nombre de float, siendo equiparable en todos los
aspectos.
float Temperatura;
real Temperatura;
Nota: Esta declaración puede combinarse con la anterior, usando y sin usar typedef. Es
posible definir más de un nuevo tipo.
Ejemplo:
<tipo_estructura>* <identificador_estructura>
Para acceder a los campos de una estructura referenciada por un puntero no se usa el operador
'.' por que se espera, como primer operando, el identificador de una estructura y no una dirección. Por
tanto, tenemos dos formas de acceder a un campo de la estructura referida por un puntero:
2. Usar el nuevo operador '->' o selector de campo indirecto que permite acceder a los distintos
campos de la estructura a partir de su dirección de comienzo.
struct Estudiante Alumno, *Otro;
gets(Alumno.Expediente);
gets(Alumno.El_Nombre.Nombre);
gets(Alumno.Final);
Otro = &Alumno;
printf("%d\n", Otro->Expediente);
printf("%d\n", (*Otro).Final);
printf("%s\n", Otro->El_Nombre.Nombre);
El operador '' puede combinarse con el operador '.' para acceder a campos de estructuras
incluidas en otras más externas.
Pasando un campo:
Si se pasan los parámetros por valor, los cambios realizados sobre estos en el cuerpo de la
función no se ven reflejados en la función llamante y los campos se referencian con el operador '.'; si
se pasan por referencia, los cambios si se ven reflejados y los campos se referencian con el operador '-
>'.
El tipo de dato que retorna una función también puede ser una estructura.
typedef struct
{ int telefono;
char nombre[30];
} agenda;
main()
{ int i;
agenda las_personas[NPERSONAS];
ordenar(las_personas, NPERSONAS);
8.- Uniones.
Una unión es una estructura en la que sus elementos ocupan la misma posición de memoria. El
tamaño de la memoria es el correspondiene al elemento mayor de la unión y los demás elementos
comparten dicha memoria.
union [<tipo_union>]
{ <tipo1> <identificador_campo_1>;
<tipo2> <identificador_campo_2>;
...
<tipoN> <identificador_campo_N>;
}[<variable_union>[,...]];
main()
{ TBbi Bbi;
scanf("%d", &Bbi.Tipo);
switch (Bbi.Tipo)
{ case Libro:
puts("Libro");
scanf("%d", &Bbi.Publicacion.DatosEd.Edicion);
break;
case Revista:
puts("Revista");
scanf("%d", &Bbi.Publicacion.Fecha.Dia);
break;
}
}
· Si el almacenamiento es limitado.
· Los dispositivos transmiten información codificada en bits.
· Ciertas rutinas de cifrado necesitan acceder a los bits de un byte.
Todas estas tareas pueden ser realizadas usando operadores para el manejo de bits pero un
campo de bits realza la estructura del código del programa. El método usado en C para acceder a los
bits es la estructura, donde un campo de bits es un tipo especial de campo que define su longitud en
bits. El formato general es el siguiente :
struct [<tipo_estructura>]
{ <tipo1> <identificador_campo_1> : longitud_1;
<tipo2> <identificador_campo_2> : longitud_2;
...
<tipoN> <identificador_campo_N> : longitud_N;
}[<variable_estructura>[,...]];
El tipo de un campo de bits debe ser int, unsigned o signed, depende del compilador. Longitud
indica el número de bits del campo.
Ejemplos :
// Empleado de una empresa
struct Emp
{ struct Dir Dirección ;
float Paga ;
unsigned baja :1 ;
unsigned PoHoras :1 ;
unsigned Deducciones :3 ;
} ;
10.- Enumeraciones.
Una enumeración es un conjunto de constantes enteras con nombre que especifica todos los
valores válidos que puede tomar una variable de ese tipo. El formato de definición de enumeraciones
es el siguiente:
La sentencia
enum Semana {Lunes,Martes,Miércoles,Jueves,Viernes,Sábado,Domingo}
enum Semana Hoy, Maniana ;
define una enumeración denominada Semana y declara dos Hoy y Maniana como dos variables
de ese tipo; estas sólo pueden tomar los valores del tipo semana enumerados.
main()
{ Hoy= Lunes;
cout << Hoy << '\n';
Maniana= Hoy + 1;
cout << Maniana << '\n';
for(Hoy= Lunes;Hoy<=Domingo;Hoy++)
cout << Hoy << '\n';
for(Hoy=Lunes; Hoy<=Domingo;Hoy++)
switch (Hoy)
{ case Lunes: cout << "Lunes" << '\n'; break;
case Martes: cout << "Martes" << '\n'; break;
case Miercoles:cout << "Miércoles" << '\n'; break;
}
printf(“%s”, Hoy) ;
//Es un error. Hoy es un entero, no una cadena
}
1.- Introducción.
Hasta ahora, los objetos tratados por un programa estaban limitados por la cantidad de memoria
principal del ordenador y por que su existencia se limita al tiempo de ejecución del programa, son
volátiles. Cada vez que se ejecuta el programa es necesario introducir los datos por teclado. Ahora,
para subsanar esto problemas, los datos se deben manipular de forma que puedan ser usados por
futuros programas utilizando estructuras de datos externas o ficheros. Estos nos permitirán almacenar
grandes cantidades de información de forma permanente para, como se dijo antes, recuperarla y
tratarla en futuras operaciones de modificación o actualización.
Registro.
Tipo de datos estructurado que consta de un conjunto de elementos que pueden ser de igual o
distinto tipo. Es la unidad lógica mínima/ básica de acceso a un fichero.
Campo.
Cada uno de los elementos o informaciones individuales que componen un registro. Un campo
puede estar dividido en subcampos.
Un archivo es, por tanto, un conjunto de datos estructurados en una colección de entidades
elementales o básicas denominadas registros que son de igual tipo y que constan, a su vez, de
diferentes entidades de nivel más bajo denominados campos.
Clave.
Campo especial de un registro que es capaz de identificarlo de forma unívoca. El valor de este
campo es único, es decir, no puede ser tomado por ningún otro campo clave de otro registro.
Registro físico.
Bloque o cantidad mínima de información que se transfiere en cada operación de
Entrada/Salida sobre un archivo. Su tamaño depende de las características físicas del ordenador
y del sistema operativo (buffers). Puede contener uno o más registros lógicos. Un registro
lógico puede ocupar menos de un registro físico, uno completo o más de uno.
Factor de bloqueo.
Número de registros lógicos que contiene cada registro físico. Se supone que el registro físico
tendrá un tamaño, en bytes, mayor o igual al del registro lógico.
ARCHIVO
Reg. 1 REGISTRO CAMPO
Reg. 2
Reg. 3 Nombre Profesión Dirección Teléfono Ciudad Comunidad
· Temporales.
· De movimiento. Contienen la información que se utilizará para actualizar los archivos
maestros; indican cuando realizar altas, bajas, etc. Suelen desaparecer una vez cumplido su
cometido.
· De maniobra o trabajo. Son archivos auxiliares que se crean temporalmente y sólo existen en
tiempo de ejecución del programa. Por ejemplo, archivos intermedios de ordenación.
· Creación. Para poder realizar cualquier operación sobre un fichero es necesario que exista
previamente, que haya sido creado grabando sobre el soporte externo la información requerida para
su posterior tratamiento.
· Apertura. Para poder acceder a los datos y así permitir realizar las operaciones necesarias de
lectura y escritura de los mismos sobre el fichero, debemos abrirlo. Un fichero no debe permanecer
abierto más tiempo del necesario para operar sobre él.
· Cierre. Una vez efectuadas las operaciones sobre el fichero, debe cerrarse para limitar el acceso a
los datos y evitar la corrupción de los mismos.
· Copiado o duplicado. Crea un nuevo fichero con la misma estructura y contenido de un fichero
fuente.
· Concatenación. Consiste en que dados dos ficheros de igual estructura, se obtiene un tercer
fichero que tiene la misma estructura y contenido la suma de los dos anteriores.
· Fusión o mezcla. Esta operación permite obtener de dos o más ficheros con la misma clasificación
y estructura interna de sus datos, un nuevo fichero que contenga los registros de todos los
anteriores sin alterar la ordenación que éstos tenían establecida.
· Partición. Permite obtener varios ficheros de uno inicial en función de alguna de las características
internas de sus campos.
· Consulta. A través de las consultas es posible acceder a los registros del fichero y conocer el
contenido de sus campos.
· Bajas. Consiste en eliminar uno o varios registros del fichero. Esta operación requiere un proceso
previo de lectura o consulta para la localización del registro que se pretende eliminar. El borrado
de registros se puede realizar de dos formas diferentes:
· Marcando de alguna manera (flag o bandera) la posición ocupada por dicho registro y
permitiendo así su posterior recuperación en el caso de que haya habido algún error.
· Eliminando físicamente el registro del fichero haciendo una compactación del mismo o
desplazando todos los registros posteriores una posición y eliminando, por tanto, el hueco que
hubiera generado. Con esta forma de borrado de registros se pierde toda posibilidad de
recuperar la información.
· Modificaciones. Consiste en realizar un cambio total o parcial de uno o varios campos de los
registros de un fichero. Requiere, igual que la baja, un proceso previo de lectura para la
localización del registro que se pretende modificar y un segundo proceso de escritura para la
actualización del registro.
· Consultas. Esta operación permite acceder a uno o varios registros con la intención de visualizar
su contenido por pantalla, impresora u otro periférico.
· Acceso. Es el procedimiento necesario que debemos seguir para situarnos sobre un registro
concreto con la intención de realizar una operación de lectura o escritura sobre el mismo. También
existe una estrecha dependencia entre el tipo de soporte utilizado y el tipo de acceso. Los tipos de
acceso son secuencial, directo y por índice.
Archivo
Registro 1 Registro 2 ... Registro N-1 Registro N EOF
Los ficheros organizados secuencialmente contienen un registro particular (el último) que
contiene la marca fin de archivo (EOF o FF).
Este tipo de organización se soporta sobre todos los tipos de dispositivo de memoria auxiliar
existentes.
Ventajas:
· Rapidez en el acceso a un bloque de registros.
· No deja espacios vacíos intermedios entre registro y registro.
Inconvenientes:
· Para consultar un registro concreto es necesario consultar todos anteriores.
· No permite la inserción de nuevos registros, sólo nos permite añadirlos al final del
fichero creando un nuevo fichero.
· No permite borrar registros, sólo marcarlos creando un fichero nuevo.
· Deben almacenarse en un soporte direccionable; cada posición se localiza por una dirección
absoluta (el programador sólo gestiona direcciones relativas).
· El almacenamiento físico de los registros sobre el soporte seleccionado se realiza a través de una
clave que indica la posición de almacenamiento del registro dentro del fichero.
· La dirección de almacenamiento del registro en el dispositivo se obtiene siempre de la clave del
propio registro:
· Si la clave no es numérica se aplican algoritmos o fórmulas de transformación para obtener
valores enteros positivos que facilitan su posterior manejo y tratamiento.
· Si la clave es numérica se aplica un algoritmo de transformación para obtener un rango de
valores comprendidos dentro del intervalo de valores de las direcciones de memoria
disponibles, estableciendo así una relación directa entre dirección lógica y dirección física.
Esta organización se da en cuando la clave es numérica y además tiene un valor que está dentro
del rango de direcciones válidas, estableciéndose así una relación directa entre la clave y la dirección
que ocupa el registro dentro del fichero. El valor de la clave siempre está en relación con la
capacidad máxima del soporte físico, por lo que nunca podemos almacenar un registro cuya clave esté
por encima de los límites máximos del fichero.
Ventajas:
· Permite un acceso inmediato, directo, a los datos.
· Permite realizar operaciones de entrada y salida simultáneamente.
· Son muy rápidos en el tratamiento individual de registros.
· Permiten acceder a los datos tanto directamente (por su clave) como secuencialmente.
Inconvenientes:
· Al realizar una lectura secuencial del fichero podemos encontrarnos con huecos vacíos
de forma que hay que tratarlos convenientemente y como consecuencia hay una perdida
de tiempo en el procesamiento de la información.
· Desaprovechamiento del soporte debido a la gran cantidad de huecos que pueden
existir.
Esta organización se da en aquellos casos en que la clave –ya sea de tipo numérico entero o de
tipo cadena- debe sufrir una transformación de la clave para obtener un valor numérico entero que esté
comprendido dentro del rango de direcciones válidas y así establecer la correspondencia directa entre
la clave y la dirección de memoria que ocupa el registro en el soporte externo en el que se almacena el
fichero.
En este tipo de organización, cada dirección puede ser ocupada por más de un registro debido a
que el algoritmo de transformación aplicado a claves con valores diferentes ha generado la misma
posición de almacenamiento en memoria. Este hecho se conoce con el nombre de “sinónimo” o
“colisión”.
Ventajas:
· Permite un acceso inmediato a los registros haciendo sólo referencia a su clave.
· No requiere procesos de ordenación.
· Permite realizar operaciones de entrada y salida a la vez.
· Son muy rápidos en el tratamiento individual de registros.
· Permiten acceder a los datos secuencialmente.
Inconvenientes.
· Los mismo que la organización directa.
· Se necesita programar la relación que existe entre la clave y la posición de memoria que
ocupa el registro y también hay que programar el tratamiento de sinónimos. Se debe
procurar que estos algoritmos generen el menor número posible de sinónimos y de
huecos vacíos.
· Área de datos o primaria: contiene los registros en forma secuencial y está organizada en
secuencia de claves sin dejar huecos intercalados.
· Área de índices: es una tabla que contiene los niveles de índice, la existencia de varios
índices enlazados se denomina nivel de indexación.
· Área de desbordamiento: utilizada, si fuese necesario, para las actualizaciones.
Ventajas.
· Acceso rápido.
· El sistema de gestión de archivos se encarga de relacionar la posición de cada registro
con su contenido mediante la tabla de índices.
Inconvenientes.
· Desaprovechamiento del espacio por quedar huecos intermedios cada vez que se
actualiza el archivo.
· Se necesita espacio adicional para el área de índices.
typedef struct
{
short level;
unsigned flags;
char fd;
unsigned char hold;
short bsize;
unsigned char *buffer, *curp;
unsigned istemp;
short token;
} FILE;
FILE <*Puntero_archivo>;
FILE *alumnos;
Nombre_Fichero:
Cadena de caracteres que especifica el nombre físico –externo- del archivo de
datos. Opcionalmente puede contener el path con su ruta de acceso; en este caso, hay
que recordar que la barra invertida de path (\) debe expresarse como dos caracteres (\\)
para que no indique sólo el carácter de escape.
Modo_Apertura:
Es una cadena que especifica el modo en que se abrirá el archivo; es decir, si se
van a realizar sobre el fichero operaciones de entrada, de salida o ambas
simultáneamente. Los caracteres que indican dicho modo son los siguientes:
-r Abre el archivo para lectura. El archivo debe existir previamente, si no se produce un error.
-w Abre un archivo para escritura. Si no existe se crea, y si existe, se destruye el contenido del
anterior.
-a Abre el archivo para escritura desde el final del mismo, para añadir. Si el archivo no existía,
se crea uno nuevo.
- r+ Abre el archivo para Lectura/Escritura por el comienzo. El archivo debe existir previamente
o se produce un error.
- w+ Abre el archivo para Lectura/Escritura por el comienzo. Si no existía se crea uno nuevo, y si
existe, se destruye el contenido del anterior.
- a+ Abre el archivo para Lectura/Escritura por el final. Si no existía se crea uno nuevo.
-t Son dos modificadores que pueden acompañar como sufijo a las seis modalidads anteriores
b con el fin de especificar si se trabaja en modo texto –dado por omisión- (t) o en modo
binario (b).
Ejemplos:
FILE *datos;
char *archivo = "c:\\trabajo\\lc\infor.dat";
datos = fopen("c:\\trabajo\\lc\infor.dat", "w");
datos = fopen(archivo, "r");
datos = fopen(archivo, "wb");
La función fopen() asigna un stream (canal por el que leer y escribir) y un buffer (memoria
intermedia temporal) a un archivo, devolviendo un puntero a dicho canal. Existen varios stream
predefinidos en C que se abren automáticamente al iniciar el programa; estos son:
Escritura de caracteres:
int fputc(int c, FILE *fichero); Función
int putc(int c, FILE *fichero); Macro
Lectura de caracteres:
int fgetc(FILE *fichero); Función.
int getc(FILE *fichero); Macro.
La función fgetc() realiza la operación inversa a fputc(); lee un carácter del archivo, el situado
en la posición actual de la cabeza de E/S, y avanza al siguiente carácter. Si se produce algún error
retorna EOF.
Ejemplo: igual que el anterior pero pasando los nombres de los ficheros como
argumentos desde el S.O.
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
FILE *abrir_archivo(char *nombre, char *modo);
main(int argc, char *argv[])
{ FILE *entrada, *salida;
int caracter;
que retorna el último carácter escrito en el archivo si no se ha producido ningún error (un número no
negativo) o EOF si se ha producido. La copia de la cadena se realiza desde el principio de la misma
hasta el carácter nulo, sin escribir este en el archivo; el carácter de fin de línea tampoco se escribe en el
archivo ; si se necesita debemos especificarlo.
que retorna una cadena de caracteres si la lectura ha sido correcta o NULL si se ha producido algún
error. Lee LONGITUD - 1 caracteres o hasta que encuentra carácter de fin de línea (leyéndolo y
añadiéndolo) y posteriormente añade el carácter nulo a la cadena.
Estas funciones son equivalentes a gets() y puts(), pero estas realizan la entrada/salida por stdin
y stdout.
fclose(entrada);
fclose(salida);
}
Ejercicios:
1. Contar el número de veces que aparecen en un archivo de texto cada uno de los caracteres
ASCII.
2. Leer un texto por teclado y escribirlo en un fichero de texto.
fprintf() escribe la lista de elementos especificada en el formato indicado; fscanf() lee del
fichero las variables de la lista con el formato especificado.
main()
{ FILE *entrada;
ALUMNO alumno;
int i;
entrada = abrir_archivo("c:\\trabajo\\alumno.txt", "r");
for (i=0; i<NALUM; i++)
{ leer_alumno(entrada, &alumno);
escribe_alumno(alumno);
}
fclose(entrada);
}
void leer_alumno(FILE *f, ALUMNO *a)
{ int j;
fscanf(f, "%30s", a->nombre); // fgets(a->Nombre, 30, f);
for (j=0; j<NOTAS;j++)
fscanf(f, "%d", &a->notas[j]);
}
void escribe_alumno(ALUMNO a)
{ int j;
printf("%-30s", a.nombre);
for (j=0; j<NOTAS;j++)
printf("%d\t", a.notas[j]);
printf("\n");
}
La función fread() lee un número “num_b” de bloques de tamaño “tam_b” bytes del archivo
'p_arch' y lo almacena en la posición indicada por el puntero 'p_bloque'. La memoria donde se
almacenarán los datos debe estar reservada. Retorna el número de bloques leídos del fichero; así, este
valor nos permite conocer si se ha producido un error en la lectura.
size_t fwrite(const void *p_bl, size_t ta_b, size_t nu_b, FILE *p_arch);
La función fwrite realiza la acción contraria a fread(); escribe un numero “num_b” de bloques
de tamaño “tam_b” bytes tomados de la posición indicada por el puntero 'p_bloque' en el archivo
'p_arch'. Retorna el número de bloques escritos en el archivo. También pueden detectarse posible
errores cometidos en la escritura.
Tanto en la lectura como en la escritura, el número de bytes transferidos viene dado por
“num_b * tam_b” (número de bloques a transferir multiplicado por el tamaño del bloque). El tamaño
del bloque se calcula, como siempre, con la función sizeof().
Ejemplo: Crear y listar un archivo con el nombre y las notas de los alumnos. Usar las
funciones del ejercicio anterior.
void escribe_alumno(FILE *salida, ALUMNO a)
{ fwrite(&a, sizeof(a), 1, salida);
}
void leer_alumno(FILE *f, ALUMNO *a)
{ fread(a, sizeof(*a), 1, f);
}
Esta función detecta el final del archivo cuando se realiza una lectura después del último
registro o byte; es decir, para detectar el final del fichero es necesario realizar una lectura del fichero,
no basta con abrirlo. Retorna 0 si no es el final del fichero o un valor distindo si estamos en el final del
mismo. La función (MACRO) puede usarse con archivos binarios y archivos de texto.
desplaza: Número de bytes a partir de la posición seleccionada (origen) que queremos que
se desplace la cabeza de E/S. Puede ser tanto positivo como negativo.
sizeof(tipo_dato) * posicion_relativa_registro
do
{ scanf("%ld", &p);
if (p>0)
{ if(leer_alumno(entrada, &alumno, p))
escribe_alumno(alumno);
else
puts("No está");
}
}while (p>0);
int leer_alumno(FILE *f, ALUMNO *a, long posicion)
{ fseek(f, sizeof(ALUMNO)*(posicion-1), SEEK_SET);
fread(a, sizeof(*a), 1, f);
return !feof(f);
}
Puede ser usado para prevenir el intento de posicionamiento más allá del final del fichero.
El EOF (error) persite hasta que se ejecuta una de estas funciones: clearerr(), rewind() o
fclose().
Retorno:
0 Correcto.
EACCES Permiso denegado. Ya existe o path inválido.
ENOENT No existe el fichero o directorio.
ENOTSAM No es el mismo periférico.
if (rename(viejo, nuevo) == 0)
printf("Renombrado.\n");
else
perror("Rename"); //Rename: descripción del error
Retorna:
0 Correcto
EACCES Permission denied
ENOENT No such file or directory
Cuando las funciones rename() o remove() retornan un error, puede invocarse a la función
perror() para que visualice cual es el error cometido.
Si se desea realizar el mismo tratamiento para todos los registros que tienen el mismo valor de
campo clave sólo hay que suprimir el interruptor 'Hallado' de la condición de terminación del bucle y
realizarlo dentro del cuerpo del bucle.
Algoritmo que busca el primer registro que cumple la condición. Este algoritmo podría dar
algún problema; conviene usar el interruptor.
Sea el archivo F, que se desea dividir en dos, F1 y F2, copiando en el primero los registros de F
que ocupan posiciones impares, y en el segundo los que ocupan posiciones pares (longitud de
secuencia 1).
int SW = -1
...
fread(&R, sizeof(REGISTRO), 1, F);
while (!feof(F))
{ SW = -SW;
if (SW==1) fwrite(&R, sizeof(REGISTRO), 1, F1);
else fwrite(&R, sizeof(REGISTRO), 1, F2);
fread(&R, sizeof(REGISTRO), 1, F);
}
Sea el archivo F, que se desea dividir en dos, F1 y F2, copiando alternativamente en uno y otro
secuencias de registros de longitud N.
int SW = 1;
int C = 0;
...
fread(&R, sizeof(REGISTRO), 1, F);
while (!feof(F))
{ if (SW)
fwrite(&R, sizeof(REGISTRO), 1, F1);
else
fwrite(&R, sizeof(REGISTRO), 1, F2);
fread(&R, sizeof(REGISTRO), 1, F);
if(N == ++C)
{ SW = -SW;
C = 0;
}
}
while (!feof(F))
{ copiar_secuencia(F, F1, N);
if (!feof(F))
copiar_secuencia(F, F2, N);
}
}
Ejercicio: Una compañía organiza una encuesta para determinar el éxito de sus productos
musicales. La población encuestada se va a dividir en cuatro categorías según sexo y edad (por
ejemplo menores o iguales a veinte años y mayores de veinte años). A cada persona se le pide que dé
el nombre de cinco éxitos según su orden de preferencia. Los éxitos se identifican por los números de
1 a N. Los resultados se almacenan en un fichero, donde cada elemento del fichero representa a un
encuestado que incluye los datos de su nombre y apellidos, además de los mencionados anteriormente.
· Crear el archivo con los datos de las encuestas.
· Listar los éxitos en orden de popularidad. Para cada uno se lista el número del éxito y el
número de veces que ha sido mencionado.
· Obtener cuatro archivos de texto con las listas de los nombres y apellidos de todos los
encuestados, según su categoría, que habían mencionado en primer lugar uno de los tres éxitos
más populares.
#include <stdio.h>
#include <stdlib.h>
#define NO "c:\\temporal\\numeros.txt"
#define NO1 "c:\\temporal\\numeros1.txt"
#define NO2 "c:\\temporal\\numeros2.txt"
typedef struct
{ int C;
} NUME;
void copiar_resto(FILE *desde, FILE *hasta, NUME R);
main()
{ FILE *F;
FILE *F1;
FILE *F2;
NUME R1;
NUME R2;
F = fopen(NO, "w");
F1 = fopen(NO1, "r");
F2 = fopen(NO2, "r");
fread(&R1, sizeof(NUME), 1, F1);
fread(&R2, sizeof(NUME), 1, F2);
while (!feof(F1) && !feof(F2))
if(R1.C < R2.C)
{ fwrite(&R1, sizeof(NUME), 1, F);
fread(&R1, sizeof(NUME), 1, F1);
}
else
{ fwrite(&R2, sizeof(NUME), 1, F);
fread(&R2, sizeof(NUME), 1, F2);
}
copiar_resto(F1, F, R1);
copiar_resto(F2, F, R2);
fclose(F);
fclose(F1);
fclose(F2);
}
void copiar_resto(FILE *temporal, FILE *fichero, NUME R)
{ while (!feof(temporal))
{ fwrite(&R, sizeof(R), 1, fichero);
fread(&R, sizeof(R), 1, temporal);
}
}
F: 15, 18, 7, 75, 14, 13, 43, 40, 51, 93, 75, 26, 64, 27, 13
F1: 15 7 14 43 51 75 64 13
F2: 18 75 13 40 93 26 27
main()
{ int S = 1, lon;;
FILE* F, F1, F2;
F = fopen(NO, "r");
fseek(F, 0, SEEK_END);
lon = ftell(F) / sizeof(REGISTRO);
fclose(F);
while(S < lon)
{ F = fopen(NO, "r");
F1 = fopen(NO1, "w");
F2 = fopen(NO2, "w");
partir(F, F1, F2, S);
F = freopen(NO, "w", F);
F1 = freopen(NO1, "r", F1);
F2 = freopen(NO2, "r", F2);
mezclar(F, F1, F2, S);
S *=2;
fclose(F);
fclose(F1);
fclose(F2);
}
}
void partir(FILE *F, FILE *F1, FILE *F2,int S)
{ while (!feof(F))
{ copiar_secuencia(F, F1, S);
copiar_secuencia(F, F2, S);
}
}
void mezclar(FILE *F, FILE *F1, FILE *F2,int S)
{ REGISTRO R1, R2;
int N1, N2;
fread(&R1, sizeof(REGISTRO), 1, F1);
fread(&R2, sizeof(REGISTRO), 1, F2);
while(!feof(F1))
{ N1 = N2 = 0;
// Copia una secuencia. Finaliza una de las dos.
while ((!feof(F1) && N1 < S) && (!feof(F2) && N2 < S))
if(R1.C < R2.C)
{ fwrite(&R1, sizeof(REGISTRO), 1, F);
fread(&R1, sizeof(REGISTRO), 1, F1);
N1++;
}
else
{ fwrite(&R2, sizeof(REGISTRO), 1, F);
fread(&R2, sizeof(REGISTRO), 1, F2);
N2++;
}
// Copia el resto de una secuencia.
while(!feof(F1) && N1++ < S)
{ fwrite(&R1, sizeof(REGISTRO), 1, F);
fread(&R1, sizeof(REGISTRO), 1, F1);
}
Utiliza tres archivos auxiliares, junto con el original, para la realización simultánea de fusión y
partición. Durante el proceso de fusión-partición, dos o más secuencias ascendentes, que estén
consecutivas, pueden constituir una única secuencia para el paso siguiente. El proceso termina cuando,
en la realización de la fusión-partición, el segundo archivo queda vacío. El archivo totalmente
ordenado estará en el primero.
F: 15, 18, 7, 75, 14, 13, 43, 40, 51, 93, 75, 26, 64, 27, 13
F1:
Partición inicial.
F2: 15 18, 14, 40 51 93, 26 64, 13
F3: 7 75, 13 43, 75, 27
Primera fusión-partición.
F: 7 15 18 75, 26 27 64
F1: 13 14 40 43 51 75 93, 13
Segunda fusión-partición.
F2: 7 13 14 15 18 40 43 51 75 75 93
F3: 13 26 27 64
Tercera fusión-partición.
F: 7 13 13 14 15 18 26 27 40 43 51 64 75 75 93
F1:
Se produce una ruptura de control por Departamento, dentro de éste por Mes y dentro de éste
por Vendedor.
fread(&R, sizeof(R), 1, F);
while (! feof(F))
{ auxRC_1 = R.RC1;
// Otras inicializaciones
while(!feof(F) && auxRC1 == R.RC1)
{ auxRC2 = R.RC2;
// Otras inicializaciones
...
while(!feof(F) && auxRC1==R.RC1 &&...&& auxRCN==R.RCN)
{ TRATAR R
fread(&R, sizeof(R), 1, F);
}
// Operaciones tras ruptura de control
...
}
// Operaciones tras ruptura de control
}
// Operaciones tras ruptura de control
Práctica 1:
Las ventas de artículos de una determinada cadena de almacenes se encuentran almacenadas en
un fichero, de tal forma que cada registro, que contiene las ventas de un determinado almacén de la
cadena, tiene la siguiente estructura:
REGISTRO almacen
Codigo ES CADENA DE 5
Ventas_articulos ES ARRAY DE 5 ENTEROS
FIN_REGISTRO
Por otra parte, en otro fichero, que contiene uno o más registros, se almacena el nombre de
cada artículo y su precio.
De pide:
a) Programa que cree el fichero de artículos.
b) Programa que cree el fichero de ventas.
c) Listado del total de ventas de cada almacen (en pesetas).
d) Listar las ventas (en cantidad y pesetas) de los artículos.
#define NARTICULOS 5
#define EX_ALMACEN "C:\\TRABAJO\\ALMACEN.FIC"
#define EX_ARTICULO "C:\\TRABAJO\\ARTICULO.FIC"
typedef struct
{ char codigo[5];
int ventas[NARTICULOS];
}ALMACEN;
typedef struct
{ char descripcion[NARTICULOS][30];
int precio[NARTICULOS];
}ARTICULOS;
main()
{ FILE *f_articulo;
ARTICULOS s_articulo;
FILE *f_almacen;
ALMACEN s_almacen;
int i, seguir;
f_articulo = fopen(EX_ARTICULO, "r");
fread(&s_articulo, sizeof(s_articulo), 1, f_articulo);
fclose(f_articulo);
f_almacen = fopen(EX_ALMACEN, "w");
do
{ do
{ printf("Entrar almacen(s/n)");
fflush(stdin);
seguir=getchar();
}while(toupper(seguir)!='S' && toupper(seguir)!='N');
if(toupper(seguir)=='S')
{ printf("Código de almacen: ");
fflush(stdin);
gets(s_almacen.codigo);
for (i=0; i<NARTICULOS; i++)
{ printf("Ventas %s: ", s_articulo.descripcion[i]);
fflush(stdin);
scanf("%d", &s_almacen.ventas[i]);
}
fwrite(&s_almacen, sizeof(s_almacen), 1, f_almacen);
}
}while(toupper(seguir)=='S');
fclose(f_almacen);
}
#define NARTICULOS 5
#define EX_ALMACEN "C:\\TRABAJO\\ALMACEN.FIC"
#define EX_ARTICULO "C:\\TRABAJO\\ARTICULO.FIC"
typedef struct
{ char codigo[5];
int ventas[NARTICULOS];
}ALMACEN;
typedef struct
{ char descripcion[NARTICULOS][30];
int precio[NARTICULOS];
}ARTICULOS;
main()
{ FILE *f_almacen;
ALMACEN s_almacen;
FILE *f_articulo;
ARTICULOS s_articulo;
int ventas_articulos[NARTICULOS], i;
Ejercicio:
Página: ...
Listado de cobros de la categoría DESCRIPCIÓN
NOMBRE EDAD DIRECCIÓN SUELDO NETO
__________________________________________________________________
.................. .. .................. ......
.................. .. .................. ......
...
SUELDO NETO TOTAL ......
Obtener el número de empleados y el sueldo bruto total de cada categoría, dando un listado de
la categoría, número de empleados y sueldo bruto ordenado por número de empleados, y en caso de ser
iguales, por sueldo bruto.
entorno:
PERSONAL es archivo de RPERSONAL
RPERSONAL es registro compuesto de
NOMBRE es de tipo alfanumérico
EDAD es de tipo numérico entero
DIRECCION es de tipo alfanumérico
SUELDOBRUTO es de tipo numérico entero
ESTADOCIVIL es de tipo numérico entero
CATEGORIA es de tipo alfanumérico
fin registro
FCATEGORIA es archivo de RCATEGORIA
RCATEGORIA es registro compuesto de
TIPOCATEGORIA es de tipo alfanumérico
DESCUENTO es de tipo numérico entero
DESCRIPCION es de tipo alfanumérico
fin registro
SUELDONETO,NETOTOAL,NR son de tipo numérico entero
MAYOREDAD,MAYORNETO son de tipo numérico entero
LINEA,PAGINA son de tipo numérico entero
AUXCAT es de tipo alfanumérico
algoritmo:
escribir 'EDAD MAYOR A :'
escribir 'SUELDO MAYOR A :'
leer MAYOREDAD, MAYORNETO
PAGINA=0
abrir PERSONAL,FCATEGORIA para lectura
leer PERSONAL,RPERSONAL
mientras no ff(PERSONAL) hacer
NR=0
repetir
NR=NR+1
leer FCATEGORIA,RCATEGORIA,NR
hasta TIPOCATEGORIA=CATEGORIA
AUXCAT=CATEGORIA, NETOTOTAL=0
mientras no ff(PERSONAL) y AUXCAT=CATEGORIA hacer
PAGINA=PAGINA+1
escribir 'Página : ',PAGINA
escribir 'Listado de .....', DESCRIPCION
escribir 'NOMBRE EDAD DIRECCION SUELDO'
escribir '____________________________________'
LINEA=0
mientras no ff(PERSONAL) y AUXCAT=CATEGORIA y
LINEAS<=55 hacer
LINEA=LINEA+1
SUELDONETO=SUELDOBRUTO-DESCUENTO
NETOTOTAL=NETOTOTAL+SUELDONETO
escribir NOMBRE,EDAD,DIRECCION,SUELDONETO
si EDAD>MAYOREDAD y SUELDONETO>MAYORNETO
NUMPER=NUMPER+1
fin si
leer PERSONAL,RPERSONAL
fin mientras
fin mientras
escribir 'NETO TOTAL... ',NETOTOTAL
fin mientras
cerrar PERSONAL, FCATEGORIA
escribir 'Los empleados con edad mayor a ',MAYOREDAD,
'y sueldo mayor a ',MAYORNETO,' son ',NUMPER
PRÁCTICA:
En el fichero secuencial CLIENTES.DAT se guardan los datos personales de los clientes de un
banco, cuyos campos son:
donde SALDO almacena la cantidad actual del cliente: puede ser un saldo deudor.
También se dispone de otro fichero secuencial con los clientes que han tenido movimiento en
sus cuentas corrientes, cuyos formatos son:
1.- Introducción.
Las estructuras estáticas necesitan un espacio de asignación de memoria fijo para almacenar los
datos. En la práctica suelen darse casos en los que una estructura de datos puede crecer y/o disminuir
sin conocer previamente la extensión o tamaño de los datos. Por ello es necesario un mecanismo que
no realice una asignación fija de memoria, si no que sea variable con el tiempo; es lo que se conoce
como asignación dinámica de memoria. El mecanismo principal de esta asignación dinámica de
memoria para almacenar ciertos elementos de una estructura es el puntero, que es en sí una variable
dinámica, y se utiliza para construir elementos de este tipo. Por tanto, no existen por sí mismas, si no
que se basan en una implementación con variables de tipo puntero, siendo necesario reservar la
memoria suficiente para almacenar los datos.
Listas enlazadas
Lineales Pilas
Colas
Árboles
No lineales
Grafos
Información Enlace
Representación gráfica de un nodo con dos campos
6 4 3 9
Representación gráfica de los elementos de un array.
2.1.- Implementación.
Para realizar la implementación del tipo lista enlazada es necesario usar estructuras
autoreferenciadas o recursivas: el nodo contiene elementos de su misma categoría; es decir, estructuras
en las que uno de sus campos es un puntero (enlace) que referencia a la propia estructura. El formato
general para la declaración de una estructura de este tipo es el siguiente:
struct <IdEstructura>
{ <tipo> <IdCampo>; //Uno o varios campos de datos.
struct <IdEstructura> *<IdCampoEnlace>;//Campo enlace
};
typedef struct <IdEstructura> *<Identificador>;
Y la componente de una lista, según el formato anterior, quedaría implementada del siguiente
modo:
Por ejemplo, para describir una lista de números enteros, se puede escribir lo siguiente:
typedef int TInformación; //Redefinición de un entero
struct Nodo
{ TInformacion Dato;
struct Nodo *Siguiente;
};
typedef struct Nodo *TLista;
main()
{ TLista LaLista;//Variable cabecera de la lista.
}
2.2.1.- Inicialización.
Esta crea una lista vacía. La variable cabecera se inicializa con el valor NULL, un puntero
vacío.
void Inicializa(tlista *Lista)
{ *Lista= NULL;
}
Esta función indica si la lista contiene elementos o no. Se pregunta por el puntero cabecera de
lista.
bool EstaVacia(TLista Lista)
{ return Lista == NULL;
}
· Reservar dinámicamente la memoria necesaria para almacenar el nuevo nodo. Para hacerlo usamos
la función ‘malloc()’, ‘calloc()’ u otras que proporcione el lenguaje.
· Asignar la información (los datos) al/los campo/s de datos de la estructura.
· Encadenar el nuevo nodo a la lista sin perder los enlaces existentes de los restantes nodos. Este
encadenamiento se realizará de diferentes maneras dependiendo del modo de inserción. El orden de
las operaciones de encadenamiento del nuevo nodo es el siguiente:
a) Se encadena el nuevo nodo a los nodos existentes en la lista para no perder los enlaces ya
establecidos.
b) Se encadenan los nodos de la lista al nuevo nodo.
Las formas de inserción de un nodo que vamos a ver son las siguientes:
Dato 1 Dato 1
Lista
Como estamos insertando al principio de la lista, delante del primer nodo, el nodo que se crea
se convierte ahora en cabecera de la misma (el puntero cabecera de lista debe apuntar al nuevo nodo);
por ello, el parámetro 'TLista *Lista' se pasa por puntero o referencia y se corresponde con el
comienzo de la lista.
Inserta un nodo detrás del nodo apuntado. Si la lista está vacía, la inserción es como la anterior.
Si la lista no está vacía, debemos conocer la posición del nodo tras el que queremos insertar el nuevo
nodo. Los pasos que damos, por orden, son: primero encadenar el nuevo nodo con el nodo siguiente al
apuntado “Nuevo->Siguiente= (*Lista)->Siguiente” y, segundo, encadenar el nodo apuntado al nuevo
“nodo(*Lista)->Siguiente= Nuevo”.
El paso (1) hay que hacerlo necesariamente en primer lugar para no perder la dirección del
siguiente nodo al apuntado.
Posicion Nuevo
Lista encadenada. Situación inicial. Reserva de memoria.
Lista
Posicion Nuevo
PRIMER PASO: Nuevo->Enlace= Posicion->Enlace
Representación gráfica de la inserción de un nodo entre otros dos
Una lista ordenada es aquella cuyos nodos están ordenados ascendente o descendentemente
según el valor de uno o varios campos de datos del nodo. Para insertar en una lista ordenada
ascendentemente, es necesario conocer la posición (mediante un puntero) del nodo cuya información
es inmediatamente inferior que la del nodo que queremos insertar. Para ello tenemos que implementar
un nueva función que realice un recorrido secuencial de la lista para determinar en que lugar de la lista
hay que insertar; ésta, devuelve un puntero a:
· NULL, si la lista está vacía o es la menor información almacenada en la lista. La acción que
debemos realizar es la de insertar al principio de la lista.
· Dirección del nodo tras el que queremos insertar; si la información que queremos insertar el la
mayor, retorna un puntero al último nodo. La información que almacena el nodo devuelto tiene
valor inferior o igual a la que queremos insertar. La acción que realizamos es la de insertar en
medio o al final de la lista.
Otra función que inserta un nodo en la lista ordenado en sentido ascendente según el valor de
uno de los campos de información del nodo y que no utiliza las funciones de inserción creadas con
anterioridad es la siguiente:
void InsertaOrdenado(TLista *Lista, TInfo Info)
{ TLista Nuevo= (TLista)malloc(sizeof(Nodo));
Nuevo->Dato= Info;
if (Vacia(*Lista) || Info <= (*Lista)->Dato)
{ Nuevo->Siguiente= *Lista;
*Lista= Nuevo;
}
else
{ TLista Aux= *Lista;
while(!Vacia(Aux->Siguiente) && Info > Aux->Siguiente->Dato)
Aux= Aux->Siguiente;
Nuevo->Siguiente= Aux->Siguiente;
Aux->Siguiente= Nuevo;
}
}
· Buscar el elemento en la lista. Se necesitan dos punteros, uno recorre la lista buscando la
información y el otro guarda la posición anterior.
· Si el elemento se ha encontrado, caben dos posibilidades:
· Es el primero de la lista: la nueva lista apunta a la segunda componente.
Ejercicios propuestos:
1. Crear una lista lineal de números enteros.
2. Dada la lista anterior, realizar una función para crear otra lista que contenga sólo los números
pares que hay en la primera lista. La lista de pares debe quedar ordenada ascendentemente y la
lista original continuará teniendo su orden primitivo.
3. Dadas las listas LISTA1 y LISTA2, obtener LISTA3 como unión de las dos anteriores.
4. Cargar en una lista el fichero de datos de artículos, visualizar la lista y escribirla de nuevo en el
archivo.
· Cada nodo es accesible desde cualquier otro nodo de la lista dado que podemos realizar una
vuelta completa de la lista sin necesidad de estar en la cabecera de la lista. En una lista
enlazada simple sólo pueden recorrerse todos los nodos si estamos en la cabecera de lista.
· Las operaciones de concatenación y división de listas son más sencillas.
Lista circular
Además de este tipo de lista circular existe otro tipo semejante con la diferencia de que
contiene un nodo falso, que siempre está en la lista (nunca está vacía) para indicar la cabecera de la
lista. Este nodo cabecera contiene información nula o especial para distinguirlo de los nodos que tienen
información real.
Las operaciones que vamos a ver se realizarán sobre una lista circular sin nodo cabecera.
Queda como ejercicio realizar las mismas operaciones sobre una lista circular con nodo cabecera.
Inserta un nodo detrás del primero insertado; el puntero cabecera se mantiene fijo a dicho
nodo.
*Lista= Nuevo;
Nuevo->Siguiente= *Lista; //Lista ha tomado la dirección de nuevo
Dato 1 Dato 1
Lista
La inserción del primer nodo hace que su campo enlace apunte al propio nodo, es una inserción
al principio de la lista salvo por este matiz.
La inserción de un nuevo nodo, cuando la lista no está vacía, se hace apuntando el campo
enlace de éste al último elemento de la lista, es decir, al nodo que apunta el nodo cabecera
Nuevo->Siguiente= (*Lista)->Siguiente;
El algoritmo es el siguiente:
void Insertar(TLista *Lista, TInformacion Info)
{ TLista Nuevo;
Nuevo= (TLista) malloc(sizeof(Nodo));
Nuevo->Dato= Info;
if (Vacia(*Lista))
{ *Lista= Nuevo;
Nuevo->Siguiente= *Lista; //Nuevo->Siguiente= Nuevo;
}
else
{ Nuevo->Siguiente= (*Lista)->Siguiente;
(*Lista)->Siguiente= Nuevo;
// Podemos variar el nodo cabecera para realizar una inserción
// al final de la lista, detrás del último insertado en la lista
//*Lista= Nuevo;
}
}
La operación de borrado de un nodo de una lista circular, igual que en operaciones de borrado
anteriores, requiere que se conozca la posición del nodo que se desea eliminar. Los pasos para eliminar
un nodo son los siguientes:
· Buscar el elemento en la lista. Se necesitan dos punteros, uno recorre la lista buscando la
información y el otro guarda la posición anterior.
· Si el elemento se ha encontrado, se pueden dar las siguientes posibilidades:
· Es el único nodo de la lista: hay que inicializar la lista.
Lista= NULL
Dato 1 Lista Free(Auxiliar)
El recorrido de la lista es similar al realizado para una lista enlazada simple salvo que para la
lista circular se necesita un puntero auxiliar. El recorrido comienza en el siguiente nodo a la cabecera
de lista y finaliza cuando el puntero auxiliar (previamente inicializado a la lista) coincide con la
cabecera de lista.
void Visualizar(TLista Lista)
{ TLista Auxiliar= Lista;
if (!Vacia(Lista))
do
{ Auxiliar= Auxiliar->Siguiente;
printf("%d\t", Auxiliar->Dato);
}while(Auxiliar != Lista);
}
Puntero Puntero
Información
Anterior Siguiente
Nodo de lista doble
También puede implementarse una lista doblemente enlazada circular, que consiste en que los
enlaces siguiente y anterior del último y primer nodo de la lista respectivamente no apunten a NULL,
si no que apunten uno al último nodo y el otro al primer nodo de la lista. En esta lista sólo es necesario
un puntero externo para identificar la lista.
Se dispone de varias formas de inserción en una lista doblemente enlazada dependiendo del
lugar en el que haya que insertar el nuevo nodo. Las diferentes maneras se ven a continuación.
· Si la lista está vacía: insertamos el primer nodo de la lista. CABECERA y FINAL apuntan
al nuevo nodo y los enlaces anterior y siguiente a NULL.
· Si la lista está vacía: Insertamos el primer nodo de la lista. CABECERA y FINAL apuntan
al nuevo nodo y los enlaces a NULL. Es el mismo proceso que la inserción al principio.
· Si la lista está vacía o el lugar donde debemos insertar no viene dado o es el último
nodo de la lista insertamos al final de la lista.
Nuevo
· Si la lista está vacía o el lugar donde debemos insertar no viene dado o es el último
nodo de la lista insertamos al principio de la lista.
Nuevo
La operación de borrado de un nodo de una lista doble, igual que en operaciones borrado
anteriores, requiere que se conozca la dirección que ocupa el nodo que se desea eliminar. Los pasos
para eliminar un nodo son los siguientes:
(1)
Auxiliar Cabecera Final
(1)
Cabecera Final Auxiliar
· Es un nodo intermedio. Enlazar el nodo anterior con siguiente del que hay que borrar.
· Liberar la memoria ocupada por el nodo que contiene la información que queremos
eliminar.
void Borrar(TLista *Cabecera, TLista *Final, TInformacion Info)
{ TLista Auxiliar= *Cabecera;
int Encontrado= 0;
while (!Encontrado && Auxiliar != NULL)
if (Auxiliar->Dato == Info)
Encontrado= 1;
else
Auxiliar= Auxiliar->Siguiente;
if (Encontrado)
{ if (*Cabecera == *Final) // Si sólo hay un nodo
Inicializa(Cabecera, Final);
else if(auxiliar == *cab) // Es el primero de la lista
{ *Cabecera= (*Cabecera)->Siguiente;
(*Cabecera)->Anterior= NULL;
}
else if(Auxiliar == *Final) // Es el último de la lista
{ *Final= (*Final)->Anterior;
(*Final)->Siguiente= NULL;
}
else // Es un nodo intermedio
{ Auxiliar->Anterior->Siguiente= Auxiliar->Siguiente;
Auxiliar->Siguiente->Anterior= Auxiliar->Anterior;
}
free(Auxiliar);
}
}
El recorrido de la lista puede realizarse en los dos sentidos: de izquierda a derecha usando el
puntero cabecera y de derecha a izquierda usando el puntero final.
// Recorrido normal; de principio a fin. Usamos el enlace SIGUIENTE
void VisualizarID(TLista Lista)
{ while(Lista != NULL)
{ printf("%d\t", Lista->Dato);
Lista= Lista->Siguiente;
}
}
5.- Pilas.
Una pila es una estructura dinámica lineal de datos en la que los elementos sólo pueden ser
accedidos por un extremo, cima de la pila. Esto quiere decir que el último elemento que se añade es el
primero que se elimina, por tanto, de acuerdo con este concepto las pilas son llamadas "último en
entrar, primero en salir" (LIFO, Last Input, First Output).
Dato 3
Dato 3 Dato 2 Dato 1
Dato 2
Dato1
Cima
Representación interna y gráfica de una pila.
Este tipo de estructura de datos suele usarse cuando se quiere recordar una secuencia de objetos
o sucesos en orden inverso al que sucedieron. Una pila queda definida con sus atributos y las
operaciones que se pueden realizar con los mismos:
//Declaración de pila.
typedef int TInformacion;
struct Pila
{ TInformacion Dato;
struct Pila *Siguiente;
};
typedef struct Pila *TPila;
//Crea una pila vacía.
void Crear(TPila *Cima)
{ *Cima = NULL;
}
//Comprueba si la pila está o no vacía.
int Vacia(TPila Cima)
{ return Cima == NULL;
}
6.- Colas.
Una cola es una estructura dinámica lineal de datos en la que los elementos sólo pueden ser
introducidos por un extremo, final de la cola, y extraídos por el extremo contrario, frente de la cola.
Esto quiere decir que el primer elemento añadido es el primero que se puede extraer y el último tiene
que aguardar su turno, por tanto, de acuerdo con este concepto las colas son llamadas "primero en
entrar, primero en salir" (FIFO, First Input, First Output).
Frente Final
Este tipo de estructura de datos suele usarse cuando se quiere recordar una secuencia de objetos
o sucesos en el mismo orden al que sucedieron. Una cola queda definida con sus atributos y las
operaciones que se pueden realizar con los mismos:
Ejercicios:
1. Dado un archivo de texto con una frase, comprobar si dicha frase es un palíndromo. La frase
puede contener espacios, mayúsculas, minúsculas, etc. Usar una pila y una cola.
Funciones necesarias :
1. Inicializar la lista.
2. Alta de cliente.
3. Salida de cliente.
4. Listado de clientes por orden de llegada.
5. Listado de clientes por orden alfabético.
6. Fin de la sesión.
1.- Recursividad.
Antes de comenzar a estudiar las estructuras dinámicas no lineales de datos es necesario
conocer el concepto de recursividad dada la estrecha relación que existe entre ambas. Se dice que una
función es recursiva (un elemento que contiene otro de su misma categoría) si forma parte de sí misma,
es decir, si se invoca a sí misma. La recursión es un medio particularmente poderoso en operaciones de
cálculo. Puede ser utilizada en lugar de la repetición o iteración (estructura repetitiva).
El uso de la recursión es idóneo para resolver aquellos problemas que pueden definirse en
forma natural en términos recursivos. Conceptualmente puede resultar más clara la aplicación de una
función recursiva que una iterativa pero su seguimiento es bastante más complejo y los resultados no
son siempre satisfactorios, en términos de velocidad de ejecución y consumo de memoria.
· Recursividad indirecta: si una función A tiene una referencia a una función B que, a su vez,
contiene una referencia a la función A.
Cada vez que se activa recursivamente una función, se crea una copia de la variables locales a
la función, incluidos los parámetros formales, de forma que los valores de las variables que tienen
validez son los más recientemente creados.
FACTORIAL = N * FACTORIAL(N - 1)
long factorial(int);
main()
{ int Numero = 3;
printf("%d", factorial(Numero));
}
long factorial(int N)
{ if (N == 0)
return 1;
else
return N * factorial(N - 1);
}
Empieza llamada 1 :
N = 3. N es distinto de 0 ; se ejecuta el else.
Return 3 * (Segunda llamada con N = 2)
Empieza llamada 2 :
N = 2. N es distinto de 0 ; se ejecuta el else.
Return 2 * (Tercera llamada con N = 1)
Empieza llamada 3 :
N = 1. N es distinto de 0 ; se ejecuta el else.
Return 1 * (Cuarta llamada con N = 0)
Empieza llamada 4 :
N = 0. N es igual a 0.
Return 1
FIB(1) = 0, FIB(2) = 1
FIB(N) = FIB(N - 1) + FIB(N - 2) para N > 2
int Fibonacci(int N)
{ if (N == 0)
return 0;
else if (N == 1)
return 1;
else //Cada llamada produce dos más
return (Fibonacci(N - 1) + Fibonacci(N - 2));
}
Esta solución repite los cálculos para varios términos de la sucesión, por tanto, lo mejor es
optar por la solución iterativa. Otra solución recursiva para este ejercicio, ahora imprimiendo la
sucesión, es la siguiente:
void Fibonacci(int T, int P, int U)
{ printf(“%d\n”, P);
if (T > 0)
Fibonacci(T-1, U, U+P);
}
Cuenta la leyenda tibetana que a los sacerdotes del templo de Brahma les fue asignada en la
creación del mundo la siguiente tarea :
Se les dio una plataforma con tres soportes verticales consistentes en tres agujas de diamante. En el
primer soporte había 64 discos de oro, cada uno de los cuales era un poco más pequeño que el de
debajo. Los sacerdotes debían mover los discos de oro desde el primer soporte al tercero, sujetos a la
condición de que sólo se podía mover un disco cada vez, y que no estaba permitido colocar un disco
colocar un disco encima de otro más pequeño. En el momento en que los sacerdotes acabaran de
mover los 64 discos significaría que había llegado el fin del mundo.
1.- Mover los discos desde la barra inicial hasta la barra final usando la barra auxiliar.
MoverTorre(n, 1, 2, 3)
1.- Pasar los n - 1 discos superiores de la barra 1 a la 2.
MoverTorre(n - 1, 1, 3, 2)
2.- Pasar el disco inferior de la barra 1 a la 3.
printf("Desde: %d, Hasta: %d\n", Uno, Tres);
3.- Pasar los n - 1 discos de la barra 2 a la 3.
MoverTorre(n - 1, 2, 1, 3)
#include <stdio.h>
#include <conio.h>
void MoverTorre(int N, int Uno, int Dos, int Tres)
{ if (N > 0)
{ MoverTorre(N - 1, Uno, Tres, Dos);
printf("Desde: %d, Hasta: %d\n", Uno, Tres);
MoverTorre(N - 1, Dos, Uno, Tres);
}
}
void main()
{ int NumDiscos = 3;
MoverTorre(NumDiscos, 1, 2, 3);
getch();
}
Ejercicios de recursividad:
1.- Buscar en una lista.
2.- Insertar al final de una lista.
3.- Insertar en una lista ordenada.
4.- Borrar un elemento de una lista.
2.- Árboles.
2.1.- Definición, elementos y representación de los árboles.
Un árbol es una lista no lineal, formada por un conjunto de elementos del mismo tipo,
jerarquizados del modo siguiente :
· Cada uno de sus elementos tiene un solo antecesor y puede tener un sucesor, varios o
ninguno; es decir, se puede considerar, a su vez, como el origen de un subárbol.
· Existe un elemento llamado raíz, que no tiene ningún antecesor.
Esta estructura de datos se denomina árbol porque se representa en forma de árbol invertido; es
decir, con las raíces situadas en la parte superior del árbol y las ramas descienden hacia las hojas que
están situadas en su zona inferior. En el concepto de árbol entran los siguientes términos:
Ejemplo :
B C D
E F G H I J K
L M
Existen varios tipos de árboles : se dice que un árbol es n-ario cuando el número máximo de
hijos por nodo es n, siendo n un número entero positivo. En particular estudiaremos el árbol binario,
donde cada nodo puede tener un máximo de dos hijos.
Se dice que un árbol binario está equilibrado si las alturas de los dos subárboles de cada nodo
son iguales o difieren en una unidad. Se denomina lleno cuando todos sus nodos tienen dos sucesores o
hijos, a excepción de las hojas.
Puntero al Puntero al
subárbol Información almacenada en el subárbol
izquierdo nodo derecho
Datos
Datos Datos
Igual que en las listas lineales, hablamos de operaciones de inserción, borrado, búsqueda, etc.,
como las más usuales de los árboles. Todas son recursivas ya que lo que se tiene a derecha e izquierda
son árboles binarios. La condición para terminar la profundidad de la recursión es que el nodo sea
terminal, que apunte a NULL.
Esta forma de creación de un árbol no es satisfactoria por que no siempre conocemos el número
de nodos con los que se debe crear.
TArbol Construir(int NumeroNodos)
{ TArbol Arbol;
int NIzda, NDcha, Info;
if (NumeroNodos == 0)
return NULL;
else
{ NIzda= NumeroNodos / 2;
NDcha= NumeroNodos - NIzda - 1;
printf("Valor: ");
scanf("%d", &Info);
Arbol= (TArbol) malloc(sizeof(Nodo));
Arbol->Dato= Info;
Arbol->AIz= Construir(NIzda);
Arbol->ADe= Construir(NDcha);
return Arbol;
}
}
Una tarea corriente que se realiza con un árbol es ejecutar una determinada operación con cada
uno de los elementos del mismo. Esta tarea se considera como un parámetro de una tarea más general
que es la visita de todos los nodos o recorrido del árbol.
Si se considera la tarea como un proceso secuencial, entonces los nodos individuales se visitan
en un orden específico, y pueden considerarse como organizados según una estructura lineal.
Hay tres órdenes de los elementos que están asociados de forma natural con la estructura de los
árboles. Tal como sucede con la estructura del árbol en sí, estos pueden expresarse de forma recursiva.
Las tres organizaciones son:
Como puede observarse, al hablar de estos tres tipos de recorrido, nos referimos a si
procesamos en primer lugar el nodo central, el de la derecha o el de la izquierda. Por ejemplo, en el
caso del Recorrido en Preorden, una vez procesado el nodo central en que nos encontramos se debe
seguir recorriendo el árbol por la izquierda, pero lo que queda de árbol por la izquierda es
precisamente un árbol binario, por lo que se vuelve a llamar a la función de recorrido para profundizar
por la izquierda y una vez que se acaba de profundizar por esa rama se profundiza por la rama de la
derecha.
2 5
3 4 6
El código en C para realizar los tres tipos de recorrido descritos sobre un árbol es el siguiente :
Los árboles binarios se usan frecuentemente para representar conjuntos de datos cuyos
elementos se identifican por una clave única. Si el árbol está organizado de tal manera que, para todo
nodo ‘A’, todas las claves de subárbol izquierdo son menores que la clave de ‘A’, y todas aquellas
claves del subárbol derecho de ‘A’, son mayores que la clave de ‘A’, se dice que este árbol es un árbol
de búsqueda.
Puede localizarse una clave arbitraria en un árbol de búsqueda empezando por la raíz, y
avanzando por un camino de forma que la decisión de continuar por el subárbol izquierdo o derecho se
toma en base únicamente al valor de la clave de dicho nodo. Como esta búsqueda sigue un camino
único desde la raíz hasta el nodo deseado, puede programarse fácilmente por medio de iteración
aunque también, por supuesto, lo hacemos de forma recursiva. El código en C de la función no
recursiva es el siguiente :
TArbol BuscarOrdenado(TArbol Arbol, TInformacion Info)
{ int Encontrado = 0;
while (Arbol != NULL && !Encontrado)
if (Arbol->Dato == Info)
Encontrado= 1;
else if (Arbol->Dato > Info)
Arbol= Arbol->AIz;
else
Arbol= Arbol->ADe;
if (Encontrado) //Sólo hace falta poner RETURN arbol
return Arbol;
else
return NULL;
}
Ejercicios:
1.- Función que decida si un valor, dado como parámetro, está o no en el árbol.
2.- Función que retorne el número de nodos que tiene un árbol.
3.- Función que calcule la profundidad de un árbol (número de nodos de la rama más larga).
El algoritmo, además de insertar un nuevo nodo en el árbol, realiza una búsqueda previa de la
información para localizar la rama donde debe ser insertada. No crea un árbol equilibrado.
void InsertarBusqueda(TArbol *Arbol, TInformacion Info)
{ if (Vacio(*Arbol))
{ *Arbol= (TArbol) malloc(sizeof(Nodo));
(*Arbol)->Dato= Info;
(*Arbol)->AIz = (*Arbol)->ADe= NULL;
}
else if (Info < (*Arbol)->Dato)
InsertarBusqueda(&(*Arbol)->AIz, Info);
else if (Info > (*Arbol)->Dato)
InsertarBusqueda(&(*Arbol)->ADe, Info);
else
(*Arbol)->Dato= Info; //Modifica la información del nodo
}
En el algoritmo no se tienen en cuenta los valores repetidos de las claves, sólo se modifica el
contenido del resto de campos de la estructura TInformacion. Esto es así por que una clave debe
identificar plenamente el conjunto de datos asociados a ella. Podemos optar por añadir un campo
contador a la estructura de datos que nos indique el número de apariciones de dicha clave o bien, a
pesar de todo, por añadir claves al árbol repetidas tratando de la misma forma Info == (*Arbol)->Dato
que Info > (*Arbol)->Dato.
El puntero externo árbol se pasa por referencia para que cuando se presente el caso de inserción
se pueda asignar un nuevo valor a la variable que contenía el valor NULL ; así los cambios se
reflejarán en la función que realizó la llamada.
La tarea consiste en borrar el nodo con clave X de un árbol que tiene las claves ordenadas. Es
fácil si el elemento a borrar es un nodo terminal o tiene un único descendiente. La dificultad está en el
borrado de un elemento que tiene dos descendientes. En esta situación, el elemento a borrar debe ser
reemplazado, bien por el elemento más a la derecha del subárbol izquierdo, o bien por el elemento más
a la izquierda del subárbol derecho, los dos como máximo con un único descendiente. Aquí optamos
por reemplazar con la información almacenada en el nodo situado más a la derecha del subárbol
izquierdo.
La función anterior borrar se apoya en la función eliminar, que se activa sólo en el tercer caso,
y cuya misión consiste en descender a lo largo de la rama más a la derecha del subárbol izquierdo del
elemento a borrar, y reemplaza la información de este por la encontrada en el nodo más a la derecha en
ese subárbol izquierdo.
void Elimina(TArbol *Arbol, TArbol *Nodo)
{ if (!Vacio((*Arbol)->ADe))
Elimina(&(*Arbol)->ADe, Nodo);
else
{ (*Nodo)->Dato= (*Arbol)->Dato;
*Nodo= *Arbol;
*Arbol= (*Arbol)->AIz;
}
}
2.- Comentarios.
En C los comentarios empiezan por los caracteres “/*” y terminan por “*/”. Pueden
comprender varias líneas y estar distribuidos de cualquier forma. En C++ se admite el mismo tipo de
comentario y además también es un comentario todo aquello que esté a continuación de los caracteres
“//” hasta el fin de línea.
Esta declaración no está permitida en lenguaje C. De cualquier forma, el array debe tener una
longitud conocida en tiempo de compilación a no ser que utilicemos reserva explícita de memoria.
1. Un puntero variable apuntando a una variable constante. Un puntero a una variable const no
puede modificar el valor de esa variable (el compilador lo detecta) pero el puntero no tiene por qué
apuntar siempre a esa variable.
const char *Nombre= “Antonio”;
Nombre= “Pedro”; // Permitido. El puntero cambia de dirección.
Nombre[0]= ‘M’; // No permitido. No puede cambiar su valor.
2. Un puntero constante apuntando a una variable cualquiera. Un puntero const apunta siempre a
la misma posición de memoria pero el valor de la variable se puede modificar.
char* const Nombre= “Antonio”;
Nombre= “Pedro”; // No Permitido. El puntero es constante.
Nombre[0]= ‘M’; // Permitido. Puede cambiar su valor.
1. Una primera forma de utilizar funciones inline es anteponer dicha palabra en la declaración
de la función, como por ejemplo:
inline void permutar(int &a, int &b);
2. Otra forma de utilizar funciones inline sin necesidad de utilizar esta palabra es introducir el
código de la función en la declaración (convirtiéndose de esta manera en definición),
poniéndolo entre llaves { } a continuación de ésta. Este segundo procedimiento suele
utilizarse por medio de ficheros cabecera (*.h), que se incluyen en todos los ficheros fuente
que tienen que tener acceso al código de las funciones inline. Considérese el siguiente
ejemplo consistente en una declaración seguida de la definición:
void permutar (int *i, int *j) { int temp; temp = *i; *i = *j; *j = temp; }
En cualquier caso, la directiva inline es sólo una recomendación al compilador, y éste puede
desestimarla por diversas razones, como coste de memoria excesivo, etc.
La sobrecarga de funciones no admite funciones que difieran sólo en el tipo del valor de
retorno, pero con el mismo número y tipo de argumentos. De hecho, el valor de retorno no influye en
la determinación de la función que es llamada; sólo influyen el número y tipo de los argumentos.
Tampoco se admite que la diferencia sea el que en una función un argumento se pasa por valor y en
otra función ese argumento se pasa por referencia.
#include <math.h>
int absoluto(int x) { return abs(x); }
double absoluto(double x) { return fabs(x); }
long double absoluto(long double x) { return fabsl(x); }
long absoluto(long int x) { return labs(x);}
En C esta función tiene que ser necesariamente llamada con dos argumentos actuales que se
corresponden con los dos argumentos formales de la declaración. En C++ la situación es diferente pues
se pueden definir valores por defecto para todos o algunos de los argumentos formales en la
declaración de la función (si no hay prototipo se hace en la definición). Después, en la llamada, en el
caso de que algún argumento esté ausente de la lista de argumentos actuales, se toma el valor asignado
por defecto a ese argumento. Por ejemplo, la función modulo() podía haberse declarado del siguiente
modo:
double modulo(double x[], int n=3);
En C++ se exige que todos los argumentos con valores por defecto estén al final de la lista de
argumentos. En la llamada a la función pueden omitirse alguno o algunos de los últimos argumentos
de la lista. Si se omite un argumento deben de omitirse todos aquellos que se encuentren detrás de él.
Ej.: void Particion(FILE* f1, FILE* f2, int Secuencia= 1);
Este programa tiene la ventaja de que no hay que utilizar el operador indirección dentro de la
función permutar(). C++ permite pasar argumentos por referencia sin más que anteponer el carácter
(&) a los argumentos correspondientes, tanto en el prototipo como en el encabezamiento de la
definición. En la llamada a la función los argumentos se ponen directamente, sin anteponerles ningún
carácter u operador.
Las variables de tipo referencia se declaran con el operador (&) y deben ser inicializadas a
otra variable o a un valor numérico. Por ejemplo:
int i=2;
int& iref = i; // declaración de referencia válida
int& jref; // declaración de referencia no válida
La variable i es una variable normal tipo int. La variable iref es una variable referencia que se
asocia con i, en el sentido de que ambas variables comparten la misma posición de memoria: si se
modifica i se modifica iref, y viceversa. En este sentido, iref es un alias de i. La diferencia con un
puntero que apuntase a la dirección de i está en que, una vez que una variable referencia ha sido
declarada como alias de i no puede ser declarada como alias de otra variable. Siempre se referirá a la
misma posición de memoria. En la función permutar() los argumentos formales, que son referencias,
se inicializan y se convierten en alias de los argumentos actuales, que son variables ordinarias.
El que una función tenga como valor de retorno una variable tipo referencia permite utilizarla
de una manera un poco singular. Considérese el siguiente ejemplo:
int& maxref(int& a, int& b)
{ if (a >= b)
return a;
else
return b;
}
La función maxref() tiene referencias como valor de retorno y como argumentos. Esto permite
utilizarla, por ejemplo, del siguiente modo:
maxref(i, j) = 0;
Ésta es una forma un poco extraña de utilizar una función: la llamada está a la izquierda del
operador de asignación, en vez de aparecer a la derecha en una expresión aritmética o de otro tipo. El
resultado de esta llamada también es un poco extraño: el valor de retorno es una referencia, esto es un
alias del argumento de valor máximo. Cuando la llamada a la función se sustituye por su valor de
retorno, el resultado de la sentencia anterior es que la variable pasada como argumento que tiene
mayor valor se hace igual a cero. Este mismo efecto puede conseguirse mediante punteros, pero con
referencias resulta mucho más sencillo.
En C++ las referencias son muy utilizadas para pasar argumentos a funciones (y como valores
de retorno), no sólo para poderlos modificar dentro de la función, sino también por motivos de
eficiencia, pues es mucho más rápido pasar un puntero o un alias de una variable que una copia del
valor de esa variable. Si además la variable es una estructura, las ventajas de eficiencia son todavía
mucho más palpables.
Un aspecto diferente con la función malloc() es que ésta devuelve un puntero a void (*void)
que es después convertido al tipo de variable que se desea. Esa conversión se evita con new.
Se puede utilizar el operador new para crear variables de cualquier tipo. New devuelve, en
todos los casos, un puntero a la variable creada. También se pueden crear variables de tipos definidos
por el usuario.
struct usuario {
..........
};
usuario* Un_Usuario;
Un_Usuario = new usuario;
Cuando una variable ya no es necesaria se destruye con el operador delete para poder utilizar la
memoria que estaba ocupando, mediante una instrucción del tipo:
delete p;
char nombre[20];
int num=2;
printf ("Introduzca el nombre del fichero %d: ", num);
scanf (" %s", nombre)
Es importante darse cuenta de que ahora ya no hace falta especificar el tipo de dato que va a
ser impreso o leído, asociándolo con un formato determinado. Es el propio programa el que decide el
tipo de dato en tiempo de ejecución. Estos operadores están sobrecargados de tal manera que admiten
tanto los tipos predefinidos como aquellos tipos de datos definidos por el usuario.
C = A + B;
Un operador puede estar sobrecargado o redefinido varias veces, de tal manera que actúe de un
modo distinto dependiendo del tipo de objetos que tenga como operandos. Es precisamente el tipo de
los operandos lo que determina qué operador se utiliza en cada caso.
Sintaxis:
1.- Introducción.
La POO intenta solucionar los problemas de una forma más natural. Puede describirse como un
conjunto de disciplinas que desarrollan programas para facilitar la construcción de sistemas complejos
a partir de componentes individuales (Khoshafian, 1990). Este conjunto de disciplinas proveen
conceptos, métodos, técnicas y herramientas para:
· Representar y modelar el mundo real tan fielmente como sea posible a la perspectiva del
usuario. Como vivimos en un mundo lleno de objetos que existen por naturaleza, habitualmente
los clasificamos, describimos, combinamos, manipulamos, creamos y destruimos. De ahí que
se pretenda dar una visión orientada a objetos para la creación de software.
En la POO los datos (atributos) y el código que actúa sobre los datos (métodos) se convierten
en una única entidad, el objeto (se estudiará más tarde). Estos objetos se pueden construir partiendo de
otros creados anteriormente, permitiendo así construcciones complejas por partes. Los objetos se
comunican entre sí por medio de mensajes. Así, pasamos de la ecuación tradicional en programación
estructurada (Wirth, 1980)
Unos autores (Khoshafian, 1990) se centran en tres elementos fundamentales para configurar la
OO: tipos abstractos de datos, herencia e identidad de objetos. Otros (Meyer, 1988) en siete: estructura
modular basada en objetos, abstracción de datos, gestión automática de memoria, clases, herencia,
polimorfismo y herencia múltiple.
2.1.- Abstracción.
Es la propiedad que permite representar las características esenciales de un objeto ignorando
determinados aspectos de la realidad para facilitar la realización de la tarea. Se centra en la vista
externa de un objeto y sirve para separar su comportamiento de su implementación; es decir, no
debemos fijarnos en la implementación concreta de las estructuras de datos desde el punto de vista de
la programación (arrays, listas enlazadas, ficheros...) si no en las características que tiene en la vida
real.
Si consideremos una calculadora, lo que nos interesa de ella es que se puede encender y
apagar, es capaz de realizar cálculos aritméticos, mostrar los resultados de dichos cálculos entre
otras características de la misma. Lo que no nos interesa son los mecanismos internos que utiliza para
proporcionar unos resultados óptimos: estado de transistores, LEDs, sumadores, acumuladores, etc.
Para definir un objeto y establecer las relaciones de este con otros objetos sólo necesitamos
conocer QUE es lo que hace el objeto (que operaciones necesitan realizarse y que información resulta
de la aplicación de dichas operaciones) y no COMO lo hace.
2.2.- Encapsulamiento.
Es la propiedad que permite empaquetar las características y comportamiento de un objeto en
un envase independiente, oculto y seguro del resto del sistema; es decir, estamos realizando un
ocultamiento de la información, almacenándola en el interior de una cápsula, para evitar la corrupción
de los datos de un objeto y protegiéndolos de un uso arbitrario.
· Interfaz de comunicación con el exterior. Captura la visión externa del objeto y es conocida
por los demás objetos del sistema permitiendo la comunicación entre ellos. Es, por tanto, el
resultado de aplicar la abstracción al objeto.
La encapsulación establece una barrera conceptual que indica si los datos y operaciones del
objeto encapsulado son o no accesibles por el resto de objetos del sistema. Así, ocultando información,
se nos permite ocultar una parte del sistema del resto, permitiendo que el código sea revisado sin
afectar a la integridad del sistema.
Yo, como objeto, me comunico con la calculadora a través del interfaz de comunicación que
ofrece al exterior: un display, un interruptor de encendido y apagado, un conjunto de teclas
numéricas, operacionales y de comandos. El resto, todos los chips y circuitos adicionales, el
operando, el acumulador, toda la implementación, está encerrado dentro de una caja para que no se
pueda manipular; es su implementación interna.
2.3.- Modularidad.
Es la propiedad que permite subdividir, descomponer, un sistema en partes más pequeñas,
denominadas módulos, que se adaptan, ciñen, a la estructura lógica elegida en el proceso de diseño. De
ésta manera, en cierta medida, se reduce la complejidad del sistema. Se suele aplicar un método de
refinamiento progresivo de los módulos; cada módulo puede dividirse en otros más pequeños a su vez.
Ya Dijkstra en 1979 dice: “la técnica de dirigir ésta complejidad ha sido conocida desde antiguo, y
consiste en ‘divide et impera’ (divide y vencerás)”. Cada uno de los módulos debe ser tan
independiente como sea posible del sistema y de las otras partes del mismo. Deben ser cohesivos y
débilmente acoplados.
2.4.- Jerarquía.
Es la propiedad que permite clasificar u ordenar las abstracciones. Las jerarquías más
importantes son:
Si pensamos en una clase superior como “Máquina de Calcular”, las clases “Calculadora”,
“Calculadora Científica” y “Calculadora programable” comparten el comportamiento de la clase
“Máquina de Calcular”. Se establece una jerarquía de clases.
3.1.- Objeto.
Desde la perspectiva del conocimiento humano, un objeto es una de las siguientes cosas:
Un objeto, por tanto, representa un elemento individual e identificable, una unidad o entidad,
real o abstracta, con un comportamiento bien definido en el dominio del problema (Smith, 88).
Definición: un objeto es una instancia de una clase que incorpora una interfaz de comunicación
con el exterior y una implementación interna de sus servicios.
El estado del objeto viene determinado por el conjunto de propiedades que tiene -estas suelen
ser estáticas, no cambian-, junto con los valores que pueden asumir cada una de estas propiedades -que
son dinámicas, cambian con el tiempo-.
Así pues, en la calculadora -y pasando del objeto tangible a una supuesta programación de la
misma con un lenguaje de programación orientado a objetos- el operando y el acumulador, además de
otros componentes, pueden configurar la parte estática del objeto: no admiten letras, sólo pueden
almacenar un valor en un instante determinado, admite un máximo de dígitos, ...; estas son sus
características y no pueden cambiar. Por otra parte, estos componentes tienen unos valores reales
para cada objeto que van variando con la actuación del objeto en el transcurso de su uso (toman
diferentes valores: 1, 345, 123.98). Estas son sus características dinámicas.
El hecho de que cada objeto tenga un estado implica que ocupa un espacio, en el mundo real o
en la memoria del ordenador, existe durante un tiempo, cambia el valor de sus atributos, es instanciado
y se puede crear y destruir.
Los objetos se relacionan entre sí, deben comunicarse y actuar unos sobre otros. El
comportamiento de un objeto viene determinado por la forma en que actúa al recibir un mensaje desde
otro objeto o desde el entorno; es decir, reacciona a un mensaje recibido del sistema y dependiendo de
éste cambia de estado. En éste sentido, el estado de un objeto, no es más que el resultado acumulado de
su comportamiento.
Los mensajes son la forma que tienen de comunicarse distintos objetos entre sí. Un mensaje es
una acción que se manda a un objeto para que realice una operación con un propósito específico.
Existen cinco tipos de operaciones que suele realizar un objeto (Booch, 94):
- Iterar. Permite que todas las partes del objeto sean accedidas en un orden bien definido.
3.2.- Clase.
Una clase es una generalización de un tipo determinado de objeto: marco, molde o prototipo
que permite crear objetos con una estructura y comportamiento comunes. Especifica una estructura de
datos y los métodos operativos que se aplican a cada uno de los objetos; es decir, una clase contiene
una completa y detallada descripción de los datos que contendrá el objetos y de las operaciones que
podrá realizar. Así, en programación, una clase es un tipo de dato y un objeto es una variable de ese
tipo de dato.
Un ejemplo utilizado habitualmente para relacionar y diferenciar un objeto y una clase es el del
molde de galletas; el molde para hacer galletas sería la clase, y las galletas que hacemos a partir de ese
molde ya son objetos concretos creados a partir de las características definidas en el molde.
Una clase se caracteriza por un identificador, unos componentes o miembros y por un nivel de
acceso a sus componentes.
3.2.2.- Identificador
Es el nombre que se da a la clase para poder especificar a cual pertenecen los objetos.
Conforman las estructuras de datos, de cualquier tipo de dato (simple o estructurado), que van
a utilizar los objetos asignándoles unos valores que permitan diferenciarlos unos de otros. Constituyen,
en definitiva, la representación interna de la clase y mantienen el estado del objeto.
Operaciones que se pueden realizar con los atributos y que son utilizables por todos los objetos
pertenecientes a la clase; por tanto, los métodos definen el comportamiento del objeto.
Khosafian (90) clasifica los métodos en tres tipos, básicamente las mismas operaciones que
pueden realizar los objetos (Booch, 94):
un constructor por defecto creado por el propio compilador-, o con los valores especificados
en un constructor general definido por el programador. También se pueden copiar unos
objetos en otros.
· Un destructor también se invoca automáticamente justo antes de que el objeto sea
eliminado de la memoria y lo incorpora el lenguaje de manera automática. Según las
operaciones realizadas con el objeto, y dependiendo del lenguaje, será necesario que el
programador cree su propio destructor.
Los componentes de la clase tienen distintos niveles de acceso para proteger la información del
exterior de la clase (encapsulación). Este nos permite diferenciar entre el interior y el exterior de la
clase. La representación interna es la implementación de la clase, es la visión del diseñador de la clase
y comprende la implementación de los métodos. La externa es el interfaz de una clase, proporciona las
operaciones que se pueden realizar sobre los objetos de la clase, es la visión del usuario –la declaración
de los atributos y métodos de la clase-. Se puede dividir según su visibilidad en tres partes:
Con esta definición de clase, un objeto es una particularización o instancia de una clase que
incorpora una interfaz y una implementación interna de sus servicios, siendo por tanto un concepto
físico y concreto que tiene una existencia real y determinada. Así, mediante las clases podemos
instanciar objetos de un mismo tipo que se distinguen entre sí a través de su estado, del valor de sus
atributos.
Clase Calculadora
Atributos y Métodos privados
Acumulador es de tipo real
Operando es de tipo real
Atributos y Métodos públicos
Encender()
Apagar()
DarOperando()
PonerOperando(Parámetro real)
Sumar()
Restar()
...
Metaclase.
El lenguaje permite instanciar clases desde otras clases; es decir son clases cuyas instancias son
también clases. Proporcionan la ventaja de que pueden almacenar información común a todas las
instancias de la clases mediante “variables de clase”. Los “métodos de clase” se pueden utilizar para
actualizar o recuperar el contenido de estas.
Existen tres niveles o especificadores de acceso que pueden estar situados en cualquier lugar
dentro de la definición de la clase y aparecer tantas veces como sea necesario; su ámbito actúa desde
que se pone hasta que aparece otro. Estos son los siguientes:
· Public. Los componentes son accesibles directamente por cualquier objeto perteneciente a la
clase, es decir, se puede acceder a los datos y funciones miembro desde otras partes del
programa.
· Private. Es el nivel de acceso por defecto a los componentes de una clase. Los componentes se
ocultan para todos los objetos de la clase, siendo sólo accesibles desde las funciones de la
clase, es decir, sólo pueden acceder a ellos los otros miembros de la clase.
· Protected. Los componentes son accesibles a través de los miembros de la clase, pero tienen
características especiales relacionadas con la herencia.
int Top()
{ if(!EstaVacia())
return Datos[Indice];
else
return -1;
}
bool EstaVacia()
{ return Indice==1000;
}
};
Los métodos se han definido dentro de la propia declaración de la clase: métodos internos. Si
suponemos que tenemos muchas funciones y muy grandes, esta forma es impracticable y sería
deseable que la declaración de la clase contuviera únicamente los atributos y los prototipos de las
funciones: métodos externos. Esto se consigue extrayendo de la clase el código de las funciones de la
siguiente forma :
int Pila::Top()
{ if(!EstaVacia())
return Datos[Indice];
else
return -1;
}
bool Pila::EstaVacia()
{ return Indice==1000;
}
TipoRetorno NombreClase::NombreFuncion(Parámetros) ;
Donde el operador de campo :: quiere decir que la función pertenece al campo de la clase.
La diferencia entre definir métodos internos y externos está en que estos se comportan como
funciones normales y aquellos se comportan como funciones inline.
NombreClase ListaObjetos ;
El formato para el acceso a los miembros de la clase es el mismo que para las estructuras. En
general es el siguiente :
Objeto.Miembro ;
Ejemplo.
Pila1.Construir() ;
Pila1.Push(1) ;
Cima = Pila1.Top() ;
Pila1.Pop() ;
Pila1.Destruir() ;
Sólo podemos acceder desde el programa a los miembros definidos como públicos, es decir, las
siguientes sentencias darían errores de compilación :
Pila1.Datos[1]= 2 ;
Pila1.Indice= 100 ;
Pero al introducirlo dentro de una clase eliminamos el primer parámetro, de manera que cuando
hacemos
Pila1.Push(1) ;
En C++ existe el puntero, denominado this, al propio objeto que llama al método. Así, la
función anterior podría haberse escrito como
void Push(int v)
{ if(!(this->EstaLlena()))
this->Datos[--this->Indice]= v;
}
Ejercicios.
Dentro de una función miembro tenemos un conflicto entre tres campos con igual identificador:
la variable global, el campo/atributo de la clase y la variable local/parámetro formal del método ;
cuando coinciden los tres identificadores se tomará primero el campo local de la función, después el
campo de clase y por último, el global :
int a ;
class Clase
{ int a ;
void f(int a)
{ a= 1 ; //Parámetro formal de la función
Clase::a= 2 ; //Campo de la clase
::a= 3 ; //Variable global. Operador de resolución de visibilidad.
... //Operador de resolución de visibilidad
}
} ;
Cuando se definen miembros como private el acceso a miembros entre clases distintas es
imposible. Desde una clase no se puede acceder a atributos o métodos privados a otra clase. Tampoco
lo tenemos desde una función normal.
Por ejemplo, la siguiente clase Punto dispone de una función que calcula la distancia entre dos
puntos :
class Punto
{ double x, y;
public:
void Pon(double xx, double yy)
{ x= xx;
y= yy;
}
double Distancia(const Punto &b)
{ return sqrt(pow(b.x - x, 2) + pow(b.y - y, 2));
}
};
El método Distancia() accede tanto a los miembros privados de p1 como a los de p2 porque
forma parte de la clase punto y, por tanto, tiene acceso a todos los miembros de la clase a la que
pertenece.
La forma de hallar la distancia es poco ortodoxa; dado que es un método que está operando
sobre dos puntos deberíamos pasarle como parámetros los dos puntos, pero uno de ellos va implícito
en la propia llamada, el parámetro this. El prototipo lógico sería
double Distancia(const Punto &a, const Punto &b) ;
El problema está en que ahora la función Distancia() no es un método de Punto si no que es una
función independiente, a no ser que la llamada sea de la forma
d= p1.Distancia(p1, p2) ;
pero igual que en el primer caso es muy poco elegante e incluso puede dar lugar a confusión.
Para solucionar el problema debemos definir la función Distancia() como función amiga de la
clase punto usando la palabra reservada friend.
class Punto
{ double x, y;
public:
void Pon(double xx, double yy)
{ x= xx;
y= yy;
}
friend double Distancia(const Punto &a, const Punto &b) ;
};
double Distancia(const Punto &a, const Punto &b) //
{ return sqrt(pow(b.x - a.x, 2) + pow(b.y - a.y, 2));
}
Si queremos que todos los métodos de una clase sean amigos de otra, entonces declaramos toda
la clase como amiga: friend class Clase ;
También podemos declarar el operador + como friend, teniendo en cuenta que ahora no existe
el parámetro this:
class Complejo
{ ...
friend Complejo operator + (const Complejo& C1, const Complejo &C2);
...
};
En el momento de definir (construir) un objeto podemos darle unos valores iniciales usando
constructores. No podemos inicializar los objetos como lo hacemos con las estructuras a no ser que
cumpla las siguientes restricciones :
Cada vez que se define un objeto, estática o dinámicamente (new), se está realizando una
llamada a un método, constructor por defecto, que ha creado el propio compilador si no ha encontrado
un constructor definido explícitamente en la clase. El constructor por defecto no tiene argumentos ni
tampoco retorna ningún valor, ni siquiera void. Para identificarlo se usa el nombre de la clase como
nombre del constructor.
class Punto
{ double x, y;
public:
Punto() //Igual que lo crea el compilador.
{
}
void Pon(double xx, double yy)
{ x= xx;
y= yy;
}
friend double Distancia(const Punto &a, const Punto &b) ;
};
En esta clase el constructor no hace nada pero podemos modificarlo para que si haga algo, por
ejemplo inicializar x e y a 0, 0 respectivamente:
class Punto
{ double x, y;
public :
Punto()
{ x= 0.0 ;
y= 0.0 ;
}
...
};
Ahora, cada vez que se define un nuevo objeto (Punto p) de la clase punto, tomará el valor (0.0,
0.0).
Podemos definir todos los constructores que queramos en una clase, todos con el mismo
nombre, el de la clase, usando las reglas de la sobrecarga de funciones. Siguiendo con la clase Punto la
definimos de la siguiente forma :
class Punto
{ double x, y;
public:
Punto()
{ x= y= 0.0;
}
Punto(double xx, double yy)
{ x= xx;
y= yy;
}
Punto(double xx)
{ x= y= xx;
}
friend double Distancia(const Punto &a, const Punto &b);
};
Ahora no es necesario el método Pon() puesto que construimos los objetos de otra forma. Pero,
¿qué sucede si eliminamos el constructor por defecto Punto(){...}? Habría que modificar el constructor
general de la siguiente forma:
Punto(double xx= 0, double yy= 0){…}
3.7.3.- Destructor.
El destructor es, al igual que el constructor, un método especial de la clase. No lleva parámetros
ni retorna ningún valor y su identificador es el mismo que el de la clase precedido por el signo ~
(Alt+126). Se invoca automáticamente justo antes de que el objeto que lo contiene sea destruido.
class Punto
{ double x, y;
public:
~Punto() ; //Destructor. No hace nada.
Punto() ;
Punto(double xx, double yy) ;
Punto(double xx) ;
friend double Distancia(const Punto &a, const Punto &b);
};
Su uso es habitual y se hace necesario cuando el objeto reserva memoria explícitamente pero
también puede finalizar asuntos pendientes, cerrar ficheros, etc.
#include <iostream.h>
#include <string.h>
#define NULO '\0'
class Cadena
{ private:
char *LaCadena;
public:
Cadena();
~Cadena();
Cadena(char *Tira);
void Print();
};
Cadena::Cadena()
{ LaCadena= new char(NULO); //Inicializa a NULO. No arrays.
}
Cadena::Cadena(char *Tira)
{ LaCadena= new char[strlen(Tira)+1]; //Esta memoria no se libera
strcpy(LaCadena, Tira); //cuando se destruye el objeto.
}
Cadena::~Cadena()
{ delete []LaCadena; //Hay que destruirla explícitamente.
}
void Cadena::Print()
{ cout << LaCadena;
}
main()
{ Cadena c("asdasdasdasd"), d;
c.Print();
}
Se denomina constructor copia a un constructor que tiene un único parámetro, una referencia,
del mismo tipo de la clase donde definimos el constructor. Su finalidad es copiar los datos de un objeto
a otro. Si no definimos un constructor copia explícitamente, el compilador crea uno por omisión. Las
llamadas al constructor copia son:
Punto p ; //Primer constructor.
Punto p(2, 3), r(2) ;
Punto s(p) ; //Crea s y con el constructor copia s=(2,3) ;
Que la última sentencia llame al constructor copia es por que este dispone de dos notaciones
(Punto p(q) y Punto p= q) mientras que el operador de asignación sólo dispone de una (p= q).
Hay otro método que implementa el compilador por omisión : el operador de asignación. Como
el constructor copia, realiza una copia de los atributos del objeto fuente. Es decir, podemos usar el
constructor copia y el operador de asignación para copiar los datos de un objeto en otro como si se
tratase de cualquier otra variable. La diferencia entre usar uno u otro está en que el primero crea un
nuevo objeto y después copia los datos y el segundo sólo copia los datos, trabaja sobre un objeto ya
creado. Por ejemplo, en la clase punto podemos añadir el constructor copia y el operador de
asignación:
class Punto
{ double x, y;
public:
~Punto() ; //Destructor. No hace nada.
Punto(double xx= 0, double yy= 0) ;
Punto(const Punto &p) //Constructor copia.
{ x= p.x ; y= p.y ;
}
Punto operator=(const Punto &p) //Operador de asignación
{ x= p.x ; y= p.y ;
}
void Desplazar(double a, double b)
{ x+= a ; x+= b ;
}
friend double Distancia(const Punto &a, const Punto &b);
};
una sentencia como s.Poner(“00000”) hará que cambie el atributo de s y también el de c puesto
que ambos tienen la misma dirección de memoria. Por tanto, el constructor copia por omisión no nos
sirve para solucionar este problema. Una solución es la siguiente :
Cadena::Cadena(const Cadena &c)
{ LaCadena= new char[strlen(c.LaCadena)+1];
strcpy(LaCadena, c.LaCadena);
}
Ejercicio: Crear una clase cadena con los siguientes elementos sin usar las funciones del
archivo cabecera string.h:
· Constructor por defecto, generales y copia.
· Operador de asignación de objetos y de asignación convencional.
· Concatenación de cadenas sobrecargando el operador +.
· Visualizar la cadena.
· Todas aquellas operaciones con cadenas que se quieran : longitud, comparación, etc.
Se pueden definir arrays de objetos igual que se definen de cualquier otro tipo de dato: el tipo
de dato base de las componentes del array seguido del identificador y entre corchetes su longitud y
número de dimensiones.
Punto Triangulo[3] ;
Esta definición llama al constructor por defecto y por tanto inicializa las tres componentes a
cero; si el constructor por defecto no está definido, da error.
que crea un puntero a un objeto. En esta declaración no se realiza la llama al constructor puesto que es
la declaración de un puntero, no de un objeto, y, por tanto, tampoco se reserva memoria para el objeto,
sólo los cuatro bytes para el puntero.
Punto P ;
Punto *Puntero ;
Puntero= &P ;
Puntero->Desplaza(2, 1) ;
Puntero->Punto::~Punto() ;
Los ejemplos anteriores muestran que el uso de punteros a objetos es el mismo de siempre. La
última sentencia elimina el objeto de la memoria (llama al destructor) pero el puntero sigue apuntando
la misma posición de memoria (no se inicializa a NULL); cualquier acceso a ese objeto, que ya no
existe, tiene consecuencias imprevisibles.
También podemos reservar y destruir memoria dinámicamente usando los operadores new y
delete respectivamente.
Punto *Puntero ;
Puntero= new Punto ; // Reserva memoria y llama al constructor
delete Puntero ; // Llama al destructor y libera la memoria
Punto *Puntero= new Punto(2, 3) ; //Construye con 2, 3
Punto *Puntero= new Punto[100] ; //Puntero a array de objetos
delete [100]Puntero ;
delete []Puntero ;
void cPila::Pop()
{ if (!Vacia())
{ cNodo *Aux= Cima;
Cima= Cima->Sig;
delete Aux;
}
}
main()
{ int i;
cPila Pila;
for (i=0; i<3; i++)
Pila.Push(i);
printf("\n");
while (!Pila.Vacia())
{ printf("%d\t", Pila.Top());
Pila.Pop();
}
getch();
}
Una clase se puede relacionar con otra de varias formas diferentes. Según G. Booch existen tres
clases básicas de relaciones: generalización (la conocemos como herencia), agregación (y/o
composición) y asociación. Otros autores añaden además la relación de uso.
Algunos autores, a ésta relación se le denomina composición cuando los objetos de una clase
contenidos en otra clase no pueden ser independientes El ejemplo de la bicicleta sirve para este caso. Y
a la relación se le denomina agregación cuando los objetos de una clase contenidos en otra clase
pueden ser independientes. Por ejemplo, la clase persona está formada por objetos de la clase alumno y
objetos de la clase profesor. Cada uno de estos, alumno y profesor, tienen autonomía propia.
Utilizaremos indistintamente un término u otro.
La clase Calculadora vista con anterioridad puede componerse del objeto Display -de la clase
Visor- y de un número determinado de objetos Teclas –de la clase Tecla-. La relación entre estas
clases viene dada como: la clase Calculadora se compone, relación de composición, de la clase Visor
y de la clase Tecla.
La clase de la que se hereda se denomina clase base o superclase y la que hereda se denomina
clase derivada o subclase. Esta, a su vez, puede ser heredada por otra clase convirtiéndose en clase
base. En general, en una jerarquía de clases, todas las clases serán base y derivadas excepto las
primeras, que sólo serán bases y las que están abajo, que sólo serán derivadas.
La herencia, por tanto, consiste en que la subclase hereda todos los atributos y métodos de la
clase base sin necesidad de cambiar nada de esta. Simplemente los utiliza.
Por ejemplo, si disponemos de una clase persona que define las características y
comportamiento de los objetos persona y creamos una clase alumno, no es necesario implementar
todos sus atributos y métodos puesto que un alumno es una persona; sólo es necesario heredar de la
clase persona, utilizar lo que nos ofrezca –sus atributos y métodos-, y añadirle las nuevas
características y comportamiento del alumno –Expediente, Notas, Faltas-.
Ahora, una vez que nuestra calculadora está implementada, puede que necesitemos añadirle
una nueva funcionalidad y con ello ampliar su comportamiento: cambiar de pesetas a euros y
viceversa. Para ello, sólo tenemos que declarar una nueva subclase de Calculadora
“EuroCalculadora” y añadirle las nuevas características: un atributo que indica si los cálculos se
están realizando en pesetas o en euros, una nueva tecla que la declaramos como una nueva subclase
de Tecla o de Comando y un método para cambiar entre euros y pesetas.
Ejemplo :
#include <iostream.h>
class Base
{ int B1, B2;
public:
void Poner(int a, int b)
{ B1= a;
B2= b;
}
void MostrarB()
{ cout << B1 << " " << B2 << endl;
}
};
class Derivada : public Base
{ int D;
public:
Derivada(int a)
{ D= a;
}
void MostrarD()
{ cout << D << endl;
}
};
void main()
{ Derivada ob(3);
ob.Poner(1, 2);
ob.MostrarB();
ob.MostrarD();
}
La clase derivada hereda todos los miembros de la clase base aunque seguimos sin tener acceso
a los miembros privados de la clase base dentro o fuera de la clase derivada. Si podemos modificar los
miembros privados de la base usando los métodos públicos disponibles para ello.
Para construir una clase derivada se pueden definir constructores de la siguiente forma :
Ejemplo :
class Base
{ ...
Base(int a, int b)
{ B1= a;
B2= b;
}
...
};
class Derivada : public Base
{ ...
Derivada(int a, int b, int c):Base(b, c)
{ D= a;
}
...
};
void main()
{ Derivada ob(1, 2, 3);
}
De esta forma inicializamos miembros de la clase base que hemos heredado. Si no ponemos
explícitamente la llamada al constructor de la clase base, se llama al constructor por defecto de la
misma, que debe existir (creado por el compilador o por nosotros).
Una clase derivada no hereda de la clase base los constructores, el destructor y los operadores
de asignación por que son los que definen los fundamentos de la clase.
Como ya se vio, los miembros de una clase pueden tener tres tipos de acceso: público, privado
y protegido.
private: están protegidos de todo acceso fuera de la clase en la que están definidos.
public: son accesibles desde cualquier lugar.
protected: están protegidos contra todo acceso fuera de la clase donde fueron definidos y de las
clases derivadas. Es decir, los miembros protegidos si son accesibles desde la clase derivada.
En el ejemplo anterior, como los atributos miembros están definidos como privados a la clase
base, no pueden ser accedidos desde la clase derivada :
class Derivada : public Base
{ ...
void PonerD(int a, int b, int c)
{ D= a ;
B1= b ;
B2= c ;//Error. Privados de la clase base.
}
...
}
La solución podría consistir en definir como públicos los miembros B1 y B2, pero entonces,
pueden ser modificados desde cualquier lugar, rompiendo la encapsulación. El problema se soluciona
definiéndolos como protegidos, pasando a ser privados de la clase derivada.
class Base
{ protected :
int B1, B2;
...
} ;
Ahora si podemos acceder a dichos atributos desde la clase derivada pero no desde el exterior,
ya que se comportan como privados de esta clase.
Al derivar la clase también podemos definir el tipo de acceso a la clase base, variando también
el nivel de acceso de los miembros. El cuadro siguiente muestra la visibilidad de los miembros según
el tipo de acceso a la clase a la clase base:
class A ;
class B:protected A ; // Acceso a los miembros no privados de A
class C:protected B ; // Acceso a los miembros no privados de A
class D:privated B ; //Acc. a miembros no priv. y los convierte en privados.
class E:public D ; // No tiene acceso a los miembros de A.
#include <iostream.h>
#include <math.h>
class Punto
{ double x, y;
public:
~Punto(){};
Punto(double xx= 0, double yy= 0)
{ x= xx;
y= yy;
}
double operator - (const Punto &a)
{ return sqrt(pow(x - a.x, 2) + pow(y - a.y, 2));
}
friend double Distancia(const Punto &a, const Punto &b)
{ return sqrt(pow((a.x - b.x), 2) + pow((a.y - b.y), 2));
}
};
class Figura
{ protected:
int NPuntos;
Punto *Puntos;
public:
Figura(int N, Punto *Ptos)
{ NPuntos= N;
Puntos= Ptos;
}
double Perimetro()
{ int i;
double Peri= 0;
Peri= Distancia(Puntos[NPuntos - 1], Puntos[0]);
for(i= 0; i<NPuntos-1; i++)
Peri+= Distancia(Puntos[i], Puntos[i + 1]);
return Peri;
}
};
class Circulo:public Figura
{ private:
double Radio;
public:
Circulo(int R, Punto *Ptos):Figura(1, Ptos)
{ Radio= R;
}
double Perimetro()
{ return 2*3.1416*Radio;
}
double Area()
{ return 3.1416*Radio*Radio;
}
};
class Rectangulo:public Figura
{ public:
Rectangulo(Punto *Ptos):Figura(4, Ptos)
{
}
double Area()
{ return (Puntos[0]-Puntos[1])*(Puntos[1]-Puntos[2]);
}
};
main()
{ Punto p[4]= {Punto(2, 2), Punto(2, 3), Punto(1, 3), Punto(1 ,2)};
Rectangulo r(p);
cout << r.Perimetro();
cout << r.Area();
getch();
}
Una clase puede heredar los atributos y los métodos de una o más clases base. En el caso de
que herede los componentes de una única clase se habla de herencia simple y en el caso de que herede
los componentes de varias clases base se trata de un caso de herencia múltiple.
Como ejemplo se puede presentar el caso de que se tenga una clase para el manejo de los datos
de la empresa. Se podría definir la clase C_CuentaEmpresarial como la herencia múltiple de dos
clases base: la clase C_Cuenta y nueva clase llamada C_Empresa, que se muestra a continuación:
#include <iostream.h>
class C_Cuenta {
private:
char *Nombre; // Nombre de la persona
double Saldo; // Saldo Actual de la cuenta
double Interes; // Interés aplicado
public:
C_Cuenta(const char *unNombre,double unSaldo=0,double unInteres=0)
{ Nombre = new char[strlen(unNombre)+1];
strcpy(Nombre, unNombre);
SetSaldo(unSaldo);
SetInteres(unInteres);
}
~Cuenta()
{ delete [] Nombre;
}
inline char *GetNombre()
{ return Nombre;
}
class C_Empresa {
private:
char *NomEmpresa;
public:
C_Empresa(const char*laEmpresa)
{ NomEmpresa = new char[strlen(laEmpresa)+1];
strcpy(NomEmpresa, laEmpresa);
}
~C_Empresa()
{ delete [] NomEmpresa;
}
// Otros métodos ...
};
class C_CuentaEmpresarial : public C_Cuenta, public C_Empresa {
public:
C_CuentaEmpresarial(
const char *unNombre,
const char *laEmpresa,
double unSaldo=0.0,
double unInteres=0.0
):C_Cuenta(unNombre,unSaldo, unInteres), C_Empresa(laEmpresa)
// se llama a los constructores de las clases base
{ // Constructor
}
// Otros métodos
};
Al utilizar la herencia múltiple puede suceder que, indirectamente, una clase herede varias
veces los miembros de otra clase. Es decir, si de la clase Calculadora heredan las clases Calculadora
de Euros y Calculadora Científica, y de estas hereda la clase Calculadora de Euros Científica, en esta
los componentes de la clase Calculadora se encontrarán duplicados.
Si la clase Madre_1 y la clase Madre_2 heredan los miembros de la clase Abuela y la clase
Hija hereda, a su vez, los miembros de las clases Madre_1 y Madre_2, los miembros de la clase
Abuela se encontrarán duplicados en la clase Hija. Para evitar este problema las clases Madre_1 y
Madre_2 deben derivar de la clase Abuela declarándola clase base virtual. Esto hace que los
miembros de una clase de ese tipo se hereden tan sólo una vez. Un ejemplo de declaración de una clase
base virtual es el que se presenta a continuación:
class Madre_1 : virtual public Abuela {
...
}
Por el contrario las conversiones o asignaciones en el otro sentido, es decir de lo más general a
lo más particular, no son posibles, porque puede suceder que no se disponga de valores para todas las
variables miembro de la clase derivada.
En el siguiente ejemplo se pueden ver las distintas posibilidades de asignación (más bien de
inicialización, en este caso), que se presentan en la clase C_CuentaEmpresarial.
void main()
{ // Válido
C_CuentaEmpresarial *c1 = new C_CuentaEmpresarial("Juan",
"Jugos SA", 100000.0, 10.0);
// Válido. Se utilizan los valores por defecto
C_Cuenta *c2 = new C_CuentaEmpresarial("Igor", "Patata CORP");
// NO VÁLIDO
C_CuentaEmpresarial *c3 = new C_Cuenta("Igor", 100.0, 1.0);
// ...
}
Al igual que sucede con los nombres de los objetos, en principio cuando se hace referencia a un
objeto por medio de un puntero, el tipo de dicho puntero determina la función miembro que se
aplica, en el caso de que esa función se encuentre definida tanto en la clase base como en la derivada.
En definitiva, un puntero a la clase base puede almacenar la dirección de un objeto perteneciente a
una clase derivada. Sin embargo, se aplicarán los métodos de la clase a la que pertenezca el puntero,
no los de la clase a la que pertenece el objeto.
5.- Polimorfismo.
Por definición, según el diccionario de la RAE, polimorfismo “es la propiedad que tienen
algunos cuerpos para cambiar de forma sin variar su naturaleza”; es decir, polimorfismo representa a la
capacidad de adoptar formas distintas. En el ámbito de la POO es la capacidad de llamar a métodos
distintos con el mismo nombre. Estos pueden actuar sobre objetos distintos dentro de una jerarquía de
clases, sin tener que especificar el tipo exacto de los objetos. Así, el polimorfismo adquiere una
relevancia especial, debido a que también los datos pueden presentar cierto tipo de polimorfismo: un
identificador puede hacer referencia a objetos de distintas clases jerárquicamente relacionadas,
dependiendo del contexto, del mismo modo que un identificador de función puede representar distintas
secciones de código.
class Base{
int B1, B2;
public:
Base(int a, int b)
{ B1= a;
B2= b;
}
void Mostrar()
{ cout << B1 << " " << B2 << endl;
}
};
int main(){
Base base(1, 2);
Derivada derivada(3, 4, 5);
base.Mostrar(); // Llama a Mostrar() de la clase base
derivada.Mostrar(); // Llama a Mostrar() de la clase derivada
}
Si todos los objetos son asociados estáticamente a una clase, y no se tienen en cuenta los
posibles cambios de referencia, el método a aplicar puede determinarse en tiempo de compilación, sin
que el sistema sufra ningún tipo de sobrecarga. Esto se conoce como vinculación o enlace estático.
Esto se debe a que el compilador comprueba el tipo del objeto y dependiendo de cual sea llama al
método correspondiente.
Rectángulo R;
R.Perimetro(); //Invoca a Perimetro() de R
Circulo C;
C.Perimetro(); //Invoca a Area() de C
Lo mismo sucede cuanto trabajamos con punteros o referencias a clases aunque no está exento
de problemas. Supongamos el siguiente fragmento de programa:
Figura F, *FP;
Circulo C;
FP= &C;
FP->Perímetro();
Por conversión entre clases un objeto o puntero a un objeto de una clase base acepta objetos o
punteros a objetos de sus clases derivadas. La última línea invoca al método Perímetro() de la clase
base, de Figura, produciéndose un error en los cálculos dado que dicho método no sabe calcular el
perímetro de un círculo. Las cosas se nos pueden poner peor si queremos calcular el área del circulo
puesto que Figura no dispone de ese método. Para solucionar el problema podemos recurrir a moldear
(cast) el objeto en cuestión:
((Circulo* )FP)->Perímetro();
Para las funciones que vayan a tener un puntero para ligar el objeto con su método se definiran
funciones virtuales. Una función virtual el un método especial que se llama a través de un puntero o
una referencia a su clase base y se enlaza dinámicamente en tiempo de ejecución. La sintaxis para
definir un método virtual es el siguiente:
main()
{ Punto p= Punto(2, 2);
Circulo c(2, &p);
Figura *pf;
pf= &c;
pf->Perimetro(); // Llama a Perimetro() de Círculo sin hacer el moldeo.
cout << MayorArea(r, c);
}
Ahora podemos trabajar con punteros o referencias a objetos con la seguridad de que llamarán
a las funciones correctas. Esto es muy útil cuando funciones que tienen como parámetros formales
punteros o referencias a una clase base reciben como parámetros actuales punteros o referencias a
objetos de clases derivadas. En el ejemplo anterior se ve claro este uso.
Una clase abstracta se construye incluyendo un método (método virtual puro) abstracto dentro
de su definición o indicando que la clase es abstracta –depende del lenguaje-. Las clases derivadas de
una clase abstracta deben declarar todos los métodos abstractos heredados, si los hay. En el caso de
que no se redefina alguno, o se redefina también como abstracto, la clase heredada también será
abstracta.
Un método abstracto define el protocolo o interfaz para que una operación polimórfica pueda
ser redefinida mediante métodos específicos correspondientes a sus subclases.
La clase Figura es una clase abstracta por que no conocemos exactamente cual es el
comportamiento del método Area(), cada figura calcula el área de una forma diferente. Por ello,
dejamos que sea la subclase que herede de Figura la que implemente su comportamiento concreto, que
además está obligada a implementar para poder instanciar sus objetos. Sirve como clase base de otras
subclases.
class Figura
{ protected:
int NPuntos;
Punto *Puntos;
public:
Figura()
{
}
· Los lenguajes basados en objetos son aquellos que soportan la funcionalidad de los
objetos, es decir, aquellos que utilizan objetos. Esta familia de lenguajes incluye Ada, CLU,
Simula, Smalltalk, Eiffel, C++, Java y excluye a lenguajes como Fortran, Cobol o Pascal,
que no proporcionan objetos como primitiva del lenguaje (aunque existen variantes de estos
lenguajes que si soportan objetos).
· Los lenguajes que además incluyen el concepto de clase de objetos como entidad de
primera clase se denominan basados en clases. Estos siguen incluyendo a CLU, Simula,
Smalltalk, Eiffel, C++ y Java, pero no a Ada, debido a que sus clases de objetos no se
identifican con un tipo, no pueden ser pasadas como parámetros, ni ser componentes de un
registro.
Los lenguajes de POO pueden clasificarse, una vez eliminados los anteriores, en dos grandes
grupos: lenguajes puros y lenguajes híbridos. Los primeros sólo trabajan con programación orientada a
objetos (clases, objetos, métodos y mensajes) –Smalltalk, Java- y los segundos mezclan la POO con la
programación procedimental -C++-.
Puros Híbridos
Ventajas · Más potencia. · Más rápido
· Flexibilidad para modificar el lenguaje. · Pasar de prog. procedimental a
· Trabajan con un único modelo. POO es fácil.
· Más fácil de aprender y enseñar.
Los tres lenguajes de POO que vamos a ver, Smalltalk, C++ y Java, tienen unas características
muy similares: encapsulación, polimorfismo, vinculación dinámica, herencia clases genéricas, clases
abstractas, etc.
· Es seudocompildor lo que hace más lento que un compilador y más rápido que un
intérprete.
· Es un lenguaje de POO puro. Sólo utiliza clases, objetos métodos y mensajes. Sin embargo
dispone de los tipos de datos simple (int, char, ...) aunque también están implementados en
clases.
· Los objetos ocultan su información (encapsulación) dependiendo de niveles de acceso; sólo
puede obtenerse por los cauces adecuados.
· Tiene una librería jerárquica de clases bien definida y estándar.
· Sólo permite herencia simple.
· Dispone de un recolector de basura.