UNIVERSIDAD DE GRANADA
Grado en Ingeniería Informática FUNDAMENTOS DE PROGRAMACIÓN
Curso: 2011/2012 Clase: Primero - Grupo: B Aviso legal: los archivos están sujetos a derechos de propiedad intelectual y su titularidad corresponde a los usuarios que los han subido a SWAD. Esto es solo una recopilación de toda la asignatura impartida en la UGR.
TEORĂ?A
Curso: 2011/2012 Clase: Primero - Grupo: B
I.4.2. Operadores en Programación . . . . . . . . . . . . .
39
I.4.3. Expresiones . . . . . . . . . . . . . . . . . . . . . .
41
I.5. Tipos de datos comunes en C++ . . . . . . . . . . . . . . .
43
I.5.1. Rango y operadores aplicables a un tipo de dato . .
43
I.5.2. Los tipos de datos enteros
. . . . . . . . . . . . . .
44
I.5.3. Los tipos de datos reales . . . . . . . . . . . . . . .
51
1
I.5.4. Operando con tipos numéricos distintos . . . . . . .
60
I.1. El ordenador, algoritmos y programas . . . . . . . . . . . .
2
I.5.5. El tipo de dato carácter . . . . . . . . . . . . . . . .
64
I.1.1. El Ordenador: Conceptos Básicos . . . . . . . . . .
2
I.5.6. El tipo de dato cadena de caracteres . . . . . . . . .
73
I.1.2. Lenguajes de programación . . . . . . . . . . . . . .
3
I.5.7. El tipo de dato lógico o booleano . . . . . . . . . . .
76
I.1.3. Datos y Algoritmos . . . . . . . . . . . . . . . . . . .
6
I.5.8. El tipo enumerado . . . . . . . . . . . . . . . . . . .
82
I.1.4. Compilación . . . . . . . . . . . . . . . . . . . . . .
11
I.2. Especificación de programas . . . . . . . . . . . . . . . . .
13
I.2.1. Escritura y organización de un programa . . . . . . .
13
I.2.2. Elementos básicos de un lenguaje de programación
19
I.2.3. Tipos de errores en la programación . . . . . . . . .
22
I.3. Datos y tipos de datos . . . . . . . . . . . . . . . . . . . . .
24
I.3.1. Representación en memoria de datos e instrucciones
24
I.3.2. Datos y tipos de datos . . . . . . . . . . . . . . . . .
26
I.4. Operadores y Expresiones . . . . . . . . . . . . . . . . . .
38
I.4.1. Terminología en Matemáticas . . . . . . . . . . . . .
38
Contenidos I. Introducción a la Programación
1
II. Estructuras de Control
85
II.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . .
86
II.2. Estructura condicional
. . . . . . . . . . . . . . . . . . . .
91
II.2.1. Introducción . . . . . . . . . . . . . . . . . . . . . .
91
II.2.2. Estructura condicional simple . . . . . . . . . . . . .
91
II.2.3. Condicional doble . . . . . . . . . . . . . . . . . . .
97
II.2.4. Anidamiento de estructuras condicionales . . . . . . 103 II.2.5. Construcción de estructuras condicionales . . . . . 116 II.2.6. Estructura condicional múltiple . . . . . . . . . . . . 127 II.3. Estructuras repetitivas
. . . . . . . . . . . . . . . . . . . . 133
II.3.1. Bucles controlados por condición: pre-test y post-test 133
IV.1. Fundamentos
. . . . . . . . . . . . . . . . . . . . . . . . . 306
II.3.2. Bucles controlador por contador . . . . . . . . . . . 161
IV.1.1. Introducción . . . . . . . . . . . . . . . . . . . . . . 306
II.3.3. Anidamiento de bucles . . . . . . . . . . . . . . . . . 170
IV.1.2. Operaciones con Vectores . . . . . . . . . . . . . . . 309
II.3.4. Particularidades de C++ . . . . . . . . . . . . . . . . 178
IV.2. La clase MiVector . . . . . . . . . . . . . . . . . . . . . . . 319 IV.3. Algoritmos de Búsqueda . . . . . . . . . . . . . . . . . . . 324
III. Funciones y Clases
198 IV.3.1. Búsqueda Secuencial . . . . . . . . . . . . . . . . . 324
III.1. Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 IV.3.2. Búsqueda Binaria . . . . . . . . . . . . . . . . . . . 332 III.1.1. Fundamentos . . . . . . . . . . . . . . . . . . . . . . 199 IV.3.3. Otras búsquedas . . . . . . . . . . . . . . . . . . . . 335 III.1.2. Funciones void . . . . . . . . . . . . . . . . . . . . . 227 IV.4. Otros métodos de la clase MiVector . . . . . . . . . . . . . 339 III.1.3. Documentación de una función . . . . . . . . . . . . 231 IV.5. Algoritmos de Ordenación
. . . . . . . . . . . . . . . . . . 348
III.1.4. Ámbito de un dato . . . . . . . . . . . . . . . . . . . 235 IV.5.1. Ordenación por Selección . . . . . . . . . . . . . . . 350 III.1.5. Las funciones y los principios de programación . . . 241 IV.5.2. Ordenación por Inserción . . . . . . . . . . . . . . . 354 III.2. Clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245 III.2.1. Motivación. Clases y Objetos . . . . . . . . . . . . . 245 III.2.2. Encapsulación . . . . . . . . . . . . . . . . . . . . . 249
IV.5.3. Ordenación por Intercambio Directo (Método de la Burbuja) . . . . . . . . . . . . . . . . . . . . . . . . 357 IV.6. Las clases vector y string
. . . . . . . . . . . . . . . . . . 362
III.2.3. Ocultación de información . . . . . . . . . . . . . . . 265
IV.6.1. La clase vector . . . . . . . . . . . . . . . . . . . . . 362
III.2.4. Constructores . . . . . . . . . . . . . . . . . . . . . 276
IV.6.2. La clase string . . . . . . . . . . . . . . . . . . . . . 370
III.2.5. Diseño de una clase . . . . . . . . . . . . . . . . . . 285
IV.7. Vectores de varias dimensiones: Matrices . . . . . . . . . . 372
III.2.6. Registros . . . . . . . . . . . . . . . . . . . . . . . . 292
IV.7.1. Declaración . . . . . . . . . . . . . . . . . . . . . . . 372
III.2.7. Datos miembro constantes . . . . . . . . . . . . . . 293
IV.7.2. Operaciones con matrices . . . . . . . . . . . . . . . 373
IV. Vectores y Matrices
305
IV.7.3. Más de dos dimensiones
. . . . . . . . . . . . . . . 381
IV.7.4. La clase MiMatriz . . . . . . . . . . . . . . . . . . . . 382
V. Clases (Segunda parte)
385
VI.1. El concepto matemático de Recursividad . . . . . . . . . . 486
V.1. Copiando objetos . . . . . . . . . . . . . . . . . . . . . . . 386
VI.1.1. Soluciones recursivas . . . . . . . . . . . . . . . . . 486
V.1.1. Operador de asignación . . . . . . . . . . . . . . . . 386
VI.1.2. Diseño de algoritmos recursivos . . . . . . . . . . . 489
V.1.2. El constructor de copia . . . . . . . . . . . . . . . . 390
VI.2. Funciones recursivas . . . . . . . . . . . . . . . . . . . . . 491
V.1.3. Copiar o no copiar: He ahí el dilema
. . . . . . . . . 393
VI.2.1. Definición de funciones recursivas . . . . . . . . . . 491
V.2. Trabajando con varios objetos . . . . . . . . . . . . . . . . 394
VI.2.2. Ejecución de funciones recursivas . . . . . . . . . . 492
V.2.1. Pasando objetos como parámetros . . . . . . . . . . 394
VI.2.3. Traza de algoritmos recursivos . . . . . . . . . . . . 496
V.2.2. Objetos declarados localmente en un método y métodos que devuelven objetos . . . . . . . . . . . . . 401 V.2.3. Objetos como datos miembro de otros objetos
. . . 426
V.2.4. Vectores de objetos. Matrices . . . . . . . . . . . . . 434 V.3. Diseño de una solución . . . . . . . . . . . . . . . . . . . . 444 V.3.1. Funciones globales versus métodos . . . . . . . . . 444 V.3.2. Tipos de relaciones entre clases . . . . . . . . . . . 450 V.3.3. Relación de dependencia . . . . . . . . . . . . . . . 452 V.3.4. Relación de asociación . . . . . . . . . . . . . . . . 454 V.3.5. Acoplamiento . . . . . . . . . . . . . . . . . . . . . . 458 V.3.6. Arquitectura por capas
. . . . . . . . . . . . . . . . 459
V.3.7. Casos prácticos de diseño
. . . . . . . . . . . . . . 461
V.4. Tratamiento de errores con excepciones . . . . . . . . . . . 473 VI. Recursividad
485
VI.3. Clases con métodos recursivos
. . . . . . . . . . . . . . . 499
VI.3.1. Métodos recursivos . . . . . . . . . . . . . . . . . . 499 VI.3.2. Ordenación rápida: Quicksort . . . . . . . . . . . . . 510 VI.4. Recursividad versus iteración
. . . . . . . . . . . . . . . . 516
El color marrón se utilizará para los títulos de las secciones, apartados, etc El color azul se usará para los términos cuya definición aparece por primera vez. En primer lugar aparecerá el término en español y entre paréntesis la traducción al inglés.
Algunos símbolos usados:
denota contenido que el alumno debe estudiar por su cuenta. Entra como materia en el examen.
El color rojo se usará para destacar partes especialmente importantes denota contenido de ampliación. El texto incluido en la transparencia es de obligada lectura aunque las referencias son de consulta opcional y no entran en el examen. denota contenido de ampliación, a nivel de experto. No entra en el examen.
Consejos sobre buenas prácticas de programación
denota código o prácticas de programación que pueden producir errores lógicos graves
denota código que nos da escalofríos de sólo verlo.
denota código bien diseñado que nos ha de servir de modelo en otras construcciones.
denota algo especialmente importante.
Reseña histórica.
Tema I Principio de Programación.
Introducción a la Programación Objetivos: Introducir los conceptos básicos de programación, para poder construir los primeros programas. Introducir los principales tipos de datos disponibles en C++ para representar información del mundo real. Enfatizar, desde un principio, la necesidad de seguir buenos hábitos de programación.
Autor: Juan Carlos Cubero. Sugerencias: por favor, enviar un e-mail a JC.Cubero@decsai.ugr.es
Introducción a la Programación El ordenador, algoritmos y programas →
I.1.
El ordenador, algoritmos y programas
I.1.1.
El Ordenador: Conceptos Básicos
"Los ordenadores son inútiles. Sólo pueden darte respuestas". Pablo Picasso
2
Introducción a la Programación El ordenador, algoritmos y programas → Lenguajes de programación
I.1.2.
Lenguajes de programación
Programa (Program): Es el conjunto de instrucciones especificadas en un lenguaje de programación concreto, que pueden ejecutarse en un ordenador y que resuelven un problema Lenguaje de programación (Programming language): Lenguaje formal utilizado para comunicarnos con un ordenador e imponerle la ejecución de un conjunto de órdenes. Código binario (Binary code):
Hardware Software Usuario (User) Programador (Programmer) Lenguaje de Programación (Programming Language)
3
"There are 10 types of people in the world, those who can read binary, and those who can’t".
Introducción a la Programación El ordenador, algoritmos y programas → Lenguajes de programación
4
Lenguaje ensamblador (Assembly language). Depende del micropocesador (Intel 8086, Motorola 88000, etc) Se usa para programar drivers, microcontroladores (que son circuitos integrados que agrupan microprocesador, memoria y periféricos), compiladores, etc. Se ve en otras asignaturas.
.model small .stack .data Cadena1 DB 'Hola Mundo.$' .code mov ax, @data mov ds, ax mov dx, offset Cadena1 mov ah, 9 int 21h end Lenguajes de alto nivel (High level language) (C, C++, Java, Lisp, Prolog, Perl, Visual Basic, C#, Go ...) En esta asignatura usaremos C++ (ISO C++).
#include <iostream> using namespace std; int main(){ cout << "Hola Mundo"; }
Introducción a la Programación El ordenador, algoritmos y programas → Lenguajes de programación
5
Reseña histórica del lenguaje C++ 1967 Martin Richards: BCPL para escribir S.O. 1970 Ken Thompson: B para escribir UNIX (inicial) 1972 Dennis Ritchie: C 1983 Comité Técnico X3J11: ANSI C 1983 Bjarne Stroustrup: C++ 1989 Comité técnico X3J16: ANSI C++ 1990 Internacional Standarization Organization http://www.iso.org Comité técnico JTC1: Information Technology Subcomité SC-22: Programming languages, their environments and system software interfaces. Working Group 21: C++
http://www.open-std.org/jtc1/sc22/wg21/ 2011 Nuevo estándar. Buscar C++11 en Internet. ¿Qué programas se han hecho en C++? Google, Amazon, sistema de reservas aéreas (Amadeus), omnipresente en la industria automovilística y aérea, sistemas de telecomunicaciones, el explorador Mars Rovers, el proyecto de secuenciación del genoma humano, videojuegos como Doom, Warcraft, Age of Empires, Halo, la mayor parte del software de Microsoft y una gran parte del de Apple, la máquina virtual Java, Photoshop, Thunderbird y Firefox, MySQL, OpenOffice, etc. Entrevista reciente a Bjarne Stroustrup:
http://www.wired.com/thisdayintech/2010/10/1014cplusplus-released/ all/1#ixzz12guDpzz8
Introducción a la Programación El ordenador, algoritmos y programas → Datos y Algoritmos
I.1.3.
6
Datos y Algoritmos
Algoritmo (Algorithm): es una secuencia ordenada de instrucciones que resuelve un problema concreto, atendiendo a las siguientes características básicas: Corrección (sin errores). Precisión (no puede haber ambigüedad). Repetitividad (en las mismas condiciones, al ejecutarlo, siempre se obtiene el mismo resultado). Finitud (termina en algún momento). Número finito de órdenes no implica finitud. Características esenciales:
Introducción a la Programación El ordenador, algoritmos y programas → Datos y Algoritmos
7
Ejemplo. Algoritmo de la media aritmética de N valores. Datos de entrada: N, valor1, valor2, ..., valorN Datos de salida: media Instrucciones en lenguaje natural: Sumar los N valores y dividir el resultado por N Ejemplo. Algoritmo para la resolución de una ecuación de primer grado ax + b = 0 Datos de entrada: a, b Datos de salida: x Instrucciones en lenguaje natural: Calcular x como el resultado de la división −b/a
Validez (resuelve el problema pedido)
Podría mejorarse el algoritmo contemplando el caso de ecuaciones degeneradas
Eficiencia (lo hace en un tiempo aceptable)
Ejemplo. Algoritmo para el cálculo de la hipotenusa de un triángulo rectángulo.
Un dato (data) es una representación simbólica de una característica o propiedad de una entidad. Los algoritmos operan sobre los datos. Usualmente, reciben unos datos de entrada con los que operan, y a veces, calculan unos nuevos datos de salida.
Datos de entrada: lado1, lado2 Datos de salida: hipotenusa Instrucciones en lenguaje natural: hipotenusa = lado12 + lado22
Introducción a la Programación El ordenador, algoritmos y programas → Datos y Algoritmos
8
Ejemplo. Algoritmo para ordenar una lista de valores numéricos. (9, 8, 1, 6, 10, 4) −→ (1, 4, 6, 8, 9, 10)
Introducción a la Programación El ordenador, algoritmos y programas → Datos y Algoritmos
9
Una vez diseñado el algoritmo, pasamos a implementarlo en un lenguaje de programación concreto. "First, solve the problem. Then, write the code".
Datos de entrada: el vector Datos de salida: el mismo vector Instrucciones en lenguaje natural: – Calcular el mínimo valor de todo el vector
Implementación de un algoritmo (Algorithm implementation): Transcripción a un lenguaje de programación de un algoritmo. Ejemplo. Implementación del algoritmo para el cálculo de la media (para 4 valores) en C++:
– Intercambiarlo con la primera posición – Volver a hacer lo mismo con el vector formado por todas las componentes menos la primera. (9, 8, 1, 6, 10, 4) → (1, 8, 9, 6, 10, 4) →
suma = valor1 + valor2 + valor3 + valor4; media = suma / 4; Ejemplo. Implementación del algoritmo para el cálculo de la hipotenusa:
hipotenusa = sqrt(lado1*lado1 + lado2*lado2);
(X, 8, 9, 6, 10, 4) → (X, 4, 9, 6, 10, 8) → (X, X, 9, 6, 10, 8) → ···
Un programa incluirá la implementación de uno o más algoritmos. Ejemplo. Programa para dibujar planos de pisos. Utilizará algoritmos para dibujar cuadrados, de medias aritméticas, salidas gráficas en plotter, etc. Muchos de los programas que se verán en FP implementarán un único algoritmo.
Introducción a la Programación El ordenador, algoritmos y programas → Datos y Algoritmos
Ampliación: Uno de los pilares fundamentales de la informática lo constituye la máquina de Turing (Turing machine). Es un modelo formal de ordenador basado en un alfabeto, un conjunto de estados y un conjunto de transiciones sobre dichos estados.
10
Introducción a la Programación El ordenador, algoritmos y programas → Compilación
I.1.4.
Compilación
Al código escrito en un lenguaje concreto se le denomina código fuente (source code). Éste se guarda en ficheros de texto normales (en el caso de C++ tienen extensión .cpp).
Pitagoras.cpp
/* La máquina de Turing permite realizar cualquier cómputo que un computador digital sea capaz de realizar. Turing demostró que existen problemas que una máquina no puede resolver. Antes de diseñar la máquina de Turing, éste ayudó a descifrar los mensajes encriptados por la máquina Enigma en la segunda guerra mundial. Consultad en Wikipedia la biografía de Turing. También puede consultarse:
http://www.zator.com/Cpp/E0_1_1.htm La importancia de Turing en el campo de la Informática es tal que el equivalente a los premios Nobel llevan su nombre: A.M.Turing Award, patrocinado por Intel y Google. Por cierto, el servidor de la ETS Informática se llama turing.ugr.es.
11
Programa simple para el cálculo de la hipotenusa de un triángulo rectángulo, aplicando el teorema de Pitágoras */ #include <iostream> #include <cmath> using namespace std;
// Inclusión de los recursos de E/S // Inclusión de los recursos matemáticos
int main(){ double lado1; double lado2; double hipotenusa;
// Programa Principal // Declara variables para guardar // los dos lados y la hipotenusa
cout << "Introduzca la longitud del primer cateto: " ; cin >> lado1; cout << "Introduzca la longitud del segundo cateto: "; cin >> lado2; hipotenusa = sqrt(lado1*lado1 + lado2*lado2); cout << "\nLa hipotenusa vale " << hipotenusa << "\n\n" ; }
Introducción a la Programación El ordenador, algoritmos y programas → Compilación
12
Pitagoras.cpp es un simple fichero de texto. Para obtener el programa ejecutable (el fichero en binario que puede ejecutarse en un ordenador) se utiliza un compilador:
Introducción a la Programación Especificación de programas →
I.2.
Especificación de programas
I.2.1. Código fuente del programa
−→ Compilación −→
13
Escritura y organización de un programa
Programa Los programas en C++ pueden dividirse en varios ficheros aunque por ahora vamos a suponer que cada programa está escrito en un único fichero (Pitagoras.cpp).
ejecutable
La extensión del programa ejecutable en Windows es .exe
Se pueden incluir comentarios en lenguaje natural. Código Fuente: Pitagoras.cpp
/* Comentario en varias líneas */ // Comentario 1 sola línea
#include <iostream> using namespace std;
El texto de un comentario no es procesado por el compilador.
int main() { double lado1;
−→
Compilador
.......... cout << "Introduzca la longitud ... cin >> lado1; .......... } Programa Ejecutable: Pitagoras.exe
10011000010000 10010000111101 00110100000001 11110001011110 11100001111100 11100101011000 00001101000111 00011000111100
−→
Al principio del fichero se indica que vamos a usar una serie de recursos definidos en un fichero externo o biblioteca (library)
#include <iostream> #include <cmath> A continuación aparece int main(){ que indica que comienza el programa principal. Éste se extiende desde la llave abierta {, hasta encontrar la correspondiente llave cerrada. Dentro del programa principal van las sentencias (sentence/statement) que componen el programa. Una sentencia es una parte del código fuente que el compilador puede traducir en una instrucción en código binario. Las sentencias van obligatoriamente separadas por punto y coma (; ) El compilador va ejecutando las sentencias secuencialmente de arriba abajo. En el tema II se verá como realizar saltos, es decir, interrumpir la estructura secuencial.
Introducción a la Programación Especificación de programas → Escritura y organización de un programa
14
Cuando llega a la llave cerrada } correspondiente a main(), y si no han habido problemas, el programa termina de ejecutarse y el Sistema Operativo libera los recursos asignados a dicho programa.
Existen varios tipos de sentencias: Sentencias de declaración de datos (data) Cada dato que usemos en un programa debe declararse al principio (después de main), asociándole un tipo de dato concreto.
double lado1; double lado2; double hipotenusa; Declara tres datos (variables) de tipo real que el programador puede usar en el programa. Sentencias de cálculo A través de la asignación =
lado1 = 7; lado2 = 5; hipotenusa = sqrt(lado1*lado1 + lado2*lado2); asigna a la variable hipotenusa el resultado de evaluar lo que aparece a la derecha de la asignación. Nota. No confundir = con la igualdad en Matemáticas
Introducción a la Programación Especificación de programas → Escritura y organización de un programa
15
Sentencias de Entrada/Salida Sentencias para mostrar información en el periférico de salida establecido por defecto. En FP, dicho periférico será la pantalla, pero podría ser un fichero en disco o dispositivo externo (impresora, plotter, puerto FireWire, etc.). Se construyen usando cout, que es un recurso externo incluido en la biblioteca iostream.
cout << "Mensaje " << variable; – Lo que haya dentro de un par de comillas dobles se muestra tal cual, excepto los caracteres precedidos de \. Por ejemplo, \n hace que el cursor salte al principio de la línea siguiente. cout << "Bienvenido. Salto a la siguiente linea ->\n"; cout << "\n<- Empiezo en una nueva linea."; – Los números se escriben tal cual (decimales con punto) cout << 3.1415927; – Si ponemos una variable, se imprime su contenido.
cout << "\nIntroduzca la longitud del primer cateto: "; ..... hipotenusa = sqrt(lado1*lado1 + lado2*lado2); cout << "\nLa hipotenusa vale ": cout << hipotenusa; Observad el espacio al final del mensaje. ¿Qué haría lo siguiente? cout << "hipotenusa"; – Podemos usar una única sentencia: cout << "\nLa hipotenusa vale " << hipotenusa;
IntroducciĂłn a la ProgramaciĂłn EspeciďŹ caciĂłn de programas â&#x2020;&#x2019; Escritura y organizaciĂłn de un programa
16
â&#x20AC;&#x201C; Aunque no es recomendable, tambiĂŠn pueden incluirse expresiones como:
cout << "\nLa hipotenusa vale " << sqrt(lado1*lado1+lado2*lado2); Es mejor almacenar previamente el valor de la expresiĂłn en una variable (hipotenusa) y luego utilizarla donde se necesite (por ejemplo, para imprimir su valor) Es importante dar un formato adecuado a la salida de datos en pantalla. El usuario del programa debe entender claramente el signiďŹ cado de todas sus salidas.
// MAL: totalVentas = 45; numeroVentas = 78; cout << totalVentas << numeroVentas;
IntroducciĂłn a la ProgramaciĂłn EspeciďŹ caciĂłn de programas â&#x2020;&#x2019; Escritura y organizaciĂłn de un programa
17
Sentencias para leer datos desde el dispositivo de entrada establecido por defecto. Por ahora, serĂĄ el teclado. Se construyen usando cin, que es un recurso externo incluido en la biblioteca iostream.
cin >> variable; Por ejemplo, cin >> lado1, espera a que el usuario introduzca un valor real (double) desde el teclado (dispositivo de entrada) y, cuando se pulsa la tecla Intro, lo almacena en la variable lado1. Conforme se va escribiendo el valor, ĂŠste se muestra en pantalla, incluyendo el salto de lĂnea.
La lectura de datos con cin puede considerarse como una asignaciĂłn en tiempo de ejecuciĂłn
// Imprime 4578
// BIEN: cout << "\nSuma total de ventas = " << totalVentas; cout << "\nNĂşmero total de ventas = " << numeroVentas;
Las entradas de datos deben etiquetarse adecuadamente:
// MAL: cin >> lado1; cin >> lado2; // BIEN: cout << "Introduzca la longitud del primer cateto: "; cin >> lado1; cout << "Introduzca la longitud del segundo cateto: "; cin >> lado2; Cuando la entrada se realiza desde un ďŹ chero (y no el teclado), la lectura es automĂĄtica sin que se espere la tecla Intro.
Introducción a la Programación Especificación de programas → Escritura y organización de un programa
18
Estructura básica de un programa (los corchetes delimitan secciones opcionales):
Introducción a la Programación Especificación de programas → Elementos básicos de un lenguaje de programación
I.2.2.
19
Elementos básicos de un lenguaje de programación
[ /* Breve descripción en lenguaje natural de lo que hace el programa */ ]
[ Inclusión de recursos externos ]
using namespace std; int main(){ [Declaración de datos]
[Sentencias del programa separadas por ;]
}
I.2.2.1. Tokens y reglas sintácticas Cada lenguaje de programación tiene una sintaxis propia que debe respetarse para poder escribir un programa. Básicamente, queda definida por: a) Los componentes léxicos (tokens). Formados por caracteres alfanuméricos y/o simbólicos. Representan la unidad léxica mínima que el lenguaje entiende. main ; ( == = hipotenusa * Pero por ejemplo, ni ! ni ((* son tokens válidos. b) Reglas sintácticas (Syntactic rules): determinan cómo han de combinarse los tokens para formar sentencias. Algunas se especifican con tokens especiales (formados usualmente por símbolos): – Separador de sentencias ; – Para agrupar varias sentencias se usa { } Se verá su uso en el tema II. Por ahora, sirve para agrupar las sentencias que hay en el programa principal
– Para agrupar expresiones (fórmulas) se usa ( ) = sqrt( (lado1*lado1) + (lado2*lado2) );
Introducción a la Programación Especificación de programas → Elementos básicos de un lenguaje de programación
20
I.2.2.2. Palabras reservadas Suelen ser tokens formados por caracteres alfabéticos. Tienen un significado específico para el compilador, y por tanto, el programador no puede definir variables con el mismo identificador. Algunos usos:
Introducción a la Programación Especificación de programas → Elementos básicos de un lenguaje de programación
Palabras reservadas comunes a C (C89) y C++ auto
break
char
const
continue default
do
double else
enum
extern
float
goto
if
int
long
register return
signed
sizeof
static
struct switch
unsigned void
main Para definir tipos de datos como por ejemplo double Para establecer el flujo de control (control flow), es decir, para especificar el orden en el que se han de ejecutar las sentencias, como if, while, for etc. Estos se verán en el tema 2.
//
Errores sintácticos
int main(){ double main; // main es un nombre de dato incorrecto double double; // double es un nombre de dato incorrecto }
21
case
typedef
for short union
volatile while
Palabras reservadas adicionales de C++ and
and_eq
asm
class
compl
const_cast delete
false
friend
inline
mutable namespace
new
not_eq
operator
or
or_eq
private
protected public
reinterpret_cast static_cast template
this
throw
true
try
typeid
virtual
wchar_t
xor
xor_eq
typename
using
bitand
Palabras reservadas añadidas en C99 _Bool _Complex _Imaginary inline restrict
bitor
bool
dynamic_cast explicit
catch export not
Introducción a la Programación Especificación de programas → Tipos de errores en la programación
I.2.3.
Tipos de errores en la programación
Errores en tiempo de compilación (Compilation error) Ocasionados por un fallo de sintaxis en el código fuente. No se genera el programa ejecutable.
/* CONTIENE ERRORES #include <iostre am>
*/
USING namespace std; int main{}( double lado1: double lado 2, double hipotenusa: lado1 = 2; lado2 = 3 hipotenusa = sqrt(lado1**lado1 + ladp2*ladp2); cout << "La hipotenusa vale << hipotenusa; )
22
Introducción a la Programación Especificación de programas → Tipos de errores en la programación
23
"Software and cathedrals are much the same. First we build them, then we pray".
Errores en tiempo de ejecución (Execution error) Se ha generado el programa ejecutable, pero se produce un error durante la ejecución.
int dato_entero; int otra_variable; dato_entero = 0; otra_variable = 7 / dato_entero; Errores lógicos (Logic errors) Se ha generado el programa ejecutable, pero el programa ofrece una solución equivocada.
......... lado1 = 4; lado2 = 9; hipotenusa = sqrt(lado1+lado1 + lado2*lado2); .........
Introducción a la Programación Datos y tipos de datos →
I.3.
24
Datos y tipos de datos
I.3.1.
Introducción a la Programación Datos y tipos de datos → Representación en memoria de datos e instrucciones
Datos
Representación en memoria de datos e instrucciones
"Juan Pérez" −→ 1 0 1 1 .. 0 1 1
75225813
−→ 1 1 0 1 .. 0 0 0
3.14159
−→ 0 0 0 1 .. 1 1 1
Un programa realiza instrucciones y opera con datos. Una vez compilado el programa, todo son combinaciones adecuadas de 0 y 1. MEMORIA MASIVA datos e instrucciones
Instrucciones
MEMORIA PRINCIPAL 0101100 11110110 00000001 10100001 0001111 11100011 10100100 10111001 1111111 00111111 11010010 00100100
ENTRADA
instrucciones
datos e instrucciones
UNIDAD DE CONTROL
CPU
Abrir Fichero −→ 0 0 0 1 .. 1 1 1 SALIDA datos
UNIDAD ARITMETICO-LOGICA
datos
Imprimir Nos centramos en los datos.
−→ 1 1 0 1 .. 0 0 0
25
Introducción a la Programación Datos y tipos de datos → Datos y tipos de datos
I.3.2.
26
Datos y tipos de datos
Introducción a la Programación Datos y tipos de datos → Datos y tipos de datos
Por ejemplo, en C++: ⎧ ⎧ ⎧ ⎨ Enteros : int, long, ... ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ Numéricos ⎪ ⎪ ⎪ ⎪ ⎩ Reales : float, double, ... ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎨ Caracteres : char ⎪ ⎪ ⎪ ⎪ Simples ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ Lógico : bool ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ Puntero : * ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎩ ⎪ ⎪ Enumerado : enum ⎪ ⎨
Al trabajar con un lenguaje de alto nivel, no haremos referencia a la secuencia de 0 y 1 que codifican un valor concreto, sino a lo que representa para nosotros. Un dato (data) es un conjunto de celdas de memoria que tiene asociado un nombre -identificador (name)- y un contenido -valor (value)-.
NombreEmpleado NumeroHabitantes Pi
Juan Pérez Tipos 75225813
3.14159
Un programa necesitará representar datos de diverso tipo (cadenas de caracteres, enteros, reales, etc). Para cada tipo, el lenguaje ofrece un tipo de dato (data type).
string NombreEmpleado; long NumeroHabitantes; double Pi; NombreEmpleado = "Juan Pérez"; NumeroHabitantes = 75225813; Pi = 3.14159;
⎪ ⎧ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ Vectores y matrices ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ Cadena de caracteres : ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎨ ⎪ Vectores de caracteres, ⎪ ⎪ ⎪ Compuestos ⎪ ⎪ ⎪ ⎪ ⎪ string ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ Registros ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎩ ⎩ Clases
Por ahora sólo usaremos double, int, char y string.
27
Introducción a la Programación Datos y tipos de datos → Datos y tipos de datos
28
I.3.2.1. Literales Son la especificación de un valor concreto de un tipo de dato. Dependiendo del tipo, tenemos: Literales numéricos (numeric literals): son tokens numéricos. Para representar datos reales, se usa el punto . para especificar la parte decimal: 2 3 3.5 Literales de cadenas de caracteres (string literals): Son cero o más caracteres encerrados entre comillas dobles: "Hola" Literales de otros tipos, como literales de caracteres (character literals) 'a', Literales lógicos (boolean literals) true, etc. I.3.2.2. Declaración de variables Cada variable que se use en un programa debe declararse al principio (después de main), asociándole un tipo de dato concreto y un identificador. Al declarar una variable, el compilador reserva una zona de memoria para trabajar con ella. Ninguna otra variable podrá usar dicha zona. Cada variable debe estar asociado a un único tipo de dato (el tipo no puede cambiarse durante la ejecución). El valor que se le puede asignar a una variable depende del tipo de dato con el que es declarado.
Introducción a la Programación Datos y tipos de datos → Datos y tipos de datos
29
Formas de declarar variables: Un único identificador por declaración:
<tipo> <identificador1>; <tipo> <identificador2>; ... double lado1; double lado2; double hipotenusa; Varios identificadores por declaración:
<tipo> <identificadores separados por coma>; double lado1, lado2, hipotenusa; Opcionalmente, se puede dar un valor inicial durante la declaración:
<tipo> <identificador> = <valor_inicial>; <tipo> <identificador> = <valor_inicial>, <identificador>...; double dato = 4.5; equivale a: double dato; dato = 4.5;
Introducción a la Programación Datos y tipos de datos → Datos y tipos de datos
30
Cuando se declara una variable y no se inicializa, ésta no tiene ningún valor asignado por defecto. Puesto que desconocemos su valor, lo podremos considerar como basura, y lo representaremos gráficamente por ?.
/* Programa para calcular la retención a aplicar en el sueldo de un empleado */ #include <iostream> using namespace std; int main(){ double salario_bruto; // Salario bruto, en euros double retencion; // Retención a aplicar, en euros salario_bruto
retencion
?
?
cout << "Introduzca salario bruto: "; cin >> salario_bruto; retencion = salario_bruto * 0.18; cout << "Retención a aplicar: " << retencion; } salario_bruto
retencion
32538.0
4229.94
Introducción a la Programación Datos y tipos de datos → Datos y tipos de datos
31
Un error lógico muy común es usar una variable no asignada:
int main(){ double salario_bruto; // Salario bruto, en euros double retencion; // Retención a aplicar, en euros retencion = salario_bruto * 0.18;
// :-(
cout << "Retención a aplicar: " << retencion; } I.3.2.3. Datos constantes Podríamos estar interesados en usar variables a las que sólo permitimos tomar un único valor, fijado de antemano. Es posible con una constante (constant). Se declaran como sigue:
const <tipo><identif>= <expresión>; A las constantes se les aplica las mismas consideraciones que hicimos con las variables, sobre tipo de dato y reserva de memoria. Suelen usarse identificadores sólo con mayúsculas para diferenciarlos de las variables.
Introducción a la Programación Datos y tipos de datos → Datos y tipos de datos
32
/*
Introducción a la Programación Datos y tipos de datos → Datos y tipos de datos
33
/* Programa que pide el radio de una circunferencia e imprime su longitud y el área del círculo
*/ #include <iostream> using namespace std; int main() { const double PI = 3.1416; double area, radio, longitud; PI = 3.15;
Programa para calcular la retención a aplicar en el sueldo de un empleado */ #include <iostream> int main(){ const double IRPF=0.18; double retencion; double salario_bruto;
// Porcentaje gravamen fiscal // Retención a aplicar, en euros // Salario bruto, en euros
// <- Error de compilación :-)
cout << "Introduzca el valor del radio "; cin >> radio; area = PI * radio * radio; longitud = 2 * PI * radio; cout << "El área vale " << area; cout << "La longitud vale " << longitud;
IRPF
retencion
?
0.18
?
cout << "Introduzca salario bruto: "; cin >> salario_bruto; retencion = salario_bruto * IRPF;
} Ventaja declaración de constantes:
salario_bruto
cout << "\n Retención a aplicar: " << retencion; }
Información al propio programador. Imposibilidad de cambiarlo por error (PI). Código menos propenso a errores. Para cambiar el valor de PI, (por ejemplo por 3.1415927), sólo hay que tocar en la línea de declaración de la constante.
salario_bruto
IRPF
retencion
32538
0.18
4229.94
Introducción a la Programación Datos y tipos de datos → Datos y tipos de datos
34
Formalmente, al conjunto de variables y constantes, se le denomina datos. Así pues, los datos se pueden clasificar como: ⎧ ⎨ variables datos ⎩ constantes Las variables son datos cuyo valor puede variar a lo largo del programa, p.e., lado1. Las constantes son datos cuyo contenido no varía a lo largo de la ejecución del programa, p.e., el número π.
Una sintaxis de programa algo más completa: [ /* Breve descripción en lenguaje natural de lo que hace el programa */ ]
[ Inclusión de recursos externos ]
using namespace std; int main(){
Introducción a la Programación Datos y tipos de datos → Datos y tipos de datos
35
I.3.2.4. Normas para la elección del identificador Cada dato necesita un identificador único. Un identificador de un dato es un token formado por caracteres alfanuméricos con las siguientes restricciones: Debe empezar por una letra o subrayado (_) No pueden contener espacios en blanco ni ciertos caracteres especiales como letras acentuadas, la letra eñe, las barras \ o /, etc. lado2 precio_con_IVA Ejemplo: lado1 El compilador determina la máxima longitud que pueden tener (por ejemplo, 31 caracteres) Sensibles a mayúsculas y minúsculas. lado y Lado son dos identificadores distintos. No se podrá dar a un dato el nombre de una palabra reservada. No es recomendable usar el nombre de algún identificador usado en las bibliotecas estándar (por ejemplo, cout)
#include <iostream> using namespace std; int main(){ double cout; // Error de sintaxis double main; // Error de sintaxis
[Declaración de constantes]
}
[Declaración de variables]
Ejercicio. Determinar cuáles de los siguientes son identificadores válidos. Si son inválidos explicar por qué.
[Sentencias del programa separadas por ;]
a) registro1
b) 1registro
e) nombre y direccion
c) archivo_3
f) dirección
d) main
g) diseño
Introducción a la Programación Datos y tipos de datos → Datos y tipos de datos
36
Consejos muy importantes para elegir el identificador de un dato: El identificador de un dato debe reflejar su semántica (contenido). Por eso, salvo excepciones (como las variables contadoras de los bucles -tema II-) no utilizaremos nombres con pocos caracteres
v, l1, l2, hp voltaje, lado1, lado2, hipotenusa No utilizaremos nombres genéricos
aux, vector1 copia, calificaciones Usaremos minúsculas para los datos variables. Las constantes se escribirán en mayúsculas. Los nombres compuestos se separarán con subrayado _:
precioventapublico, tasaanual precio_venta_publico, tasa_anual No se nombrarán dos datos con identificadores que difieran únicamente en la capitalización, o un sólo carácter.
cuenta, cuentas, Cuenta cuenta, coleccion_cuentas, cuenta_ppal Hay una excepción a esta norma. Cuando veamos las clases, podremos definir una con el nombre Cuenta y un objeto instancia de dicha clase con el nombre cuenta. Cuando veamos clases, funciones y métodos, éstos se escribirán con la primera letra en mayúscula. Los nombres compuestos se separarán con una mayúscula. Clase CuentaBancaria, Método Ingresa
Introducción a la Programación Datos y tipos de datos → Datos y tipos de datos
En el examen, se baja puntos por no seguir las anteriores normas.
37
Introducción a la Programación Operadores y Expresiones →
I.4. I.4.1.
38
Operadores y Expresiones Terminología en Matemáticas
Notaciones usadas en los operadores matemáticos: Notación prefija (Prefix notation). El operador va antes de los argumentos. Estos suelen encerrarse entre paréntesis. seno(3), tangente(x), media(valor1, valor2) Notación infija (Infix notation). El operador va entre los argumentos. 3+5 x/y Según el número de argumentos, diremos que un operador es: Operador unario (Unary operator). Sólo tiene un argumento: seno(3), tangente(x) Operador binario (Binary operator). Tienes dos argumentos: media(valor1, valor2) 3+5 x/y Operador n-ario (n-ary operator). Tiene más de dos argumentos.
Introducción a la Programación Operadores y Expresiones → Operadores en Programación
I.4.2.
39
Operadores en Programación
Los lenguajes de programación proporcionan operadores que permiten manipular los datos. Se denotan a través de tokens alfanuméricos o simbólicos. Suelen devolver un valor y actúan sobre ciertos datos (variables o constantes), literales y sobre el resultado que dan otros operadores. Tipos de operadores: Los definidos en el núcleo del compilador. No hay que incluir ninguna biblioteca Suelen usarse tokens simbólicos para su representación y suelen ser infijos Ejemplos: + (suma), - (resta), * (producto), etc. Los definidos en bibliotecas externas. Por ejemplo, cmath Suelen usarse tokens alfanuméricos para su representación y suelen ser prefijos. Si hay varios argumentos se separan por una coma.
sqrt(4)
sin(6.4)
pow(3,6)
Tradicionalmente se usa el término operador (operator) a secas para denotar los primeros, y el término función (function) para los segundos. A los argumentos de las funciones se les denomina parámetros (parameter).
Introducción a la Programación Operadores y Expresiones → Operadores en Programación
40
int main() { const double PI = 3.1415926; double area, radio; cout << "Introduzca el valor del radio "; cin >> radio; // Asignaciones válidas: area = PI * pow(radio, 2); area = PI * radio * radio; En general:
variable = Expresión; Primero se evalúa la expresión en la parte derecha de la asignación y luego se realiza la asignación.
Introducción a la Programación Operadores y Expresiones → Expresiones
I.4.3.
41
Expresiones
Una expresión (expression) es una combinación de datos y operadores sintácticamente correcta, que el compilador evalúa y devuelve un valor.
3 3+5 lado1 lado1*lado1 lado1*lado1 + lado2*lado2 sqrt(lado1*lado1 + lado2*lado2) Así pues, los operadores y funciones actúan, en general, sobre expresiones. Las expresiones pueden aparecer a la derecha de una asignación, pero no a la izquierda. 3+5 = lado; // Error de compilación ¿Qué haría lo siguiente?
double dato = 4; dato = dato + 3; Una expresión NO es una sentencia de un programa: Expresión: sqrt(lado1*lado1 + lado2*lado2) Sentencia: hipotenusa = sqrt(lado1*lado1 + lado2*lado2);
Introducción a la Programación Operadores y Expresiones → Expresiones
42
Cuando el compilador evalúa una expresión, devuelve un valor de un tipo de dato (entero, real, carácter, etc.). Diremos que la expresión es de dicho tipo de dato. Por ejemplo:
3 + 5 es una expresión entera 3.5 + 6.7 es una expresión de reales int main(){ double dato_real; int dato_entero; dato_real = 3.5 + 6.7; datos_entero = 3 + 5; ...... } Nota. Cuando se usa una expresión dentro de cout, el compilador detecta el tipo de dato y la imprime de forma adecuada.
cout << "\nResultado = " << 3+5; cout << "\nResultado = " << 3.5+6.7; Imprime en pantalla:
Resultado = 8 Resultado = 10.2
Introducción a la Programación Tipos de datos comunes en C++ →
I.5.
43
Tipos de datos comunes en C++
I.5.1.
Rango y operadores aplicables a un tipo de dato
El comportamiento de un tipo de dato viene dado por: El rango (range) de valores que puede representar, que depende de la cantidad de memoria que dedique el compilador a su representación interna. Intuitivamente, cuanta más memoria se dedique para un tipo de dato, mayor será el número de valores que podremos representar. El conjunto de operadores que pueden aplicarse a los datos de ese tipo. Ampliación: La cantidad de memoria que el compilador asigna a cada tipo se puede consultar con el operador sizeof(<tipo>) cuyo resultado es un entero que contiene el número de bytes asignados a <tipo>. sizeof es un operador pues está en el núcleo del compilador, pero se usa en forma prefija, encerrando el argumento entre paréntesis, como las funciones. Los valores mínimos y máximos que se pueden representar para cada tipo, se pueden consultar en los ficheros climits y cfloat. Consultad también:
http://msdn2.microsoft.com/en-us/library/d4zh6te4.aspx
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos enteros
I.5.2.
44
Los tipos de datos enteros
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos enteros
45
I.5.2.2. Rango de los enteros
I.5.2.1. Representación de los enteros
Subconjunto del conjunto matemático Z. La cardinalidad dependerá del número de bits (r) que cada compilador utiliza para su almacenamiento.
Propiedad fundamental: Cualquier entero puede descomponerse como la suma de determinadas potencias de 2.
Los compiladores suelen ofrecer distintos tipos enteros. En C++: short, int, long, __int64, etc. El más usado es int.
53 = 0∗215 +0∗214 +0∗213 +0∗212 +0∗211 +0∗210 +0∗29 +0∗28 +0∗27 +
El estándar de C++ no obliga a los compiladores a usar un tamaño determinado.
+0 ∗ 26 + 1 ∗ 25 + 1 ∗ 24 + 0 ∗ 23 + 1 ∗ 22 + 0 ∗ 21 + 1 ∗ 20
Lo usual es que un int ocupe la longitud de la palabra (word) del segmento de datos del sistema operativo. Hoy en día, lo usual es 32 bits (8 bytes) y 64 bits (16 bytes). En el caso de 32 bits el rango de un int es:
La representación en binario sería la secuencia de los factores (1,0) -bitsque acompañan a las potencias: 0000000000110101 Dos elementos a combinar: 1, 0 r posiciones. Por ejemplo, r = 16 Se permiten repeticiones e importa el orden 0000000000110101 = 0000000000110110 Número de datos distintos representables = 2r Ampliación: La forma fácil de representar el signo sería añadiendo otro bit al principio para indicar si es positivo o negativo (bit de signo). Los ordenadores actuales usan otras técnicas que agilizan los cómputos. Buscar en Internet: Representación de números con signo
232 232 , − 1 = [−2147483648, 2147483647] − 2 2
int entero; entero = 53; Cuando necesitemos un entero mayor, podemos usar __int64. Ocupa 64 bits (16 bytes) y el rango es: [−9223372036854775808, 9223372036854775807]
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos enteros
46
Un literal (literal) es la especificación (dentro del código fuente) de un valor concreto de un tipo de dato. Los literales enteros (integer literals) se construyen con tokens formados por símbolos numéricos. Pueden empezar con un signo -
-406778
47
I.5.2.3. Operadores
Literales enteros
53
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos enteros
0
Nota. En el código se usa el sistema decimal (53) pero internamente, el ordenador usa el código binario (000000000110101) ¿Qué tipo de dato se usa para representar un literal entero? Depende de la memoria reservada y de los tipos disponibles en cada compilador. El compilador usará el tipo de dato que se ajuste al tamaño del literal entero. Así, si un compilador usa 32 bits para un int, 600000000 será un literal de tipo int.
int entero; entero = 600000000; // int = int __int64 gran_entero; gran_entero = 600000000000000; // __int64 = __int64 Los literales demasiado grandes provocarán un error:
int entero; entero = 60000000000000000000; // Error compilación
Operadores binarios
+
-
*
/
%
suma, resta, producto, división entera y módulo. El operador módulo (%) representa el resto de la división entera Devuelven un entero. Binarios. Notación infija: a*b
int n = n = n = n = n = n = n = n = n = n = 5 /
n; 5 * 7; n + 1; 25 / 9; 25 % 9; 5 / 7; 7 / 5; 173 / 10; 5 % 7; 7 % 5; 173 % 10; 7 = n;
// // // // // // // // // // //
Asigna a la variable n Asigna a la variable n Asigna a la variable n Asigna a la variable n Asigna a la variable n Asigna a la variable n Asigna a la variable n Asigna a la variable n Asigna a la variable n Asigna a la variable n Sentencia Incorrecta.
el el el el el el el el el el
valor valor valor valor valor valor valor valor valor valor
35 36 2 7 0 1 17 5 2 3
Operaciones usuales: Extraer el dígito menos significativo: 5734 % 10 → 4 Truncar desde el dígito menos significativo: 5734 / 10 → 573
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos enteros
48
Operadores unarios de incremento y decremento
++ y --
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos enteros
49
I.5.2.4. Expresiones enteras Son aquellas expresiones, que al evaluarlas, devuelven un valor entero.
Unarios de notación postfija.
Incrementan y decrementan, respectivamente, el valor de la variable entera sobre la que se aplican (no pueden aplicarse sobre una expresión).
<variable>++;
<variable>--;
/* Incrementa la variable en Es equivalente a: <variable> = <variable> + /* Decrementa la variable en Es equivalente a: <variable> = <variable> -
int dato = 4; dato = dato+1; dato++;
1 1; */ 1 1; */
entera
56
(entera/4+56)%3
¿Cómo evalúa el compilador la siguiente expresión?
3 + 5 * 7; Posibilidad 1: 1. Primero hacer 3+5 2. Después, el resultado se multiplica por 7 El resultado sería: 56
// Asigna 5 a dato // Asigna 6 a dato
Posibilidad 2: 1. Primero hacer 5*7
Operador unario de cambio de signo
2. Después, el resultado se suma a 3
-
Unario de notación prefija.
El resultado sería: 38
Cambia el signo de la variable sobre la que se aplica.
int dato = 4, dato_cambiado; dato_cambiado = -dato; dato_cambiado = -dato_cambiado;
// Asigna -4 a dato_cambiado // Asigna 4 a dato_cambiado
La forma que adopta el compilador depende de la precedencia de los operadores. Ante la duda, forzad la evaluación deseada mediante la utilización de paréntesis:
dato = 3+(5*7);
dato = (3+5)*7;
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos enteros
50
Reglas de precedencia:
I.5.3.
() - (operador unario de cambio de signo) * / % + Cualquier operador de una fila superior tiene más prioridad que cualquiera de la fila inferior.
variable = 3+5*7;
// equivale a 3+(5*7)
Los operadores de una misma fila tienen la misma prioridad. Se evalúan de izquierda a derecha según aparecen en una expresión.
variable = 3/5*7;
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos reales
// equivale a (3/5)*7
51
Los tipos de datos reales
Reales: subconjunto finito de R Parte entera de 4,56 −→ 4 Parte real de 4,56 −→ 56 C++ ofrece distintos tipos para representar valores reales. Principalmente, float (usualmente 32 bits) y double (usualmente 64 bits).
double valor_real; valor_real = 541.341; I.5.3.1.
Literales reales
Son tokens formados por dígitos numéricos y con un único punto que separa la parte decimal de la real. Pueden llevar el signo - al principio. Ejercicio. Teniendo en cuenta el orden de precedencia de los operadores, indicad el orden en el que se evaluarían las siguientes expresiones: a) a + b * c -d
b) a * b / c
c) a * c % b - d
Ejemplo. Incrementar el salario en 100 euros y calcular el número de billetes de 500 euros a usar en el pago.
int salario, num_bill500; salario = 43000; num_bill500 = (salario + 100) / 500; num_bill500 = salario + 100 / 500;
// ok // Error lógico
800.457
4.0
-3444.5
Importante: El literal 3 es un entero. El literal 3.0 es un real. Los compiladores suelen usar el tipo double para representar literales reales. También se puede utilizar notación científica:
5.32e+5 representa el número 5,32 ∗ 105 = 532000 42.9e-2 representa el número 42,9 ∗ 10−2 = 0,429
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos reales
52
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos reales
I.5.3.2. Representación de los reales
=1∗
1 2
+0∗
1 4
+1∗
1 8
+1∗
53
1 16
= 0,6875
¿Cómo podría el ordenador representar 541,341? Problema: Usando este método NO es posible representar de forma exacta muchos reales. Valores tan sencillos como 0,1 o 0,01 no se pueden representar de forma exacta.
Lo fácil sería: Representar la parte entera 541 en binario Representar la parte real 341 en binario
0,1 1 ∗
De esa forma, con 64 bits (32 bits para cada parte) podríamos representar: Partes enteras en el rango [−2147483648, 2147483647]
1 24
+0∗
1 25
+0∗
1 26
+0∗
1 27
+0∗
1 28
+ ···
Por tanto:
Partes reales en el rango [−2147483648, 2147483647] Sin embargo, la forma usual de representación no es así. Se utiliza la representación en coma flotante (floating point). La idea es representar un valor y la escala. En aritmética decimal, la escala se mide con potencias de 10: 42,001
→ valor = 4,2001 escala = 10
42001
→ valor = 4,2001 escala = 104
0,42001 → valor = 4,2001 escala = 10−1 El valor se denomina mantisa (mantissa) y el coeficiente de la escala exponente (exponent). En la representación en coma flotante, la escala es 2. A grosso modo se utilizan m bits para la mantisa y n bits para el exponente. La forma explícita de representación en binario se verá en otras asignaturas. Podemos adelantar que la representación de la parte real se hace en potencias inversas de 2. Por ejemplo, 1011 representaría 1∗
1 21
+0∗
1 22
+1∗
1 23
+1∗
1 24
=
¡Todas las operaciones realizadas con los reales pueden devolver valores que sólo sean aproximados!
Especial cuidado tendremos con operaciones del tipo
Repite varias veces Ir sumándole a una variable_real varios valores reales; ya que los errores de aproximación se irán acumulando.
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos reales
54
I.5.3.3. Rango y Precisión La codificación en coma flotante separa el valor de la escala. Esto permite trabajar (en un mismo tipo de dato) con magnitudes muy grandes y muy pequeñas.
double valor_real; valor_real = 100000000000000000000.0; valor_real = 0.000000000000000000001;
// ok // ok!
double masa_tierra_kg, masa_electron_kg; masa_tierra_kg = 5.98e24; // ok masa_electron_kg = 9.11e-31; // ok Pero el precio a pagar es muy elevado: muy poca precisión (tanto en la parte entera como la real)
double valor_real; valor_real = 123456789012345.0; // :-) valor_real = 12345678901234567.0; // :-( // Se almacenará un valor aproximado. // Por ejemplo: 12345678901234568.0; La regla a usar es que con 32 bits, se consigue una precisión aproximada de 7 dígitos y con 64 bits, se consiguen aproximadamente 16 dígitos.
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos reales
55
valor_real = 0.12345678901234567; // Se almacenará un valor aproximado. Por ejemplo: // 0.12345678901234568; valor_real = 0.1; // Ya lo vimos antes // También se almacenará un valor aproximado. // Por ejemplo: 0.99999899897 Cuanto mayor sea la parte entera a representar, menor será la precisión en decimales que obtendremos (sin contar que, además, muchos decimales como 0.1 no se pueden representar de forma exacta) Por ejemplo, usando 32 bits, hay aproximadamente 8388607 números entre 1,0 y 2,0, mientras que sólo hay 8191 entre 1023,0 y 1024,0 Debido a la poca precisión, los programas financieros no trabajan con reales en coma flotante. Soluciones: Trabajar con enteros y potencias de 10.
int salario; salario = 175023; // 1750 euros y 23 céntimos salario = 143507; // 1435 euros y 7 céntimos salario = 130000; // 1300 euros Usar tipos o clases específicas (BCD por ejemplo)
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos reales
56
En resumen:
57
I.5.3.4. Operadores $
'
Los tipos enteros representan datos enteros de forma exacta. Los tipos reales representan la parte entera de forma exacta, siempre que trabajemos con menos de 16 dígitos (en una representación con 64 bits) La parte real será sólo aproximada. Además, la aproximación será mejor cuanto menor sea la parte entera del número.
&
INF 0.0 NaN NaN
+, -, *, / Binarios, de notación infija. También se puede usar el operador unario de cambio de signo (-). Aplicados sobre reales, devuelven un real.
double real; real = 5.0 * 7.0; real = 5.0 / 7.0; %
Los reales en coma flotante también permiten representar valores especiales como infinito (infinity) y una indeterminación (undefined) NaN (Not a Number)
double valor_real, divisor = 0.0; valor_real = 17/divisor; // Almacena valor_real = 1/valor_real; // Almacena valor_real = divisor/divisor; // Almacena valor_real = 1/valor_real; // Almacena
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos reales
// Asigna a real el valor 35.0 // Asigna a real el valor 0.7142857
¡Cuidado! El comportamiento del operador / depende del tipo de los operandos: si todos son enteros, es la división entera. Si todos son reales, es la división real.
double real = real = real =
real; 5.0 / 7.0; 5 / 7; 5 / 7.0;
// Asigna a real el valor 0.7142857 // Asigna a real el valor 0 // <- Lo vemos después
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos reales
58
Introducción a la Programación Tipos de datos comunes en C++ → Los tipos de datos reales
59
I.5.3.5. Funciones estándar
I.5.3.6. Expresiones reales
Hay algunas bibliotecas estándar que proporcionan funciones que trabajan sobre datos numéricos (enteros o reales) y que suelen devolver un real. Por ejemplo, cmath
Son expresiones cuyo resultado es un número real.
pow(), cos(), sin(), sqrt(), tan(), log(), log10(), fabs(), .... Todos los anteriores son unarios excepto pow, que es binario. Devuelven un real.
#include <iostream> #include <cmath> using namespace std; int main(){ double abscisa, ordenada; abscisa = 5.4; ordenada = sqrt(abscisa); ordenada = pow(4.3, abscisa); }
sqrt(abscisa) es una expresión real pow(4.3, abscisa) es una expresión real sqrt(b*b - 4.0*a*c) es una expresión real En general, diremos que las expresiones aritméticas (arithmetic expression) o numéricas son las expresiones o bien enteras o bien reales. Precedencia de operadores:
() - (operador unario de cambio de signo) * / + '
Consejo:
$
Para facilitar la lectura de las fórmulas matemáticas, evitad el uso de paréntesis cuando esté claro cuál es la precedencia de cada operador.
&
%
−b +
√
b2 − 4ac
2a
-b+sqrt(b*b-4.0*a*c)/2.0*a ((-b)+(sqrt(b*b - (4.0*a)*c)))/(2.0*a) (-b + sqrt(b*b - 4.0*a*c))/(2.0*a)
INCORRECTO CORRECTO RECOMENDADO
Introducción a la Programación Tipos de datos comunes en C++ → Operando con tipos numéricos distintos
I.5.4.
60
Operando con tipos numéricos distintos
I.5.4.1. Usando enteros y reales en una misma expresión Muchos operadores numéricos permiten como argumentos, expresiones de tipos distintos.
5.0 5.0 5 / 5.0
* 7.0 / 7.0 7 / 7
Devuelve Devuelve Devuelve Devuelve
35.0 0.7142857 0 0.7142857
double real; real = 5.0 / 7; // // real = 5 / 7; // //
double Asigna double Asigna
= a = a
61
double real el valor 0.7142857 int real el valor 0.0
Muy importante: Primero se evalúa la expresión en la parte derecha de la asignación y luego se realiza la asignación.
int edad1 = 10, edad2 = 5; double media; media = (edad1 + edad2)/2; // :-(
#
asigna a media el valor 7.0.
Para evaluar el resultado de una expresión que contenga datos con tipos distintos, el compilador usará el tipo adecuado que sea capaz de albergar el resultado. "
Introducción a la Programación Tipos de datos comunes en C++ → Operando con tipos numéricos distintos
Es un error lógico, difícil a veces de detectar. !
Por ejemplo, usará un double para almacenar el resultado de 5.0 / 7, es decir, 0.7142857. El operador de asignación también permite trabajar con tipos distintos.
double real; int entero = 5; real = 5; // double = int real = entero; // double = int Internamente se produce una transformación de tipo (casting) del valor 5 (en su representación como un int) al valor 5.0 (en su representación como un double)
Posible solución: ¿Declarar edad1 y edad2 como double? El resultado sería 7.5, pero no debemos cambiar el tipo asociado a la edad, pues su semántica es de un entero. Forzaremos la división real usando un literal real en un operando.
int edad1 = 10, edad2 = 5; double media; media = (edad1 + edad2)/2.0;
// :-)
Introducción a la Programación Tipos de datos comunes en C++ → Operando con tipos numéricos distintos
62
¿Qué pasa cuando asignamos un real a un entero? Simplemente, se pierde la parte decimal, es decir, se trunca.
double real; int entera; real = 5.7; entera = real
// Asigna a entera el valor 5
Este truncamiento también ocurre si ejecutamos
cin >> entera; e introducimos un valor real.
Introducción a la Programación Tipos de datos comunes en C++ → Operando con tipos numéricos distintos
63
I.5.4.2. Problemas de desbordamiento Recordemos que con un literal demasiado grande, el compilador da un error al intentar asignarlo a una variable. Pero al operar con varios datos enteros, el compilador no puede realizar ninguna comprobación. Por tanto, es fácil salirse del rango representable. En este caso, se produce un desbordamiento aritmético (arithmetic overflow) y el resultado es un valor cualquiera impredecible (basura). Se produce un error lógico.
int entero, incremento; entero = 600000000000; entero = 600000000; incremento = 1000; entero = entero * incremento;
// Error Compilación :-) // Correcto. "Le cabe" // Desbordamiento
:-(
El compilador es capaz de evaluar la expresión
Casos especiales: El casting a entero de un double que contenga infinito o indeterminación es un valor basura.
int entero, denominador; double real;
600000000*1000 El resultado 600000000000 lo almacena temporalmente en un __int64. El problema viene al hacer la asignación de una expresión de tipo __int64 a una variable de tipo int. Esto mismo ocurre también con los reales.
denominador = 0; real = 17/(denominador*1.0); entero = real; real = 0/(denominador*1.0); entero = real; entero = 17/denominador;
// // // // //
Almacena INF Almacena basura Almacena NaN Almacena basura Error ejecución ¾Por qué?
En general, debemos tener cuidado con las asignaciones a datos de un tipo más pequeño que la expresión de la asignación. Algunos compiladores pueden advertir de este hecho, durante el proceso de compilación, con un aviso (warning) (pero no es un error de compilación).
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato carácter
64
I.5.4.3. Un int no es siempre un entero de 32 bits con signo Una de las principales críticas al estándar C++ es que cada compilador tiene cierto margen de maniobra y puede implementar cada tipo de dato de forma distinta. Una restricción fundamental es:
1 byte
==
char
<=
short
<=
int
<=
long
Por tanto, podría ser que un int fuese un entero de 32 bits con signo en un compilador y en otro fuese un entero de 16 bits con signo. Obviamente, esto puede ocasionar problemas de desbordamiento como hemos visto en la sección anterior. Los ficheros de cabecera <climits> y <float.h> contienen definiciones de los rangos de valor de todos los tipos fundamentales. Ampliación: Consultad las restricciones básicas en:
http://www.zator.com/Cpp/E2_2_4c.htm y una referencia más avanzada en:
http://www.zator.com/Cpp/E2_2_4.htm
I.5.5.
El tipo de dato carácter
I.5.5.1. Rango Es un conjunto finito y ordenado de caracteres: letras minúsculas, mayúsculas, dígitos del 0 al 9 y otros caracteres especiales. Hay varios tipos de dato de carácter: char, wchar, signed char, etc. En FP usaremos char.
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato carácter
65
Código ASCII (256 caracteres) Cada carácter tiene un código numerado de 0 a 255.
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato carácter
66
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato carácter
67
Literales de carácter Son tokens formados por: Un único carácter encerrado entre comillas simples: '!' 'A' 'a' '5' 'ñ' Observad que '5' es un literal de carácter y 5 es un literal entero ¡Cuidado!: 'cinco' o '11' no son literales de carácter. O bien una secuencia de escape, es decir, el símbolo \ seguido de otro símbolo, como por ejemplo: Secuencia Significado
\n
Nueva línea (retorno e inicio)
\t
Tabulador
\b
Retrocede 1 carácter
\r
Retorno de carro
\f
Salto de página
\'
Comilla simple
\"
Comilla doble
\\
Barra inclinada
Las secuencias de escape también deben ir entre comillas simples, por ejemplo, '\n', '\t', etc.
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato carácter
68
#include <iostream> using namespace std; int main(){ const char NUEVA_LINEA = '\n'; char letra_piso;
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato carácter
I.5.5.2. Funciones estándar El fichero de cabecera cctype contiene varias funciones relativas a caracteres. Por ejemplo:
tolower letra_piso = 'B'; // Almacena 'B' cout << letra_piso << "ienvenidos"; cout << '\n' << "Empiezo a escribir en la siguiente línea"; cout << '\n' << '\t' << "Acabo de tabular esta línea"; cout << NUEVA_LINEA; cout << '\n' << "Esto es una comilla simple: " << '\''; } Escribiría en pantalla:
Bienvenidos Empiezo a escribir en la siguiente línea Acabo de tabular esta línea Esto es una comilla simple: ' Nota. Para escribir un retorno de carro, también puede usarse una constante llamada endl en la forma: cout << endl << "Adiós" << endl
Observad la diferencia:
double r; char letra; letra = 'r'; r = 23;
// literal de carácter 'r' // variable real r
69
toupper
#include <cctype> using namespace std; int main(){ char letra_piso; letra_piso = tolower('A'); letra_piso = toupper('A'); letra_piso = tolower('B'); letra_piso = tolower('?'); ..................
// // // //
Almacena Almacena Almacena Almacena
'a' 'A' 'b' '?'
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato carácter
70
I.5.5.3. El tipo char como un tipo entero En C++, el tipo char es realmente un tipo entero pequeño. Cuando hacemos
char letra_piso; letra_piso = 'A'; estamos asignando a la variable letra_piso el número de orden en la tabla ASCII del carácter 'A', es decir, el 65.
char letra_piso; letra_piso = 'A';
// letra_piso contiene 65
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato carácter
71
Veamos algunas consecuencias: Podemos operar con los caracteres como si fuesen números:
char letra_piso; letra_piso = 'A'+1; letra_piso = 65+1; letra_piso = '7'-1; letra_piso = 'A' + '7';
// // // // //
También valdría int letra_piso; Almacena 66 = 'B' Almacena 66 = 'B' Almacena 54 = '6' Almacena 120
También podemos usar el operador - con dos argumentos de carácter. Devuelve un entero:
int orden; orden = 'c' - 'a' // Almacena 2
De hecho, también podríamos poner:
char letra_piso; letra_piso = 65;
// letra_piso contiene 65
Lo anterior es posible con cualquier tipo entero:
int letra_piso; letra_piso = 'A'; letra_piso = 65; letra_piso = '7'; letra_piso = 7;
// // // //
Almacena Almacena Almacena Almacena
65 65 55 7
Así pues, el tipo char es un entero pequeño ya que está pensado para albergar los números de órdenes de la tabla ASCII.
El recurso cout se ha programado de la siguiente forma: Si se pasa a cout un literal o variable de tipo char, imprime el carácter correspondiente al entero almacenado. Si se pasa a cout un literal o variable de tipo entero (distinto a char), imprime el entero almacenado. ¿Qué imprimiría la sentencia cout << 'A'+1? No imprime 'B' como cabría esperar sino 66 ya que 1 es un literal entero y por tanto de tipo int. El tipo int es más grande que el tipo char, por lo que 'A'+1 es una expresión de tipo int.
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato carácter
Estudiar: C++ ofrece un amplio repertorio de tipos de datos para representar, por ejemplo, enteros sólo positivos, reales muy grandes, etc. Consultad http://www.zator.com/Cpp/E2_2_3.htm http://www.cplusplus.com/doc/tutorial/variables/
72
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato cadena de caracteres
I.5.6.
73
El tipo de dato cadena de caracteres
Los literales de cadenas de caracteres son una sucesión de caracteres encerrados entre comillas dobles: "Hola", "a" son literales de cadena de caracteres
cout << "Esto es un literal de cadena de caracteres"; Las secuencias de escape también pueden aparecer en los literales de cadena de caracteres presentes en cout
int main(){ cout << "Bienvenidos"; cout << "\nEmpiezo a escribir en la siguiente línea"; cout << "\n\tAcabo de tabular esta línea"; cout << "\n"; cout << "\nEsto es una comilla simple '"; cout << " y esto es una comilla doble \""; } Escribiría en pantalla:
Bienvenidos Empiezo a escribir en la siguiente línea Acabo de tabular esta línea Esto es una comilla simple ' y esto es una comilla doble "
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato cadena de caracteres
74
Ejercicio. Determinar cuales de las siguientes son constantes de cadena de caracteres válidas, y determinar la salida que tendría si se pasase como argumento a cout a) "8:15 P.M."
b) "'8:15 P.M."
d) "Dirección\n" e) "Dirección'n" g) "Dirección\\'n"
c) '"8:15 P.M."' f) "Dirección\'n"
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato cadena de caracteres
75
C++ ofrece dos alternativas para trabajar con cadenas de caracteres: A la vieja usanza, usando vectores de caracteres con terminador '\0'. Se verá en otras asignaturas. Usando el tipo string (la recomendada en esta asignatura)
int main(){ string mensaje_bienvenida; mensaje_bienvenida = "\tCubero Corporation\n"; cout << mensaje_bienvenida; ........ } Para poder operar con un string debemos incluir la biblioteca string:
#include <iostream> #include <string> using namespace std; int main(){ string cad; cad = "Hola y "; cad = cad + "adiós"; cout << cad; } Realmente, string es una clase (lo veremos en temas posteriores) por lo que le serán aplicables métodos en la forma:
cad = "Hola y "; cad.append("adiós");
// :-O
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato lógico o booleano
I.5.7.
76
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato lógico o booleano
El tipo de dato lógico o booleano
p ≡ Carlos es varón q ≡ Carlos es joven
Es un tipo de dato muy común en los lenguajes de programación que se utiliza para representar los valores verdadero y falso que suelen estar asociados a una condición. En C++ se usa el tipo bool. I.5.7.1. Rango
p
q
p && q p || q
true
true
true
true
true
false
true
false
false false
true
false
true
false
true
false false false
false
Por ejemplo: Si p es false, y q es true,
Una expresión lógica es una expresión cuyo resultado es un tipo de dato lógico.
p && q será false p || q será true
I.5.7.2. Operadores
Recordad:
Son los operadores clásicos de la lógica Y, O, NO que en C++ son los operadores &&, ||, ! respectivamente.
!p
true
false true
Un dato lógico, sólo puede tomar dos valores: verdadero y falso. Para representarlos, se usan los siguientes literales:
77
false && expresión true || expresión
siempre es false. siempre es true.
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato lógico o booleano
Muchas funciones de cctpye devuelven un valor lógico
isalpha
isalnum
isdigit
...
isalpha('3') es una expresión lógica (devuelve false)
*/
I.5.7.3. Operadores Relacionales
Algunos ejemplos: La expresión (4 < 5) devuelve valor true
es_alfa = isalpha('3'); // Asigna false a es_alfa es_alfanum = isalnum('3'); // Asigna true a es_alfanum es_digito_numerico = isdigit('3'); // Asigna true a es_digito_numerico
}
79
== (igual), != (distinto), <, >, <= (menor o igual) y >= (mayor o igual)
int main() { bool es_alfa, es_alfanum, es_digito_numerico; bool compuesto;
compuesto = (es_alfa && es_alfanum); compuesto = (es_alfa || es_alfanum); compuesto = !es_alfa; .......
Introducción a la Programación Tipos de datos comunes en C++ → El tipo de dato lógico o booleano
Son los operadores habituales de comparación de expresiones numéricas. Pueden aplicarse a operandos tanto enteros, reales, como de caracteres y tienen el mismo sentido que en Matemáticas. El resultado es de tipo bool.
Por ejemplo,
/* Ejemplo de tipo de dato lógico (bool) #include <cctype> using namespace std;
78
// Resultado: false // Resultado: true // Resultado: true
La expresión (4 > 5) devuelve el valor false La relación de orden entre caracteres se establece según la tabla ASCII. La expresión ('a' > 'b') devuelve el valor false.
!= es el operador relacional distinto. ! es la negación lógica. == es el operador relacional de igualdad. = es la operación de asignación. Tanto en == como en != se usan 2 signos para un único operador
IntroducciĂłn a la ProgramaciĂłn Tipos de datos comunes en C++ â&#x2020;&#x2019; El tipo de dato lĂłgico o booleano
//
Ejemplo de operadores relacionales
int main(){ int entero1 = 3, entero2 = 5; double real1, real2; bool menor, iguales; menor = menor = menor = real1 = real2 = menor = menor = iguales
entero1 < entero2; entero2 < entero1; (entero1 < entero2) && !(entero2 < 7); 3.8; 8.1; real1 > entero1; !menor && (real1 > real2); = real1 == real2;
}
80
IntroducciĂłn a la ProgramaciĂłn Tipos de datos comunes en C++ â&#x2020;&#x2019; El tipo de dato lĂłgico o booleano
Reglas de Precedencia:
() ! < <= > >= == != && || A es menor o igual que B y B no es mayor que C
int A = 40, B = 34, C = 50; bool condicion; condicion = A <= B && !B > C; condicion = (A <= B) && (!(B > C)); condicion = (A <= B) && !(B > C); condicion = (A <= B) && (B <= C);
// // // //
MAL BIEN Recomendado Mucho mejor
Veremos su uso en la sentencia condicional:
if (4 < 5) cout << "4 es menor que 5"; if (!(4 > 5)) cout << "4 es menor o igual que 5";
81
Consejo:
SimpliďŹ cad las expresiones lĂłgicas, para asĂ aumentar su legibilidad.
Ejercicio. Escribid una expresiĂłn lĂłgica que devuelva true si un nĂşmero entero edad estĂĄ en el intervalo [0,100]
Introducción a la Programación Tipos de datos comunes en C++ → El tipo enumerado
I.5.8.
82
El tipo enumerado
Supongamos un programa de contabilidad en el que queremos representar tres posibles valores de desgravación, a saber 7, 16 y 30 %. Alternativas: Usar una única variable desgravacion
int desgravacion; desgravacion = 7; ...... Propenso a errores: podemos poner por error el literal 17, que es aceptado en un int. Usar tres constantes:
const int desgravacion_7 = 7; const int desgravacion_16 = 16; const int desgravacion_33 = 33; Tedioso, pues siempre tendremos que manejar tres variables
Introducción a la Programación Tipos de datos comunes en C++ → El tipo enumerado
83
Usar un tipo enumerado (enumerated type). Es un tipo entero que sólo acepta unos valores concretos especificados por el programador.
double salario; enum TipoDesgravacion {baja=7, media=16, alta=33}; TipoDesgravacion desgravacion; cout << baja; // Imprime 7 desgravacion = baja; // Almacena internamente 7 cin >> salario; salario = salario - salario*desgravacion/100.0; desgravacion = 5; cin >> desgravacion;
// Error compilación :-) // Error compilación :-)
#
Si a una constante sólo se le puede asignar un único valor, a un tipo enumerado sólo se le puede asignar un número muy limitado de valores. "
!
Ampliación: Si no se pone ningún valor en la definición del tipo, C++ le asigna a cada valor del enumerado el siguiente entero del asignado al valor anterior. Al primero le asigna el cero.
enum TipoPocoUsual {baja, media, alta=4, muy_alta}; TipoPocoUsual raro; raro = baja; cout << raro; // 0 cout << media; // 1 cout << alta; // 4 cout << muy_alta; // 5
Introducción a la Programación Tipos de datos comunes en C++ → El tipo enumerado
84
Bibliografía recomendada para este tema: A un nivel menor del presentado en las transparencias: – Primer capítulo de Deitel & Deitel – Primer capítulo de Garrido.
Tema II
A un nivel similar al presentado en las transparencias: – Capítulo 1 y apartados 2.1, 2.2 y 2.3 de Savitch A un nivel con más detalles:
Estructuras de Control
– Los tres primeros capítulos de Stephen Prata. – Los dos primeros capítulos de Lafore. Objetivos: Introducir las estructuras condicionales que nos permitirán realizar saltos hacia adelante durante la ejecución del código. Introducir las estructuras repetitivas que nos permitirán realizar saltos hacia atrás durante la ejecución del código. Introducir pautas de programación en la construcción de las estructuras condicionales y repetitivas.
Autor: Juan Carlos Cubero. Sugerencias: por favor, enviar un e-mail a JC.Cubero@decsai.ugr.es
Estructuras de Control Introducción →
II.1.
86
87
Ejemplo. Calcular las raíces de una ecuación de 2o grado.
Introducción ⎧ ⎪ ⎪ Secuencial ⎪ ⎨ Estructuras
Estructuras de Control Introducción →
Condicional
⎪ ⎪ ⎪ ⎩ Repetitiva
Estructura Secuencial (Sequential control flow structure): las instrucciones se van ejecutando sucesivamente, siguiendo el orden de aparición de éstas. No hay saltos.
p(x) = ax2 + bx + c = 0 Algoritmo: Entradas: Los parámetros de la ecuación a, b, c. Salidas: Las raíces de la parábola r1, r2 Descripción: Calcular r1, r2 en la forma siguiente: r1 =
−b +
√
b2 − 4ac
r2 =
2a
−b −
p(x)
0
-5
-10
-15
-20 -4
-2
0
2
b2 − 4ac
2a
p(x) = ax2 + bx + c 5
√
4
Estructuras de Control Introducción →
88
Estructuras de Control Introducción →
#include <iostream> #include <cmath> using namespace std;
#include <iostream> #include <cmath> using namespace std;
int main(){ double a, b, c; // Parámetros de la ecuación double raiz1, raiz2; // Raíces obtenidas
int main(){ double a, b, c; // Parámetros de la ecuación double raiz1, raiz2; // Raíces obtenidas double radical, denominador;
cout << "\nIntroduce coeficiente de 2 o grado: "; cin >> a; cout << "\nIntroduce coeficiente de 1er grado: "; cin >> b; cout << "\nIntroduce coeficiente independiente: "; cin >> c; raiz1 = ( -b + sqrt( b*b-4*a*c ) ) / (2*a); raiz2 = ( -b - sqrt( b*b-4*a*c ) ) / (2*a); cout << "\nLas raíces son" << raiz1 << " y " << raiz2; }
Principio de Programación: Una única vez (Once and only once) Cada descripción de comportamiento debe aparecer una única vez en nuestro programa.
Dicho de otra forma: JAMÁS ha de repetirse código La repetición de código hace que los programas sean difíciles de actualizar ya que cualquier cambio ha de realizarse en todos los sitios en el que está repetido el código.
1 2 3 4 5 6
cout << "\nIntroduce coeficiente de 2 o grado: "; cin >> a; cout << "\nIntroduce coeficiente de 1er grado: "; cin >> b; cout << "\nIntroduce coeficiente independiente: "; cin >> c;
7 8
radical = sqrt( b*b-4*a*c ); denominador = 2*a;
9 raiz1 = (-b + radical) / denominador; 10 raiz2 = (-b - radical) / denominador; 11 cout << "\nLas raíces son" << raiz1 << " y " << raiz2; } Flujo de control: (1,2,3,4,5,6,7,8,9,10,11).
89
Estructuras de Control Introducción →
¿Es a distinto de cero?
90
Estructuras de Control Estructura condicional →
II.2.
91
Estructura condicional
– Si. Entonces sigue la ejecución. – No. Entonces salta las sentencias 7–11 y termina.
#include <iostream> #include <cmath> using namespace std; int main(){ double a, b, c; // Parámetros de la ecuación double raiz1, raiz2; // Raíces obtenidas double radical, denominador; cout << "\nIntroduce coeficiente de 2 o grado: "; cin >> a; cout << "\nIntroduce coeficiente de 1er grado: "; cin >> b; cout << "\nIntroduce coeficiente independiente: "; cin >> c; if (a!=0) { denominador = 2*a; radical = sqrt( b*b-4*a*c ); raiz1 = (-b + radical) / denominador; raiz2 = (-b - radical) / denominador; cout << "\nLas raíces son" << raiz1 << " y " << raiz2; } }
II.2.1.
Introducción
Una condición (condition) es una expresión lógica. Una ejecución condicional (conditional execution) consiste en la ejecución de una (o más) sentencia(s) dependiendo de la evaluación de una condición. Una estructura condicional (conditional structure) es una estructura que permite una ejecución condicional. También se les denomina sentencia condicional (conditional statement) ya que todo el bloque de la estructura forma una sentencia. Existen tres tipos: Simple, Doble y Múltiple.
II.2.2.
Estructura condicional simple if (<condición>) <bloque if>
<condición> es una expresión lógica <bloque if> es el bloque que se ejecutará si la expresión lógica se evalúa a true. Nota. Los paréntesis sobre la condición son obligatorios.
Estructuras de Control Estructura condicional â&#x2020;&#x2019; Estructura condicional simple
92
Una notaciĂłn para describir algoritmos: Diagramas de ďŹ&#x201A;ujo (ďŹ&#x201A;owchart)
Estructuras de Control Estructura condicional â&#x2020;&#x2019; Estructura condicional simple
Ejemplo. RaĂces de una ecuaciĂłn de segundo grado
SĂmbolos bĂĄsicos
INICIO
Simbolo terminal
93
Estructura secuencial
true <CONDICION> FIN
Operacion de E/S
false
Direccion del flujo
Bifurcador de flujo
Delimitadores
! "# $ % !" & $ ' % !" " $ ' ( ) % , %
-
Estructuras de Control Estructura condicional → Estructura condicional simple
94
Ejemplo. Leer la edad de una persona e imprimir "Es mayor de edad" en el caso de que su edad sea mayor o igual que 18.
int edad; cin >> edad; if (edad >= 18) cout << "\nEs mayor de edad"; Ejemplo. Comprobad si es menor de edad o es mayor de 65, pero en cualquier caso con unos ingresos menores a 40000
int edad, ingresos; edad = 20; ingresos = 23000; if (edad<18 || edad>=65 && ingresos<40000) // :-( cout << "\nContribuyente de especial tratamiento"; Precedencia de operadores:
if ( (edad<18 || edad>=65) && ingresos<40000) // :-) cout << "\nContribuyente de especial tratamiento";
Estructuras de Control Estructura condicional → Estructura condicional simple
95
Si hay varias sentencias, es necesario encerrarlas entre llaves. Dicho de otra forma: si no ponemos llaves, el compilador entiende que la única sentencia del bloque if es la que hay justo debajo.
if (a!=0) denominador = 2*a; radical = sqrt( b*b-4*a*c ); raiz1 = (-b + radical) / denominador; raiz2 = (-b - radical) / denominador; cout << "\nLas raíces son" << raiz1 << " y " << raiz2; Para el compilador es como si fuese:
if (a!=0){ denominador = 2*a; } radical = sqrt( b*b-4*a*c ); raiz1 = (-b + radical) / denominador; raiz2 = (-b - radical) / denominador; cout << "\nLas raíces son" << raiz1 << " y " << raiz2;
Estructuras de Control Estructura condicional â&#x2020;&#x2019; Estructura condicional simple
ÂżQuĂŠ pasa si a = 0 en la ecuaciĂłn de segundo grado?
96
Estructuras de Control Estructura condicional â&#x2020;&#x2019; Condicional doble
II.2.3.
97
Condicional doble
El algoritmo debe devolver â&#x2C6;&#x2019;c/b. Podemos hacer lo siguiente:
if (a!=0){ denominador = 2*a; radical = sqrt( b*b-4*a*c );
if <condiciĂłn> <bloque if>;
else
raiz1 = (-b + radical) / denominador; raiz2 = (-b - radical) / denominador;
<bloque else>;
cout << "\nLas raĂces son" << raiz1 << " y " << raiz2; }
if (a==0) cout << "\nTiene una Ăşnica raĂz" << -c/b; Problema: Se evalĂşan dos condiciones equivalentes. SoluciĂłn: Usar un condicional doble.
Estructuras de Control Estructura condicional â&#x2020;&#x2019; Condicional doble
98
Estructuras de Control Estructura condicional â&#x2020;&#x2019; Condicional doble
#include <iostream> #include <cmath> using namespace std; int main(){ double a, b, c; // ParĂĄmetros de la ecuaciĂłn double raiz1, raiz2; // RaĂces obtenidas double radical, denominador; cout << "\nIntroduce coeficiente de 2 o grado: "; cin >> a; cout << "\nIntroduce coeficiente de 1er grado: "; cin >> b; cout << "\nIntroduce coeficiente independiente: "; cin >> c;
% " ' ( . )% ///
if (a!=0) { denominador = 2*a; radical = sqrt( b*b-4*a*c );
! "# $
raiz1 = (-b + radical) / denominador; raiz2 = (-b - radical) / denominador;
% !" & $ '
cout << "\nLas raĂces son" << raiz1 << " y " << raiz2;
% !" " $ '
} else { raiz1 = -c/b; cout << "\nLa Ăşnica raĂz es " << raiz1;
( ) % , %
-
} }
99
Estructuras de Control Estructura condicional → Condicional doble
100
Ejemplo. Calculad el mayor de dos valores a y b.
Estructuras de Control Estructura condicional → Condicional doble
Ejemplo. El mayor de tres números a, b y c (primera aproximación). Algoritmo:
cin >> a; cin >> b;
Entradas: a, b y c Salidas: El mayor entre a, b y c
if (a >= b) max = a; else max = b;
Descripción:
Si a es mayor que los otros, el mayor es a Si b es mayor que los otros, el mayor es b Si c es mayor que los otros, el mayor es c
cout << "\nEl mayor es " << max;
Implementación:
cin >> a; cin >> b;
Línea en blanco if (a>=b) max = a; else max = b;
Tabulador (recomendación: 3 espacios)
if (a>=b) max = a; else max = b;
Compila correctamente
:-(
PERO Suspenso automático
:-O
if ((a>=b) && (a>=c)) max = a; if ((b>=a) && (b>=c)) max = b; if ((c>=a) && (c>=b)) max = c;
101
Estructuras de Control Estructura condicional → Condicional doble
102
Estructuras de Control Estructura condicional → Anidamiento de estructuras condicionales
II.2.4. (a>=b) y (a>=c)
max = a
false
cout << max
Dentro de un bloque if (else), puede incluirse otra estructura condicional, anidándose tanto como permita el compilador. ¿Cuándo se ejecuta cada instrucción?
true
max = b
false
(c>=a) y (c>=b)
Anidamiento de estructuras condicionales
true
false
(b>=a) y (b>=c)
103
true
max = c
if (condic_1) { inst_1; if (condic_2) { inst_2; } else { inst_3; } inst_4; } else { inst_5; }
condic_1
condic_2
inst_1
true
independiente
inst_2
true
true
inst_3
true
false
inst_4
true
independiente
inst_5
false
independiente
Estructuras de Control Estructura condicional → Anidamiento de estructuras condicionales
104
Estructuras de Control Estructura condicional → Anidamiento de estructuras condicionales
105
Ejemplo. El mayor de tres números (segunda aproximación) Algoritmo:
true
a>=b
false
Entradas y Salidas: idem Descripción:
Si a es mayor que b, entonces Calcular el máximo entre a y c En otro caso, Calcular el máximo entre b y c Implementación:
if (a>=b) if (a>=c) max = a; else max = c; else if (b>=c) max = b; else max = c;
true
max = a
a>=c
false
true
max = c
max = b
cout << max
b>=c
false
max = c
Estructuras de Control Estructura condicional → Anidamiento de estructuras condicionales
106
Estructuras de Control Estructura condicional → Anidamiento de estructuras condicionales
107
Ejemplo. El mayor de tres números (tercera aproximación) Algoritmo:
false
Entradas y Salidas: idem
a>=b
true
Descripción e Implementación:
/* Calcular el máximo (max) entre a y b. Calcular el máximo entre max y c. */ if (a>=b) max = a; else max = b; if (c>max) max = c;
max = b
max = a
c>max false
cout << max
true
max = c
Estructuras de Control Estructura condicional → Anidamiento de estructuras condicionales
108
¿Con qué algoritmo nos quedamos? Supongamos que todos los valores son distintos La solución 1 siempre evalúa 3 condiciones y realiza 1 asignación. La solución 2 siempre evalúa 2 condiciones y realiza 1 asignación. La solución 3 siempre evalúa 2 condiciones y realiza 1 ó 2 asignaciones. La solución 2 realiza alguna evaluación menos, pero la solución 3 es muchísimo mejor: Es mucho más fácil de entender No repite código Es mucho más fácil de extender a varios valores.
Estructuras de Control Estructura condicional → Anidamiento de estructuras condicionales
El mayor de cuatro números con la segunda aproximación:
if (a>=b) if (a>=c) if (a>=d) max = a; else max = d; else if (c>=d) max = c; else max = d; else if (b>=c) if (b>=d) max = b; else max = d; else if (c>=d) max = c; else max = d;
109
Estructuras de Control Estructura condicional → Anidamiento de estructuras condicionales
El mayor de cuatro números con la tercera aproximación:
110
Estructuras de Control Estructura condicional → Anidamiento de estructuras condicionales
111
¿Cómo describimos un algoritmo?
Algoritmo: Entradas y Salidas: idem Descripción e Implementación:
Descripción de un algoritmo: Se trata de describir la idea principal del algoritmo, de forma concisa y esquemática, sin entrar en detalles innecesarios.
/* Calcular el máximo (max) entre a y b. Calcular el máximo entre max y c. Calcular el máximo entre max y d. */
if (a>=b) max = a; else max = b;
if (a>=b) max = a; else max = b;
Una lamentable descripción del algoritmo:
if (c>max) max = c; if (d>max) max = d;
if (c>max) max = c;
Compruebo si a>=b. En ese caso, lo que hago es asignarle a la variable max el valor a y si no le asigno el otro valor, b. Una vez hecho esto, paso a comprobar si el otro valor, es decir, c, es mayor que max (la variable anterior), en cuyo caso le asigno a max el valor c y si no, no hago nada. El colmo:
En general:
Calcular el máximo (max) entre a y b. Para cada uno de los valores restantes, calcular el máximo entre max y dicho valor.
Compruevo si a>=b y en ese caso lo que ago es asignarle a la variable max el valor a y sino le asigno el otro valor b una vez echo esto paso a comprovar si el otro valor es decir c es mayor que max (la variable anterior) en cuyo caso le asigno a max el valor c y sino no ago nada
Estructuras de Control Estructura condicional → Anidamiento de estructuras condicionales
Nunca parafrasearemos el código
/* Si a es >= b, asignamos a max el valor a En otro caso, le asignamos b. Una vez hecho lo anterior, vemos si c es mayor que max, en cuyo caso le asignamos c */ if (a>=b) max = a; else max = b; if (c>max) max = c; Seremos esquemáticos (pocas palabras en cada línea)
/* Calcular el máximo entre a y b, y una vez hecho esto, pasamos a calcular el máximo entre el anterior, al que llamaremos max, y el nuevo valor c */ /* Calcular el máximo (max) entre a y b. Calcular el máximo entre max y c. */
112
Estructuras de Control Estructura condicional → Anidamiento de estructuras condicionales
113
Comentaremos un bloque completo La descripción del algoritmo la incluiremos antes de un bloque, pero nunca entre las líneas del código. Esto nos permite separar las dos partes y poder leerlas independientemente.
// Calcular el máximo entre a y b if (a>=b) max = a; else // En otro caso: max = b; // Calcular el máximo entre max y c if (c>max) max = c; También es válido incluir la descripción a la derecha del código, pero es mucho más difícil de mantener (al incluir líneas de código nuevas, se descompone todo el comentario):
if (a>=b) if (a>=c) max = a; else max = c;
// El comentario es correcto pero el sitio no: // Calcular el máximo entre a y c // Calcular el máximo entre max y c
Usad descripciones de algoritmos que sean concisas, pero claras y completas, con una presentación visual agradable y esquemática. No seguir estas normas baja puntos en el examen.
Estructuras de Control Estructura condicional → Anidamiento de estructuras condicionales
114
Estructuras de Control Estructura condicional → Anidamiento de estructuras condicionales
Ejemplo. Estructuras condicionales anidadas
Ejemplo. Estructuras condicionales anidadas (continuación)
// Programa que permite operar con dos números enteros #include <iostream> using namespace std;
Lo hacemos más eficientemente:
int main(){ int dato1, dato2; char opcion; cout << "\nIntroduce el primer operando: "; cin >> dato1; cout << "\nIntroduce el segundo operando: "; cin >> dato2; cout << "\nElija (S)Sumar, (R)Restar, (M)Multiplicar: "; cin >> opcion;
if (opcion == 'S') cout << "\nSuma = " << dato1+dato2; else if (opcion == 'R') cout << "\nResta = " << dato1-dato2; else if (opcion == 'M') cout << "\nMultiplicación = " << dato1*dato2; else cout << "\nNinguna operación";
Una forma también válida de tabular:
if (opcion cout << if (opcion cout << if (opcion cout << if (opcion cout << }
== 'S') "\nSuma = " << dato1+dato2; == 'R') "\nResta = " << dato1-dato2; == 'M') "\nMultiplicación = " << dato1*dato2; != 'R' && opcion != 'S' && opcion != 'M') "\nNinguna operación";
if (opcion == 'S') cout << "\nSuma = " << dato1+dato2; else if (opcion == 'R') cout << "\nResta = " << dato1-dato2; else if (opcion == 'M') cout << "\nMultiplicación = " << dato1*dato2; else cout << "\nNinguna operación"; }
115
Estructuras de Control Estructura condicional → Construcción de estructuras condicionales
II.2.5.
116
Construcción de estructuras condicionales
Estructuras de Control Estructura condicional → Construcción de estructuras condicionales
do porque no duplica código (el de las condiciones) y es, por tanto, menos propenso a errores de escritura o ante posibles cambios futuros. Un ejemplo en el que es mejor no anidar
II.2.5.1. ¿Anidar o no anidar? Un ejemplo en el que es mejor anidar Ejemplo. Reescribid el siguiente ejemplo utilizando únicamente estructuras condicionales no anidadas:
if (A>B) if (B<=C) if (C!=D) <S1>; else <S2>; else <S3>; else <S4>;
if (c1 && c2) bloque_A else bloque_B
Primer caso:
if (c1) if (c2) bloque_A else bloque_B else bloque_B bloque_A: bloque_B:
Segundo caso: ↓
if ((A>B) && (B<=C) && (C!=D)) <S1>; if ((A>B) && (B<=C) && (C==D)) <S2>; if ((A>B) && (B>C)) <S3>; if (A<=B) <S4>; Preferimos el primero ya que realiza menos comprobaciones, y sobre to-
117
bloque_A: bloque_B:
c1 true c2 true (c1&&c2) false: c1 true y c2 false c1 false y c2 true c1 false y c2 false c1 true c2 true c1 true y c2 false o bien c1 false
Estructuras de Control Estructura condicional → Construcción de estructuras condicionales
118
Son equivalentes. Elegimos la primera porque:
Muy importante: la segunda opción repite código (bloque_B) y por tanto es más propenso a errores de escritura o ante posibles cambios futuros. $
Los factores a tener en cuenta (entre otros) para anidar o no estructuras condicionales son: Que no se repita código. Que se aumente la legibilidad de éste.
&
119
II.2.5.2. Separación de entradas/salidas y cómputos
Al aumentar el anidamiento se va perdiendo en legibilidad y concisión.
'
Estructuras de Control Estructura condicional → Construcción de estructuras condicionales
int edad; cin >> edad; if (edad >= 18) cout << "\nEs mayor de edad"; '
Para favorecer la reutilización de código entre distintas plataformas (Linux, Windows, etc) los bloques que realizan entradas o salidas de datos (cin, cout) serán independientes de los bloques que realizan cómputos. &
%
$
%
int edad; bool es_mayor_edad; // Entrada de datos: cin >> edad; // Cómputos: es_mayor_edad = (edad>18); // Salida de resultados: if (es_mayor_edad == true) cout << "\nEs mayor de edad"; Esto me permite reutilizar el código es_mayor_edad = (edad>18); en otros sitios:
es_mayor_edad = (edad>18); if (es_mayor_edad == true) System::Windows::Forms::MessageBox::Show("Es mayor de edad");
Estructuras de Control Estructura condicional → Construcción de estructuras condicionales
120
Nota. Realmente, la reutilización del código no será a través de copiar/pegar sino encapsulando el código a reutilizar en una función, método de una clase, etc.
Sobre el uso de variables lógicas en los if:
if (es_mayor_edad == true) cout << "\nEs mayor de edad"; equivale a:
if (es_mayor_edad) cout << "\nEs mayor de edad"; Al igual que
if (es_mayor_edad==false) cout << "\nEs menor de edad"; equivale a
if (!es_mayor_edad) cout << "\nEs menor de edad"; Ejercicio. Utilizad una variable lógica contribuyente_especial y reescribid el código siguiente, separando E/S y Cómputos:
if ( (edad<18 || edad>=65) && ingresos<40000) // :-) cout << "\nContribuyente de especial tratamiento";
Estructuras de Control Estructura condicional → Construcción de estructuras condicionales
121
II.2.5.3. Simplificación de expresiones: Álgebra de Boole Debemos intentar simplificar las expresiones lógicas, para que sean fáciles de entender. Usaremos las leyes del Álgebra de Boole, muy similares a las conocidas en Matemáticas elementales como: −(a + b) = −a − b a(b + c) = ab + ac
!(A || B) !(A && B) A && (B || C) A || (B && C)
equivale equivale equivale equivale
a a a a
!A !A (A (A
&& || && ||
!B !B B) || (A && C) B) && (A || C)
Nota. Las dos primeras (relativas a la negación) se conocen como Leyes de Morgan. Las dos siguientes son la ley distributiva (de la conjunción con respecto a la disyunción y viceversa). Ampliación: Consultad:
http://es.wikipedia.org/wiki/Algebra_booleana http://serbal.pntic.mec.es/~cmunoz11/boole.pdf
Estructuras de Control Estructura condicional → Construcción de estructuras condicionales
122
Ejemplo. Es trabajador activo si su edad no está por debajo de 18 o por encima de 65.
bool es_activo; int edad; cin >> edad; es_activo = !(edad<18 || edad>65); if (es_activo) cout << "\nTrabajador en activo"; ......
Aplicando las leyes de Morgan:
!(edad<18 || edad>65) equivale a !(edad<18) && !(edad>65) equivale a (edad >= 18) && (edad <= 65) Es más fácil de entender el siguiente código:
es_activo = (edad >= 18) && (edad <= 65); if (es_activo) cout << "\nTrabajador en activo";
Estructuras de Control Estructura condicional → Construcción de estructuras condicionales
123
Ejemplo. Es posible beneficiario de ayudas si no es un trabajador activo. Además, ya sea menor de 18 años o mayor de 65, la unidad familiar a la que pertenezca no puede tener una base imponible mayor de 35000 euros.
bool beneficiario; ....... beneficiario = ((base_imponible <= 35000) && (edad < 18)) || ((base_imponible <= 35000) && (edad > 65)); Es mucho más fácil de entender lo siguiente (aplicando la lay distributiva):
beneficiario = (base_imponible <= 35000) && ( edad < 18 || edad > 65 ); Además, no repetimos código (base_imponible <= 35000)
Estructuras de Control Estructura condicional → Construcción de estructuras condicionales
124
II.2.5.4. Cuidado con la comparación entre reales La expresión 1.0e15 == (1.0/3.0)*3.0 podría evaluarse a false debido a la precisión finita para calcular (1.0/3.0) (0.333333333) Es más:
double descuento_base, porcentaje; descuento_base = 0.1; porcentaje = descuento_base * 100; if (porcentaje == 10) // Podría evaluarse a false :-O cout << "Descuento del 10%"; .... Recordad que la representación en coma flotante no es precisa y 0.1 será internamente un valor próximo a 1, pero no exacto. Soluciones: Fomentar condiciones de desigualdad Mejor: Fijar un error de precisión y aceptar igualdad cuando la diferencia de las cantidades sea menor que dicho error.
double real1, real2; const double epsilon = 0.00001; if ((real1-real2) < epsilon) cout << "Son iguales";
Estructuras de Control Estructura condicional → Construcción de estructuras condicionales
125
Ampliación: Como no es lo mismo comparar si 0.000001 está cerca de 0.000002, que comparar si 130.001 está cerca de 130.002, el método anterior hay que refinarlo. Para más información:
http://www.codeguru.com/forum/showthread.php?t=323835 http://www.cygnus-software.com:80/papers/comparingfloats/ comparingfloats.htm
Estructuras de Control Estructura condicional → Construcción de estructuras condicionales
126
Estructuras de Control Estructura condicional → Estructura condicional múltiple
II.2.5.5. Evaluación en ciclo corto y en ciclo largo
II.2.6.
Evaluación en ciclo corto (Short-circuit evaluation): El compilador optimiza la evaluación de expresiones lógicas evaluando sus términos de izquierda a derecha hasta que sabe el resultado de la expresión completa (lo que significa que puede dejar términos sin evaluar). La mayoría de los compiladores realizan este tipo de evaluación por defecto.
Recordemos el ejemplo de lectura de dos enteros y una opción:
Evaluación en ciclo largo (Eager evaluation): El compilador evalúa todos los términos de la expresión lógica para conocer el resultado de la expresión completa. Ejemplo.
if ( (a>0) && (b<0) && (c<1) ) ... Supongamos a = -3. Ciclo largo: El compilador evalúa (innecesariamente) todas las expresiones lógicas Ciclo corto: El compilador evalúa sólo la primera expresión lógica. Por lo tanto, pondremos primero aquellas condiciones que sean más probables de evaluarse como false Nota. Lo mismo pasa cuando el operador es ||
Estructura condicional múltiple
int dato1, dato2; char opcion; cout << "\nIntroduce el primer operando: "; cin >> dato1; cout << "\nIntroduce el segundo operando: "; cin >> dato2; cout << "\nSelecciona (S) sumar, (R) restar, " << "(M) multiplicar: "; cin >> opcion; if (opcion == 'S') cout << "\nSuma = " << dato1+dato2; else if (opcion == 'R') cout << "\nResta = " << dato1-dato2; else if (opcion == 'M') cout << "\nMultiplicación = " << dato1*dato2; else cout << "\nNinguna operación";
127
Estructuras de Control Estructura condicional → Estructura condicional múltiple
128
Estructuras de Control Estructura condicional → Estructura condicional múltiple
Se usa frecuentemente en la construcción de menús:
switch (<expresión>) { case <constante1>: <sentencias1>
break; case <constante2>:
#include <iostream> using namespace std; int main(){ int dato1,dato2; char opcion;
<sentencias2>
cout << "\nIntroduce el primer operando: "; cin >> dato1; cout << "\nIntroduce el segundo operando: "; cin >> dato2; cout << "\nSelecciona (S) sumar, (R) restar: "; cin >> opcion;
break; ......................... [default: <sentencias>] }
switch (opcion) { case 'S': cout << break; case 'R': cout << break; case 'M': cout << break; default: cout << }
<expresión> es un expresión entera. <constante i> ó <constante2> es un literal entero. switch sólo comprueba la igualdad. No debe haber dos cases con la misma <constante> en el mismo switch. Si esto ocurre, sólo se ejecutan las sentencias del caso que aparezca primero. El identificador especial default permite incluir un caso por defecto, que se ejecutará si no se cumple ningún otro. Se suele colocar como el último de los casos.
}
"\nSuma = " << dato1+dato2; "\nResta = " << dato1-dato2; "\nMultiplicación = " << dato1*dato2;
"\nNinguna operación";
129
Estructuras de Control Estructura condicional → Estructura condicional múltiple
130
El gran problema con la estructura switch es que el programador olvidará en más de una ocasión, incluir la sentencia break. La única ventaja es que se pueden realizar las mismas operaciones para un grupo determinado de constantes:
switch (opcion) { case 's': case 'S': cout << "\nSuma = " << dato1+dato2; break; case 'r': case 'R': cout << "\nResta = " << dato1-dato2; break; case 'm': case 'M': cout << "\nMultiplicación = " << dato1*dato2; break; default: cout << "\nNinguna operación"; break; // Ponemos break; aunque no sea necesario }
Estructuras de Control Estructura condicional → Estructura condicional múltiple
131
Pero para conseguir ese objetivo, la siguiente solución sería mucho mejor:
opcion = toupper(opcion); switch (opcion) { case 'S': cout << "\nSuma = " << dato1+dato2; break; case 'R': cout << "\nResta = " << dato1-dato2; break; case 'M': cout << "\nMultiplicación = " << dato1*dato2; break; default: cout << "\nNinguna operación"; break; // Ponemos break; aunque no sea necesario } Y para no tener errores lógicos si se nos olvida incluir break, podemos volver a lo que ya sabemos:
opcion = toupper(opcion); if (opcion == 'S') cout << "\nSuma = " << dato1+dato2; else if (opcion == 'R') cout << "\nResta = " << dato1-dato2; else if (opcion == 'M') cout << "\nMultiplicación = " << dato1*dato2; else cout << "\nNinguna operación";
Estructuras de Control Estructura condicional → Estructura condicional múltiple '
Consejo:
&
132
Estructuras de Control Estructuras repetitivas →
133
$
Sólo para C++ → En la medida de lo posible, evitad el uso de la estructura switch por los errores lógicos producidos al olvidarnos incluir algún break
II.3. %
Estructuras repetitivas
Una estructura repetitiva (iteration/loop) (también conocidas como bucles, ciclos o lazos) permite la ejecución de una secuencia de sentencias: o bien, hasta que se satisface una determinada condición → Bucle controlado por condición (Condition-controlled loops) o bien, un número determinado de veces → Bucle controlado por contador (Counter controlled loop)
II.3.1.
Bucles controlados por condición: pre-test y post-test
II.3.1.1. Formato
Pre-test
Post-test
while (<condición>) {
do {
<cuerpo bucle> }
<cuerpo bucle> } while (<condición>);
Funcionamiento: En ambos, se va ejecutando el cuerpo del bucle mientras la condición sea verdad. En un bucle while, primero se pregunta y luego (en su caso) se ejecuta. En un bucle do while, primero se ejecuta y luego se pregunta. Cada vez que se ejecuta el cuerpo del bucle diremos que se ha producido una iteración (iteration)
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
134
(Post−test)
(Pre−test)
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
135
II.3.1.2. Algunos usos de los bucles Ejemplo. Crear un filtro (filter) de entrada de datos: Leer un valor y no permitir al usuario que lo introduzca fuera de un rango determinado.
// Introducir un único valor, pero positivo. // Imprimir el coseno.
false
<cuerpo bucle>
#include <iostream> #include <cmath> using namespace std;
<condicion>
true
int main(){ int valor;
<condicion>
true do{ cout << "\nIntroduzca un valor positivo: "; cin >> valor; }while (valor < 0);
<cuerpo bucle> false
cout << cos(valor); } Nota. El filtro anterior no nos evita todos los posibles errores. Por ejemplo, si se introduce un valor demasiado grande, se produce un desbordamiento y el resultado almacenado en valor es impredecible
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
136
Elección Pre-test, o post-test: ¿Quiero que siempre se ejecute al menos una vez el cuerpo?
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
137
O bien:
num_lineas = 1;
Ejemplo. Escribir 20 líneas con 5 estrellas cada una.
while (num_lineas <= 20){ cout << '\n' << "*****" ; num_lineas = num_lineas + 1; }
num_lineas = 1; do{ cout << '\n' << "*****" ; num_lineas = num_lineas + 1; }while (num_lineas <= 20);
// nuevo = antiguo + 1
Preferible:
num_lineas = 0;
// Llevo 0 líneas impresas
O bien:
num_lineas = 0; do{ cout << '\n' << "*****" ; num_lineas = num_lineas + 1; }while (num_lineas < 20);
while (num_lineas < 20){ cout << '\n' << "*****" ; num_lineas = num_lineas + 1; } Dentro del bucle, JUSTO antes de comprobar la condición, la variable total contiene el número de líneas que hay impresas :-) Nota. Podríamos usar el operador de incremento:
while (num_lineas < 20){ cout << '\n' << "*****" ; num_lineas++; }
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test '
Consejo:
138 $
Fomentad el uso de los bucles pre-test. Casi siempre habrá algún caso en el que no queramos ejecutar el cuerpo del bucle ni siquiera una vez.
&
%
Ejercicio. Leer un número positivo tope e imprimir tope líneas con 5 estrellas cada una.
cout << "\n¾Cuántas líneas de asteriscos quiere imprimir? "; cin >> tope;
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
Ejemplo. Leed un entero tope y escribir los pares ≤ tope
cin >> tope; par = 0; while (par <= tope){ par = par + 2; cout << par; } '
Consejo:
num_lineas = 0; do{ cout << '\n' << "*****" ; num_lineas++; }while (num_lineas < tope); Problema: ¿Qué ocurre si tope = 0? Proponer una solución:
139
$
En el diseño de los bucles siempre hay que comprobar el correcto funcionamiento en los casos extremos (primera y última iteración)
&
%
Al final, escribe uno más. Cambiamos el orden de las instrucciones:
cin >> tope; par = 0; while (par <= tope){ cout << par; par = par + 2; }
// Primer candidato // ¾Es bueno? // Si => imprímelo // calcular nuevo // candidato // No => Salir
Si no queremos que salga 0, cambiaríamos la inicialización:
par = 2
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
Ejemplo. Calculad el número de dígitos que tiene un entero
57102 45
-> 5 dígitos -> 2 dígitos
Algoritmo: Entradas: n Salidas: num_digitos Descripción e implementación:
140
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
II.3.1.3. Lectura Anticipada Ejemplo. Realizad un programa que sume una serie de valores leídos desde teclado, hasta que se lea el valor -1 (terminador = -1)
#include<iostream> using namespace std; int main(){ int suma, numero;
Ir dividiendo n por 10 hasta llegar a una cifra El número de dígitos será el número de iteraciones
suma = 0;
num_digitos = 1;
do{ cin >> numero; suma = suma + numero; }while (numero != -1);
while (n > 9){ n = n/10; num_digitos++; } Mejor si no modificamos la variable original:
n_copia = n; num_digitos = 1; while (n_copia > 9){ n_copia = n_copia/10; num_digitos++; }
141
cout << "\nLa suma es " << suma; } Problema: Procesa el -1 y lo suma.
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
142
Solución:
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
Otra posible solución, usando variables lógicas:
do{ cin >> numero; if (numero != -1) suma = suma + numero; }while (numero != -1); Funciona, pero evalúa dos veces la misma condición, lo cual es ineficiente y, mucho peor, duplica código.
bool seguir_leyendo; suma = 0; seguir_leyendo = true; while (seguir_leyendo){ cin >> numero; if (numero == -1) seguir_leyendo = false; else{ seguir_leyendo = true; suma = suma + numero; }
Recordad: Evitad el uso de bucles do while Solución: técnica de lectura anticipada. Leemos el primer valor con cin antes de entrar al bucle y comprobamos si hay que procesarlo (el primer valor podría ser ya el terminador!)
suma = 0; cin >> numero;
// Primer candidato
} O mejor aún:
while (numero != -1) { suma = suma + numero; cin >> numero; }
// ¾Es bueno? // Leer siguiente candidato
seguir_leyendo = true; while (seguir_leyendo){ cin >> numero;
cout << "\nLa suma es " << suma; }
seguir_leyendo = (numero != -1);
Nota. La primera vez que entra al bucle, la instrucción while (numero != -1) hace las veces de un condicional.
if (seguir_leyendo) suma = suma + numero; }
143
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
144
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
Otro ejemplo de lectura anticipada:
// Programa que va leyendo números enteros hasta que se // lea el cero. Imprimir el número de pares e impares leídos. #include <iostream> using namespace std; int main(){ int ContPar, ContImpar, valor; ContPar=0; ContImpar=0; cout << "\nIntroduce valor: "; cin >> valor; while ( if (
Línea en blanco antes while (valor != 0){ if (valor % 2 == 0) ContPar = ContPar + 1; else ContImpar = ContImpar + 1; cout << "Introduce ... cin >> valor;
) { )
} ;
else ; cout << "\nIntroduce valor: "; cin >> valor; } cout << "\nFueron " << ContPar << " pares y " << ContImpar << " impares"; }
cin >> valor;
Línea en blanco después cout << "Fueron " ....
145
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
146
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
147
Ejemplo. Leer dos valores desde el teclado forzando a que ambos sean distintos de cero.
II.3.1.4. Condiciones compuestas Es normal que necesitemos comprobar más de una condición en un bucle. Dependiendo del algoritmo necesitaremos conectarlas con && o con ||. Ejemplo. Leer una opción de un menú. Sólo se admite s ó n.
char opcion; do{ cout << "¾Desea formatear el disco?"; cin >> opcion; }while ( opcion!='s' opcion!='S' opcion!='n' opcion!='N' ); ¿Cuándo quiero salir del bucle? Cuando cualquiera de las condiciones sea false.
do{ cout << "\nIntroduzca un numero: "; cin >> dato1; cout << "\nIntroduzca otro numero: "; cin >> dato2; }while (dato1==0 dato2==0);
¿Cuándo quiero salir del bucle? Cuando ambas condiciones, simultáneamente, sean false. En cualquier otro caso, entro de nuevo al bucle. ¿Cual es el operador que cumple lo siguiente?:
false true
Operador Operador
false = false <lo que sea> =
true
¿Cual es el operador que cumple lo siguiente?:
false
Operador
<lo que sea>
=
false
Nota. En este ejemplo, mucho mejor si pasamos la opción a mayúscula dentro del bucle y comprobamos únicamente las condiciones con los caracteres en mayúscula
En cualquier caso, hubiese sido mucho mejor leer cada valor en un bucle independiente. De esa forma, si nos equivocamos al introducir uno pero no el otro, sólo tendremos que volver a leer el valor incorrecto. Hacedlo en la casa. '
Consejo:
&
$
En la construcción de condiciones compuestas, empezad planteando las condiciones simples que la forman. Pensad cuando queremos salir del bucle y conectad adecuadamente dichas condiciones simples, dependiendo de si nos queremos salir cuando todas simultáneamente sean false (OR) o cuando cualquiera de ellas sea false (AND)
%
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
Ejemplo. Calcular el máximo común divisor de dos números a y b. Algoritmo: Entradas: Los dos enteros a y b Salidas: el entero max_com_div, máximo común divisor de a y b Descripción e Implementación:
/* Empezar como primer posible divisor con el menor de ambos números Mientras divisor no divida a ambos, probar con el divisor anterior */
divisor = menor;
max_com_div = divisor;
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
(b % divisor != 0))
149
El uso de una variable lógica hará que el algoritmo sea más fácil de entender:
mcd_encontrado = false; while (!mcd_encontrado){ if ((a%divisor == 0) (b%divisor == 0)) mcd_encontrado = true; else divisor--; } max_com_div = divisor; ¿Qué pasaría si quitásemos el else?
if (b<a) menor = b; else menor = a;
while ((a % divisor != 0) divisor --;
148
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
150
Ejemplo. Encontrar el primer divisor propio impar de un número.
Recorrer los impares menores que el número hasta encontrar un divisor o hasta llegar a dicho número
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
151
Repetimos evaluación de condiciones equivalentes. Solución: Usamos variables lógicas:
int impar; cin >> n; impar = 3; while (n % impar != 0 impar = impar + 2;
&&
n > impar)
Una vez terminado el bucle, ¿cómo comprobamos cual fue la condición que nos hizo salir?
if (n % impar == 0) cout << "\nPrimer divisor impar: " << impar; else cout << "\n" << n << " no tiene divisores impares propios"; ¿Y si el bucle terminó por la otra condición (n es un primo igual a impar)? En este caso n % impar sería también cero, pero no queremos imprimir el mismo n como un divisor suyo.
impar = 3; while (n % impar != 0 impar = impar+2;
&&
n > impar)
Nota. Podríamos salir del bucle en
Consejo:
if (impar < n && n % impar == 0) cout << "\nPrimer divisor impar: " << impar; else cout << "\n" << n << " no tiene divisores impares propios";
$
'
&
Fomentad el uso de variables lógicas para controlar las condiciones de salida de los bucles. Usaremos tantas variables lógicas como condiciones necesitemos controlar, y las conectaremos apropiadamente con los operadores lógicos. %
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
152
Ejemplo. Leer números enteros de la entrada por defecto hasta que se introduzcan 10 ó hasta que se introduzca un número negativo. Imprimir la media aritmética.
suma = 0; cin >> valor; total_introducidos = 1; while ((valor >= 0) && (total_introducidos <= 10)){ suma = suma + valor; total_introducidos++; cin >> valor; } media = suma/total_introducidos; Problema: Lee el undécimo y se sale, pero ha tenido que leer dicho valor. Solución: O bien cambiamos la inicialización de total_introducidos a 0, o bien leemos hasta 9. En cualquier caso, el décimo valor hay que procesarlo fuera del bucle.
....... while ((valor >= 0) && (total_introducidos < 10)){ suma = suma + valor; total_introducidos++; cin >> valor; } suma = suma + valor; media = suma/total_introducidos;
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
153
Problema: Si se ha salido con un negativo lo suma. Además, total_introducidos también se incrementa en uno con el valor negativo, por lo que habría que tenerlo en cuenta a la hora de hallar la media aritmética. Quedaría:
suma = 0; cin >> valor; total_introducidos = 1; while ((valor >= 0) && (total_introducidos<10)){ suma = suma + valor; total_introducidos++; cin >> valor; } if (total_introducidos == 10) suma = suma + valor; else total_introducidos--; media = suma/total_introducidos; ¿Pero y si el último leído es negativo?
if ((total_introducidos == 10) && (valor >=0)) suma = suma + valor; else total_introducidos--; Podemos observar la complejidad (por no decir chapucería) innecesaria que ha alcanzado el programa.
Estructuras de Control Estructuras repetitivas â&#x2020;&#x2019; Bucles controlados por condiciĂłn: pre-test y post-test
Consejo:
154
Construid los bucles de forma que no haya que arreglar nada despuĂŠs de su ďŹ nalizaciĂłn
Replanteamos desde el inicio la soluciĂłn y usamos mejor variables lĂłgicas:
Estructuras de Control Estructuras repetitivas â&#x2020;&#x2019; Bucles controlados por condiciĂłn: pre-test y post-test
155
Bajemos puntos en el examen. El algoritmo es el mismo, pero observad los nombres de variables:
aux = 0; contador = 0; seguir_1 = true; seguir_2 = true; while (seguir_1 && seguir_2){ cin >> v; if (v < 0) seguir_1 = false; else{ aux = aux + v; contador++; if (contador == 10) seguir_2 = false; } } resultado = aux/(1.0*contador); if (contador == 0) cout << "\nNo se introdujeron valores"; else cout << "\nMedia aritmĂŠtica = " << resultado;
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
156
Algunas citas sobre la importancia de escribir código que sea fácil de entender: "Any fool can write code that a computer can understand. Good programmers write code that humans can understand". Martin Fowler "Programs must be written for people to read, and only incidentally for machines to execute". Abelson & Sussman "Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live".
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
157
Ejemplo. Comprobar si un número es primo.
#include <iostream> #include <cmath> using namespace std; int main(){ int valor, divisor; bool es_primo; cout << "Introduzca un numero natural: "; cin >> valor; es_primo = true;
Un principio de programación transversal:
divisor = 2 Principio de Programación:
while (divisor < valor){ if (valor % divisor == 0) es_primo = false;
Sencillez (Simplicity) Fomentad siempre la sencillez y la legibilidad en la escritura de código
divisor++; } "There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult.". C.A.R. Hoare
if (es_primo) cout << valor << " es primo\n"; else cout << valor << " no es primo\n"; } Para hacerlo más eficiente, nos salimos en cuanto sepamos que no es primo. Nos salimos del bucle con un bool. La misma variable es_primo nos sirve:
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
158
es_primo = true; divisor = 2
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
159
Ampliación: Cuando valor es un entero muy grande, el algoritmo anterior no termina en un tiempo razonable. ¿Qué es un número grande?
while (divisor < valor && es_primo){ if (valor % divisor == 0) es_primo = false; else divisor++; } Incluso podríamos quedarnos en sqrt(valor), ya que si valor no es primo, tiene al menos un divisor menor que sqrt(valor). En cualquier caso, sqrt es una operación costosa y habría que evaluarlo empíricamente.
es_primo = true; tope = sqrt(valor); divisor = 2 while (divisor < tope && es_primo){ if (valor % divisor == 0) es_primo = false; else divisor++; } Nota. Realmente, para que compile sin problemas debemos poner tope = sqrt(1.0*valor);
http://www.naturalnumbers.org/bignum.html ¿Qué significa tiempo razonable? Está fuera del alcance del curso e implica nociones más profundas sobre complejidad computacional Para una introducción informal pero clara:
http://www.claymath.org/Popular_Lectures/Minesweeper/ El problema teórico más importante en Informática es determinar si existen problemas que no pueden resolverse en un tiempo razonable con un algoritmo (aunque sí exista un algoritmo que compruebe rápidamente si una propuesta es una solución o no). Este problema está aún sin resolver y es uno de los siete problemas del milenio, recompensado con 1 millón de dólares por el Instituto Clay de Matemáticas. En Agosto de 2010 un científico de HP proclamó haberlo demostrado, pero en pocas semanas se encontraron fallos en la demostración. El primer algoritmo para determinar en un tiempo razonable si un número es primo, es bastante reciente (2002):
Wikipedia -> AKS primality test Sobre números primos en general:
http://primes.utm.edu/
Estructuras de Control Estructuras repetitivas → Bucles controlados por condición: pre-test y post-test
II.3.1.5. Bucles sin fin
160
Estructuras de Control Estructuras repetitivas → Bucles controlador por contador
II.3.2.
161
Bucles controlador por contador
Ejercicio. ¿Cuantas iteraciones se producen? II.3.2.1. Motivación
contador = 2; while (contador < 3) { contador--; cout << '\n' << contador; }
contador = 1;
Se utilizan para repetir un conjunto de sentencias un número de veces fijado de antemano. Se necesita una variable contadora, un valor inicial, un valor final y un incremento. Ejemplo. Hallar la media aritmética de cinco enteros leídos desde teclado.
int main(){ int contador, valor, suma, inicio, final; double media; inicio = 1; final = 5; suma = 0; contador = inicio;
while (contador != 10) { contador = contador + 2; }
while (contador <= final){ cout << "\nIntroduce un número "; cin >> valor; suma = suma + valor;
Fomentad el uso de condiciones de desigualdad.
contador = 1; while (contador <= 10) { contador = contador + 2; }
contador = contador + 1; } media = suma / (final *1.0); cout << "\nLa media es " << media; }
Estructuras de Control Estructuras repetitivas â&#x2020;&#x2019; Bucles controlador por contador
162
Estructuras de Control Estructuras repetitivas â&#x2020;&#x2019; Bucles controlador por contador
163
II.3.2.2. Formato
La sentencia for permite la construcciĂłn de una forma compacta de los ciclos controlados por contador, aumentando la legibilidad del cĂłdigo.
int main(){ int contador, valor, suma, inicio, final; double media; inicio = 1; final = 5; suma = 0;
for (contador = inicio ; contador <= final ; contador = contador + 1){ cout << "\nIntroduce un nĂşmero "; cin >> valor; suma = suma + valor; }
media = suma / (final*1.0); cout << "\nLa media es " << media; }
contador es la variable controladora del ciclo. inicio es el valor inicial que toma la variable controladora. final es el valor ďŹ nal que toma la variable controladora.
Estructuras de Control Estructuras repetitivas â&#x2020;&#x2019; Bucles controlador por contador
164
for (contador = inicio ; contador <= final ; contador = contador + 1){ cout << "\nIntroduce un nĂşmero "; cin >> valor; suma = suma + valor; }
Estructuras de Control Estructuras repetitivas â&#x2020;&#x2019; Bucles controlador por contador
165
for ([<contador = valor inicial>] ; [<condiciĂłn sobre contador>]; [<actualizaciĂłn del contador>]) <cuerpo del bucle>
contador = inicio es la asignaciĂłn inicial de la variable contadora. Si usamos como condiciĂłn
contador < final se producirĂĄ una iteraciĂłn menos. contador = contador + 1 aumenta en 1 el valor de contador en cada iteraciĂłn. Por abreviar, suele usarse contador++ en vez de contador = contador + 1
for (contador = inicio ; contador <= final ; contador++) Podemos usar cualquier otro incremento:
contador--; contador = contador - 4; En el caso de incrementos negativos, la condiciĂłn de terminaciĂłn del bucle tendrĂĄ que ser del tipo: contador >= final
Estructuras de Control Estructuras repetitivas → Bucles controlador por contador
Encuentre errores en este código:
For {x = 100, x>=1, x++} cout << x << " ";
166
Estructuras de Control Estructuras repetitivas → Bucles controlador por contador
167
Ejemplo. Imprimir 9 veces el mensaje Hola
int i; for (i = 1; i <= 9; i++) cout << "\nHola";
Ejemplo. ¿Cuántos pares hay en [−10, 10]?
int candidato, num_pares; num_pares = 0; for(candidato = -10; candidato <= 10; candidato++) { if (candidato % 2 == 0) num_pares++; } cout << "\nHay " << num_pares << " pares"; Este problema también se podría haber resuelto como sigue:
int num_pares, par; for (par = -10; par <= 10; par = par+2) num_pares++; cout << "\nHay " << num_pares << " pares";
¿Con qué valor sale la variable i? 10 Cuando termina un bucle for, la variable contadora se queda con el primer valor que hace que la condición del bucle sea falsa. ¿Cuántas iteraciones se producen en un for? Si incremento = 1, inicio es menor que final y la condición es contador <= final final - inicio + 1 Si incremento = 1, inicio es menor que final y la condición es contador < final final - inicio
for (i = 0; i < 9; i++) cout << "\nHola"; for (i = 9; i > 0; i--) cout << "\nHola";
Estructuras de Control Estructuras repetitivas → Bucles controlador por contador
168
Número de iteraciones con incrementos cualesquiera. Si es del tipo contador < final, tenemos que contar cuantos intervalos de longitud igual a incremento hay entre los valores inicio y final. Como es un menor estricto, debemos quedarnos justo en el elemento anterior al final, es decir, final-1. El número de intervalos será
(final - 1 - inicio) / incremento
Estructuras de Control Estructuras repetitivas → Bucles controlador por contador
Ejercicio. ¿Qué salida producen los siguientes trozos de código?
int i, total; total = 0; for (i = 1 ; i <= 10; i++) total = total + 3; cout << total;
Y el número de iteraciones será
(final - 1 - inicio) / incremento + 1 Si el bucle es del tipo contador <= final, el número de iteraciones sería:
(final - inicio) / incremento + 1
total = 0; for (i = 5 ; i <= 36 ; i = i+4) total++;
total = 0; i = 5; while (i <= 36){ total++; i = i + 4; }
169
Estructuras de Control Estructuras repetitivas → Anidamiento de bucles
II.3.3.
170
171
Ejemplo. Imprimir la tabla de multiplicar de los TOPE primeros números.
Anidamiento de bucles
Dos bucles se encuentran anidados, cuando uno de ellos está en el bloque de sentencias del otro. En principio no existe límite de anidamiento, y la única restricción que se debe satisfacer es que deben estar completamente inscritos unos dentro de otros.
ANIDAMIENTO PERMITIDO
Estructuras de Control Estructuras repetitivas → Anidamiento de bucles
#include <iostream> using namespace std; int main(){ const int TOPE = 3; int i, j; for (i = 1 ; i <= TOPE ; i++) { for (j = 1 ; j <= TOPE ; j++) cout << i << "*" << j << "=" << i*j << " cout << "\n";
ANIDAMIENTO NO PERMITIDO
";
} }
j=1 Salida:
j=2
j=3
i=1 1*1 = 1
1*2 = 2
1*3 = 3
i=2 2*1 = 2
2*2 = 4
2*3 = 6
i=3 3*1 = 3
3*2 = 6
3*3 = 9
Estructuras de Control Estructuras repetitivas → Anidamiento de bucles
¿Qué salida producen los siguientes trozos de código?
iteraciones = 0; for (i = 0; i < 4; i++) for (j = 0; j < 3 ; j++) iteraciones++;
iteraciones = 0;
172
Estructuras de Control Estructuras repetitivas → Anidamiento de bucles
173
Ejemplo. Imprimir en pantalla los divisores primos del entero n Algoritmo: Entradas: n Salidas: ninguna Descripción: Recorrer todos los enteros menores que n Comprobar si el entero es primo. En dicho caso, comprobar si divide a n Ineficiente. Es mejor el siguiente:
for (i = 0; i < 4; i++) for (j = i; j < 3 ; j++) iteraciones++;
Recorrer todos los enteros menores que n Comprobar si el entero divide a n. En dicho caso, comprobar si el entero es primo. Nota. Mejor aún si empezamos desde n/2
total = 3; for (i = 2 ; i <= 10 ; i = i+2) for (j = 1 ; j <= 8 ; j++) total++; cout << total;
Estructuras de Control Estructuras repetitivas → Anidamiento de bucles
n divisor_n
18 9 8
7
6
5
4
174
3
2
1
for (divisor_n = n/2 ; divisor_n > 1 ; divisor_n--){ if (n % divisor_n == 0){ es_primo = true; tope = sqrt(1.0*divisor_n); divisor_primo = 2; while (divisor_primo <= tope && es_primo){ if (divisor_n % divisor_primo == 0) es_primo = false; else divisor_primo++; } if (es_primo) cout << "\nEl primo " << divisor_n << " divide a " << n; } }
Estructuras de Control Estructuras repetitivas → Anidamiento de bucles
175
Ejemplo. El Teorema fundamental de la Aritmética (Euclides 300 A.C/Gauss 1800) nos dice que podemos expresar cualquier entero como producto de factores primos. Imprimir en pantalla dicha descomposición.
n 360 180 90 45 15 5 1
primo 2 2 2 3 3 5
Fijamos un valor de primo cualquiera. Por ejemplo primo=2
// Dividir n por primo cuantas veces sea posible primo = 2; while (n % primo == 0){ cout << primo << " "; n = n / primo; } Ahora debemos pasar al siguiente primo primo y volver a ejecutar el bloque anterior. Condición de parada: n>=primo o bien n>1
Mientras n > 1 Dividir n por primo cuantas veces sea posible primo = siguiente primo mayor que primo ¿Cómo pasamos al siguiente primo?
Estructuras de Control Estructuras repetitivas → Anidamiento de bucles
176
primo++; Recorrer todos los enteros menores de primo hasta encontrar un divisor o hasta llegar a primo Este recorrido implica un bucle, pero no es necesario. Hagamos simplemente primo++:
/* Mientras n > 1 Dividir n por primo cuantas veces sea posible primo++ */ primo = 2; while (n > 1){ while (n % primo == 0){ cout << primo << " "; n = n / primo; } primo++; } ¿Corremos el peligro de intentar dividir n por un valor primo que no sea primo? No. Por ejemplo, n=40. Cuando primo sea 4, ¿podrá ser n divisible por 4, es decir n%4==0? Después de dividir todas las veces posibles por 2, me queda n=5 que ya no es divisible por 2, ni por tanto, por ningún múltiplo de 2. En general, al evaluar n%primo, n ya ha sido dividido por todos los múltiplos de primo. Nota. Podemos sustituir p++ por primo=primo+2 (tratando el primer caso primo=2 de forma aislada)
Estructuras de Control Estructuras repetitivas → Anidamiento de bucles
177
Ampliación: No hay forma de calcular el siguiente primo a uno dado (con una fórmula directa). Como mucho, se puede establecer una cota del número de primos que hay menores a uno dado. Este problema está ligado (demostrado por Von Koch en 1901) a la Hipótesis de Riemann, uno de los problemas matemáticos más importantes sin resolver, y por el cual se ofrece una recompensa de 1 millón de dólares (por el Instituto Clay de Matemáticas) Riemann fue un gran matemático, discípulo de Gauss. Entre otras cosas, desarrolló la geometría de Riemann, base fundamental de la teoría de la relatividad de Einstein.
Ampliación: El problema de factorizar un número (expresarlo como producto de factores primos) es muy importante en la vida real. Hasta la fecha, no se ha conseguido construir (ni se espera que se consiga) un algoritmo que termine en un tiempo razonable para factorizar un número cuando éste es grande. Los conceptos de número grande y tiempo razonable se presentaron en la página 159. ¿Dónde se aplica? En Criptografía (Cryptography), para ocultar nuestros datos en Internet (por ejemplo, el número de la tarjeta de crédito). Mientras no se diseñe un algoritmo en este sentido, nuestros datos bancarios están bien protegidos de los sniffers. Consultad:
http://angelrey.wordpress.com/2009/05/27/ numeros-primos-criptologia-y-codificacion/ Buscad en Internet: Criptografía asimétrica, Integer factorization, RSA numbers
Estructuras de Control Estructuras repetitivas → Particularidades de C++
II.3.4.
178
int dato = if (! dato cout << else cout <<
Particularidades de C++
C++ es un lenguaje muy versátil. A veces, demasiado · · · II.3.4.1. Expresiones y sentencias son similares El tipo bool como un tipo entero En C++, el tipo lógico es compatible con un tipo entero. Cualquier expresión entera que devuelva el cero, se interpretará como false. Si devuelve cualquier valor distinto de cero, se interpretará como true.
bool var_logica; var_logica = false; var_logica = (4>5); var_logica = 0;
Estructuras de Control Estructuras repetitivas → Particularidades de C++
// Correcto: resultado false // Correcto: resultado 0 (false) :-O
179
4; < 5) dato <<" es mayor o igual que 5"; dato <<" es menor de 5";
El operador ! tiene más precedencia que <. Por lo tanto, la evaluación es como sigue: ! dato < 5 ⇔ (!dato) < 5 ⇔ (dato vale 4) (!true) < 5 ⇔ ⇔ false < 5 ⇔ 0 < 5 ⇔ true ¡Imprime 4 es mayor o igual que 5! Solución:
if (! (dato < 5)) cout << dato <<" es mayor o igual que 5"; else cout << dato <<" es menor de 5"; o mejor:
var_logica = (4<5); var_logica = true; var_logica = 2;
// Correcto: resultado true // Correcto: resultado 2 (true)
:-O
if (dato >= 5) cout << dato <<" es mayor o igual que 5"; else cout << dato <<" es menor de 5";
Nota. Algunos compiladores, al ejecutar cout << false, imprimen en pantalla un cero, mientras que cout << true imprime un uno.
La dualidad entre los tipos enteros y lógicos nos puede dar quebraderos de cabeza en los condicionales
Consejo:
No hay que ser rácano con los paréntesis
Estructuras de Control Estructuras repetitivas → Particularidades de C++
180
El operador de asignación en expresiones El operador de asignación = se usa en sentencias del tipo:
valor = 7; Pero además, devuelve un valor: el resultado de la asignación. Así pues, valor = 7 es una expresión que devuelve 7
un_valor = otro_valor = valor = 7; Esto producirá fuertes dolores de cabeza cuando por error usemos una expresión de asignación en un condicional:
valor = 5; if (valor = 7) <acciones if> // Siempre se ejecuta este bloque! else <acciones else> // Además, valor se queda con 7 valor = 7 devuelve 7. Al ser distinto de cero, es true. Por tanto, se ejecuta el bloque if (y además valor se ha modificado con 7) a = 7; if (a=0) cout << "\nRaíz= " << -c/b; // Nunca se ejecuta else{ r1 = -b + sqrt(b*b -4*a*c) / (2*a) ; // Error lógico
Estructuras de Control Estructuras repetitivas → Particularidades de C++
181
El operador de igualdad en sentencias
C++ permite que una expresión constituya una sentencia Esta particularidad no da beneficios salvo en casos muy específicos y sin embargo nos puede dar quebraderos de cabeza. Así pues, el siguiente código compila perfectamente:
int entero; 4 + 3; C++ evalúa la expresión entera 4 + 3;, devuelve 7 y no hace nada con él, prosiguiendo la ejecución del programa.
int entero; entero == 7; C++ evalúa la expresión lógica entero == 7;, devuelve true y no hace nada con él, prosiguiendo la ejecución del programa.
Estructuras de Control Estructuras repetitivas â&#x2020;&#x2019; Particularidades de C++
182
Bucles for con cuerpo vacĂo
variable = 9; if (variable + 1 == 10) cout << variable << " es igual a 9"; cout << variable;
El siguiente cĂłdigo no imprime los enteros del 1 al 20. ÂżPor quĂŠ?
// Correcto
for (x = 1; x <= 20; x++); cout << x;
// Imprime 9
El operador ++ (y --) forma una expresiĂłn: devuelve el valor incrementado:
variable = 9; if (variable++ == 10) cout << variable << " es igual a 9"; // "10 es igual a 9" cout << variable;
Realmente, el cĂłdigo bien tabulado es:
for (x = 1; x <= 20; x++) ; cout << x; Bucles for con sentencias de incremento incorrectas
// "10"
183
II.3.4.2. El bucle for en C++
El operador de incremento en expresiones
Consejo:
Estructuras de Control Estructuras repetitivas â&#x2020;&#x2019; Particularidades de C++
No usad = ni ++ en la expresiĂłn lĂłgica de una sentencia condicional
Lo siguiente es un error lĂłgico:
for (par = -10; par <= 10; par + 2) num_pares++;
// en vez de par = par + 2
equivale a:
par = -10; while (par <= 10){ num_pares++; par + 2; } Compila correctamente pero la sentencia par + 2; no incrementa par (recordad lo visto en la pĂĄgina 181) Resultado: bucle inďŹ nito.
Estructuras de Control Estructuras repetitivas → Particularidades de C++
184
Modificación del contador Únicamente mirando la cabecera de un bucle for sabemos cuantas iteraciones se van a producir (recordar lo visto en la página 168). Por eso, en los casos en los que sepamos de antemano cuántas iteraciones necesitamos, usaremos un bucle for. En otro caso, usaremos un bucle while ó do while. Para mantener dicha finalidad, es necesario respetar la siguiente restricción: No se debe modificar el valor de la variable controladora, ni el valor final dentro del cuerpo del bucle for
Sin embargo, C++ no impone dicha restricción. Será responsabilidad del programador. Ejemplo. Sumar los divisores de valor. ¿Dónde está el fallo en el siguiente código?:
suma = 0; tope = valor/2; for (divisor = 2; divisor <= tope ; divisor++) { if (valor % divisor == 0) suma = suma + divisor; divisor++; }
Estructuras de Control Estructuras repetitivas → Particularidades de C++
185
Supongamos que quiero salir de un bucle for cuando se produce una condición. ¿Cómo lo hago? Ejemplo. Escribe los enteros entre inf y sup, hasta llegar al primer múltiplo de 13:
cin >> inf; cin >> sup; for (entero = inf; entero <= sup; entero++) { cout << entero << " "; if (entero % 13 == 0) entero = sup + 1;
//
:-(
} Esto funciona pero es una chapuza. Mejor usamos un while:
cin >> inf; cin >> sup; entero = inf; while (entero <= sup) && (entero % 13 != 0){ cout << entero << " "; }
Estructuras de Control Estructuras repetitivas → Particularidades de C++
Ejemplo. Comprobar si un número es primo
186
Estructuras de Control Estructuras repetitivas → Particularidades de C++
187
El bucle for como ciclo controlado por condición
es_primo = true; for (divisor = 2 ; divisor < valor ; divisor++) if (valor % divisor == 0){ es_primo = false; divisor = valor; // :-( }
Si bien en muchos lenguajes tales como PASCAL, FORTRAN o BASIC el comportamiento del ciclo for es un bucle controlado por contador, en C++ es un ciclo más versátil, controlado por condición. En el caso concreto de C++, su sintaxis es la siguiente:
for ([<sentencia inicial>];
Mejor la solución que vimos en la página 158:
[<expresión lógica>];
es_primo = true;
[<sentencia final>]) <cuerpo bucle>
divisor = 2 while (divisor < valor && es_primo){ if (valor % divisor == 0) es_primo = false; else divisor++; }
donde, <sentencia inicial> es la sentencia que se ejecuta antes de entrar al bucle, <expresión lógica> es cualquier condición que verifica si el ciclo debe terminar o no, <sentencia final> es la sentencia que se ejecuta antes de volver arriba para comprobar el valor de la expresión lógica. Por tanto, la condición impuesta en el ciclo no tiene por qué ser de la forma contador < final, sino que puede ser cualquier tipo de condición.
Estructuras de Control Estructuras repetitivas â&#x2020;&#x2019; Particularidades de C++
188
Estructuras de Control Estructuras repetitivas â&#x2020;&#x2019; Particularidades de C++
Ejemplo. Comprobar si un nĂşmero es primo. Lo resolvimos en la pĂĄgina 158:
es_primo = true; divisor = 2
189
while (divisor < valor && es_primo){ if (valor % divisor == 0) es_primo = false; else divisor++; } Con un for quedarĂa:
es_primo = true; divisor = 2 for (divisor = 2; divisor < valor && es_primo; divisor++) if (valor % divisor == 0) es_primo = false;
Estructuras de Control Estructuras repetitivas → Particularidades de C++
190
Ejemplo. Escribe los enteros entre inf y sup, hasta llegar al primer múltiplo de 13 (lo vimos en la página 185)
cin >> inf; cin >> sup; entero = inf; while (entero <= sup && entero % 13 != 0){ cout << entero << " "; entero++; }
cin >> inf; cin >> sup; for (entero = inf; entero <= sup && entero % 13 != 0 ; entero++) cout << entero << " "; En los ejemplos anteriores
for (entero = inf; entero<=sup && entero%13 != 0 ; entero++) for (divisor = 2; divisor <= tope && es_primo; divisor++) se ha usado dentro del for dos condiciones que controlan el bucle: La condición relativa a la variable contadora. Otra condición adicional. Este código es completamente aceptable en C++.
Estructuras de Control Estructuras repetitivas → Particularidades de C++ '
191 $
En resumen, usaremos un bucle for en los casos en los que siempre exista: Una sentencia de inicialización del contador Una condición de continuación que involucre al contador (pueden haber otras condiciones adicionales) Una sentencia final que involucre al contador
&
%
Estructuras de Control Estructuras repetitivas → Particularidades de C++
192
Pero ya puestos, ¿puede usarse entonces, cualquier condición dentro de la cabecera del for? Sí, pero no es muy recomendable.
Estructuras de Control Estructuras repetitivas → Particularidades de C++
Lo hacemos ahora con un for:
Ejemplo. Construir un programa que indique el número de valores que introduce un usuario hasta que se encuentre con un cero.
// Programa para contar el número de valores que se // introducen hasta que se encuentra un cero. // --- Usando un ciclo for ---
// Programa para contar el numero de valores que se // introducen hasta que se encuentra un cero. // --- Usando un ciclo while ---
#include <iostream> using namespace std;
#include <iostream> using namespace std;
int main(){ int num_valores, valor; cin >> valor;
int main(){ int num_valores, valor;
for (num_valores=0; valor!=0; num_valores++) cin >> valor;
cin >> valor; num_valores = 0; while (valor != 0){ cin >> valor; num_valores++; } cout << "El número de valores introducidos es " << num_valores; }
193
cout << "\nEl número de valores introducidos es " << num_valores; } Debemos evitar este tipo de bucles for en los que la(s) variable(s) que aparece en la condición, no aparece en las otras dos expresiones.
Estructuras de Control Estructuras repetitivas → Particularidades de C++
194
En el anterior ejemplo, si cambiamos los nombres de las variables, tendríamos:
for (recorr=0; recoger!=0; recorr++) .....
Estructuras de Control Estructuras repetitivas → Particularidades de C++
195
II.3.4.3. Otras (perniciosas) estructuras de control Existen otras sentencias en la mayoría de los lenguajes que permiten alterar el flujo normal de un programa. En concreto, en C++ existen las siguientes sentencias:
Y el cerebro de muchos programadores le engañará y le harán creer que está viendo lo siguiente, que es a lo que está acostumbrado:
for (recorr=0; recorr!=0; recorr++) ..... Esto no quiere decir que, ADEMÁS de la variable contadora, no podamos usar condiciones adicionales para salirnos del bucle. Será algo usual (lo hicimos en el ejemplo del primo)
Y ya puestos, ¿podemos suprimir algunas expresiones de la cabecera de un bucle for? La respuesta es que sí, pero hay que evitarlas SIEMPRE. Oscurecen el código.
goto
continue
break
exit
Durante los 60, quedó claro que el uso incontrolado de sentencias de transferencia de control era la principal fuente de problemas para los grupos de desarrollo de software. Fundamentalmente, el responsable de este problema era la sentencia goto que le permite al programador transferir el flujo de control a cualquier punto del programa. Esta sentencia aumenta considerablemente la complejidad tanto en la legibilidad como en la depuración del código. Referencias:
Ejemplo. Sumar valores leídos desde la entrada por defecto, hasta introducir un cero.
Teorema de Bohm y Jacopini -1966-. Flow diagrams, Turing machines and languages only with two formation rules. Communications of the ACM, 1966. Vol. 9, No.5, pp. 366-371
int main(){ int valor, suma; cin >> valor;
Dijkstra, E.W. Goto statement considered harmful. Communications of the ACM, 1968. Vol. 11, No.3, pp. 147-148
for ( ; valor!=0 ; ){ suma = suma + valor cin >> valor; }
Estructuras de Control Estructuras repetitivas → Particularidades de C++
196
Ampliación: Consultad el libro Code Complete de McConnell, disponible en la biblioteca. Incluye numerosos consejos y hábitos aconsejables de programación. En el tema de Estructuras de Control incluye una referencia a un informe en el que se reconoce que un uso inadecuado de un break, provocó un apagón telefónico de varias horas en los 90 en NY.
En FP, no se permitirá el uso de ninguna de las sentencias anteriores, excepto la sentencia break con el propósito aquí descrito dentro de la sentencia switch. En caso contrario, el Suspenso está garantizado.
Estructuras de Control Estructuras repetitivas → Particularidades de C++
197
Bibliografía recomendada para este tema: A un nivel menor del presentado en las transparencias: – Segundo capítulo de Deitel & Deitel A un nivel similar al presentado en las transparencias: – Segundo y tercer capítulos de Garrido. A un nivel con más detalles: – Capítulos quinto y sexto de Stephen Prata. – Tercer capítulo de Lafore. Los autores anteriores presentan primero los bucles junto con la expresiones lógicas y luego los condicionales.
Funciones y Clases Funciones →
III.1.
Tema III
III.1.1.
199
Funciones Fundamentos
III.1.1.1. Las funciones realizan una tarea
Funciones y Clases
A la hora de programar, es normal que haya que repetir las mismas acciones con distintos valores. Para no repetir el mismo código, tendremos que: Identificar las acciones repetidas
Objetivos: Introducir el concepto de función y parámetro para no tener que duplicar código. Encapsular datos y funciones en un mismo módulo: la CLASE. Construir objetos sencillos que cumplan principios básicos de diseño.
Identificar los valores que pueden variar (esto es, los parámetros) Definir una función que encapsule dichas acciones Las funciones como sqrt, tolower, etc., no son sino ejemplos de funciones incluidas en cmath y cctype, respectivamente, que resuelven tareas concretas y devuelven un valor. Se suele utilizar una notación prefija, con parámetros (en su caso) separados por comas y encerrados entre paréntesis.
int main(){ double lado1, lado2, hipotenusa, radicando; <Asignación de valores a los lados> radicando = lado1*lado1 + lado2*lado2; hipotenusa = sqrt(radicando);
Autor: Juan Carlos Cubero. Sugerencias: por favor, enviar un e-mail a JC.Cubero@decsai.ugr.es
Funciones y Clases Funciones → Fundamentos
200
Si queremos calcular la hipotenusa de dos triángulos rectángulos:
.............................. int main(){ double lado1_A, lado2_A, lado1_B, lado2_B, hipotenusa_A, hipotenusa_B, radicando_A, radicando_B; <Asignación de valores a los lados> radicando_A = lado1_A*lado1_A + lado2_A*lado2_A; hipotenusa_A = sqrt(radicando_A); radicando_B = lado1_B*lado1_B + lado2_B*lado2_B; hipotenusa_B = sqrt(radicando_B); .............................. } ¿No sería más claro, menos propenso a errores y más reutilizable el código si existiese en alguna biblioteca una función Hipotenusa? El fragmento de código anterior quedaría como sigue:
.............................. int main(){ double lado1_A, lado2_A, lado1_B, lado2_B, hipotenusa_A, hipotenusa_B; <Asignación de valores a los lados> hipotenusa_A = Hipotenusa(lado1_A, lado2_A); hipotenusa_B = Hipotenusa(lado1_B, lado2_B); ..............................
Funciones y Clases Funciones → Fundamentos
201
En este ejemplo, Hipotenusa es una función que resuelve la tarea de calcular la longitud hipotenusa de un triángulo, sabiendo la longitud de los lados. Podría dar la sensación de que primero debemos escribir el programa con todas las sentencias y luego construiríamos la función Hipotenusa. Esto no debería hacerse así: el programador debe identificar las funciones antes de escribir una sola línea de código. En cualquier caso, no siempre se detectan a priori las funciones, por lo que, una vez escrito el código, si detectamos bloques que se repiten, deberemos englobarlos en una función.
Funciones y Clases Funciones â&#x2020;&#x2019; Fundamentos
202
Funciones y Clases Funciones â&#x2020;&#x2019; Fundamentos
203
double Cuadrado(double entrada){ return entrada*entrada; }
III.1.1.2. DeďŹ niciĂłn
<tipo> <nombre-funciĂłn> ([<parĂĄm. formales>]) { [<sentencias>]
En C++ no se pueden deďŹ nir funciones dentro de otras. Todas estĂĄn al mismo nivel.
return <expresiĂłn>;
Como norma de escritura de cĂłdigo, escribiremos la primera letra de las funciones en mayĂşscula.
} III.1.1.3. ParĂĄmetros formales y actuales Por ahora, la deďŹ niciĂłn se pondrĂĄ despuĂŠs de la inclusiĂłn de bibliotecas y antes del main. Antes de usar una funciĂłn en cualquier sitio, hay que poner su deďŹ niciĂłn. Diremos que <tipo> <nombre-funciĂłn> (<parĂĄm. formales>) es la cabecera (header) de la funciĂłn.
Los parĂĄmetros actuales (actual parameters) son las expresiones pasadas como argumentos en la llamada a una funciĂłn.
El cuerpo de la funciĂłn debe contener:
Llamada:
return <expresiĂłn>; donde <expresiĂłn> ha de ser del mismo tipo que el especiďŹ cado en la cabecera de la funciĂłn (tambiĂŠn puede ser un tipo compatible). El valor que contenga dicha expresiĂłn es el valor que devuelve la funciĂłn cuando es llamada.
La llamada a una funciĂłn constituye una expresiĂłn.
Cuadrado(valor) es una expresiĂłn.
Los parĂĄmetros formales (formal parameters) son aquellos especiďŹ cados en la cabecera de la funciĂłn. Al declarar un parĂĄmetro formal hay que especiďŹ car su tipo de dato. Los parĂĄmetros formales sĂłlo se conocen dentro de la funciĂłn.
<nombre-funciĂłn> (<lista parĂĄmetros actuales>); Nota. En inglĂŠs se usan los tĂŠrminos formal and actual parameters. Hubiese sido mejor llamarlos parĂĄmetros de llamada o algo asĂ.
Funciones y Clases Funciones → Fundamentos
204
double Cuadrado(double entrada){ return entrada*entrada; } int main(){ double resultado, valor;
Funciones y Clases Funciones → Fundamentos
double Cuadrado(double entrada){ return entrada*entrada; } int main(){ double resultado, valor;
valor = 4; resultado = Cuadrado(valor); // resultado = 16 cout << "El cuadrado de " << valor << " es " << resultado; }
Cuando se ejecuta la llamada resultado = Cuadrado(valor); el flujo de control salta a la definición de la función. Se realiza la correspondencia entre los parámetros. El correspondiente parámetro formal recibe una copia del parámetro actual, es decir, se realiza la siguiente asignación en tiempo de ejecución: parámetro formal = parámetro actual En el ejemplo, entrada = 4 Esta forma de pasar parámetros se conoce como paso de parámetros por valor (pass-by-value). Empiezan a ejecutarse las sentencias de la función y, cuando se llega a alguna sentencia return <expresión>, termina la ejecución de la función y devuelve el resultado de evaluar <expresión> al lugar donde se realizó la invocación. A continuación, el flujo de control prosigue por donde se detuvo al realizar la invocación de la función.
entrada = valor
valor = 4; resultado = Cuadrado(valor); // resultado = 16 cout << "El cuadrado de " << valor << " es " << resultado;
} Flujo de control
205
Funciones y Clases Funciones → Fundamentos
206
Ejercicio. Definid la función Hipotenusa
} int main(){ double lado1_A, lado2_A, lado1_B, lado2_B, hipotenusa_A, hipotenusa_B; <Asignación de valores a los lados> hipotenusa_A = Hipotenusa(lado1_A, lado2_A); hipotenusa_B = Hipotenusa(lado1_B, lado2_B); ............. }
207
Correspondencia entre parámetros actuales y formales
#include <iostream> #include <cmath> using namespace std; double Hipotenusa(
Funciones y Clases Funciones → Fundamentos
Debe haber exactamente el mismo número de parámetros actuales que de parámetros formales.
){
double Cuadrado(double entrada){ return entrada*entrada } int main(){ double resultado; resultado = Cuadrado(5, 8); // Error en compilación } Debemos garantizar que el parámetro actual tenga un valor correcto antes de llamar a la función.
double Cuadrado(double entrada){ return entrada*entrada } int main(){ double resultado, valor; resultado = Cuadrado(valor); // Error lógico } La correspondencia se establece por orden de aparición, uno a uno y de izquierda a derecha.
#include <iostream> #include <cmath> using namespace std; double Resta(double valor_1, double valor_2){ return valor_1 - valor2; } int main(){ double un_valor = 5.0, otro_valor = 4.0;
Funciones y Clases Funciones → Fundamentos
208
cout << "\nResta = " << Resta(un_valor, otro_valor); cout << "\nResta = " << Resta(otro_valor, un_valor); } El parámetro actual puede ser una expresión. Primero se evalúa la expresión y luego se realiza la llamada a la función. hipotenusa_A = Hipotenusa(ladoA+3 , ladoB*5); Cada parámetro formal y su correspondiente parámetro actual han de ser del mismo tipo (o compatible)
double Cuadrado(double entrada){ return entrada*entrada } int main(){ double resultado; int valor = 7; resultado = Cuadrado(valor); } Problema: que el tipo del parámetro formal sea más pequeño que el actual. La conversión de tipos puede que no se realice correctamente, por lo que el parámetro formal toma un valor distinto al que pretendíamos y la función devuelve un valor erróneo
int Cuadrado(int entrada){ return entrada*entrada; } int main(){ int resultado; resultado = Cuadrado(400000000000); // devuelve basura
Funciones y Clases Funciones → Fundamentos
209
Dentro de una función, se puede llamar a cualquier otra función que esté definida con anterioridad. El paso de parámetros entre funciones se realiza igual independientemente de dónde esté la llamada.
#include <iostream> #include <cmath> using namespace std; double Cuadrado(double entrada){ return entrada*entrada; } double Hipotenusa(double un_lado, double otro_lado){ return sqrt(Cuadrado(un_lado) + Cuadrado(otro_lado)); } int main(){ double lado1_A, lado2_A, lado1_B, lado2_B, hipotenusa_A, hipotenusa_B; <Asignación de valores a los lados> hipotenusa_A = Hipotenusa(lado1_A, lado2_A); hipotenusa_B = Hipotenusa(lado1_B, lado2_B); ............. }
Funciones y Clases Funciones → Fundamentos
210
Funciones y Clases Funciones → Fundamentos
211
Las modificaciones del parámetro formal no afectan al parámetro actual. Recordemos que el paso por valor conlleva trabajar con una copia del valor correspondiente al parámetro actual, por lo que éste no se modifica.
double Cuadrado(double entrada){ entrada = entrada*entrada; return entrada; } int main(){ double resultado, valor = 3; resultado = Cuadrado(valor); // valor sigue siendo 3 } Ampliación: Algunos lenguajes de programación proporcionan otros métodos de pasar parámetros, en los que una modificación del parámetro formal sí afecta al correspondiente parámetro actual. En C++ se utiliza el paso por referencia (pass-by-reference). Se verá en el segundo cuatrimestre.
Funciones y Clases Funciones → Fundamentos
212
III.1.1.4. Datos locales Dentro de una función podemos declarar constantes y variables. Estas constantes y variables sólo se conocerán dentro de la función, por lo que se les denomina datos locales (local data). De hecho, los parámetros formales se pueden considerar datos locales. <tipo> <nombre-función> (<lista parámetros formales>) { [<Constantes Locales>]
Funciones y Clases Funciones → Fundamentos
Ejemplo. Calculad el factorial de un valor. Recordemos la forma de calcular el factorial de un entero n:
fact = 1; for (i = 2; i <= n ; i++) fact = fact * i; Como una función, quedaría:
int Factorial (int n){ int i; int fact = 1;
[<Variables Locales>]
for (i = 2; i <= n; i++) fact = fact * i;
[<Sentencias>]
return <expresión>; } Al igual que ocurre con la declaración de variables del main, las variables locales a una función no inicializadas a un valor concreto tendrán un valor indeterminado (basura) al inicio de la ejecución de la función.
return fact; } int main(){ int valor, resultado; cout << "\nIntroduzca valor"; cin >> valor; resultado = Factorial(valor); cout << "\nFactorial de " << valor << " = " << resultado; }
213
Funciones y Clases Funciones â&#x2020;&#x2019; Fundamentos
214
Dentro de una funciĂłn no podemos acceder a los datos locales deďŹ nidos en otras funciones ni a los de main.
int Factorial (int n){ int i, fact = 1; valor = 5;
//
Error de compilaciĂłn :-)
for (i = 2; i <= n; i++) fact = fact * i; return fact; } int main(){ int valor, resultado; i = 5; n = 5;
// //
Error de compilaciĂłn :-) Error de compilaciĂłn :-)
cout << "\nIntroduzca valor"; cin >> valor; resultado = Factorial(valor); cout << "\nFactorial de " << valor << " = " << resultado; }
Funciones y Clases Funciones â&#x2020;&#x2019; Fundamentos
215
En deďŹ nitiva:
Cada funciĂłn tiene sus propios datos (parĂĄmetros formales y datos locales) y no se conocen en ningĂşn otro sitio.
Funciones y Clases Funciones → Fundamentos
216
Por tanto, los nombres dados a los parámetros formales pueden coincidir con los nombres de los parámetros actuales: son variables distintas.
int Factorial (int n){ int i; int fact = 1;
// <- n de Factorial. OK
Funciones y Clases Funciones → Fundamentos
#include <iostream> #include <cmath> using namespace std; double Hipotenusa(double un_lado, double otro_lado){ return sqrt(un_lado*un_lado + otro_lado*otro_lado); } int main(){ double un_lado, otro_lado, hipotenusa;
for (i=2; i<=n; i++) fact = fact * i; return fact; } int main(){ int n = 3 , resultado;
// <- n de main. OK
resultado = Factorial(n); cout << "Factorial de " << n << " = "
hipotenusa = Hipotenusa(un_lado, otro_lado); cout << "\nLa hipotenusa vale " << hipotenusa;
<< resultado;
// Imprime en pantalla lo siguiente: // Factorial de 3 = 6 }
cout << "\nIntroduzca primer lado"; cin >> un_lado; cout << "\nIntroduzca segundo lado"; cin >> otro_lado;
}
217
Funciones y Clases Funciones → Fundamentos
218
III.1.1.5. La Pila Cada vez que se llama a una función, se crea dentro de una zona de memoria llamada pila (stack), un compartimento de trabajo asociado a ella, llamado marco de pila (stack frame).
Funciones y Clases Funciones → Fundamentos
#include <iostream> using namespace std; int Factorial (int n){ int i; int fact = 1;
Cada vez que se llama a una función se crea el marco asociado.
for (i = 2; i <= n; i++) fact = fact * i;
En el marco asociado a cada función se almacenan, entre otras cosas:
return fact;
– Los parámetros formales. – Los datos locales (constantes y variables). – La dirección de retorno de la función. Cuando una función llama a otra, el marco de la función llamada se apila sobre el marco de la función desde donde se hace la llamada (de ahí el nombre de pila). Hasta que no termine de ejecutarse la última función llamada, el control no volverá a la anterior.
219
} int Combinatorio(int a, int b){ return Factorial(a)/(Factorial(b) * Factorial(a-b)); } int main(){ int resultado, elementos=5, grupos=2; resultado = Combinatorio(elementos, grupos); cout << elementos << " sobre " << grupos << " = " << resultado; }
Funciones y Clases Funciones → Fundamentos
220
Funciones y Clases Funciones → Fundamentos
221
main es de hecho una función como otra cualquiera, por lo que también se almacena en la pila. Es la primera función llamada al ejecutarse el programa. Devuelve un entero al Sistema Operativo y puede tener más parámetros, pero en FP sólo veremos el caso sin parámetros. Por eso, la hemos declarado siempre como:
int main(){ ........... } Si el programa termina sin errores, se debe devolver 0. Puede indicarse incluyendo return 0; al final de main (antes de }) En C++, si no se devuelve nada, se devuelve 0 por defecto, por lo que podríamos suprimir return 0; Si el programa termina con un error, debe devolver un entero distinto de 0.
Funciones y Clases Funciones → Fundamentos
III.1.1.6. Ejemplos de funciones
222
Funciones y Clases Funciones → Fundamentos
Ejemplo. Comprobar si dos reales son iguales, aceptando un margen en la diferencia de 0,000001
Ejemplo. Comprobar si un número es par.
bool EsPar (int n) { if (n % 2 == 0) return true; else return false; } int main(){ int un_numero; bool es_par_un_numero;
bool SonIguales (double uno, double otro){ return (fabs(uno - otro) <= 0.000001); } Ejemplo. Comprobar si un número es primo (recordad el algoritmo visto en la página 158)
bool EsPrimo(int valor){ bool es_primo; int divisor; es_primo = true;
cin >> un_numero; es_par_un_numero = EsPar (un_numero); if (es_par_un_numero) cout << un_numero << " es par"; else cout << un_numero << " es impar"; } De forma más compacta:
bool EsPar (int n) { return (n % 2 == 0); }
223
for (divisor = 2 ; divisor < valor && es_primo ; divisor++) if (valor % divisor == 0) es_primo = false; return es_primo; } Ejemplo. Calcular el MCD de dos enteros.
int MCD(int primero, int segundo){ /* Vamos dividiendo los dos enteros por todos los enteros menores que el menor de ellos hasta que: - ambos sean divisibles por el mismo valor - o hasta que lleguemos al 1 */ bool hay_divisores, mcd_encontrado; int divisor, mcd;
Funciones y Clases Funciones → Fundamentos
224
if (primero==0 || segundo==0) mcd = 0; else{ if (primero > segundo) divisor = segundo; else divisor = primero;
Funciones y Clases Funciones → Fundamentos
Veamos ahora ejemplos de funciones con errores:
int Factorial (int n) int i, fact;
} return mcd; }
{
fact = 1; for (i = 2; i <= n; i++) fact = fact * i; return fact; }
hay_divisores = true; mcd_encontrado = false; while (hay_divisores && !mcd_encontrado){ if (divisor == 1){ hay_divisores = false; mcd = 1; } else{ if ((primero%divisor == 0) && (segundo%divisor == 0)){ mcd_encontrado = true; mcd = divisor; } else{ divisor--; } } }
225
{ //
:-(
} La función termina cuando se ejecuta la primera iteración.
Estilo de programación a evitar
bool EsPrimo(int valor){ bool es_primo;
// falta algo
es_primo = true; for (divisor = 2 ; divisor < valor && es_primo ; divisor++) if (valor % divisor == 0) return false; return es_primo; } '
Consejo:
&
$
No introducir sentencias return en varios sitios distintos del cuerpo de la función. Dejar un único return al final de la función
%
Funciones y Clases Funciones → Fundamentos
bool EsPar (int n) { if (n%2==0) return true; }
226
Funciones y Clases Funciones → Funciones void
III.1.2. //
:-(
La función debe devolver siempre un valor. En caso contrario, el comportamiento es indeterminado.
227
Funciones void
Ejemplo. Supongamos que en el main nos encontramos este trozo de código:
int i, tope_lineas; .................... for (i=1; i<=tope_lineas ; i++) cout << "\n************"; cout << "Programa básico de Trigonometría"; for (i=1; i<=tope_lineas ; i++) cout << "\n************"; .................... ¿No sería más fácil de entender si el código del programa principal hubiese sido el siguiente?
.................... Presentacion(tope_lineas); .................... En este ejemplo, Presentacion resuelve la tarea de realizar la presentación del programa por pantalla, pero no calcula (devuelve) ningún valor, como sí ocurre con las funciones sqrt o Hipotenusa. Por eso, su llamada constituye una sentencia y no aparece dentro de una expresión. Caso conocido: system("pause") Este tipo particular de funciones que no devuelven ningún valor, se definen como sigue:
Funciones y Clases Funciones → Funciones void
228
void <nombre-función> (<lista parámetros formales>) { [<Constantes Locales>] [<Variables Locales>] [<Sentencias>]
} Obsérvese que no hay sentencia return. La función void termina cuando se ejecuta la última sentencia de la función. El paso de parámetros y la definición de datos locales sigue las mismas normas que el resto de funciones. Para llamar a una función void, simplemente, ponemos su nombre y la lista de parámetros actuales con los que realizamos la llamada:
void MiFuncionVoid(int parametro_formal, double otro_parametro_formal){ ............... } int main(){ int parametro_actual; double otro_parametro_actual; ............... MiFuncionVoid(parametro_actual, otro_parametro_actual); Presentacion(tope_lineas);
Funciones y Clases Funciones → Funciones void
#include <iostream> #include <cmath> using namespace std; double Cuadrado(double entrada){ return entrada*entrada; } double Hipotenusa(double un_lado, double otro_lado){ return sqrt(Cuadrado(un_lado) + Cuadrado(otro_lado)); } void Presentacion(int tope_lineas){ int i; for (i=1; i<=tope_lineas ; i++) cout << "\n************"; cout << "Programa básico de Trigonometría"; for (i=1; i<=tope_lineas ; i++) cout << "\n************"; } int main(){ double lado1, lado2, hipotenusa; Presentacion(3); cin >> lado1; cin >> lado2; hipotenusa = Hipotenusa(lado1,lado2); cout << "\nLa hipotenusa vale " << hipotenusa; }
229
Funciones y Clases Funciones → Funciones void
Llamadas entre funciones void.
void ImprimeLineas (int num_lineas){ int i; for (i = 1; i <= num_lineas ; i++) cout << "\n************"; } void Presentacion(int tope_lineas){ ImprimeLineas (tope_lineas); cout << "Programa básico de Trigonometría"; ImprimeLineas (tope_lineas); } O incluso:
void ImprimeAsteriscos (int num_asteriscos){ int i; for (i = 1 ; i <= num_asteriscos ; i++) cout << "*"; } void ImprimeLineas (int num_lineas){ int i; for (i = 1; i <= num_lineas ; i++){ cout << "\n" << ImprimeAsteriscos(10); } } void Presentacion(int tope_lineas){ ImprimeLineas (tope_lineas); cout << "Programa básico de Trigonometría"; ImprimeLineas (tope_lineas); }
230
Funciones y Clases Funciones → Documentación de una función
III.1.3.
231
Documentación de una función
Hay dos tipos de comentarios: Descripción del algoritmo que implementa la función Describen cómo se resuelve la tarea encomendada a la función. Se incluyen dentro del código de la función Sólo describimos la esencia del algoritmo (tema II) Ejemplo. El mayor de tres números
int Max3 (int a, int b, int c){ int max; /* Calcular el máximo entre a y b -> max Calcular el máximo entre max y c */ if (a>=b) max = a; else max = b; if (c>max) max = c; return max; }
Funciones y Clases Funciones → Documentación de una función
Descripción de la cabecera de la función Describen qué tarea resuelve la función. También describen los parámetros (cuando no sea obvio).
232
Funciones y Clases Funciones → Documentación de una función
Incluiremos: Una descripción breve del cometido de la función. '
Consejo:
Se incluyen fuera , justo antes de la cabecera de la función
// Calcula el máximo de tres enteros int Max3 (int a, int b, int c){ int max; /* Calcular el máximo entre a y b Calcular el máximo entre max y c */ if (a>=b) max = a; else max = b; if (c>max) max = c; return max; }
233
&
$
Si no podemos resumir el cometido de una función en un par de líneas a lo sumo, entonces la función es demasiado compleja y posiblemente debería dividirse en varias funciones.
Una descripción de lo que representan los parámetros (salvo que el significado sea obvio) También incluiremos las restricciones que deben satisfacer los parámetros para que la función pueda ejecutarse sin problemas. Dichas restricciones se denominan precondiciones (preconditions) de la función. Por ejemplo, si trabajamos con el tipo int, para garantizar el cómputo correcto del denominador en la expresión ⎛ ⎝
a b
⎞ ⎠=
a! b!(a − b)!
debemos obligar a que a y b sean menores de 8
// Número combinatorio a sobre b // Precondición: a y b menores de 8 int Combinatorio(int a, int b){ return Factorial(a)/(Factorial(b) * Factorial(a-b)); }
%
Funciones y Clases Funciones → Documentación de una función
234
En el examen es imperativo incluir la descripción del algoritmo.
Ampliación: Consultad el capítulo 19 del libro Code Complete de Steve McConnell sobre normas para escribir comentarios claros y fáciles de mantener.
Funciones y Clases Funciones → Ámbito de un dato
III.1.4.
235
Ámbito de un dato
Ámbito (scope) de un dato (variable o constante) v es el conjunto de todas aquellas funciones que pueden acceder a v. El ámbito depende del lugar en el que se declara el dato. Lo que ya sabemos: Datos locales (declarados dentro de una función) El ámbito es la propia función. No se puede acceder a v desde otras funciones. Nota. Lo anterior se aplica también, como caso particular, a la función main Parámetros formales de una función Se consideran datos locales de la función.
Funciones y Clases Funciones → Ámbito de un dato
236
Lo nuevo (ni bueno ni malo):
Funciones y Clases Funciones → Ámbito de un dato
Debemos tener cuidado con la declaración de variables con igual nombre que otra definida en un ámbito superior. Prevalece la de ámbito más restringido → prevalencia de nombre (name hiding)
Datos declarados dentro de un bloque El ámbito es el propio bloque
int Suma_desde_0_hasta(int tope){ int suma;
while (condicion){ int i; ............ }
suma = 0; while (tope>0){ int suma = 0; suma = suma + tope; tope--; } return suma;
for (int i=0; i<10; i++){ ............ } En este caso, el ámbito de la variable termina con la llave } que cierra la estructura condicional, repetitiva, etc. '
Consejo:
&
237
// <-
suma (1)
// <- suma (2) // <- suma (2)
} int main(){ int tope = 5, resultado;
$
No abusar del uso de las variables definidas en un bloque. Restringidlo, por ahora, a variables auxiliares y contadores
resultado = Suma_desde_0_hasta(tope); cout << "Suma hasta " << tope << " = " << resultado;
%
// Imprime en pantalla lo siguiente: // Suma hasta 5 = 0 :-( }
Funciones y Clases Funciones → Ámbito de un dato
238
Lo nuevo (muy malo):
Funciones y Clases Funciones → Ámbito de un dato
239
#include <iostream> using namespace std;
Datos globales (global data) Son datos declarados fuera de las funciones y del main. El ámbito de los datos globales está formado por todas las funciones que hay definidas con posterioridad El uso de variables globales permite que todas las funciones las puedan modificar. Esto es pernicioso para la programación, fomentando la aparición de efectos laterales (side effects). Ejemplo. Supongamos un programa para la gestión de un aeropuerto. Tendrá dos funciones: GestionMaletas y ControlVuelos. El primero controla las cintas transportadoras y como máximo puede gestionar 50 maletas. El segundo controla los vuelos en el área del aeropuerto y como máximo puede gestionar 30. El problema es que ambos van a usar el mismo dato global Max para representar dichos máximos.
int Max; bool saturacion;
// Variables globales
:-((
void GestionMaletas(){ Max = 50; // <- !Efecto lateral! if (NumMaletasActual <= Max) [acciones maletas] else ActivarEmergenciaMaletas(); } void ControlVuelos(){ if (NumVuelosActual <= Max) [acciones vuelos] else{ ActivarEmergenciaVuelos(); saturacion = true; } } int main(){ Max = 30; // <- Máximo número de vuelos saturacion = false; while (!saturacion){ GestionMaletas(); ControlVuelos(); }
// Efecto lateral: Max = 50
Funciones y Clases Funciones → Ámbito de un dato
240
El que un lenguaje como C++ permita asignar un ámbito global a una variable, no significa que esto sea adecuado o recomendable. El uso de variables globales puede provocar graves efectos laterales, por lo que su uso está completamente prohibido.
Funciones y Clases Funciones → Las funciones y los principios de programación
III.1.5.
241
Las funciones y los principios de programación
¿Cuándo podemos decir que hemos construido software de calidad? No debe tener errores. Debe ser fácil de mantener y reutilizar en otros sitios.
Nota. El uso de constantes globales es menos perjudicial ya que, si no pueden modificarse, no se producen efectos laterales
Para conseguirlo, debemos respetar ciertas normas o principios de programación. Hemos visto el principio transversal de sencillez (página 156) y el de una única vez -no duplicar código- (página 88) Los lenguajes de programación proporcionan mecanismos de encapsulación (encapsulation) de código, para no tener que repetirlo. Las funciones son un mecanismo (pero no el único) de encapsulación. Definimos una única vez la función y la llamamos donde sea necesario, con distintos parámetros actuales. Esto hace que el código sea: Menos propenso a errores Después de un copy-paste del código a repetir, si queremos que funcione con otros datos, debemos cambiar una a una todas las apariciones de dichos datos, por lo que podríamos olvidar alguna. Más fácil de mantener Ante posibles cambios futuros, sólo debemos cambiar el código que hay dentro de la función. El cambio se refleja automáticamente en todos los sitios en los que se realiza una llamada a la función
Funciones y Clases Funciones → Las funciones y los principios de programación
242
Otro principio esencial de programación es el siguiente:
Principio de Programación: Ocultación de información (Information Hiding) Al usar un componente software, no deberíamos tener que preocuparnos de sus detalles de implementación.
Funciones y Clases Funciones → Las funciones y los principios de programación
Ejemplo. Recordad la definición de la función Combinatorio vista en la página 219
int Combinatorio(int a, int b){ return Factorial(a)/(Factorial(b) * Factorial(a-b)); } Es una implementación ineficiente, pues la fracción puede simplificarse. a!
El ámbito de los datos definidos en las funciones es un mecanismo (pero no el único) para cumplir el principio de ocultación de información. En la llamada a una función, por ejemplo:
hipotenusa = Hipotenusa(lado1, lado2);
b!(a − b)!
b!(a − b)!
=
a(a − 1) · · · (a − b + 1) b!
numerador = 1; fact_b = 1; for (int i=1 ; i<=b ; i++){ fact_b = fact_b*i; numerador = numerador*(a-i+1); }
Código propenso a errores, ya que podríamos modificar los datos locales por accidente y provocar errores (lo mismo que ocurre con los datos globales)
A lo largo del curso y de la carrera, se irán introduciendo otros principios. Por ejemplo, una forma más general de una única vez se denomina DRY (Do not Repeat Yourself) y hace alusión a no repetir (además de comportamientos) otros conceptos como por ejemplo restricciones lógicas.
a(a − 1) · · · (a − b + 1)(a − b)!
int Combinatorio(int a, int b){ int numerador, fact_b;
¿Qué pasaría si los datos locales fuesen accesibles desde otras funciones?
Ampliación:
=
Cambiemos la implementación de la función como sigue:
sólo nos preocupamos de saber su nombre y cómo se utiliza (los parámetros y el valor devuelto), pero no de saber cómo realiza su tarea.
Código difícil de mantener, ya que no podríamos cambiar la definición de una función A, suprimiendo, por ejemplo, alguna variable local de A a la que se accediese desde otra función B.
243
return numerador/fact_b; } Al no cambiar la cabecera de la función, no tenemos que cambiar nada en ningún sitio en el que sea llamada.
Funciones y Clases Funciones → Las funciones y los principios de programación
244
En la siguiente sección introducimos las clases. Proporcionan un mecanismo complementario que permite cumplir los principios de programación: Una única vez: Encapsulando datos y funciones en un mismo sitio. Ocultación de información: Definiendo nuevas reglas de ámbito para los datos.
Funciones y Clases Clases →
III.2. III.2.1.
245
Clases Motivación. Clases y Objetos
Las funciones pueden usarse desde cualquier sitio tras su declaración (puede decirse que son funciones globales (global functions)) y se comunican con el resto del programa a través de sus parámetros y del resultado que devuelven (siempre que no se usen variables globales) La Programación procedural (Procedural programming) establece técnicas para el correcto diseño de una solución utilizando funciones. El diseño de una solución a un problema de complejidad mediana se complica al tener que trabajar con multitud de funciones hermanas, todas al mismo nivel. La Programación dirigida a objetos (Object oriented programming) (PDO) surgió para resolver este problema, abordando la solución a un problema de programación de una forma distinta. En vez de identificar tareas a resolver (funciones), en PDO se identifican objetos que contienen datos y funciones. Las funciones contenidas en objetos se les llama métodos (methods). Éstos actúan sobre los datos miembro
Funciones y Clases Clases → Motivación. Clases y Objetos
246
Sobre los nombres: Programación estructurada (Structured programming). Metodología de programación en la que la construcción de un algoritmo se basa en el uso de las estructuras de control vistas en el tema II, prohibiendo, entre otras cosas, el uso de estructuras de saltos arbitrarios del tipo goto Programación modular (Modular programming): Cualquier metodología de programación que permita agrupar conjuntos de sentencias en módulos o paquetes. Programación procedural (Procedural programming). Metodología de programación modular en la que los módulos son las funciones. Muchos libros denominan, desafortunadamente, a la programación procedural, programación estructurada. Programación orientada a objetos (Object oriented programming). Metodología de programación modular en la que los módulos son objetos. Nota. No se usa el término programación funcional (functional programming) para referirse a la programación con funciones, ya que se acuñó para otro tipo de programación, a saber, un tipo de programación declarativa (declarative programming)
Funciones y Clases Clases → Motivación. Clases y Objetos
247
Objeto: una ventana en Windows Datos: posición esquina superior izquierda, altura, anchura, está_minimizada Métodos: Minimiza(), Maximiza(), Agranda(int tanto_por_ciento), etc. Objeto: una cuenta bancaria Datos: identificador de la cuenta, saldo actual, descubierto que se permite Métodos: Ingresa(double cantidad), Retira(double cantidad), etc. Objeto: un triángulo rectángulo Datos: los tres puntos que lo determinan A, B, C Métodos: ObtenerHipotenusa(), ObtenerSegmentoAB(), etc. Objeto: una fracción Datos: Numerador y denominador Métodos: Simplifica(), Súmale(Fracción otra_fracción), etc.
Funciones y Clases Clases → Motivación. Clases y Objetos
248
Una clase (class) es un tipo de dato definido por el programador para representar una entidad, es decir, objetos que comparten una serie de características. Un objeto (object) es un dato cuyo tipo de dato es una clase. También se dirá que un objeto es la instancia (instance) de una clase.
class MiClase{ .... }; int main(){ MiClase un_objeto_instancia_de_MiClase; MiClase otro_objeto_instancia_de_MiClase; .......
Funciones y Clases Clases → Encapsulación
III.2.2.
249
Encapsulación
En Programación Procedural, la encapsulación se materializa al incluir en el mismo componente software (la función) un conjunto de instrucciones. En PDO, la encapsulación se materializa al incluir en el mismo componente software (la clase) los datos y los métodos (funciones que actúan sobre dichos datos)
Una clase se compone de: Datos miembro (data member): son datos definidos en la clase.
class CuentaBancaria{ .... }; int main(){ CuentaBancaria una_cuenta; CuentaBancaria otra_cuenta; .......
// <- clase
Todos los objetos que pertenecen a una misma clase contienen dichos datos, pero tendrán un valor distinto en cada objeto. Determinan el estado (state) de cada objeto.
// <- un objeto // <- otro objeto
Los objetos una_cuenta y otra_cuenta existirán mientras esté ejecutándose main.
Funciones miembro (member functions) o métodos (methods): son funciones que pertenecen a la clase. La definición de los métodos es la misma para todos los objetos. Determinan el comportamiento (behaviour) de todos los objetos de la clase.
Funciones y Clases Clases â&#x2020;&#x2019; EncapsulaciĂłn
class MiClase{ public: int dato; void UnMetodo(){ .... } }; int main(){ MiClase un_objeto; un_objeto.dato = 4; un_objeto.UnMetodo(); .... }
250
Funciones y Clases Clases â&#x2020;&#x2019; EncapsulaciĂłn
251
! " "
#$
Funciones y Clases Clases → Encapsulación
252
III.2.2.1. Datos miembro Desde el main o desde otros objetos, accederemos a los datos y métodos de los objetos a través del nombre del objeto, un punto y el nombre del dato o método. Los datos miembro representan el estado del objeto en cada momento, es decir: Cada objeto tiene sus propios datos miembro. Los datos miembro no se comparten entre objetos. Ejemplo. Cuenta bancaria. Nota. Esta clase tiene problemas importantes de diseño que se irán arreglando a lo largo de este tema.
#include <iostream> #include <string> using namespace std; class CuentaBancaria{ public: double saldo; string identificador; }; int main(){ CuentaBancaria cuenta; string identificador_cuenta; double ingreso, retirada; cout << "\nIntroduce identificador a asignar a la cuenta:"; cin >> identificador_cuenta;
Funciones y Clases Clases → Encapsulación
253
cuenta.identificador = identificador_cuenta; // o bien directamente cin >> cuenta.identificador cout << "\nIntroduce cantidad inicial a ingresar: "; cin >> ingreso; cuenta.saldo = ingreso; cout << "\nIntroduce cantidad a ingresar: "; cin >> ingreso; cuenta.saldo = cuenta.saldo + ingreso; cout << "\nIntroduce cantidad a retirar: "; cin >> retirada; cuenta.saldo = cuenta.saldo - retirada;
CuentaBancaria otra_cuenta; otra_cuenta.saldo = 30000;
// <- No afecta a cuenta
Funciones y Clases Clases → Encapsulación
254
Funciones y Clases Clases → Encapsulación
255
Ejemplo.
III.2.2.2. Métodos El comportamiento de los métodos es el mismo para todos los objetos. Pueden modificar el estado del objeto sobre el que actúan. Los datos miembro se comportan como datos globales dentro del objeto, por lo que son directamente accesibles desde cualquier método definido dentro de la clase y no hay que pasarlos como parámetros a dichos métodos. Por tanto, ¿los métodos de las clases en PDO suelen tener menos parámetros que las funciones globales usadas en Programación Procedural? ¡Sí, por supuesto! Pero habrá muchos casos en los que los métodos necesiten información adicional que no esté descrita en el estado del objeto (por ejemplo, la cantidad a ingresar o retirar).
class CuentaBancaria{ public: double saldo; string identificador;
// <- clase
void Ingresa(double cantidad){ saldo = saldo + cantidad; } void Retira(double cantidad){ saldo = saldo - cantidad; } }; int main(){ CuentaBancaria una_cuenta; CuentaBancaria otra_cuenta;
// <- un objeto // <- otro objeto
una_cuenta.saldo = 90; una_cuenta.identificador = "02000345"; una_cuenta.Ingresa(25); otra_cuenta.saldo = 130; otra_cuenta.identificador = "01004532"; otra_cuenta.Ingresa(45); otra_cuenta.Retira(15); ...........
Funciones y Clases Clases â&#x2020;&#x2019; EncapsulaciĂłn
256
Para representar las clases se utiliza una caja con el nombre de la clase, debajo los datos miembro y debajo otra caja con los mĂŠtodos. Pondremos un signo + para indicar que son pĂşblicos (posteriormente se verĂĄn los privados)
Funciones y Clases Clases â&#x2020;&#x2019; EncapsulaciĂłn
257
Ejemplo.
class SegmentoDirigido{ public: double x_1, y_1, x_2, y_2; double Longitud(){ return sqrt((x_2 - x_1)*(x_2 - x_1) + (y_2 - y_1)*(y_2 - y_1)); } }; int main(){ SegmentoDirigido un_segmento; un_segmento.x_1 un_segmento.y_1 un_segmento.x_2 un_segmento.y_2
= = = =
3.4; 5.6; 4.5; 2.3;
cout << "\nLongitud del segmento = " << un_segmento.Longitud();
Funciones y Clases Clases â&#x2020;&#x2019; EncapsulaciĂłn
258
Ejercicio. DeďŹ nir sendos mĂŠtodos TrasladaHorizontal y TrasladaVertical para trasladar un segmento un nĂşmero de unidades.
Funciones y Clases Clases â&#x2020;&#x2019; EncapsulaciĂłn
259
NotaciĂłn Usaremos nombres para denotar las clases y verbos para los mĂŠtodos. No existe un consenso entre los distintos lenguajes de programaciĂłn, a la hora de determinar el tipo de letra usado para las clases, objetos, mĂŠtodos, etc. Nosotros seguiremos el siguiente: Tanto los identiďŹ cadores de las clases como de los mĂŠtodos empezarĂĄn con una letra mayĂşscula. Si el nombre es compuesto usaremos la primera letra de la palabra en mayĂşscula: CuentaBancaria Los identiďŹ cadores de los objetos, como cualquier otro dato, empezarĂĄn con minĂşscula. Si el nombre es compuesto usaremos el sĂmbolo de subrayado para separar los nombres: mi_cuenta_bancaria
Funciones y Clases Clases â&#x2020;&#x2019; EncapsulaciĂłn
260
Ejemplo. Controlemos ingresos Ăşnicamente positivos y retiradas de fondos inferiores al saldo.
Funciones y Clases Clases â&#x2020;&#x2019; EncapsulaciĂłn
261
public: double saldo; string identificador;
#include <iostream> #include <string> using namespace std;
void Ingresa(double cantidad){ if (cantidad > 0) saldo = saldo + cantidad; } void Retira(double cantidad){ if (cantidad > 0 && cantidad < saldo) saldo = saldo - cantidad; }
class CuentaBancaria{ public: double saldo; string identificador; }; }; int main(){ CuentaBancaria cuenta; double ingreso, retirada;
int main(){ CuentaBancaria cuenta; double ingreso, retirada;
cuenta.saldo = 0; cuenta.saldo = 0; cin >> ingreso; if (ingreso>0) cuenta.Ingresa(ingreso); cin >> retirada; if (retirada>0 && retirada<cuenta.saldo) cuenta.Retira(retirada); } Cada vez que realicemos un ingreso o retirada de fondos, habrĂĄ que realizar las anteriores comprobaciones. SoluciĂłn: Lo programamos Ăşnicamente una vez dentro del mĂŠtodo:
class CuentaBancaria{
cin >> ingreso; cuenta.Ingresa(ingreso); cin >> retirada; cuenta.Retira(retirada); }
Los mĂŠtodos permiten establecer la polĂtica de acceso a los datos miembro.
Funciones y Clases Clases → Encapsulación
262
III.2.2.3. Llamadas entre métodos dentro del propio objeto Ya sabemos cómo ejecutar los métodos de un objeto desde fuera del objeto.
Funciones y Clases Clases → Encapsulación
Ejemplo. Aplicar un interés a la cuenta bancaria.
class CuentaBancaria{ public: double saldo; string identificador;
Todos los métodos de un objeto pueden llamar a los métodos del mismo objeto. La llamada se realiza por el nombre del método, sin anteponer el nombre del objeto.
}; int main(){ CuentaBancaria una_cuenta; CuentaBancaria otra_cuenta;
// <- un objeto // <- otro objeto
una_cuenta.saldo = 90; una_cuenta.identificador = "02000345"; una_cuenta.Ingresa(25); una_cuenta.AplicaInteresPorcentual(3);
cout << una_cuenta.saldo;
// <- clase
void AplicaInteresPorcentual (int tanto_porciento){ Ingresa (saldo * tanto_porciento / 100.0); } void Ingresa(double cantidad){ if (cantidad > 0) saldo = saldo + cantidad; } void Retira(double cantidad){ if (cantidad > 0 && cantidad < saldo) saldo = saldo - cantidad; }
¿Pero pueden ejecutarse desde dentro del propio objeto? Por supuesto.
263
}
// (90 + 25)*1.03
Funciones y Clases Clases → Encapsulación
264
Ejercicio. Implementar el método Traslada para que traslade el segmento tanto en horizontal como en vertical.
class SegmentoDirigido{ public: double x_1, y_1, x_2, y_2; double Longitud(){ return sqrt((x_2 - x_1)*(x_2 - x_1) + (y_2 - y_1)*(y_2 - y_1)); } void TrasladaHorizontal(double unidades){ x_1 = x_1 + unidades; x_2 = x_2 + unidades; } void TrasladaVertical(double unidades){ y_1 = y_1 + unidades; y_2 = y_2 + unidades; } void Traslada( ){
} };
Funciones y Clases Clases → Ocultación de información
III.2.3.
265
Ocultación de información
En Programación Procedural, la ocultación de información se consigue con el ámbito local a la función (datos locales y parámetros formales). En PDO, la ocultación de información se consigue con el ámbito local a los métodos (datos locales y parámetros formales) y además con el ámbito private en las clases, tanto en los datos miembro como en los métodos.
Ampliación:
// En vertical y // en horizontal
En muchos textos académicos (no digamos en la web) suelen confundirse los términos encapsulación y ocultación de información. El término encapsulación también suele utilizarse en un contexto más restrictivo para referirse a la encapsulación ofrecido por la PDO. Puede verse una referencia muy completa en:
http://www.itmweb.com/essay550.htm
Funciones y Clases Clases â&#x2020;&#x2019; OcultaciĂłn de informaciĂłn
266
Funciones y Clases Clases â&#x2020;&#x2019; OcultaciĂłn de informaciĂłn
III.2.3.1. Ă mbitos pĂşblico y privado
Al declarar los miembros de una clase, se debe indicar su ĂĄmbito (scope), es decir, desde dĂłnde se van a poder utilizar: Ă mbito privado (Private scope). â&#x20AC;&#x201C; Se especiďŹ ca con la declaraciĂłn private (este es el ĂĄmbito por defecto si no se pone nada)
267
â&#x20AC;&#x201C; Los miembros privados sĂłlo se pueden usar desde dentro del objeto. No son accesibles desde fuera. â&#x20AC;&#x201C; Desde dentro de cualquier mĂŠtodo del objeto se acceden por su nombre. Ă mbito pĂşblico (Public scope). â&#x20AC;&#x201C; Se especiďŹ ca con la declaraciĂłn public â&#x20AC;&#x201C; Los miembros pĂşblicos son visibles dentro y fuera del objeto. â&#x20AC;&#x201C; Desde dentro de cualquier mĂŠtodo del objeto se acceden por su nombre y desde fuera, hay que anteponer el nombre del objeto seguido de un punto. Otros ĂĄmbitos que no se verĂĄn en esta asignatura.
En la representaciĂłn grĂĄďŹ ca de las clases con cajas se utiliza un signo para los miembros privados. La grĂĄďŹ ca de la izquierda serĂa una representaciĂłn con el estĂĄndar UML (UniďŹ ed Modeling Language) puro. Nosotros usaremos una adaptaciĂłn como aparece a la derecha.
Acceso a los datos miembro: Todos los mĂŠtodos de una clase A (independientemente de si son pĂşblicos o privados) , pueden acceder a los datos miembro de A (independientemente de si son pĂşblicos o privados). Todos los mĂŠtodos de una clase A (independientemente de si son pĂşblicos o privados), pueden llamar a los mĂŠtodos de A (independientemente de si son pĂşblicos o privados).
Funciones y Clases Clases → Ocultación de información
268
¿El ámbito public rompe la ocultación de información? No, si hablamos de métodos Sí, si hablamos de datos miembro. Por lo tanto habrá que evitar el ámbito público en los datos miembro. Veamos ejemplos que lo justifiquen. Ejemplo. ¿Cual es el problema en este código?
int main(){ CuentaBancaria cuenta; cuenta.saldo = 90; una_cuenta.Ingresa(25); cuenta.saldo = cuenta.saldo + 50; } Estamos permitiendo acceder al saldo de dos formas, directamente a través del dato miembro o a través del método Ingresa. Por tanto, corremos el peligro de poner
cuenta.saldo = -3000; El programador tiene la libertad de hacerlo de las dos formas (la mala y la buena) Solución: Cambiamos el ámbito público de los datos miembro a privado. Pero, entonces, debemos añadir: Métodos para modificar los datos miembro (aquellos que se deseen modificar desde fuera) Métodos para obtener el valor actual de los datos miembro (aquellos a los que se desee acceder desde fuera)
Funciones y Clases Clases → Ocultación de información
269
Esto nos obliga a escribir más líneas de código, pero es conveniente que lo hagamos, ya que así controlamos desde dónde se puede manipular el valor de los datos con los que trabajamos (algo que nos será muy útil para localizar posibles errores). Salvo casos muy justificados, no definiremos datos miembro públicos. Siempre serán serán privados. El acceso a ellos desde fuera de la clase se hará a través de métodos públicos de la clase.
Funciones y Clases Clases â&#x2020;&#x2019; OcultaciĂłn de informaciĂłn
270
Funciones y Clases Clases â&#x2020;&#x2019; OcultaciĂłn de informaciĂłn
// cuenta.saldo = 0; cuenta.Ingresa(0);
271
Error de compilaciĂłn
cin >> ingreso; cuenta.Ingresa(ingreso); cout << cuenta.Saldo(); // <- ParĂŠntesis, pues es un mĂŠtodo cout << cuenta.Identificador();
Funciones y Clases Clases â&#x2020;&#x2019; OcultaciĂłn de informaciĂłn
272
Lo que C++ permite:
Lo que nosotros haremos:
273
Ejemplo. Vamos a prohibir la construcciĂłn de segmentos degenerados (con un solo punto).
Funciones y Clases Clases â&#x2020;&#x2019; OcultaciĂłn de informaciĂłn
Interfaz (Interface) de una clase: Es el conjunto de mĂŠtodos pĂşblicos de dicha clase.
class SegmentoDirigido{ public: double x_1, y_1, x_2, y_2; ......... }; int main(){ SegmentoDirigido un_segmento; double origen_abscisa, origen_ordenada; double final_abscisa, final_ordenada; ........ if (!(origen_abscisa == final_abscisa && origen_ordenada == final_ordenada)){ un_segmento.x_1 = origen_abscisa; un_segmento.y_1 = origen_ordenada; un_segmento.x_2 = final_abscisa; un_segmento.y_2 = final_ordenada; } ....... } Problema: La comprobaciĂłn de igualdad de coordenadas la debemos hacer SIEMPRE, antes de crear el objeto segmento. Esto, obviamente viola el principio de una Ăşnica vez. SoluciĂłn: Lo encapsulamos en un mĂŠtodo pĂşblico SetCoordenadas que accederĂĄ a los datos privados.
void SetCoordenadas(double origen_abscisa, double origen_ordenada, double final_abscisa, double final_ordenada) Al ser ahora los datos privados, debemos proporcionar los mĂŠtodos que devuelvan su valor.
Funciones y Clases Clases â&#x2020;&#x2019; OcultaciĂłn de informaciĂłn
274
class SegmentoDirigido{ private: double x_1, y_1, x_2, y_2; public: void SetCoordenadas(double origen_abscisa, double origen_ordenada, double final_abscisa, double final_ordenada){ if (!(origen_abscisa == final_abscisa && origen_ordenada == final_ordenada)){ x_1 = origen_abscisa; y_1 = origen_ordenada; x_2 = final_abscisa; y_2 = final_ordenada; } } double OrigenAbscisa(){ return x_1; } double OrigenOrdenada(){ return y_1; } double FinalAbscisa(){ return x_2; } double FinalOrdenada(){ return y_2; } < Los mĂŠtodos Longitud, Traslada, TrasladaHorizontal y TrasladaVertical no varĂan > };
Funciones y Clases Clases â&#x2020;&#x2019; OcultaciĂłn de informaciĂłn
275
int main(){ SegmentoDirigido un_segmento, segmento_imposible; un_segmento.SetCoordenadas(3.4, 5.6, 4.5, 2.3); segmento_imposible.SetCoordenadas(3.4, 5.6, 3.4, 5.6); cout << " " << un_segmento.OrigenAbscisa(); un_segmento.TrasladaHorizontal(); cout << " " << un_segmento.OrigenAbscisa(); }
! " ! " # $ % & $ '
Observad que a travĂŠs del mĂŠtodo SetCoordenadas, obligamos al programador a asignar un valor a los cuatro puntos a la misma vez. En este ejemplo, parece que es lo adecuado ya que el estado de un segmento requiere, al menos, de la existencia de los dos puntos (4 valores).
Funciones y Clases Clases → Constructores
III.2.4.
276
III.2.4.1. Constructores por defecto (sin parámetros) Revisemos el código de la página 270.
CuentaBancaria cuenta; double ingreso; // ¾Qué valor inicial tiene saldo?
Necesitamos inicializar saldo a 0. #
Todas las acciones que queramos realizar en el momento de construir un objeto, se incluyen en el constructor (constructor) de la clase. "
277
class CuentaBancaria{ private: double saldo; string identificador; public: CuentaBancaria() // <- Constructor { saldo = 0; } <el resto de métodos no cambia> }; int main(){ CuentaBancaria cuenta; cout << cuenta.Saldo(); // Imprime 0 :-)
Constructores
cuenta.Ingresa(0);
Funciones y Clases Clases → Constructores
!
Se define dentro de la clase, en la sección public. Puede verse como un método especial void que se llama, obligatoriamente, igual que la clase. Pero no se pone void. Cuando se cree un objeto, el compilador ejecutará las instrucciones especificadas en el constructor. Se le pueden pasar parámetros. Si no se le pasan parámetros, se le denomina constructor por defecto (default constructor).
Una alternativa para la inicialización de los datos miembro es utilizar la lista de inicialización del constructor (constructor initialization list). Son construcciones del tipo <dato_miembro> (<valor inicial>) separadas por coma, antes del paréntesis del constructor, y con dos puntos al inicio:
public: CuentaBancaria() : saldo(0) { }
// saldo = 0;
Esta es la forma recomendada en C++ de inicializar los datos miembro.
Funciones y Clases Clases → Constructores
278
Ampliación: Con los tipos de datos integrados (int, double, etc) no hay diferencia entre la asignación saldo = 0 y la inicialización saldo(0). Pero cuando los datos miembro son objetos, sí la hay. Consultad, por ejemplo:
http://www.parashift.com/c++-faq-lite/ctors.html En la mayor parte de los lenguajes (Java, C#, etc) no hay lista de inicialización, ya que el tratamiento de los datos miembro en dichos lenguajes es distinto. En C++, al declarar dentro de una clase un dato miembro de otra clase, se crea el objeto automáticamente. En los otros lenguajes, no. En Java, por ejemplo, para dar un valor inicial a los datos miembro se utilizan los inicializadores de instancia (instance initializers).
Funciones y Clases Clases → Constructores
279
Si no definimos ningún constructor, C++ proporciona un constructor oculto -siempre sin parámetros- que, simplemente, no hace nada, pero permite crear objetos. A este constructor se le suele denominar constructor de oficio. No confundir con constructor por defecto que es todo aquel constructor que no tiene parámetros. III.2.4.2. Constructores con parámetros
Si queremos
obligar
a crear un objeto con ciertos valores iniciales,
podemos añadir parámetros al constructor:
class CuentaBancaria{ private: double saldo; string identificador; public: CuentaBancaria(double saldo_inicial) :saldo(saldo_inicial) { } <el resto de métodos no cambia> }; int main(){ // CuentaBancaria cuenta; <- Error compilación // Al haber definido nosotros un constructor, // C++ no proporciona el constructor por defecto CuentaBancaria una_cuenta(0); // Saldo inicial a 0 CuentaBancaria otra_cuenta(120);// Saldo inicial a 120
Funciones y Clases Clases → Constructores
280
Ejemplo. Si queremos, podemos añadir parámetros al constructor del ejemplo del segmento:
class SegmentoDirigido{ private: double x_1, y_1, x_2, y_2; public: SegmentoDirigido(double origen_abscisa, double origen_ordenada, double final_abscisa, double final_ordenada){ SetCoordenadas(origen_abscisa, origen_ordenada, final_abscisa, final_ordenada); } <Recordemos la definición de SetCoordenadas:> void SetCoordenadas(double origen_abscisa, double origen_ordenada, double final_abscisa, double final_ordenada){ if (!(origen_abscisa == final_abscisa && origen_ordenada == final_ordenada)){ x_1 = origen_abscisa; y_1 = origen_ordenada; x_2 = final_abscisa; y_2 = final_ordenada; } } <El resto de métodos no varía> }; int main(){ SegmentoDirigido un_segmento; // <- Error compilación SegmentoDirigido otro_segmento(3.4, 5.6, 4.5, 2.3); // OK SegmentoDirigido otro_segmento(3.4, 5.6, 3.4, 5.6); // Se crea el objeto // pero los datos miembro no tienen // un valor asignado (se quedan a basura)
Funciones y Clases Clases → Constructores
281
En la página 277, indicábamos que se recomienda usar la lista de inicialización. Sin embargo, en el ejemplo anterior hemos usado una llamada a un método de la clase (SetCoordenadas) en vez de la lista de inicialización. Esto es correcto ya que hay ciertas condiciones (establecidas en el condicional del método SetCoordenadas) bajo las cuales no queremos dar un valor inicial a los datos miembro. Esto nos lleva a otra pregunta: ¿Debemos permitir crear un objeto cuyo estado no sea válido? Es decir, que los datos miembro esenciales del objeto no tengan un valor asignado. Realmente no deberíamos permitirlo. La solución pasará por lanzar una excepción, tal y como se verá en el tema V.
void SetCoordenadas(double origen_abscisa, double origen_ordenada, double final_abscisa, double final_ordenada){ if (!(origen_abscisa == final_abscisa && origen_ordenada == final_ordenada)){ x_1 = origen_abscisa; y_1 = origen_ordenada; x_2 = final_abscisa; y_2 = final_ordenada; } else <lanzar una excepción> }
Funciones y Clases Clases → Constructores
282
III.2.4.3. Sobrecarga de constructores ¿Y si queremos permitir la creación de un objeto de varias formas distintas? Podemos usar varias versiones del constructor, pero deben diferenciarse en: O bien en el número de parámetros O bien en el tipo de los parámetros
class CuentaBancaria{ private: double saldo; string identificador; public: CuentaBancaria() :saldo(0) { } CuentaBancaria(double saldo_inicial) :saldo(saldo_inicial) { } <el resto de métodos no cambia> }; int main(){ CuentaBancaria cuenta; // Correcto. Primer constructor CuentaBancaria una_cuenta(0); // Correcto. Segundo constructor CuentaBancaria otra_cuenta(120);// Correcto. Segundo constructor
Funciones y Clases Clases → Constructores
283
En cualquier caso, para no complicar la clase CuentaBancaria, en las páginas siguientes optaremos por el uso de un único constructor, al que pasaremos el saldo inicial de la cuenta. El código completo de la clase quedaría:
class CuentaBancaria{ private: double saldo; string identificador; public: CuentaBancaria(double saldo_inicial) :saldo(saldo_inicial) { } string Identificador(){ return identificador; } void SetIdentificador(string identificador_cuenta){ identificador = identificador_cuenta; } double Saldo(){ return saldo; } void Ingresa(double cantidad){ if (cantidad > 0) saldo = saldo + cantidad; } void Retira(double cantidad){ if (cantidad > 0 && cantidad < saldo) saldo = saldo - cantidad; } };
Funciones y Clases Clases → Constructores
284
Ampliación: Otra forma de implementar la clase anterior sería utilizando un constructor con un parámetro por defecto/opcional (default/optional parameter). Simplemente, en la declaración de un método o constructor, se especifica un valor concreto para un parámetro. Éste será el valor que tomará el parámetro (de ahí el nombre por defecto) en el caso de que no se pase nada como parámetro actual.
class CuentaBancaria{
// valor por defecto 0 CuentaBancaria(double saldo_inicial = 0) :saldo(saldo_inicial) { } .........
}; int main(){ CuentaBancaria cuenta(35000); // Correcto CuentaBancaria cuenta; // Correcto. saldo_inicial = 0 Nota. Los parámetros por defecto no están disponibles en Java, y hay que recurrir a la sobrecarga de constructores, o utilizar recursos más avanzados como el patrón de diseño Builder.
Ampliación: Supongamos que dos sobrecargas del constructor tienen en común un conjunto de instrucciones. ¿Puede un constructor llamar al otro como si se tratasen de métodos usuales? Lamentablemente, en C++ no se podía hacer, por lo que había que definir un método privado con las instrucciones que se repiten y llamarlo dentro de los constructores. Afortunadamente, eso ha cambiado en el nuevo estándar de C++ 11 y un constructor sí puede llamar a otro. Por cierto, cuando se vea herencia, un constructor de una clase derivada sí puede llamar al constructor de la clase base. Es más, es obligatorio cuando el constructor de la clase base tiene parámetros. Una página interesante FAQ sobre constructores en C++:
http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.3
Funciones y Clases Clases → Diseño de una clase
III.2.5.
285
Diseño de una clase
Conceptos fundamentales vistos hasta ahora en la construcción de una clase: Ámbitos private y public. Encapsulación de datos y métodos dentro de una clase. Ahora bien, ¿cuanto metemos en una clase? ¿Qué preferimos: menos clases que hagan muchas cosas distintas o más clases que hagan cosas concretas? Por supuesto, las segundas. III.2.5.1. Principio de Responsabilidad Única
Principio en Programación Dirigida a Objetos. Principio de Responsabilidad Única (Single Responsability Principle) Un objeto debería tener una única responsabilidad, la cual debe estar completamente encapsulada en la definición de su clase.
A cada clase le asignaremos una única responsabilidad. Esto facilitará: La reutilización en otros contextos La modificación independiente de cada clase (o al menos paquetes de clases)
Funciones y Clases Clases â&#x2020;&#x2019; DiseĂąo de una clase
286
Funciones y Clases Clases â&#x2020;&#x2019; DiseĂąo de una clase
287
Mejor usamos dos clases:
HeurĂsticas: Si para describir el propĂłsito de la clase se usan mĂĄs de 20 palabras, puede que tenga mĂĄs de una responsabilidad. Si para describir el propĂłsito de una clase se usan las conjunciones y u o, seguramente tiene mĂĄs de una responsabilidad.
Ejemplo. La siguiente clase tiene dos responsabilidades: gestionar las operaciones bĂĄsicas de movimientos de saldo Y gestionar las transac-
ciones
! "# $
! "# $
Responsabilidad de CuentaBancaria: gestionar las operaciones bĂĄsicas de movimientos de saldo. Responsabilidad de Transaccion: gestionar una transacciĂłn bancaria.
Funciones y Clases Clases → Diseño de una clase
288
Funciones y Clases Clases → Diseño de una clase
Ejemplo. ¿Es correcto este diseño?:
class CuentaBancaria{ private: double saldo; string identificador; public: CuentaBancaria(double saldo_inicial) :saldo(saldo_inicial) { } string Identificador(){ return identificador; } void SetIdentificador(string identificador_cuenta){ identificador = identificador_cuenta; } double Saldo(){ return saldo; } void ImprimeSaldo(){ cout << "\nSaldo = " << saldo; } void ImprimeIdentificador(){ cout << "\nIdentificador = " << identificador; } }; La responsabilidad de esta clase es gestionar los movimientos de una cuenta bancaria Y comunicarse con el dispositivo de salida por defecto. Esto viola el principio de única responsabilidad. Esta clase no podrá reutilizarse en un entorno de ventanas, en el que cout no funciona.
289
Jamás mezclaremos en una misma clase métodos de cómputo con métodos que accedan a los dispositivos de entrada/salida. La violación de esta norma supone el suspenso automático.
Nota. El principio de responsabilidad única se viola con mucha más frecuencia de lo que sería deseable. La separación básica de C-E/S no se respeta en multitud de libros de texto.
¿Qué hacemos entonces para notificar que ha habido un problema en la ejecución de un método?
void Ingresa(double cantidad){ if (cantidad > 0) saldo = saldo + cantidad; else cout << "Cantidad a ingresar negativa"; } Esto nunca lo haremos. Ningún método de la clase CuentaBancaria debe acceder al periférico de entrada o salida. Resolveremos este problema cuando veamos el tratamiento de errores y excepciones en el tema V.
void Ingresa(double cantidad){ if (cantidad > 0) saldo = saldo + cantidad; else <lanzar excepción> }
Funciones y Clases Clases → Diseño de una clase
290
III.2.5.2. Cohesión de una clase Un concepto ligado al anterior es el de cohesión (cohesion), que mide cómo de relacionadas entre sí están las funciones desempeñadas por un componente software (paquete, módulo, clase o subsistema en general)
"
!
Ejemplo. Queremos gestionar la nómina de un trabajador. El salario base se calcula en función de su categoría laboral y de los años de antigüedad. El trabajador recibe un complemento por número de horas extraordinarias trabajadas. La cuantía por hora se calcula en función de su categoría laboral.
Puede apreciarse que hay grupos de métodos que acceden a otros grupos de datos miembro. Esto nos indica que pueden haber varias responsabilidades mezcladas en la clase anterior. Tendremos que encapsular cada una de las responsabilidades (grupos) en clases distintas (este ejemplo se ve con detalle en el tema V)
Trabajador DatosPersonales
291
Fecha Datos_del_Puesto Salario Ampliación: Se han diseñado numerosas métricas de cohesión, es decir, medidas numéricas que cuantifican el grado de cohesión de un componente software. Buscad en Internet class cohesion measure o measure of cohesion. La mayor parte de estas medidas se construyen basándose en el número de datos miembro a los que se accede desde los distintos métodos.
#
Por normal general, cuantos más métodos de una clase utilicen todos los datos miembros de la misma, mayor será la cohesión de la clase.
Funciones y Clases Clases → Diseño de una clase
Funciones y Clases Clases → Registros
III.2.6.
292
Registros
En algunas (pocas) ocasiones, la clase que queremos construir es tan simple que no tiene métodos (comportamiento) asociados. Únicamente sirve para agrupar algunos datos.
class Punto2D{ public: int abscisa; int ordenada; }; Para estos casos, usamos mejor un tipo específico proporcionado por C++ (también habitual en los lenguajes de programación): el tipo struct
struct Punto2D{ int abscisa; int ordenada; };
// <- Por defecto son public
Todos los datos miembro son públicos y se acceden con la notación punto, como en las clases.
int main(){ Punto2D punto; punto.abscisa = 4; punto.ordenada = 6; C++ también permite introducir datos privados e incluso métodos en un registro, pero nosotros nunca lo haremos. Dejaremos el tipo struct para aquellos (pocos) casos en los que únicamente queremos agrupar algunos datos, sin un comportamiento asociado.
Funciones y Clases Clases → Datos miembro constantes
III.2.7.
293
Datos miembro constantes
¿No sería deseable obligar a que en la creación de la cuenta, se le asigne un identificador y ya no se pueda cambiar? Pasamos el identificador al constructor y suprimimos el método SetIdentificador
class CuentaBancaria{ private: double saldo; string identificador; public: CuentaBancaria(string identificador_cuenta, double saldo_inicial) :saldo(saldo_inicial), identificador(identificador_cuenta) { } // El método SetIdentificador se ha suprimido <Los métodos Identificador, Saldo, Ingresa, Retira no varían> }; int main(){ string identificador_cuenta; double ingreso, retirada; cout << "\nIntroduce el identificador de la cuenta: "; cin >> identificador_cuenta; cout << "\nIntroduce cantidad inicial a ingresar: "; cin >> ingreso; CuentaBancaria cuenta(identificador_cuenta, ingreso); cout << "\nIntroduce cantidad a ingresar: "; cin >> ingreso; cuenta.Ingresa(ingreso);
Funciones y Clases Clases → Datos miembro constantes
294
cout << "\nIntroduce cantidad a retirar: "; cin >> retirada; cuenta.Retira(retirada);
Funciones y Clases Clases → Datos miembro constantes
295
III.2.7.1. Constantes a nivel de objeto
Las constantes a nivel de objeto se
declaran
dentro de la clase y se
definen (se les asigna un valor) en el constructor (obligatoriamente en
cout << "\nIdentificador = " << cuenta.Identificador(); cout << "\nSaldo = " << cuenta.Saldo(); cout << "\n\n"; system("pause"); } La solución anterior es adecuada, pero ¿no deberíamos obligar (además) a que el programador no pudiese cambiar por accidente (dentro de la clase CuentaBancaria) el dato miembro privado identificador? Tenemos que declararlo como una constante.
Tipos de constantes: Constantes a nivel de objeto (object constants): Cada objeto de una misma clase tiene su propio valor de constante. Constantes estáticas (static constants) o constantes a nivel de clase (class constants): Todos los objetos de una clase comparten el mismo valor de constante.
la lista de inicialización). Cada objeto de una misma clase tiene su propio valor de constante.
class CuentaBancaria{ private: double saldo; const string identificador; public: CuentaBancaria(string identificador_cuenta, double saldo_inicial) :saldo(saldo_inicial), identificador(identificador_cuenta) { } <Cualquier método>{ identificador = "lo que sea"; // Error compilación :-) } ........... // El método SetIdentificador se ha suprimido }; int main(){ CuentaBancaria una_cuenta("20310087370100001345", 100); ........... }
Funciones y Clases Clases → Datos miembro constantes
296
Funciones y Clases Clases → Datos miembro constantes
class MiClase{ private: const double CTE_REAL_PRIVADA; public: const double CTE_REAL_PUBLICA; MiClase(double un_real, double otro_real) :CTE_REAL_PRIVADA(un_real), CTE_REAL_PUBLICA(otro_real) { } }; int main(){ MiClase un_objeto(6.7, 4.5), otro_objeto(7.8, 9.3); un_objeto.CTE_REAL_PRIVADA = 3.5; // Error compilación // (private y cte) un_objeto.CTE_REAL_PUBLICA = 3.5; // Error compilación (cte) cout << un_objeto.CTE_REAL_PRIVADA;
// Error compilación // (private) cout << un_objeto.CTE_REAL_PUBLICA; // Imprime 4.5 cout << otro_objeto.CTE_REAL_PUBLICA; // Imprime 9.3
297
Funciones y Clases Clases â&#x2020;&#x2019; Datos miembro constantes
298
III.2.7.2. Constantes estĂĄticas (a nivel de clase) En este caso, todos los objetos de una clase comparten el mismo valor de constante. En UML, las constantes estĂĄticas se resaltan subrayĂĄndolas.
Constantes estĂĄticas NO enteras Se declaran dentro de la deďŹ niciĂłn de la clase:
class <nombre_clase>{ static const <tipo> <nombre_cte>; }; Se deďŹ nen fuera de la deďŹ niciĂłn de la clase:
const <tipo> <nombre_clase>::<nombre_cte> = <valor>;
Funciones y Clases Clases â&#x2020;&#x2019; Datos miembro constantes
299
class MiClaseStatic{ private: static const double CTE_REAL_PRIVADA; public: static const double CTE_REAL_PUBLICA; }; const double MiClaseStatic::CTE_REAL_PRIVADA = 6.7; const double MiClaseStatic::CTE_REAL_PUBLICA = 4.5; int main(){ MiClaseStatic un_objeto, otro_objeto; un_objeto.CTE_REAL_PUBLICA = 3.5; // Error compilaciĂłn cout << un_objeto.CTE_REAL_PUBLICA; // Imprime 4.5 cout << otro_objeto.CTE_REAL_PUBLICA; // Imprime 4.5 :: es el operador de resoluciĂłn de ĂĄmbito (scope resolution operator). Se utiliza, en general, para poder realizar la declaraciĂłn de un miembro dentro de la clase y su deďŹ niciĂłn fuera. Esto permite la compilaciĂłn separada (separate compilation). Se verĂĄ con mĂĄs detalle en el segundo cuatrimestre. OpiniĂłn personal: Separar la declaraciĂłn y la deďŹ niciĂłn rompe el principio DRY (Do not repeat yourself) ya que una modiďŹ caciĂłn en la declaraciĂłn obliga a una modiďŹ caciĂłn de la deďŹ niciĂłn (para mantener la coherencia). Esto era necesario antiguamente cuando se compilaba un conjunto de funciones o clases y se proporcionaba al cliente el ďŹ chero de texto con las cabeceras, pero no el cĂłdigo fuente, sino el compilado. Java utiliza variables de entorno para no tener que separar declaraciĂłn y deďŹ niciĂłn. En .NET se hace a travĂŠs de referencias a los ensamblados que contienen la deďŹ niciĂłn de las clases.
Funciones y Clases Clases → Datos miembro constantes
300
class CuentaBancaria{ private: double saldo; const string identificador; static const string IdBanco; public: CuentaBancaria(string identificador_cuenta, double saldo_inicial) :saldo(saldo_inicial), identificador(IdBanco + identificador_cuenta) { } string Identificador(){ return identificador; } double Saldo(){ return saldo; } void Ingresa(double cantidad){ if (cantidad > 0) saldo = saldo + cantidad; } void Retira(double cantidad){ if (cantidad > 0 && cantidad < saldo) saldo = saldo - cantidad; } void AplicaInteresPorcentual(int tanto_porciento){ Ingresa (saldo * tanto_porciento / 100.0); } }; const string CuentaBancaria::IdBanco = "20310381"; int main(){ CuentaBancaria una_cuenta("370100001345", 100);
Funciones y Clases Clases → Datos miembro constantes
301
string identificador_cuenta; double ingreso, retirada; cout << "\nIntroduce el identificador de la cuenta: "; cin >> identificador_cuenta; cout << "\nIntroduce cantidad inicial a ingresar: "; cin >> ingreso; CuentaBancaria otra_cuenta(identificador_cuenta, ingreso); cout << "\nIntroduce cantidad a ingresar: "; cin >> ingreso; otra_cuenta.Ingresa(ingreso); cout << "\nIntroduce cantidad a retirar: "; cin >> retirada; otra_cuenta.Retira(retirada); cout << "\nIdentificador = " << otra_cuenta.Identificador(); cout << "\nSaldo = " << otra_cuenta.Saldo(); } Ejercicio. ¿Qué cambios habría que hacer en la clase para permitir que una cuenta pudiese tener un descubierto limitado?
Funciones y Clases Clases → Datos miembro constantes
302
Ampliación:
Funciones y Clases Clases → Datos miembro constantes
303
Constantes estáticas enteras
Podríamos haber definido un método privado y llamarlo en la lista de inicialización:
class CuentaBancaria{ private: double saldo; const string identificador; static const string IdBanco; string ConstruyeIdentificador(string cadena_entrada}{ return IdBanco + cadena_entrada; } public: CuentaBancaria(string identificador_cuenta, double saldo_inicial) :saldo(saldo_inicial), identificador(ConstruyeIdentificador(identificador_cuenta)) { } ............ }; const string CuentaBancaria::IdBanco = "20310087"; Esto resulta especialmente útil en aquellos casos en los que debemos realizar inicializaciones más complejas que una simple asignación.
O bien como antes:
class MiClaseStaticInt{ private: static const int CTE_ENTERA_PRIVADA; public: static const int CTE_ENTERA_PUBLICA; }; const int MiClaseStaticInt::CTE_ENTERA_PUBLICA = 4; const int MiClaseStaticInt::CTE_ENTERA_PRIVADA = 6; O bien se definen en el mismo sitio de la declaración:
class MiClaseStaticInt{ private: static const int CTE_ENTERA_PRIVADA = 4; public: static const int CTE_ENTERA_PUBLICA = 6; }; Ampliación: C++ también permite definir variables estáticas. Son variables que pueden modificarse (y utilizarse) desde cualquier objeto de la clase en la que se definen. Al igual que ocurría con las variables globales cuando veíamos las funciones, debemos evitar este tipo de ámbito. Por otra parte, también se pueden definir métodos estáticos, que sólo pueden actuar sobre datos estáticos. Este tipo de métodos son similares a las funciones globales, por lo que su uso debe estar limitado a aquellos (pocos) casos en los que es preferible la definición de dichos tipos de funciones, en vez de la creación de una clase.
Funciones y Clases Clases → Datos miembro constantes
304
Bibliografía recomendada para este tema: A un nivel menor del presentado en las transparencias: – Capítulo 3 (para las funciones) y primera parte del capítulo 6 (para las clases) de Deitel & Deitel A un nivel similar al presentado en las transparencias:
Tema IV
– Capítulos 4, 5 y 6 de Mercer – Capítulo 4 de Garrido (sólo funciones, ya que este libro no incluye nada sobre clases)
Vectores y Matrices
A un nivel con más detalles: – Capítulos 10 y 11 de Stephen Prata.
Objetivos: Introduciremos los vectores de C++ que nos permitirán almacenar conjuntamente datos del mismo tipo. Los vectores de C++ son potentes pero peligrosos. Por ello, definiremos nuestra propia clase MiVector para trabajar con vectores de forma segura. De esta forma, afianzaremos los conceptos vistos en el tema de Clases. Construiremos distintos métodos de la clase MiVector. Veremos varios algoritmos de búsqueda y ordenación. Introduciremos los vectores de varias dimensiones. Daremos un breve resumen de las clases estándar vector y string.
Autor: Juan Carlos Cubero. Sugerencias: por favor, enviar un e-mail a JC.Cubero@decsai.ugr.es
Vectores y Matrices Fundamentos →
IV.1. IV.1.1.
306
Vectores y Matrices Fundamentos → Introducción
if (nota2 > media) cuantos++; if (nota3 > media) cuantos++;
Fundamentos Introducción
cout << "Media Aritmetica = " << media << endl; cout << cuantos << " alumnos han superado la media\n";
IV.1.1.1. Motivación Un vector (array) es un tipo de dato, compuesto de un número fijo de componentes del mismo tipo y donde cada una de ellas es directamente accesible mediante un índice. Ejemplo. Leed notas desde teclado y decid cuántos alumnos superan la media
#include <iostream> using namespace std;
}
Problema: ¿Qué sucede si queremos almacenar las notas de 50 alumnos? Número de variables imposible de sostener y recordar. Solución: Introducir un tipo de dato nuevo que permita representar dichas variables en una única estructura de datos, reconocible bajo un nombre único. notas[0]
int main(){ int cuantos; double nota1, nota2, nota3, media; notas=
cout << "Introduce nota 1: "; cin >> nota1; cout << "Introduce nota 2: "; cin >> nota2; cout << "Introduce nota 3: "; cin >> nota3; media = (nota1 + nota2 + nota3) / 3.0; cuantos = 0; if (nota1 > media) cuantos++;
307
2.4
notas[1]
4.9
notas[2]
6.7
Vectores y Matrices Fundamentos â&#x2020;&#x2019; IntroducciĂłn
308
Vectores y Matrices Fundamentos â&#x2020;&#x2019; Operaciones con Vectores
Otros ejemplos:
IV.1.1.2. DeclaraciĂłn
int main(){ const int NUM_REACTORES = 20; int longitud = 50;
<tipo> <identificador>[<N.Componentes>]; <tipo> indica el tipo de dato comĂşn a todas las componentes (elements/components) del vector. <N.Componentes> determina el nĂşmero de componentes del vector, al que llamaremos tamaĂąo (size) del vector. El nĂşmero de componentes debe conocerse a priori y no es posible alterarlo durante la ejecuciĂłn del programa. Pueden usarse literales Ăł constantes enteras (char, int, ...), pero nunca una variable
int TemperaturasReactores[NUM_REACTORES]; bool casados[40]; char NIF[8]; bool vector[longitud]; ..............
// // // //
Correcto. Correcto. Correcto. Error de compilaciĂłn.
}
IV.1.2.
No puede declararse un vector con un tamaĂąo variable
Operaciones con Vectores
IV.1.2.1.
int main(){ const int TOTAL_ALUMNOS = 3; double notas[TOTAL_ALUMNOS]; }
Consejo:
Fomentad el uso de constantes para especiďŹ car el tamaĂąo de los vectores.
Las componentes ocupan posiciones contiguas en memoria.
const int TOTAL_ALUMNOS = 3; double notas[TOTAL_ALUMNOS]; notas =
?
Acceso
Dada la declaraciĂłn: <tipo> <identificador>[<tamaĂąo>]; A cada componente se accede de la forma:
309
?
?
<IdentiďŹ cador de vector> [ <Ăndice> ] 0 â&#x2030;¤ Ăndice < tamaĂąo El Ăndice de la primera componente del vector es 0. El Ăndice de la Ăşltima componente es <tamaĂąo>-1.
Vectores y Matrices Fundamentos → Operaciones con Vectores
310
const int TOTAL_ALUMNOS = 3; double notas[TOTAL_ALUMNOS]; notas[0]
IV.1.2.2.
notas[1] ?
?
Vectores y Matrices Fundamentos → Operaciones con Vectores
?
?
notas[2] ?
?
notas notas[9] y notas['3'] no son componentes correctas. Cada componente es una variable más del programa, del tipo indicado en la declaración del vector. Por ejemplo, notas[0] es una variable de tipo double. Por lo tanto, con dichas componentes podremos realizar todas las operaciones disponibles (asignación, lectura con cin, pasarlas como parámetros actuales, etc). Lo detallamos en los siguientes apartados.
311
Asignación
No se permiten asignaciones globales sobre todos los elementos del vector. Las asignaciones se deben realizar componente a componente.
int main(){ const int TOTAL_ALUMNOS = 3; double notas[TOTAL_ALUMNOS]; notas = <lo que sea>
// Error de compilación
Asignación componente a componente: <Ident. vector> [ < índice> ] = <Expresión>; <Expresión> ha de ser del mismo tipo que el definido en la declaración del vector (o al menos compatible).
int main(){ const int TOTAL_ALUMNOS = 3; double notas[TOTAL_ALUMNOS]; notas[0] = 5.3; int i = 0; notas[i] = 5.3; notas[0]
notas[1] ?
5.3
?
?
notas
notas[2] ?
?
Vectores y Matrices Fundamentos → Operaciones con Vectores
int
312
Vectores y Matrices Fundamentos → Operaciones con Vectores
313
main(){ const int TOTAL_ALUMNOS = 3; double notas[TOTAL_ALUMNOS]; double una_nota; una_nota = notas[0]; cout << una_nota;
// double = double // Error lógico. Contiene basura
for (int i = 0 ; i < TOTAL_ALUMNOS ; i++) cin >> notas[i]; una_nota = notas[0];
// OK. Ya tiene un valor
} ¿Y si accedemos a una componente no reservada?
notas[15] = 7;
// Posible error grave en ejecución!
Veamos por qué: IV.1.2.3. Representación en memoria El compilador trata a un vector como un dato constante que almacena la dirección de memoria dónde empiezan a almacenarse las componentes. En un SO de 32 bits, para guardar una dirección de memoria, se usan 32 bits (4 bytes).
int main(){ double vector[3]; vector[0] = 4.5; vector[1] = 6.7; vector[2] = 8.2;
Es como si el programador hiciese la asignación siguiente:
vector = 0010002; Realmente, dicha asignación es realizada por el compilador que declara vector como un dato constante:
double vector[3];
// Compilador -> vector = 0010002;
vector = 00100004; vector[0] = 4.5;
// Error compilación. vector es cte. // Correcto
Para saber dónde está la variable vector[2], el compilador debe dar dos saltos, a partir de la dirección de memoria 0010002. Cada salto es de 8 bytes (suponiendo que los double ocupan 8 bytes). Por tanto, se va a la variable que empieza en la dirección 0010018
Vectores y Matrices Fundamentos → Operaciones con Vectores
314
Vectores y Matrices Fundamentos → Operaciones con Vectores
315
Ampliación: Un método típico para introducir virus es, precisamente, aprovecharse de que con este tipo de vectores, no se comprueba durante la ejecución el correcto acceso a las componentes. La idea consiste en introducir código de ejecución en dichas zonas de memoria (a las que no debería tenerse acceso) y cambiar la dirección de retorno de la función o método al inicio de dicho código intruso. Buscad en Internet desbordamiento del búfer de la pila (stack buffer overflow). Muchos gusanos (worms) utilizan este método (por ejemplo, el bien conocido Blaster)
En general, si declaramos
TipoBase vector[TAMANIO]; para poder acceder a vector[indice], el compilador dará un total de indice saltos tan grandes como diga el TipoBase, a partir de la primera posición de vector, es decir:
vector[indice] ≡ vector + indice * sizeof(<tipo base>) int main(){ const int TOTAL_ALUMNOS = 3; double vector[TOTAL_ALUMNOS]; vector[15] = 7;
// Posible error grave en ejecución!
'
$
Para aumentar la eficiencia, el compilador no comprueba que el índice de acceso a las componentes esté en el rango correcto, por lo que cualquier modificación de una componente inexistente tiene consecuencias imprevisibles. &
%
Vectores y Matrices Fundamentos → Operaciones con Vectores
316
Vectores y Matrices Fundamentos → Operaciones con Vectores
317
IV.1.2.4. Lectura y escritura
IV.1.2.5.
La lectura y escritura se realiza componente a componente.
No es necesario utilizar todas las componentes del vector. Los huecos suelen dejarse en la zona de índices altos. ¿Cómo sabemos en cada momento cuántos huecos hay?
Para leer con cin o escribir con cout todas las componentes utilizaremos un bucle, como por ejemplo:
#include <iostream> using namespace std; int
main(){ const int TOTAL_ALUMNOS = 10; double notas[TOTAL_ALUMNOS], media; for (int i = 0; i < TOTAL_ALUMNOS; i++){ cout << "Introducir nota del alumno " << i << ": "; cin >> notas[i]; } media = 0; for (int i = 0; i < TOTAL_ALUMNOS; i++){ media = media + notas[i]; } media = media / TOTAL_ALUMNOS; cout << "\nMedia = " << media;
}
Trabajando con el vector
O bien ponemos un valor especial en la última componente utilizada O bien usamos una variable entera, util que indique el número de componentes usadas. Los índices de las componentes utilizadas irán desde 0 hasta util-1.
#include <iostream> using namespace std; int main(){ const int TOTAL_ALUMNOS = 100; int util_notas; double notas[TOTAL_ALUMNOS], media; do{ cout << "Introduzca el número de alumnos (entre 1 y " << TOTAL_ALUMNOS << "): "; cin >> util_notas; }while (util_notas < 1 || util_notas > TOTAL_ALUMNOS); for (int i=0; i < util_notas; i++){ cout << "nota[" << i << "] --> "; cin >> notas[i]; }
Vectores y Matrices Fundamentos → Operaciones con Vectores
318
media=0; for (int i = 0; i < util_notas; i++){ media = media + notas[i]; } media = media/util_notas; cout << "\nMedia: " << media << endl; return 0; } IV.1.2.6. Inicialización C++ permite inicializar una variable vector en la declaración.
int vector[3]={4,5,6}; inicializa vector[0]=4, vector[1]=5, vector[2]=6
int vector[7]={3,5}; inicializa vector[0]=3, vector[1]=5 y el resto se inicializan a cero.
int vector[7]={0}; inicializa todas las componentes a cero.
int vector[7]={8}; inicializa la primera a 8 y el resto a cero.
int vector[]={1,3,9}; automáticamente el compilador asume int vector[3]
Vectores y Matrices La clase MiVector →
IV.2.
319
La clase MiVector
Inconvenientes de los vectores de C++: Permiten acceder a zonas de memoria no reservadas Debemos mantener con una variable aparte el número de componentes utilizadas Solución: Creemos nuestra propia clase MiVector. Nos planteamos qué interfaz debería tener
MiVectorCaracteresPeligroso vector; vector.Asigna(0,'h'); vector.Asigna(1,'o'); vector.Asigna(2,'l'); vector.Asigna(3,'a'); // hola Podemos dejar huecos involuntariamente:
MiVectorCaracteresPeligroso vector; vector.Asigna(0,'h'); vector.Asigna(1,'o'); vector.Asigna(1,'l'); vector.Asigna(3,'a'); // hl?a
:-(
Para evitar este problema, mejor usamos un método Aniade (podríamos haber optado por otra política, como por ejemplo, sólo permitir insertar al principio)
MiVectorCaracteres vector; vector.Aniade('h'); vector.Aniade('o'); vector.Aniade('l'); vector.Aniade('a');
Vectores y Matrices La clase MiVector â&#x2020;&#x2019;
320
! "
class MiVectorInts{ private: static const int TAMANIO = 50 ; int vector_privado[TAMANIO]; int total_utilizados; public: MiVectorInts() :total_utilizados(0) {} int TotalUtilizados() { ... } void Aniade(int nuevo) { ... } int Elemento(int indice) { ... } }; int main(){ MiVectorInts vector; vector.Aniade(2); vector.Aniade(-3); vector.Aniade(6); for (int i = 0; i < vector.TotalUtilizados(); i++) cout << vector.Elemento(i) << " ";
}
321
A destacar:
Una primera versiĂłn (para enteros):
vector.vector_privado[2] = 3; vector.vector_privado[70] = 3;
Vectores y Matrices La clase MiVector â&#x2020;&#x2019;
// Error compilaciĂłn // Error compilaciĂłn
La variable vector_privado como cualquier dato miembro, serĂĄ liberada por el compilador cuando el objeto de la clase MiVectorInts deje de existir. Debemos ďŹ jar de antemano el tope de componentes con la constante TAMANIO. Esto supone una restricciĂłn importante :-( La soluciĂłn pasa por usar vectores dinĂĄmicos (dynamic arrays) (segundo cuatrimestre) Sabemos que las constantes estĂĄticas enteras se declaran dentro de la clase pero pueden deďŹ nirse (darles un valor) dentro o fuera. Pero si son usadas para dimensionar un vector, deben deďŹ nirse obligatoriamente dentro de la clase.
Vectores y Matrices La clase MiVector →
322
Vectores y Matrices La clase MiVector →
Definamos los métodos:
Si queremos un vector de caracteres:
class MiVectorInts{ private: static const int TAMANIO = 50 ; int vector_privado[TAMANIO]; int total_utilizados; public: MiVectorInts() :total_utilizados(0) { } int TotalUtilizados(){ return total_utilizados; } void Aniade(int nuevo){ if (total_utilizados < TAMANIO){ vector_privado[total_utilizados] = nuevo; total_utilizados++; } } int Elemento(int indice){ return vector_privado[indice]; } };
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: MiVectorCaracteres() :total_utilizados(0) { } int TotalUtilizados(){ return total_utilizados; } void Aniade(char nuevo){ if (total_utilizados < TAMANIO){ vector_privado[total_utilizados] = nuevo; total_utilizados++; } } char Elemento(int indice){ return vector_privado[indice]; } }; Los lenguajes de programación ofrecen recursos para no tener que escribir el mismo código para tipos distintos :
¿Debería añadirse el siguiente método? Obviamente no. La gestión de total_utilizados es responsabilidad de la clase, y ha de actualizarse automáticamente, nunca de forma manual.
void SetTotalUtilizados(int nuevo_total){ total_utilizados = nuevo_total; }
323
Plantillas (Templates) en C++ Genéricos (Generics) en Java y .NET Por ahora, tendremos que duplicar el código :-(
Vectores y Matrices Algoritmos de Búsqueda →
IV.3.
324
Algoritmos de Búsqueda
Al proceso de encontrar un elemento específico en un vector se denomina búsqueda. Búsqueda secuencial (Linear/Sequential search). Técnica más sencilla. Búsqueda binaria (Binary search). Técnica más eficiente, aunque requiere que el vector esté ordenado.
IV.3.1.
Búsqueda Secuencial
Vectores y Matrices Algoritmos de Búsqueda → Búsqueda Secuencial
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: ..................................... int PrimeraOcurrencia (char buscado){ .... } }; int main(){ MiVectorCaracteres cadena; int pos_encontrado;
El vector no se supone ordenado.
cadena.Aniade('h'); cadena.Aniade('o'); cadena.Aniade('l'); cadena.Aniade('a');
Si el elemento a buscar se encuentra en el vector, devolvemos la posición correspondiente. En caso contrario, devolvemos un valor imposible de posición (por ejemplo, -1)
pos_encontrado = cadena.PrimeraOcurrencia('l'); if (pos_encontrado == -1) cout << "\nNo encontrado"; else cout << "\nEncontrado en la posición " << pos_encontrado; }
325
Vectores y Matrices Algoritmos de Búsqueda → Búsqueda Secuencial
Algoritmo:
326
Vectores y Matrices Algoritmos de Búsqueda → Búsqueda Secuencial
Observad que también podríamos haber puesto lo siguiente:
Ir recorriendo las componentes del vector a) Mientras no se encuentre el elemento buscado Y b) Mientras no lleguemos al final del mismo Devolver la posición donde se encontró o -1 en caso contrario class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: ..................................... int PrimeraOcurrencia (char buscado){ bool encontrado = false; int i = 0; while (i < total_utilizados && !encontrado) if (vector_privado[i] == buscado) encontrado = true; else i++; if (encontrado) return i; else return -1; } };
327
int PrimeraOcurrencia (char buscado){ ..... while (i < total_utilizados && !encontrado) if (Elemento(i) == buscado) .... } }; Como la llamada al método Elemento conlleva una recarga computacional, preferimos trabajar dentro de los métodos de la clase, accediendo directamente al vector privado con la notación corchete. '
Consejo:
&
$
Procurad implementar los métodos de una clase lo más eficientemente posible, pero sin que ello afecte a ningún cambio en la interfaz de ésta (los prototipos de los métodos públicos) %
Vectores y Matrices Algoritmos de Búsqueda → Búsqueda Secuencial
328
329
class MiVectorCaracteres{ .................................... int PrimeraOcurrencia (char buscado){ bool encontrado = false; int pos_encontrado;
Versión con un bucle for:
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: ..................................... int PrimeraOcurrencia (char buscado){ bool encontrado = false; int i; for (i = 0 ; i < total_utilizados && if (buscado == vector_privado[i]) encontrado = true; }
Vectores y Matrices Algoritmos de Búsqueda → Búsqueda Secuencial
for (int i = 0 ; i < total_utilizados if (buscado == vector_privado[i]){ encontrado = true; pos_encontrado = i; } } !encontrado ; i++){
&&
!encontrado ; i++){
if (encontrado) return pos_encontrado; else return -1; }
if (encontrado)
};
return i-1; else return -1;
Tanto la versión con bucle while de la página 326 como ésta son perfectamente válidas.
} }; El anterior código no es muy elegante. El bloque que busca el elemento debería proporcionar directamente la posición (y no uno más). Podemos usar una variable auxiliar pos_encontrado:
Batería de pruebas: Que el valor a buscar esté. Que el valor a buscar no esté. Que el valor a buscar esté varias veces (devuelve la primera ocurrencia). Que el valor a buscar esté en la primera o en la última posición. Que el vector esté vacío o tenga una única componente.
Vectores y Matrices Algoritmos de BĂşsqueda â&#x2020;&#x2019; BĂşsqueda Secuencial
330
A veces, podemos estar interesados en buscar entre una componente izquierda y otra derecha, ambas inclusive.
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: ..................................... int PrimeraOcurrenciaEntre (int pos_izda, int pos_dcha, char buscado){ int i = pos_izda; bool encontrado = false; while (i <= pos_dcha && !encontrado) if (vector_privado[i] == buscado) encontrado = true; else i++; if (encontrado) return i; else return -1; } };
Vectores y Matrices Algoritmos de BĂşsqueda â&#x2020;&#x2019; BĂşsqueda Secuencial
331
La clase puede proporcionar los dos mĂŠtodos PrimeraOcurrencia y PrimeraOcurrenciaEntre. Pero para no repetir cĂłdigo, cambiarĂamos la implementaciĂłn del primero como sigue:
int PrimeraOcurrencia (char buscado){ return PrimeraOcurrenciaEntre (0, total_utilizados - 1, buscado); } Hasta ahora, nos quedarĂa:
!" ! " # $ ! " % $ ! & " % $ # ! ' ' & "
Vectores y Matrices Algoritmos de Búsqueda → Búsqueda Binaria
IV.3.2.
332
Búsqueda Binaria
Precondiciones de uso: Se aplica sobre un vector ordenado. La cabecera del método no cambia, aunque para destacar que no es una búsqueda cualquiera, sino que necesita una precondición, cambiamos el nombre del identificador.
class MiVectorCaracteres{ .................................... int BusquedaBinaria (char buscado){ ..... } Algoritmo:
Vectores y Matrices Algoritmos de Búsqueda → Búsqueda Binaria
333
Ejemplo. Buscar el 40 0
1
2
3
4
5
6
7
8
−8 4
5
9 12 18 25 40 60 dch=8
izq=0 centro=4
40 > 12 5
6
7
8
18 25 40 60 izq=5
El elemento a buscar se compara con el elemento que ocupa la mitad del vector. Si coinciden, se habrá encontrado el elemento. En otro caso, se determina la mitad del vector en la que puede encontrarse. Se repite el proceso con la mitad correspondiente.
dch=8 centro=6
40 > 25 7
8
40 60 izq=7
dch=8 centro=7
Encontrado
Vectores y Matrices Algoritmos de Búsqueda → Búsqueda Binaria
334
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: ..................................... int BusquedaBinaria (char buscado){ int izqa, dcha, centro; izqa = 0; dcha = total_utilizados-1; centro = (izqa+dcha)/2; while (izqa <= dcha && vector_privado[centro] != buscado) { if (buscado < vector_privado[centro]) dcha = centro-1; else izqa = centro+1; centro = (izqa+dcha)/2; } if (izqa > dcha) return -1; else return centro; } };
Vectores y Matrices Algoritmos de Búsqueda → Otras búsquedas
IV.3.3.
335
Otras búsquedas
Ejemplo. Calcular el mínimo elemento de un vector.
v = (h,b,c,f,d,?,?,?) Mejor que devolver el elemento b, devolvemos su posición 1 #
Consejo:
"
Cualquier método que busque algo en un vector, debe devolver la posición en la que se encuentra y no el elemento en sí.
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: MiVectorCaracteres() :total_utilizados(0) {} int PosicionMinimo(){ .... } ................. }; int main(){ MiVectorCaracteres cadena; int pos_minimo; cadena.Aniade('h'); ......... pos_minimo = cadena.PosicionMinimo();
!
Vectores y Matrices Algoritmos de Búsqueda → Otras búsquedas
Algoritmo:
Inicializar minimo a la primera componente Recorrer el resto de componentes v[i] (i>0) Actualizar, en su caso, el mínimo y la posición
336
Vectores y Matrices Algoritmos de Búsqueda → Otras búsquedas
337
Ejemplo. Calcular el mínimo elemento de un vector, pero pasándole al método las componentes izda y dcha donde se quiere trabajar.
v = (h,b,t,c,f,i,d,f,?,?,?)
izda=2 , dcha=5
PosicionMinimo(izda,dcha) devuelve la posición 3 (índice del vector) class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: ................. int PosicionMinimo(){ int posicion_minimo; char minimo;
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: ........ // Precond: 0 <= izda <= dcha < total_utilizados int PosicionMinimoEntre(int izda, int dcha){ int posicion_minimo; char minimo;
minimo = vector_privado[0]; posicion_minimo = 0;
minimo = vector_privado[izda]; posicion_minimo = izda;
for (int i = 1; i < total_utilizados ; i++){ if (vector_privado[i] < minimo){ minimo = vector_privado[i]; posicion_minimo = i; } }
for (int i = izda+1 ; i <= dcha ; i++) if (vector_privado[i] < minimo){ minimo = vector_privado[i]; posicion_minimo = i; } return posicion_minimo;
return posicion_minimo; } };
} };
Vectores y Matrices Algoritmos de Búsqueda → Otras búsquedas
int main(){ MiVectorCaracteres cadena; int pos_minimo;
338
Vectores y Matrices Otros métodos de la clase MiVector →
IV.4.
339
Otros métodos de la clase MiVector
Ejemplo. Insertar un elemento en una posición concreta de un vector.
cadena.Aniade('h'); ......... pos_minimo = cadena.PosicionMinimoEntre(1,3);
v = (t,e,c,a,?,?,?) v = (t,e,r,c,a,?,?)
pos_insercion = 2
valor_nuevo = r
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: ......... void Inserta(int pos_insercion, char valor_nuevo){...} ......... }; int main(){ MiVectorCaracteres cadena; cadena.Aniade('t'); cadena.Aniade('e'); cadena.Aniade('c'); cadena.Aniade('a'); cadena.Inserta(2, 'r');
Vectores y Matrices Otros métodos de la clase MiVector →
Algoritmo:
Recorrer (i) las componentes posteriores a pos_insercion hasta llegar al final de v v[i] = v[i-1]; i++; v[pos_insercion] = valor; ¿Dónde está el error en el algoritmo anterior?
Recorrer (i) las componentes desde el final del vector hasta llegar a pos_insercion v[i] = v[i-1]; i--; v[pos_insercion] = valor; class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: .......... void Inserta(int pos_insercion, char valor_nuevo){ for (int i = total_utilizados ; i > pos_insercion ; i--) vector_privado[i] = vector_privado[i-1]; vector_privado[pos_insercion] = valor_nuevo; total_utilizados++; } };
340
Vectores y Matrices Otros métodos de la clase MiVector →
Batería de pruebas: Que el vector esté vacío Que el elemento a insertar se sitúe el último Que el elemento a insertar se sitúe el primero Que el número de componentes utilizadas sea igual al tamaño Un análisis de estos casos nos lleva a proponer la siguiente mejora:
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: .......... void Inserta(int pos_insercion, char valor_nuevo){ if (total_utilizados < TAMANIO && pos_insercion >= 0 && pos_insercion <= total_utilizados){ for (int i = total_utilizados ; i > pos_insercion ; i--) vector_privado[i] = vector_privado[i-1]; vector_privado[pos_insercion] = valor_nuevo; total_utilizados++; } }
341
Vectores y Matrices Otros métodos de la clase MiVector →
342
Sobre las precondiciones de los métodos Dentro de un método, ¿debemos comprobar siempre que se satisfacen sus precondiciones? Depende.
Sin embargo, puede evitar errores graves
Consejo:
$
Si la violación de una precondición de un método puede provocar errores de ejecución graves, dicha precondición debe comprobarse dentro del método. En otro caso, puede omitirse.
&
343
void Aniade(char nuevo){ vector_privado[total_utilizados] = nuevo; total_utilizados++; } Si el índice pasado a Elemento no es correcto, devolverá un valor basura, pero no corremos el peligro de modificar una componente no reservada.
La comprobación supone una carga computacional adicional
'
Vectores y Matrices Otros métodos de la clase MiVector →
%
class MiVectorCaracteres{ ......... void Aniade(char nuevo){ if (total_utilizados < TAMANIO){ vector_privado[total_utilizados] = nuevo; total_utilizados++; } } char Elemento(int indice){ return vector_privado[indice]; } }; Si no comprobamos que existan componentes sin usar, la ejecución de Aniade podría acceder a componentes no reservadas con el peligro que eso conlleva.
char Elemento(int indice){ if (indice >= 0 && indice <= total_utilizados) return vector_privado[indice]; }
//No es necesario
Por la misma razón, decidimos no comprobar las precondiciones dentro de los métodos:
int PrimeraOcurrenciaEntre (int pos_izda, int pos_dcha, char buscado) int PosicionMinimoEntre(int izda, int dcha){ Lo mismo ocurrió con
int BusquedaBinaria (char buscado) Además, en este caso, comprobar la precondición de que el vector esté ordenado es una operación muy costosa. Por contra, sí comprobamos la precondición en el método
void Inserta(int pos_insercion, char valor_nuevo) para impedir insertar en componentes no reservadas.
Vectores y Matrices Otros mĂŠtodos de la clase MiVector â&#x2020;&#x2019;
344
Por ahora, nos quedarĂa:
!" ! " # $ ! " % $ ! & " % $ # ! ' ' & " ( ) ( ! & " % $ !" % $ # ! ' " ! ' "
Vectores y Matrices Otros mĂŠtodos de la clase MiVector â&#x2020;&#x2019;
345
} int PrimeraOcurrenciaEntre (int pos_izda, int pos_dcha, char buscado){ int i = pos_izda; bool encontrado = false; while (i <= pos_dcha && !encontrado) if (vector_privado[i] == buscado) encontrado = true; else i++; if (encontrado) return i; else return -1; } int PrimeraOcurrencia (char buscado){ return PrimeraOcurrenciaEntre (0, total_utilizados - 1, buscado); } int PosicionMinimoEntre(int izda, int dcha){ int posicion_minimo; char minimo; minimo = vector_privado[izda]; posicion_minimo = izda; for (int i = izda+1 ; i <= dcha ; i++) if (vector_privado[i] < minimo){ minimo = vector_privado[i]; posicion_minimo = i; } return posicion_minimo;
Vectores y Matrices Otros métodos de la clase MiVector →
346
} int PosicionMinimo(){ return PosicionMinimoEntre(0, total_utilizados-1); } int BusquedaBinaria (char buscado){ int izqa, dcha, centro; izqa = 0; dcha = total_utilizados-1; centro = (izqa+dcha)/2; while (izqa <= dcha && vector_privado[centro] != buscado) { if (buscado < vector_privado[centro]) dcha = centro-1; else izqa = centro+1; centro = (izqa+dcha)/2; } if (izqa > dcha) return -1; else return centro; } void Inserta(int pos_insercion, char valor_nuevo){ if (total_utilizados < TAMANIO && pos_insercion >= 0 && pos_insercion <= total_utilizados){ for (int i = total_utilizados ; i > pos_insercion ; i--) vector_privado[i] = vector_privado[i-1]; vector_privado[pos_insercion] = valor_nuevo;
Vectores y Matrices Otros métodos de la clase MiVector →
total_utilizados++; } } };
347
Vectores y Matrices Algoritmos de Ordenación →
IV.5.
348
Algoritmos de Ordenación
La ordenación es un procedimiento mediante el cual se disponen los elementos de un vector en un orden especificado, tal como orden alfabético u orden numérico. Aplicaciones: Mostrar los ficheros de un directorio ordenados alfabéticamente, ordenar los resultados de una búsqueda en Internet (PageRank es un método usado por Google que asigna un número de importancia a una página web), etc.
Vectores y Matrices Algoritmos de Ordenación →
#include <iostream> using namespace std; class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[]; int total_utilizados; public: ......... void Ordena(){ ....... }
Técnicas de ordenación: – Ordenación interna: Todos los datos están en memoria principal durante el proceso de ordenación. ◦ En la asignatura veremos métodos básicos como Inserción, Selección e Intercambio.
}; int main(){ MiVectorCaracteres cadena; cadena.Aniade('h'); cadena.Aniade('o'); cadena.Aniade('l'); cadena.Aniade('a'); cadena.Ordena(); .......
◦ En el segundo cuatrimestre se verán métodos más avanzados como Quicksort o Mergesort. – Ordenación externa: Parte de los datos a ordenar están en memoria externa mientras que otra parte está en memoria principal siendo ordenada. Se verán en el segundo cuatrimestre. Aproximaciones: Supondremos que los datos a ordenar están en un vector. – Construir un segundo vector con las componentes del primero, pero ordenadas – Modificar el vector original, cambiando de sitio las componentes. Esta aproximación es la que seguiremos.
349
}
// -> 'h' 'o' 'l' 'a' // -> 'a' 'h' 'l' 'o'
Vectores y Matrices Algoritmos de OrdenaciĂłn â&#x2020;&#x2019; OrdenaciĂłn por SelecciĂłn
350
Vectores y Matrices Algoritmos de OrdenaciĂłn â&#x2020;&#x2019; OrdenaciĂłn por SelecciĂłn
Idea comĂşn a algunos algoritmos de ordenaciĂłn: El vector se dividirĂĄ en dos sub-vectores. El de la izquierda, contendrĂĄ componentes ordenadas. Las del sub-vector derecho no estĂĄn ordenadas.
23 78 45 8 32 56
Vector Original
8
Tras la pasada 1
Se irĂĄn cogiendo componentes del sub-vector derecho y se colocarĂĄn adecuadamente en el sub-vector izquierdo. OrdenaciĂłn por selecciĂłn (Selection sort): En cada iteraciĂłn, se selecciona la componente mĂĄs pequeĂąa del sub-vector derecho y se coloca al ďŹ nal del sub-vector izquierdo.
78 45 23 32 56
45 78 32 56
Tras la pasada 2
8 23 32 78 45 56
Tras la pasada 3
8 23 32 45
Tras la pasada 4
8 23
IV.5.1.
OrdenaciĂłn por SelecciĂłn
78 56
Algoritmo:
Recorrer todos los elementos v[izda] de v Hallar la posición pos_min del menor elemento del subvector delimitado por las componentes [izda , total_utilizados-1] ½ambas inclusive! Intercambiar v[izda] con v[pos_min]
8 23 32 45 56 78
Tras la pasada 5
351
Vectores y Matrices Algoritmos de Ordenación → Ordenación por Selección
352
En el bucle principal no es necesario recorrer todos los elementos de v, ya que si sólo queda 1 componente, el vector está ordenado. Por lo tanto, el bucle principal será de la forma izda<total_utilizados
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; void IntercambiaComponentes_en_Posiciones(int pos_izda, int pos_dcha){ char intercambia; intercambia = vector_privado[pos_izda]; vector_privado[pos_izda] = vector_privado[pos_dcha]; vector_privado[pos_dcha] = intercambia; } public: void Ordena_por_Seleccion(){ int pos_min; for (int izda=0 ; izda<total_utilizados ; izda++){ pos_min = PosicionMinimoEntre (izda, total_utilizados-1); IntercambiaComponentes_en_Posiciones (izda, pos_min); } } ............. Nota. Podríamos optar por elevar a público el método que intercambia posiciones. Pero entonces deberíamos comprobar (dentro de dicho método) el cumplimiento de las precondiciones (posiciones en el rango adecuado). Para una operación tan costosa como la ordenación, conllevaría una recarga excesiva (e innecesaria si nos preocupamos de definir adecuadamente los métodos que utilizan el método para intercambiar posiciones)
Vectores y Matrices Algoritmos de Ordenación → Ordenación por Selección
353
Batería de pruebas: Que el vector esté vacío Que el vector sólo tenga una componente Que tenga un número de componentes par/impar Que el vector ya estuviese ordenado Que el vector ya estuviese ordenado, pero de mayor a menor Que el vector tenga todas las componentes iguales Que tenga dos componentes iguales al principio Que tenga dos componentes iguales al final Que tenga dos componentes iguales en medio Nota. Esta batería también es aplicable al resto de algoritmos de ordenación Applet de demostración del funcionamiento:
http://www.sorting-algorithms.com/
Vectores y Matrices Algoritmos de OrdenaciĂłn â&#x2020;&#x2019; OrdenaciĂłn por InserciĂłn
IV.5.2.
354
Vectores y Matrices Algoritmos de OrdenaciĂłn â&#x2020;&#x2019; OrdenaciĂłn por InserciĂłn
OrdenaciĂłn por InserciĂłn
OrdenaciĂłn por inserciĂłn (Insertion sort): El vector se divide en dos subvectores: el de la izquierda ordenado, y el de la derecha desordenado. Cogemos el primer elemento del subvector desordenado y lo insertamos de forma ordenada en el subvector de la izquierda (el ordenado).
23
78 45
8 32 56
Vector Original
45 8 32 56
Tras la pasada 1
23 78
23 45 78
8 32 56
Tras la pasada 2
Nota. La componente de la posiciĂłn (primer elemento del subvector desordenado) serĂĄ reemplazada por la anterior (despuĂŠs de desplazar)
8 23 45 78
32 56
Tras la pasada 3
8 23 32 45 78 56
Tras la pasada 4
8 23 32 45 56 78
Tras la pasada 5
Algoritmo:
355
Vectores y Matrices Algoritmos de OrdenaciĂłn â&#x2020;&#x2019; OrdenaciĂłn por InserciĂłn
356
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: ............. void Ordena_por_Insercion(){ int izda, i; char a_desplazar;
Vectores y Matrices Algoritmos de OrdenaciĂłn â&#x2020;&#x2019; OrdenaciĂłn por Intercambio Directo (MĂŠtodo de la Burbuja)
IV.5.3.
OrdenaciĂłn por Intercambio Directo (MĂŠtodo de la Burbuja)
OrdenaciĂłn por intercambio directo (burbuja) (Bubble sort): Al igual que antes, a la izquierda se va dejando un subvector ordenado. Desde el ďŹ nal y hacia atrĂĄs, se van comparando elementos dos a dos y se deja a la izquierda el mĂĄs pequeĂąo (intercambiĂĄndolos)
for (izda = 1; izda < total_utilizados; izda++){ a_desplazar = vector_privado[izda];
for (i = izda; i>0 && a_desplazar < vector_privado[i-1]; i--) vector_privado[i] = vector_privado[i-1]; vector_privado[i] = a_desplazar; } } };
357
Algoritmo:
Ir fijando el inicio del subvector derecho con un contador izda desde 0 hasta total_utilizados - 1 Recorrer el subvector de la derecha desde el final hasta el principio (izda) con un contador i Si v[i] < v[i-1] intercambiarlos
Vectores y Matrices Algoritmos de Ordenación → Ordenación por Intercambio Directo (Método de la Burbuja)
358
Vectores y Matrices Algoritmos de Ordenación → Ordenación por Intercambio Directo (Método de la Burbuja)
Resto de Pasadas
Primera Pasada
23 78 45 8 32 56
Vector Original 23 78 45 8 32 56
Vector Original
8
Tras la pasada 1
No intercambiar
23 78 45 8 32 56
23 78 45 32 56
No intercambiar
23 78 45 8 32 56 Intercambiar
8 23
32 78 45 56
Tras la pasada 2
8 23 32 45 78 56
Tras la pasada 3
8 23 32 45
Tras la pasada 4
Proceso Durante Pasada 1
23 78 8 45 32 56 Intercambiar
23 8 78 45 32 56
56 78
Intercambiar
8 23 78 45 32 56
Tras la pasada 1
8 23 32 45 56 78
Tras la pasada 5
359
Vectores y Matrices Algoritmos de Ordenación → Ordenación por Intercambio Directo (Método de la Burbuja)
360
Vectores y Matrices Algoritmos de Ordenación → Ordenación por Intercambio Directo (Método de la Burbuja)
Primera Aproximación
Segunda Aproximación
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados;
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados;
void IntercambiaComponentes_en_Posiciones(int pos_izda, int pos_dcha){ char intercambia; intercambia = vector_privado[pos_izda]; vector_privado[pos_izda] = vector_privado[pos_dcha]; vector_privado[pos_dcha] = intercambia; } public: ............. void Ordena_por_Burbuja(){ int izda, i;
void IntercambiaComponentes_en_Posiciones(int pos_izda, int pos_dcha){ ............. } public: ............. void Ordena_por_BurbujaMejorado(){ int izda, i; bool cambio; cambio = true; for (izda = 0; izda < total_utilizados && cambio; izda++){ cambio = false;
for (izda = 0; izda < total_utilizados; izda++) for (i = total_utilizados-1 ; i>izda ; i--) if (vector_privado[i] < vector_privado[i-1]) IntercambiaComponentes_en_Posiciones(i, i-1);
for (i = total_utilizados-1; i > izda; i--) if (vector_privado[i] < vector_privado[i-1]){ IntercambiaComponentes_en_Posiciones(i, i-1); cambio = true; }
} }; Mejora. Si en una pasada del bucle más interno no se produce ningún intercambio, el vector ya está ordenado. Lo comprobamos con una variable lógica.
361
} } };
Vectores y Matrices Las clases vector y string →
IV.6.
362
Las clases vector y string
Vectores y Matrices Las clases vector y string → La clase vector
363
Resumen de los métodos que usaremos:
push_back. Añade un elemento.
IV.6.1.
La clase vector
Las plantillas (templates) permiten crear clases que trabajan con varios tipos de datos, tal y como ya indicamos en la página 323. Podríamos definir nuestra propia plantilla MiVector para trabajar en una misma clase con enteros, caracteres, etc. Esto se verá en el segundo cuatrimestre. C++ ofrece una plantilla estándar llamada vector que permite trabajar con vectores de forma segura, y con capacidad dinámica. Pertenece a la biblioteca estándar STL (Standard Template Library). Debemos incluir la biblioteca vector:
#include <vector> Para definir un objeto de la clase vector, se especifica el tipo base del vector:
int main(){ vector <double> vector_reales; vector <int> vector_enteros; vector <MiClase> vector_mi_clase; ......... La plantilla vector proporciona métodos para añadir componentes, acceder a ellas, etc. A diferencia de los vectores que hemos visto (declaración con corchete), cuando se añaden o suprimen componentes, se reajusta el tamaño de forma automática.
size. Devuelve el número de elementos que, en ese momento, están siendo utilizados. capacity. Devuelve el máximo número de elementos que, en ese momento, están reservados para poder utlizarse. pop_back. Elimina el último elemento. front y back. Devuelven el primero y último elemento respectivamente. empty. Nos dice si el vector está vacío. clear. Borra todos los elementos. Hay dos formas de acceder a las componentes: Con notación corchete: Cuando se accede a una componente que está por encima de size(), el comportamiento es imprevisible. Con el método at: Cuando se accede a una componente que está por encima de size(), se produce un error en tiempo de ejecución (salta una excepción)
Vectores y Matrices Las clases vector y string â&#x2020;&#x2019; La clase vector
364
Vectores y Matrices Las clases vector y string â&#x2020;&#x2019; La clase vector
365
#include <iostream> #include <vector> using namespace std;
Muchos mĂŠtodos de vector usan o devuelven iteradores (iterators), que no se ven hasta el segundo cuatrimestre. Estos iteradores permiten recorrer de una forma genĂŠrica un conjunto de datos.
int main(){ vector <double> vector_reales;
La STL proporciona otras plantillas importantes como stack, list, etc. TambiĂŠn se verĂĄn en el segundo cuatrimestre.
vector_reales.push_back(9.3); vector_reales.push_back(5.8); cout << "\nTamaĂąo actual del vector = " cout << "\nTamaĂąo mĂĄximo del vector = "
<< vector_reales.size(); //2 << vector_reales.capacity(); //>=2
for (int i=0 ; i < vector_reales.size() ; i++) cout << "\nValor en posiciĂłn " << i << " = " << vector_reales.at(i) << " = " << vector_reales[i]; vector_reales.pop_back(); vector_reales.pop_back(); vector_reales.pop_back();
// // // //
Elimina el Ăşltimo (5.8) => Size=1 Elimina el Ăşltimo (9.3) => Size=0 Ya no hay elementos, pero no se produce ningĂşn error
// vector_reales[0] = 3; // cout << vector_reales.at(0); vector_reales.push_back(15.2); vector_reales.at(0) = 17.2;
// Componente no existe => Imprevisible // Componente no existe => Error ejecuciĂłn
// at tambiĂŠn permite acceder a una // componente para modificarla // Esto es posible porque at devuelve una // referencia a la componente en cuestiĂłn. // Lo mismo ocurre con front y back cout << "\nPrimero = " << vector_reales.front(); cout << "\nĂ&#x161;ltimo = " << vector_reales.back(); }
PodrĂamos usar un objeto de la clase vector como vector privado de nuestra clase MiVector. De esa forma, ya no serĂa necesaria el lĂmite de elementos impuesto por la constante estĂĄtica TAMANIO. Observad que la interfaz no varĂa nada, por lo que los antiguos clientes de la clase MiVector pueden seguir usĂĄndola tal y como lo hacĂan antes. SĂłlo hemos cambiado la implementaciĂłn interna. Nos quedarĂa: ! " " ! # $ # ! " % " &
' &
% &
# ! (
Vectores y Matrices y Las clases
â&#x2020;&#x2019; La clase
366
Vectores y Matrices Las clases vector y string â&#x2020;&#x2019; La clase vector
367
else return -1; } int PrimeraOcurrencia (char buscado){ return PrimeraOcurrenciaEntre(0, vector_privado.size()-1, buscado); } int PosicionMinimoEntre(int izda, int dcha){ int posicion_minimo; char minimo; minimo = vector_privado[izda]; posicion_minimo = izda; for (int i = izda+1 ; i <= dcha ; i++) if (vector_privado[i] < minimo){ minimo = vector_privado[i]; posicion_minimo = i; } return posicion_minimo; } int PosicionMinimo(){ return PosicionMinimoEntre(0, vector_privado.size() - 1); } int BusquedaBinaria (char buscado){ int izqa, dcha, centro; izqa = 0; dcha = vector_privado.size() - 1; centro = (izqa+dcha)/2; while (izqa <= dcha && vector_privado[centro] != buscado) { if (buscado < vector_privado[centro])
Vectores y Matrices Las clases vector y string â&#x2020;&#x2019; La clase vector
368
Vectores y Matrices Las clases vector y string â&#x2020;&#x2019; La clase vector
dcha = centro-1; else izqa = centro+1;
369
char a_desplazar; int totales = vector_privado.size(); for (izda = 1; izda < totales; izda++){ a_desplazar = vector_privado[izda];
centro = (izqa+dcha)/2; }
for (i = izda; i>0 && a_desplazar < vector_privado[i-1]; i--) vector_privado[i] = vector_privado[i-1];
if (izqa > dcha) return -1; else return centro;
vector_privado[i] = a_desplazar; } } void Ordena_por_Burbuja(){ int izda, i; bool cambio; int totales = vector_privado.size();
} void Inserta(int pos_insercion, char valor_nuevo){ int totales = vector_privado.size(); if (pos_insercion >= 0 && pos_insercion <= totales){ for (int i = totales ; i > pos_insercion ; i--) vector_privado[i] = vector_privado[i-1];
cambio = true; for (izda = 0; izda < totales && cambio; izda++){ cambio = false;
vector_privado[pos_insercion] = valor_nuevo; } } void Ordena_por_Seleccion(){ int pos_min; int totales = vector_privado.size(); for (int izda = 0 ; izda < totales ; izda++){ pos_min = PosicionMinimoEntre (izda, totales - 1); IntercambiaComponentes_en_Posiciones (izda, pos_min); } } void Ordena_por_Insercion(){ int izda, i;
for (i = totales-1; i > izda; i--) if (vector_privado[i] < vector_privado[i-1]){ IntercambiaComponentes_en_Posiciones(i, i-1); cambio = true; } } } };
Vectores y Matrices Las clases vector y string → La clase string
IV.6.2.
370
Al igual que ocurre con la plantilla vector, a los elementos se accede a través de la notación corchete o el método at. Los siguientes métodos realizan la misma función que en la plantilla vector:
push_back,
size,
capacity,
371
cadena = "Fundamentos "; otra_cadena = "Programació";
La clase string
El tipo string usado hasta ahora es, realmente, una clase.
at,
Vectores y Matrices Las clases vector y string → La clase string
empty,
clear
Otros métodos interesantes:
otra_cadena.push_back('n'); cadena.append(otra_cadena); cadena.insert(12, "de ");
cadena[12] = 'e'; cadena.at(13) = 'n'; cadena.replace(12, 2, "de la"); cadena.erase(15, 3);
// // // // // // // // //
"Programación" "Fundamentos Programación" "de " se sitúa en la posición 12 -desde 0"Fundamentos de Programación" "Fundamentos ee Programación" "Fundamentos en Programación" "Fundamentos de la Programación" "Fundamentos de Programación"
length. Hace lo mismo que size.
int posicion_encontrado;
append. Añade una cadena al final de otra.
posicion_encontrado = cadena.find("de");
// 12
posicion_encontrado = cadena.find('m');
// 5
posicion_encontrado = cadena.find('m', 7);
// 21 primera ocurrencia de 'm' // a partir de la posic.7
posicion_encontrado = cadena.rfind('m');
// 21 Última ocurrencia de 'm' // -right find-
insert. Inserta una subcadena a partir de una posición. replace. Reemplaza una serie de caracteres consecutivos por una subcadena. erase. Borra un número de caracteres, a partir de una posición. find. Encuentra un carácter o una subcadena. substr. Obtiene una subcadena. #include <iostream> #include <vector> #include <string> using namespace std; int main(){ string cadena; string otra_cadena;
primera ocurrencia de 'm'
posicion_encontrado = cadena.rfind('m', 9); // 5 Última ocurrencia de 'm' // buscando entre las posic 0,9 otra_cadena = cadena.substr(12, 2); ..... }
// "de" Extrae la subcadena que // hay entre las posic 12, 12+2
Vectores y Matrices Vectores de varias dimensiones: Matrices →
IV.7.
372
Vectores de varias dimensiones: Matrices
IV.7.1.
Declaración
Vectores y Matrices Vectores de varias dimensiones: Matrices → Operaciones con matrices
373
<tipo de dato><identificador>[TAMANIO_FIL][TAMANIO_COL]; Como con los vectores, el tipo base de la matriz es el mismo para todas las componentes, ambas dimensiones han de ser de tipo entero, y comienzan en cero.
Supongamos una finca rectangular dividida en parcelas. Queremos almacenar la producción de aceitunas, en TM.
int main(){ const int TAMANIO_FIL = 2; const int TAMANIO_COL = 3;
La forma natural de representar la parcelación sería usando el concepto matemático de matriz.
}
double parcela[TAMANIO_FIL][TAMANIO_COL];
9.1
0.4
5.8
IV.7.2.
4.5
5.9
1.2
IV.7.2.1.
Operaciones con matrices Acceso y asignación
<Ident>[<ind1>] [<ind2>]
Para representarlo en C++ podríamos usar un vector parcela:
0 ≤ indi < TAMANIOi 9.1
0.4
5.8
4.5
5.9
1.2
<Ident>[<ind1>] [<ind2>] es una variable más del programa y se comporta como cualquier variable del tipo de dato base de la matriz. pero la forma de identificar cada parcela (por ejemplo parcela[4]) es poco intuitiva para el programador.
Vectores y Matrices Vectores de varias dimensiones: Matrices → Operaciones con matrices
374
Por ejemplo, para asignar una expresión a una celda:
<Ident>[<ind1>] [<ind2>] = <Expresión>; <Expresión> ha de ser del mismo tipo de dato que el tipo base de la matriz. Ejemplo.
int main(){ const int TAMANIO_FIL = 2; const int TAMANIO_COL = 3; double parcela[TAMANIO_FIL][TAMANIO_COL]; parcela[0][0] parcela[0][1] parcela[2][0] parcela[0][3] parcela[0][0] }
= = = = =
4.5; 5.9; 7.2; 7.2; 4;
// // // // // //
Correcto; Correcto; Error lógico: Fila 2 No existe. Error lógico: Columna 3 No existe. Correcto: Casting automático: double=int
Vectores y Matrices Vectores de varias dimensiones: Matrices → Operaciones con matrices
IV.7.2.2.
375
Gestión de componentes útiles con matrices
Por cada dimensión necesitamos gestionar una variable que indique el número de componentes útiles.
#include <iostream> using namespace std; int main(){ const int TAMANIO_FIL = 5, TAMANIO_COL = 8; int util_filas_parcela, util_columnas_parcela; int fil, col; double parcela[TAMANIO_FIL][TAMANIO_COL]; double produccion; utilFil_parcela = 2; utilCol_parcela = 3; for (fil = 0 ; fil < util_filas_parcela ; fil++) for (col = 0 ; col < util_columnas_parcela ; col++){ cout << "Introduce la producción en TM de la parcela " << fil << "," << col; cin >> produccion; parcela[fil][col] = produccion; } cout << "\nDatos introducidos:\n";
Vectores y Matrices Vectores de varias dimensiones: Matrices → Operaciones con matrices
for (fil = 0 ; fil < util_filas_parcela ; fil++){ for (col = 0 ; col < util_columnas_parcela ; col++){ cout << "\nProducción en TM de la parcela " << fil << "," << col << " --> "; cout << parcela[fil][col]; } cout << endl; }
376
Vectores y Matrices Vectores de varias dimensiones: Matrices → Operaciones con matrices
IV.7.2.3.
Inicialización
En la declaración de la matriz se pueden asignar valores a toda la matriz. Posteriormente, no es posible: es necesario acceder a cada componente independientemente. La forma segura es poner entre llaves los valores de cada fila.
int parc[2][3]={{1,2,3},{4,5,6}};
} Nota. Podemos poner directamente: cin >> parcela[fil][col];
377
// parc tendrá: 1 2 3 // 4 5 6
Si no hay suficientes inicializadores para una fila determinada, los elementos restantes se inicializan a 0.
int parc[2][3]={{1},{3,4,5}};
// parc tendrá: 1 0 0 // 3 4 5
Si se eliminan las llaves que encierran cada fila, se inicializan los elementos de la primera fila y después los de la segunda, y así sucesivamente.
int parc[3][4]={1, 2, 3, 4, 5}; // parc tendrá: 1 2 3 4 // 5 0 0 0 // 0 0 0 0
Vectores y Matrices Vectores de varias dimensiones: Matrices → Operaciones con matrices
378
IV.7.2.4. Representación en memoria Todas las posiciones de una matriz están realmente contiguas en memoria. La representación exacta depende del lenguaje. En C++ se hace por filas:
m contiene la dirección de memoria de la primera componente. Para calcular dónde se encuentra la componente m[1][2] el compilador debe pasar a la segunda fila. Lo consigue trasladándose tantas posiciones como diga TAMANIO_COL, a partir del comienzo de m. Una vez ahí, salta 2 posiciones y ya está en m[1][2].
Conclusión: Para saber dónde está m[i][j], el compilador necesita saber cuánto vale TAMANIO_COL, pero no TAMANIO_FIL. Para ello, da tantos saltos como indique la expresión i*TAMANIO_COL + j
Vectores y Matrices Vectores de varias dimensiones: Matrices → Operaciones con matrices
Ejemplo. Buscar un elemento en una matriz.
#include <iostream> using namespace std; int main(){ const int TAMANIO_FIL = 2, TAMANIO_COL = 3; double m[TAMANIO_FIL][TAMANIO_COL]; int fila_encontrado, columna_encontrado, util_filas_m, util_columnas_m, fil, col; double buscado; bool encontrado; do{ cout << "Introducir el numero de filas: "; cin >> util_filas_m; }while (util_filas_m < 1 || util_filas_m > TAMANIO_FIL); do{ cout << "Introducir el numero de columnas: "; cin >> util_columnas_m; }while (util_columnas_m < 1 || util_columnas_m > TAMANIO_COL);
379
Vectores y Matrices Vectores de varias dimensiones: Matrices → Operaciones con matrices
380
for (fil = 0 ; fil<util_filas_m ; fil++) for (col = 0 ; col<util_columnas_m ; col++){ cout << "Introducir el elemento (" << fil << "," << col << "): "; cin >> m[fil][col]; } cout << "\nIntroduzca elemento a buscar: "; cin >> buscado; encontrado=false; for (fil = 0; !encontrado && fil < util_filas_m ; fil++){ for (col = 0; !encontrado && col < util_columnas_m ; col++) if (m[fil][col] == buscado){ encontrado = true; fila_encontrado = fil; columna_encontrado = col; } } } if (encontrado) cout << "\nElemento encontrado en la posición " << fila_encontrado << "," << columna_encontrado << endl; else cout << "\nElemento no encontrado\n"; Importante: la variable encontrado ha de ser la misma en los dos bucles.
Vectores y Matrices Vectores de varias dimensiones: Matrices → Más de dos dimensiones
IV.7.3.
381
Más de dos dimensiones
Podemos declarar tantas dimensiones como queramos. Sólo es necesario añadir más corchetes. Por ejemplo, para representar la producción de una finca dividida en 2×3 parcelas, y dónde en cada parcela se practican cinco tipos de cultivos, definiríamos:
const int DIV_HOR = 2, DIV_VERT = 3, TOTAL_CULTIVOS = 5; double parcela[DIV_HOR][DIV_VERT][TOTAL_CULTIVOS]; // Asignación al primer cultivo de la parcela 1,2 parcela[1][2][0] = 4.5;
Vectores y Matrices Vectores de varias dimensiones: Matrices â&#x2020;&#x2019; La clase MiMatriz
IV.7.4.
382
La clase MiMatriz
Una matriz tiene los mismos problemas de acceso a componentes no reservadas que los vectores. Para evitarlo, debemos construir nuestra propia clase MiMatriz. ÂżCuĂĄl serĂa la interfaz de la clase MiMatriz? Podemos establecer un tope ďŹ jo de columnas en el constructor y aĂąadir elemento a elemento hasta llegar a dicho tope, en cuyo caso, pasarĂamos automĂĄticamente a la siguiente ďŹ la: # #
! " ! # !" # # $ # !" % # !" $% ! & ! $% #$ "# % '!# # $ ! ( !" %
Vectores y Matrices Vectores de varias dimensiones: Matrices â&#x2020;&#x2019; La clase MiMatriz
383
En vez de aĂąadir carĂĄcter a carĂĄcter, serĂa interesante aĂąadir ďŹ las enteras:
! " ! ! # ! $% $ " % ! $ ! %
Para ello necesitamos saber cĂłmo se pasa un objeto como parĂĄmetro y cĂłmo un mĂŠtodo puede devolver un objeto. Lo vemos en el siguiente tema.
Vectores y Matrices Vectores de varias dimensiones: Matrices → La clase MiMatriz
384
Bibliografía recomendada para este tema: Los libros suelen introducir el concepto de vector antes de ver las clases. En este caso, los autores muestran cómo pasar los vectores como parámetros a las funciones. Esta forma de trabajar implica que también hay que pasar como parámetro el número de componentes útiles. En definitiva, corresponde a una filosofía de programación procedural y no orientada a objetos. Los libros que lo hacen al revés (introduciendo primero las clases y luego los vectores) utilizan la clase estándar vector. La aproximación seguida por nosotros es distinta pues, habiendo introducido primero las clases, optamos por el uso del vector clásico de C++, para ver su manejo en memoria y para ilustrar la construcción de nuestra propia clase MiVector. Por tanto, la bibliografía recomendada para este tema pasa por la consulta de fuentes que detallen los algoritmos de búsqueda y ordenación de una forma genérica (hablando de un conjunto de elementos, en vez de un array de C++) En este sentido, una referencia bastante adecuada es Wikipedia (pero la versión en inglés)
http://en.wikipedia.org/wiki/Category:Search_algorithms
Tema V
Clases (Segunda parte) Objetivos: Ver cómo gestiona C++ el copiado entre objetos y sus implicaciones en el diseño de clases. Empezar a diseñar clases que colaboran entre sí para poder resolver problemas más complejos. Introducir el concepto de excepción y ver cómo se aplica en la gestión de errores.
http://en.wikipedia.org/wiki/Category:Sorting_algorithms
Autor: Juan Carlos Cubero. Sugerencias: por favor, enviar un e-mail a JC.Cubero@decsai.ugr.es
Clases (Segunda parte) Copiando objetos →
V.1. V.1.1.
386
Copiando objetos Operador de asignación
Clases (Segunda parte) Copiando objetos → Operador de asignación
class MiClase{ private: int privada; public: int publica;
V.1.1.1. Funcionamiento del operador de asignación
MiClase(int priv, int pub) :privada(priv), publica(pub) { } int Privada(){ return privada; } int Publica(){ return publica; }
C++ permite asignar objetos entre sí a través del operador de asignación (assignment operator) = Este operador lo define automáticamente el compilador para cualquier clase, y funciona como si trabajásemos con tipos básicos:
objeto = otro_objeto; Ambos objetos han de ser de la misma clase. Se realiza una copia de todos los datos miembro, tanto los privados como los públicos. No tiene sentido hablar de copiar los métodos. ¡Ya estaban disponibles!
387
}; int main(){ MiClase uno(6,7), otro(5,8); otro cout cout cout cout
El programador puede definir otro comportamiento. Se verá en el segundo cuatrimestre al ver la sobrecarga (overloading) del operador de asignación.
= uno; << "\nPrivada << "\nPública << "\nPrivada << "\nPública
de de de de
uno: " << uno.Privada(); uno: " << uno.Publica(); otro: " << otro.Privada(); otro: " << otro.Publica();
// // // //
6 7 6 7
} Nota. Recordemos que jamás usaremos datos miembro públicos. El anterior es un ejemplo para mostrar los efectos del operador de asignación.
Clases (Segunda parte) Copiando objetos â&#x2020;&#x2019; Operador de asignaciĂłn
388
V.1.1.2. El operador de asignaciĂłn y los datos miembro constantes ÂżQuĂŠ pasa cuando una clase contiene datos miembro constantes? Una constante estĂĄtica es la misma para todos los objetos de una clase. Por tanto, ĂŠstas no dan problemas en la asignaciĂłn entre objetos.
class MiVectorCaracteres{ private: static const int DIM = 50; char vector_privado[DIM]; int total_utilizados; public: MiVectorCaracteres() :total_utilizados(0) {} ................. }; int main(){ MiVectorCaracteres cadena, otra_cadena; cadena.Aniade('t'); cadena.Aniade('e'); cadena.Aniade('c'); cadena.Aniade('a'); otra_cadena = cadena;
Clases (Segunda parte) Copiando objetos â&#x2020;&#x2019; Operador de asignaciĂłn
389
Una constante no estĂĄtica es propia de cada objeto. ÂżQuĂŠ sentido tiene cambiar su valor con la constante de otro objeto? Ninguno.
C++ NO permite asignar objetos entre sĂ cuando la clase contiene datos miembro constantes no estĂĄticos
class CuentaBancaria{ private: const string identificador; ........... public: CuentaBancaria(string identificador_cuenta, double saldo_inicial) :saldo(saldo_inicial), identificador(identificador_cuenta) { } ........... }; int main(){ CuentaBancaria cuenta("Un identificador", 20000); CuentaBancaria otra_cuenta("Otro identificador", 30000); otra_cuenta = cuenta; // Error de compilaciĂłn, al tener // la clase datos miembro constantes
// Es posible ya que la constante // es estĂĄtica, es decir, la misma // para todos los objetos
Nota. Cuando se deďŹ ne una clase con datos constantes no estĂĄticos, Visual Studio da un aviso durante la compilaciĂłn, indicando que no se pudo generar el operador de asignaciones.
Clases (Segunda parte) Copiando objetos → El constructor de copia
V.1.2.
390
C++ permite inicializar un objeto con los datos de otro:
MiClase
.....
//
<- Constructor de copia
En la sentencia MiClase otro_objeto (un_objeto) , el compilador llama automáticamente al llamado constructor de copia (copy constructor). Es un constructor que recibe como parámetro un objeto de la misma clase y es proporcionado automáticamente (de oficio) por el compilador. El constructor de copia realiza la asignación de los datos miembro que tenía un_objeto a otro_objeto. Es como si fuese una asignación del tipo otro_objeto = un_objeto.
int main(){ MiVectorCaracteres vector; vector.Aniade('d');
Un constructor sin parámetros. Se usa para poder crear objetos de una clase que no incluya la definición explícita de ningún constructor. Un constructor con un único parámetro y cuyo tipo es la misma clase que se está definiendo. Se usa para inicializar un objeto con los datos de otro objeto de la misma clase.
}
otro_objeto (un_objeto) ;
391
En resumen, C++ proporciona dos constructores de oficio:
El constructor de copia
class MiClase{ public: MiClase (int parametro){ ..... }; int main(){ MiClase un_objeto(9);
Clases (Segunda parte) Copiando objetos → El constructor de copia
int tope;
vector.Aniade('o');
MiVectorCaracteres otro_vector(vector); tope = otro_vector.TotalUtilizados(); for (int i = 0; i < tope; i++) cout << vector.Elemento(i) << " ";
vector.Aniade('s');
Es como si fuese una asignación del tipo otro_objeto = un_objeto. Pero hay diferencias entre el operador de asignación y el constructor de copia: El operador de asignación es invocado cuando ya tenemos creados dos objetos y ejecutamos la sentencia objeto_1 = objeto_2; El constructor de copia es invocado en el momento de la declaración del objeto, justo en el momento en el que se va a crear éste. Veremos otros sitios en los que el compilador también invoca automáticamente al constructor de copia. Por esta razón, si un objeto tiene datos constantes, sí podemos inicializar otro objeto con los datos del primero (porque todavía no tenía definida la constante):
class CuentaBancaria{ private: const string identificador; ........... }; int main(){ CuentaBancaria cuenta("Constante a nivel de objeto", 20000); CuentaBancaria otra_cuenta(cuenta); // Correcto otra_cuenta = cuenta; // Error de compilación
Clases (Segunda parte) Copiando objetos → El constructor de copia
392
Pero si al final, el operador de asignación y el constructor de copia hacen lo mismo (copiar los datos miembro), sólo que en sitios distintos ¿dónde está entonces la diferencia? (aparte de lo que hemos visto sobre los datos constantes) C++ permite redefinir ambos. Pero además, pueden redefinirse de forma distinta. Así pues, podríamos tener distintos comportamientos para el mismo concepto de asignación. Esto no es coherente y es una seria crítica a C++. '
$
C++ distingue entre “asignación” de objetos a través de = y “asignación” de objetos a través del constructor de copia. Esto sólo da problemas. &
%
Clases (Segunda parte) Copiando objetos → Copiar o no copiar: He ahí el dilema
V.1.3.
393
Copiar o no copiar: He ahí el dilema
El problema de copiar objetos (independientemente de que C++ distinga además entre operador de asignación y constructor de copia) es aún más problemático de lo que hemos visto. Las cosas se complicarán cuando se vea el concepto de puntero y se distinga entre copia superficial (shallow copy) y copia profunda (deep copy). Java, C# y Visual Basic.NET resuelven el problema utilizando únicamente referencias a objetos. Al hacer una asignación no se realiza una copia de todos los datos de un objeto sino que simplemente se copia su dirección de memoria. Esto lo podremos hacer también en C++ usando punteros. De esta forma, no será necesario definir operadores de asignación propios ni constructores de copia. En resumen: La asignación entre objetos se realiza a través del operador de asignación y del constructor de copia. En principio, el comportamiento básico de ambos es el mismo (se copian los datos miembro) y así lo supondremos en esta asignatura. Sin embargo, el programador puede cambiar dicho comportamiento y, además, de forma independiente (lo cual no es recomendable). Lo usual será utilizar punteros a objetos y copiar los punteros, en vez de recurrir a la copia entre objetos.
Clases (Segunda parte) Trabajando con varios objetos â&#x2020;&#x2019;
V.2.
394
Clases (Segunda parte) Trabajando con varios objetos â&#x2020;&#x2019; Pasando objetos como parĂĄmetros
395
Ejemplo. La ecuaciĂłn de una recta viene dada en la forma
Trabajando con varios objetos
Ax + By + C = 0
V.2.1.
Pasando objetos como parĂĄmetros
ÂżPodemos pasar un objeto como parĂĄmetro por valor a un mĂŠtodo? SĂ, porque acabamos de ver que es posible la asignaciĂłn entre objetos. El parĂĄmetro formal recibirĂĄ una copia del actual. Esta copia se realiza a travĂŠs del constructor de copia visto anteriormente.
#
nuevo_objeto.UnMetodo(parametro_actual); } En tiempo de ejecuciĂłn se invoca automĂĄticamente al constructor de copia: parametro_formal(parametro_actual)
Al pasar los objetos como parĂĄmetros, se invoca al constructor de copia
Queremos deďŹ nir la clase Recta y ver si contiene a un punto.
class MiClase{ ........... }; class OtraClase{ public: void UnMetodo(MiClase parametro_formal){ .......... } }; int main(){ MiClase parametro_actual(6,7); OtraClase nuevo_objeto;
es decir, todos los puntos (x, y) que satisfacen dicha ecuaciĂłn forman una recta.
!"
Clases (Segunda parte) Trabajando con varios objetos → Pasando objetos como parámetros
396
Clases (Segunda parte) Trabajando con varios objetos → Pasando objetos como parámetros
#include <iostream> using namespace std;
SetCoeficientes (coef_x, coef_y, coef_indep); } void SetCoeficientes(double coef_x, double coef_y, double coef_indep){ if (! (coef_x == 0 && coef_y == 0)){ A = coef_x; B = coef_y; C = coef_indep; } } double GetCoeficienteA(){ return A; } double GetCoeficienteB(){ return B; } double GetCoeficienteC(){ return C; } double Ordenada(double x){ return (-C - x*A ) / B; } double Abscisa(double y){ return (-C - y*B ) / A; } double Pendiente(){ return -A/B; } bool Contiene(Punto2D punto){ return SonIguales(A * punto.GetAbscisa() + B * punto.GetOrdenada() + C , 0); }
bool SonIguales(double uno, double otro) { return fabs(uno-otro) <= 1e-6; } class Punto2D{ private: double abscisa; double ordenada; public: Punto2D(double abscisaPunto, double ordenadaPunto) { SetCoordenadas (abscisaPunto, ordenadaPunto); } void SetCoordenadas (double abscisaPunto, double ordenadaPunto){ abscisa = abscisaPunto; ordenada = ordenadaPunto; } double GetAbscisa(){ return abscisa; } double GetOrdenada(){ return ordenada; } }; class Recta{ private: double A, B, C; // Ax + By + C = 0 public: Recta(double coef_x, double coef_y, double coef_indep){
397
};
Clases (Segunda parte) Trabajando con varios objetos → Pasando objetos como parámetros
int main(){ Punto2D un_punto(4, 1); Recta una_recta(2, 3, -11); bool contiene;
398
Clases (Segunda parte) Trabajando con varios objetos → Pasando objetos como parámetros
399
Un método puede recibir como parámetro un objeto de la MISMA clase
// 2x + 3y - 11 = 0
contiene = una_recta.Contiene(un_punto); if (contiene) cout << "\nLa recta contiene el punto"; else cout << "\nLa recta no contiene el punto"; cout << "\n\n"; system("pause"); } El parámetro formal punto recibirá una copia del parámetro actual un_punto, que se creará a través del constructor de copia.
Ejercicio. Comprobar si dos puntos son iguales. Con una función sería:
bool SonIguales (double un_real, double otro_real){ return fabs(un_real-otro_real) <= 1e-6; } bool SonIgualesPuntos(Punto2D un_punto, Punto2D otro_punto){ return SonIguales(un_punto.GetAbscisa(), otro_punto.GetAbscisa()) && SonIguales(un_punto.GetOrdenada(), otro_punto.GetOrdenada()); } Pero no estamos viendo programación orientada a funciones sino orientada a objetos ¿Cómo se definiría el método dentro de la clase?
bool SonIguales (double un_real, double otro_real){ return fabs(un_real-otro_real) <= 1e-6; } class Punto2D{ private: double abscisa; double ordenada; public: Punto2D (double abscisaPunto, double ordenadaPunto) { SetCoordenadas (abscisaPunto, ordenadaPunto); } void SetCoordenadas (double abscisaPunto, double ordenadaPunto){ abscisa = abscisaPunto; ordenada = ordenadaPunto; }
Clases (Segunda parte) Trabajando con varios objetos → Pasando objetos como parámetros
400
double GetAbscisa (){ return abscisa; } double GetOrdenada (){ return ordenada; } bool EsIgual_a (Punto2D otro_punto){ return (SonIguales(abscisa, otro_punto.GetAbscisa()) && SonIguales(ordenada, otro_punto.GetOrdenada())); } }; int main(){ bool son_iguales; Punto2D un_punto(5.1, 3.2); Punto2D otro_punto(5.1, 3.5); son_iguales = un_punto.EsIgual_a (otro_punto); La función SonIguales entre dos double sí la mantenemos como función global ya que es muy genérica, reutilizable en muchos sitios y los parámetros son tipos básicos.
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
V.2.2.
401
Objetos declarados localmente en un método y métodos que devuelven objetos
Dentro de un método podemos declarar objetos El objeto se creará en la pila, dentro del marco del método. Como cualquier otra variable local (automática), existirá mientras se esté ejecutando el método. Cuando termine la ejecución del método, automáticamente se libera la memoria asociada al objeto
class UnaClase{ ...... }; class OtraClase{ ...... int UnMetodo(){ UnaClase objeto ; ....... También podemos declarar un vector local clásico dentro de un método:
class MiClase{ int Metodo(){ int vector_local [TAMANIO]; ...... return entero; } pero:
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
402
Un método NO puede devolver un vector clásico
class MiClase{ int [] Metodo(){ int vector [TAMANIO]; ...... return vector; }
// Error de compilación
Un método SÍ puede devolver un objeto Se declara local al método y se devuelve una copia.
class Otra_Clase{ ...... Una_Clase Metodo(){ Una_Clase objeto_local; ...... return objeto_local ; } Al ejecutar la sentencia return también se produce una llamada al constructor de copia, ya que es necesario crear un objeto temporal con los datos del objeto local, para así poder realizar la devolución del objeto.
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
403
Ejemplo. Progresión geométrica: añadimos un método que calcule los primeros términos. Devuelve un vector de double (usamos la implementación de MiVectorDoubles que usa internamente la clase estándar vector)
class MiVectorDoubles{ private: vector <double> vector_privado; ....... public: int TotalUtilizados(){ return vector_privado.size(); } double Elemento(int indice){ return vector_privado.at(indice); } void Aniade(double nuevo){ vector_privado.push_back(nuevo); } int PrimeraOcurrenciaEntre(int pos_izda, int pos_dcha, double buscado){ ....... } ....... }; class ProgresionGeometrica{ private: double primer_termino; double razon; public: ProgresionGeometrica(double primer_termino_progresion, double razon_progresion) :primer_termino(primer_termino_progresion), razon(razon_progresion) { }
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
double PrimerTermino(){ return primer_termino; } double Razon(){ return razon; } double Termino(int n){ return primer_termino * pow(razon, n-1); } double SumaHasta(int tope){ return primer_termino * (1-pow(razon,tope)) / (1-razon); } MiVectorDoubles PrimerosTerminos(int tope){ MiVectorDoubles primeros; for (int i = 1; i <= tope ; i++) primeros.Aniade(Termino(i));
404
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
También podríamos haber construido el objeto vector, usando el constructor de copia proporcionado de oficio por el compilador:
int main(){ ProgresionGeometrica progresion(3.2, 2.4); MiVectorDoubles primeros_terminos (progresion.PrimerosTerminos(5)); ............. '
return primeros; }; int main(){ MiVectorDoubles primeros_terminos; ProgresionGeometrica progresion(3.2, 2.4); primeros_terminos = progresion.PrimerosTerminos(5); .............
$
Asignaremos el objeto devuelto por un método: O bien a través del operador de asignación (si está disponible)
objeto_claseA = objeto_claseB.Metodo(); O bien a través del constructor de copia: &
}
405
ClaseA
objeto_claseA (objeto_claseB.Metodo());
%
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
406
Si hubiésemos preferido devolver un objeto de la clase estándar vector, el código sería muy parecido:
class ProgresionGeometrica{ ........ vector <double> PrimerosTerminos(int tope){ vector <double> primeros; for (int i = 1; i <= tope ; i++) primeros.push_back(Termino(i)); return primeros; } int main(){ vector <double> primeros_terminos; ProgresionGeometrica progresion(3.2, 2.4); primeros_terminos = progresion.PrimerosTerminos(5); Ejercicio. ¿Cómo sería a través del constructor de copia?
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
407
Ejercicio. Añadimos un método a la clase MiVectorCaracteres que devuelva un vector con los índices de aquellas componentes que sean mayúsculas.
class MiVectorEnteros{ private: vector <int> vector_privado; public: int TotalUtilizados(){ return vector_privado.size(); } void Aniade(int nuevo){ vector_privado.push_back(nuevo); } ...... }; class MiVectorCaracteres{ private: vector <char> vector_privado; public: MiVectorEnteros PosicionesMayusculas(){ MiVectorEnteros posiciones_mayusculas; int tope = vector_privado.size(); for (int i = 0; i < tope; i++){ char caracter = vector_privado.at(i); if ( caracter >= 'A' && caracter <= 'Z') posiciones_mayusculas.Aniade(i); } return posiciones_mayusculas; }
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
...... }; int main(){ MiVectorCaracteres cadena; MiVectorEnteros posiciones_mayusculas; cadena.Aniade('T'); cadena.Aniade('e'); cadena.Aniade('c'); cadena.Aniade('A'); posiciones_mayusculas = cadena.PosicionesMayusculas(); int tope = posiciones_mayusculas.TotalUtilizados(); for (int i = 0; i < tope; i++) cout << posiciones_mayusculas.Elemento(i) << " ";
Ejercicio. ¿Cómo sería a través del constructor de copia?
408
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
409
Ejemplo. En el tema III, habíamos definido la clase SegmentoDirigido de la siguiente forma (observad que NO tiene un constructor por defecto):
bool SonIguales(double uno, double otro) { return fabs(uno-otro) <= 1e-6; } class SegmentoDirigido{ private: double x_1, y_1, x_2, y_2; public: SegmentoDirigido(double origen_abscisa, double origen_ordenada, double final_abscisa, double final_ordenada){ SetCoordenadas(origen_abscisa, origen_ordenada, final_abscisa, final_ordenada); } void SetCoordenadas(double origen_abscisa, double origen_ordenada, double final_abscisa, double final_ordenada){ if (! (SonIguales(origen_abscisa , final_abscisa) && SonIguales(origen_ordenada , final_ordenada)) ){ x_1 = origen_abscisa; y_1 = origen_ordenada; x_2 = final_abscisa; y_2 = final_ordenada; } } double Longitud(){ return sqrt((x_2 - x_1)*(x_2 - x_1) + (y_2 - y_1)*(y_2 - y_1)); } void TrasladaHorizontal(double unidades){ x_1 = x_1 + unidades; x_2 = x_2 + unidades; } void TrasladaVertical(double unidades){ y_1 = y_1 + unidades; y_2 = y_2 + unidades; } double OrigenAbscisa(){
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
};
410
return x_1; } double OrigenOrdenada(){ return y_1; } double FinalAbscisa(){ return x_2; } double FinalOrdenada(){ return y_2; }
Podemos usar la definición de la clase Punto2D de la página 399 y añadir un método a la clase Recta para que obtenga el segmento incluido en la recta delimitado por dos valores de abscisas:
class Punto2D{ ...... }; class Recta{ private: ............ public: ............ SegmentoDirigido Segmento(double abscisa_origen, double abscisa_final){ double ordenada_origen = Ordenada(abscisa_origen); double ordenada_final = Ordenada(abscisa_final); SegmentoDirigido segmento(abscisa_origen, ordenada_origen, abscisa_final, ordenada_final); return segmento; } };
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
411
¿Cómo sería la llamada en el main? Observad que no podemos hacer lo siguiente:
int main(){ Recta una_recta(2, 3, -11); SegmentoDirigido segmento;
// Error compilación
segmento = una_recta.Segmento(3.5, 4.6); ya que el segmento no tiene un constructor por defecto (sin parámetros). Por tanto, tenemos que recurrir a construir el segmento a partir de la recta, y asignarlo utilizando el constructor de copia:
int main(){ Recta una_recta(2, 3, -11);
// 2x + 3y - 11 = 0
SegmentoDirigido segmento(una_recta.Segmento(3.5, 4.6)); // Const.copia cout << "\nLongitud segmento = " << segmento.Longitud();
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
412
Sabemos que no podemos mezclar en una misma clase cómputos con E/S. ¿Cómo encapsularíamos la tarea de leer o imprimir los datos miembro de una clase? Con clases específicas que implementen dichas responsabilidades. Ejemplo. Definimos clases para leer e imprimir puntos y rectas:
#include <iostream> using namespace std; class Punto2D{ <no cambia nada> }; class Recta{ <no cambia nada> }; class LectorRectas{ private: string mensaje; public: LectorRectas(string mensaje_entrada_datos) :mensaje (mensaje_entrada_datos) { } void ImprimeMensajeEntrada(){ cout << mensaje; } Recta Lee(){ double A, B, C; cin >> A; cin >> B; cin >> C; Recta recta(A, B, C); return recta; }
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
}; class LectorPuntos{ private: string mensaje; public: LectorPuntos(string mensaje_entrada_datos) :mensaje (mensaje_entrada_datos) { } void ImprimeMensajeEntrada(){ cout << mensaje; } Punto2D Lee(){ double abscisa, ordenada; cin >> abscisa; cin >> ordenada; Punto2D punto(abscisa, ordenada); return punto; } }; class ImpresorPuntos{ public: void Imprime(Punto2D punto){ cout << "(" << punto.GetAbscisa() << "," << punto.GetOrdenada() << ")"; } }; class ImpresorRectas{ public: void Imprime (Recta una_recta){ cout << una_recta.GetCoeficienteA() << " x + " << una_recta.GetCoeficienteB() << " y + " << una_recta.GetCoeficienteC() << " = 0";
413
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
414
} }; int main(){ bool contiene; LectorPuntos lector_de_puntos("\nIntroduzca las dos coordenadas del punto\n"); LectorRectas lector_de_rectas("\nIntroduzca los tres coeficientes de la recta\n"); ImpresorRectas impresor_de_rectas; ImpresorPuntos impresor_de_puntos; lector_de_puntos.ImprimeMensajeEntrada(); Punto2D un_punto(lector_de_puntos.Lee()); lector_de_rectas.ImprimeMensajeEntrada(); Recta una_recta(lector_de_rectas.Lee()); contiene = una_recta.Contiene(un_punto); cout << "\nLa recta "; impresor_de_rectas.Imprime(una_recta); if (contiene) cout << " contiene al punto "; else cout << " no contiene al punto "; impresor_de_puntos.Imprime(un_punto);
// Constructor de copia // Constructor de copia
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
415
Un método de una clase C puede definir y/o devolver un objeto de la MISMA clase C
class MiClase{ MiClase Metodo(){ MiClase objeto; ...... return objeto; }
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
416
Ejercicio. Dado un vector de caracteres, construir otro vector nuevo con los caracteres del actual pasados a mayúsculas:
class MiVectorCaracteres{ private: vector <char> vector_privado; public: ............ void Aniade(char nuevo){ vector_privado.push_back(nuevo); } MiVectorCaracteres ToUpper(){ MiVectorCaracteres en_mayuscula; int tope = vector_privado.size();
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
Ejemplo. Supongamos una recta dada por Ax + By + C = 0. La ecuación de un recta perpendicular a la anterior y que pase por un punto (x0 , y0 ) es: y − mx + mx0 − y0 = 0 dónde m es la pendiente de la nueva recta e igual al opuesto de la inversa de la pendiente de la otra recta, es decir: m = B/A
class Recta{ <el resto no cambia nada> Recta Perpendicular (Punto2D pasando_por_aqui){ double coef_y = 1; double pendiente_nueva = B/A; double coef_x = -pendiente_nueva; double coef_ind = pasando_por_aqui.GetAbscisa() * pendiente_nueva - pasando_por_aqui.GetOrdenada();
for(int i = 0; i < tope; i++) en_mayuscula.Aniade(toupper(vector_privado.at(i))); return en_mayuscula;
Recta perpendicular(coef_x, coef_y, coef_ind);
} }; int main(){ MiVectorCaracteres cadena, cadena_mayuscula; cadena.Aniade('t'); cadena.Aniade('e');
417
return perpendicular; } }; int main(){ Punto2D un_punto(5, 3); Recta una_recta(2, 3, -11);
cadena_mayuscula = cadena.ToUpper();
// 2x + 3y - 11 = 0
Recta perpendicular_a_una_recta(una_recta.Perpendicular(un_punto)); ......... }
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
418
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
mcd = MCD(numerador, denominador); numerador = numerador/mcd; denominador = denominador/mcd;
Ejemplo. Añadimos a la clase Fraccion un método para sumarle otra fracción.
int MCD(int primero, int segundo){ bool hay_divisores, mcd_encontrado; int divisor, mcd; ............... return mcd; }
// función global
class Fraccion{ private: int numerador, denominador; public: Fraccion(int numerador_fraccion, int denominador_fraccion){ if (denominador_fraccion != 0){ numerador = numerador_fraccion; denominador = denominador_fraccion; } else{ numerador = 0; denominador = 0; } } int Numerador(){ return numerador; } int Denominador(){ return denominador; } void Normaliza(){ int mcd;
419
} Fraccion Suma(Fraccion otra_fraccion){ int suma_numerador; int suma_denominador; suma_numerador = numerador * otra_fraccion.Denominador() + denominador * otra_fraccion.Numerador() ; suma_denominador = denominador * otra_fraccion.Denominador(); Fraccion suma(suma_numerador, suma_denominador); suma.Normaliza(); return suma; } }; int main(){ int una_fraccion_numerador, una_fraccion_denominador, otra_fraccion_numerador, otra_fraccion_denominador; string mensaje_numerador = "\nIntroduce el numerador -> "; string mensaje_denominador = "\nIntroduce el denominador -> "; cout << mensaje_numerador; cin >> una_fraccion_numerador; cout << mensaje_denominador; cin >> una_fraccion_denominador; cout << mensaje_numerador; cin >> otra_fraccion_numerador; cout << mensaje_denominador;
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
cin >> otra_fraccion_denominador; Fraccion una_fraccion (una_fraccion_numerador, una_fraccion_denominador); Fraccion otra_fraccion(otra_fraccion_numerador, otra_fraccion_denominador); Fraccion suma_fracciones (una_fraccion.Suma(otra_fraccion));
420
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
421
Es preferible construir la clase Fraccion con los métodos
class Fraccion{ ....... Fraccion Suma(Fraccion otra_fraccion) { .... } Fraccion Multiplica(Fraccion otra_fraccion) { .... } }; que una clase específica para realizar operaciones con dos fracciones:
cout << "\nResultado de la suma: " << suma_fracciones.Numerador() << " / " << suma_fracciones.Denominador(); }
class ParejaFracciones{ private: Fraccion una_fraccion; Fraccion otra_fraccion; public: ParejaFracciones (Fraccion fraccion_primera, Fraccion fraccion_segunda) :una_fraccion(fraccion_primera), otra_fraccion(fraccion_segunda) { } Fraccion Suma(){ int suma_numerador, denominador_suma; suma_numerador = fraccion_primera.Numerador() * fraccion_segunda.Denominador() + fraccion_primera.Denominador() * fraccion_segunda.Numerador() ; suma_denominador = fraccion_primera.Denominador() * otra_fraccion.Denominador(); Fraccion suma(suma_numerador, suma_denominador); suma.Normaliza(); return suma; } };
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
422
Si tuviésemos que definir una clase Pareja_C para cada clase C sobre la que quisiésemos realizar operaciones con pares de objetos, el código se complicaría innecesariamente.
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
423
Ejemplo. En la relación de problemas III se construyeron las clases MiEntero y MiVectorEnteros. Añadir un método a MiEntero para que devuelva un vector de enteros con los dígitos del primero.
entero = 1563
-->
vector_enteros = {1, 5, 6, 3}
class MiVectorEnteros{ private: static const int DIM = 50; int vector_privado[DIM]; int total_utilizados; public: MiVectorEnteros() :total_utilizados(0) { } int TotalUtilizados(){ return total_utilizados; } void Aniade(int nuevo){ ........ } int Elemento(int indice){ ........ } ........ }; class MiEntero{ private: int entero; public: MiEntero(int un_entero) :entero(un_entero){ }
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
424
int Valor(){ return entero; } long int Factorial(){ ....... } MiVectorEnteros ToVector(){ MiVectorEnteros to_vector; ........ return to_vector; }
Clases (Segunda parte) Trabajando con varios objetos → Objetos declarados localmente en un método y métodos que devuelven objetos
Una posible implementación del método ToVector:
MiVectorEnteros ToVector(){ MiVectorEnteros to_vector; int digito = entero % 10; int entero_div_10 = entero; to_vector.Aniade(digito); entero_div_10 = entero_div_10 / 10; while (entero_div_10 != 0){ digito = entero_div_10 % 10; to_vector.Aniade(digito); entero_div_10 = entero_div_10 / 10; }
}; int main(){ MiEntero objeto_entero(1563); int total_componentes_vector; MiVectorEnteros vector; vector = objeto_entero.ToVector();
to_vector.Invierte(); // constructor sin parámetros // operador de asignación
total_componentes_vector = vector.TotalUtilizados(); for (int i=0; i<total_componentes_vector; i++) cout << vector.Elemento(i) << " "; } Otra forma de construir el objeto vector (usando el constructor de copia):
int main(){ MiEntero objeto_entero(1563); int total_componentes_vector; MiVectorEnteros vector(objeto_entero.ToVector()); //constructor de copia <resto de código no varía>
return to_vector; }
425
Clases (Segunda parte) Trabajando con varios objetos → Objetos como datos miembro de otros objetos
V.2.3.
426
Objetos como datos miembro de otros objetos
¿Un objeto puede ser dato miembro de otro objeto? Por supuesto. Como cualquier otro dato miembro, C++ permite que sea privado o público. Nosotros siempre usaremos datos privados. Su uso sigue las mismas normas de programación que cualquier otro dato miembro. Sólo usaremos objetos como datos miembro, cuando esté claro que forman parte del núcleo de la clase. La clase del objeto contenido debe declararse antes que la clase del objeto contenedor. La clase contenedora y la clase contenida no pueden ser la misma (con punteros sí se puede) En el objeto de la clase contenedora se crea una instancia de la clase contenida.
class ClaseContenida{ <datos miembro y métodos> }; class ClaseContenedora{ <private o public>: ClaseContenida dato_miembro; }; Ya lo usamos en la página 366 cuando declaramos un objeto vector dentro de nuestra clase MiVectorCaracteres:
class MiVectorCaracteres{ private: vector <char> vector_privado;
Clases (Segunda parte) Trabajando con varios objetos → Objetos como datos miembro de otros objetos
427
¿Qué ocurre cuando el constructor de la clase contenida tiene parámetros? Hay que crear el objeto con los parámetros adecuados. ¿Dónde? En la lista de inicialización del constructor.
class ClaseContenida{ public: ClaseContenida(int parametro){ ...... } <datos miembro y métodos> }; class ClaseContenedora{ private: // ClaseContenida dato_miembro(5); Error compilación ClaseContenida dato_miembro; public: ClaseContenedora() :dato_miembro(5) // Llamada al constructor con parámetros { } ........ };
Clases (Segunda parte) Trabajando con varios objetos → Objetos como datos miembro de otros objetos
428
En vez de construir el objeto interno en la clase contenedora, también podríamos construir el objeto fuera y pasarlo como parámetro al constructor. En dicho caso, debemos inicializar el dato miembro en la lista de inicialización, asignándole una copia del objeto pasado como parámetro (el compilador invoca al constructor de copia)
class ClaseContenida{ public: ClaseContenida(int parametro){ ...... } <datos miembro y métodos> }; class ClaseContenedora{ private: ClaseContenida dato_miembro; public: ClaseContenedora(ClaseContenida objeto) :dato_miembro(objeto) // Llamada al constructor de copia { } ........ };
Clases (Segunda parte) Trabajando con varios objetos → Objetos como datos miembro de otros objetos
429
Ejemplo. Recordad la clase SegmentoDirigido que contenía como datos miembro las 4 coordenadas (double) de los dos puntos que delimitan el segmento. En su lugar, podríamos haber declarado dos puntos dentro del segmento. Usamos la clase Punto2D tal y como aparece en la página 399.
class Punto2D{ <no cambia nada> }; class SegmentoDirigido{ private: Punto2D origen; Punto2D final; public: SegmentoDirigido(Punto2D punto_origen, Punto2D punto_final) :origen (punto_origen), // constructor de copia final (punto_final) // constructor de copia { } Punto2D GetOrigen(){ return origen; } Punto2D GetFinal(){ return final; } double Longitud(){ double sumando_abscisa = origen.GetAbscisa()-final.GetAbscisa(); double sumando_ordenada = origen.GetOrdenada()-final.GetOrdenada(); return sqrt(sumando_abscisa * sumando_abscisa + sumando_ordenada * sumando_ordenada); }
Clases (Segunda parte) Trabajando con varios objetos → Objetos como datos miembro de otros objetos
430
.......... }; int main(){ Punto2D origen(3.4, 4.5); Punto2D final(6.7, 9.2); SegmentoDirigido segmento (origen, final); El método que, a partir de una recta, devolvía el segmento delimitado por dos valores de abscisas podría haberse definido como sigue;
class Punto2D{ ...... }; class Recta{ private: ............ public: ............ SegmentoDirigido Segmento(double abscisa_origen, double abscisa_final){ double ordenada_origen = Ordenada(abscisa_origen); double ordenada_final = Ordenada(abscisa_final); Punto2D pto_origen(abscisa_origen, ordenada_origen); Punto2D pto_final(abscisa_final, ordenada_final); SegmentoDirigido segmento(pto_origen, pto_final); return segmento; } };
Clases (Segunda parte) Trabajando con varios objetos → Objetos como datos miembro de otros objetos
431
Ejemplo. Añadimos una fecha de apertura a la cuenta bancaria.
class Fecha{ private: unsigned int dia, mes, anio; public: Fecha(unsigned int el_dia, unsigned int el_mes, unsigned int el_anio) { bool es_bisiesto; bool es_fecha_correcta; const unsigned int anio_inferior = 1900; const unsigned int anio_superior = 2500; unsigned int dias_por_mes[12] = {31,28,31,30,31,30,31,31,30,31,30,31}; // Meses de Enero a Diciembre es_bisiesto = (el_anio % 4 == 0 && el_anio % 100 != 0) || el_anio % 400 == 0; es_fecha_correcta = (1 <= el_dia && el_dia <= dias_por_mes[el_mes - 1] && 1 <= el_mes && el_mes <= 12 && anio_inferior <= el_anio && el_anio <= anio_superior); if (es_fecha_correcta){ if (el_mes == 2) if (es_bisiesto){ if (el_dia > 29) es_fecha_correcta = false; } else if (el_dia > 28) es_fecha_correcta = false; }
Clases (Segunda parte) Trabajando con varios objetos â&#x2020;&#x2019; Objetos como datos miembro de otros objetos
432
if (es_fecha_correcta){ dia = el_dia; mes = el_mes; anio = el_anio; } } unsigned int GetDia(){ return dia; } unsigned int GetMes(){ return mes; } unsigned int GetAnio(){ return anio; } }; class CuentaBancaria{ private: Fecha fecha_apertura; double saldo; const string identificador; static const string IdBanco; public: CuentaBancaria(string identificador_cuenta, double saldo_inicial, Fecha fecha_apertura_cuenta) :saldo(saldo_inicial), identificador(IdBanco + identificador_cuenta), fecha_apertura(fecha_apertura_cuenta) // constructor de copia { } string Identificador(){ return identificador; }
Clases (Segunda parte) Trabajando con varios objetos â&#x2020;&#x2019; Objetos como datos miembro de otros objetos
double Saldo(){ return saldo; } void Ingresa(double cantidad){ if (cantidad > 0) saldo = saldo + cantidad; } void Retira(double cantidad){ if (cantidad > 0 && cantidad < saldo) saldo = saldo - cantidad; } }; const string CuentaBancaria::IdBanco = "2031 0381 "; int main(){ Fecha una_fecha(12, 11, 2011); CuentaBancaria cuenta("45 0100001367", 2000, ........
una_fecha);
433
Clases (Segunda parte) Trabajando con varios objetos → Vectores de objetos. Matrices
V.2.4.
434
Vectores de objetos. Matrices
Clases (Segunda parte) Trabajando con varios objetos → Vectores de objetos. Matrices
435
Por lo tanto, la clase debe proporcionar un constructor por defecto, es decir, sin parámetros.
V.2.4.1. Usando vectores clásicos
O bien porque no tenga ningún constructor definido, y sea el proporcionado de oficio por el compilador.
Para crear un vector clásico de objetos, se usa la notación habitual:
O bien porque esté definido explícitamente en la clase.
const int TAMANIO = 50; MiClase vector_de_objetos[TAMANIO];
Si la clase no tiene constructor por defecto, hay que crear los objetos en la misma declaración del vector:
Recordemos que el compilador crea un objeto cuando se declara:
class MiClase{ ........ }; int main(){ MiClase objeto; ........ } Lo mismo ocurre con las componentes de un vector; se crean en el momento de definir el vector:
class MiClase{ ........ }; int main(){ const int TAMANIO = 50; MiClase vector_de_objetos[TAMANIO]; ........ }
// <- Se crean 50 objetos
class MiClase{ public: MiClase(int parametro){ ....... } }; int main(){ const int TAMANIO = 2; MiClase vector_de_objetos[TAMANIO] = {MiClase(2), MiClase(4)}; ........ }
Clases (Segunda parte) Trabajando con varios objetos → Vectores de objetos. Matrices
436
V.2.4.2. Usando la plantilla vector
V.2.4.3.
Podemos usar la plantilla vector donde el tipo base es una clase:
class MiClase{ ........ }; int main(){ vector <MiClase> vector_STL_de_objetos;
Clases (Segunda parte) Trabajando con varios objetos → Vectores de objetos. Matrices
// No se crea ningún objeto // de MiClase
vector_STL_de_objetos.push_back(un_objeto); ........ } En este caso, al definir el vector, no se crea ningún objeto de MiClase. Por lo tanto, la clase MiClase no está obligada a proporcionar un constructor por defecto (sin parámetros).
El operador de asignación y los vectores
class MiClase{ ........ }; int main(){ const int TAMANIO = 50; MiClase vector_de_objetos[TAMANIO]; MiClase un_objeto;
MiClase un_objeto;
437
vector_de_objetos[0] = un_objeto; ........
// <- Se crean 50 objetos
// Asignación entre objetos
} '
$
Si tenemos un vector de objetos y queremos asignarle a alguna componente un objeto, el tipo base del vector debe proporcionar un operador de asignación (tanto si usamos vectores clásicos como si usamos la plantilla vector) Ya sea el proporcionado de oficio por el compilador. Ya sea el sobrecargado por el programador (la sobrecarga del operador de asignación se verá en el segundo cuatrimestre) &
%
Recordemos que en el caso de que hayamos definido un dato miembro constante (no estático) en una clase, el compilador no puede proporcionar de oficio un operador de asignación por defecto (ver página 389) En estos casos, no podremos utilizar dicha clase como tipo base de un vector (clásico o la plantilla vector), produciéndose un error de compilación.
Clases (Segunda parte) Trabajando con varios objetos → Vectores de objetos. Matrices
438
Clases (Segunda parte) Trabajando con varios objetos → Vectores de objetos. Matrices
MiVectorCaracteres cadena_1; MiVectorCaracteres cadena_2; int num_cadenas;
V.2.4.4. Matrices Una vez que sabemos definir objetos dentro de otros, podemos definir una clase matriz obligando a que únicamente puedan añadirse filas enteras. Para ello, definiremos en la sección private un vector de vectores.
class MiMatrizCaracteres{ private: static const int MAXIMO_FILAS = 40; MiVectorCaracteres matriz_privada[MAXIMO_FILAS]; //Se crean 40 objetos! int filas_utilizadas; public: MiMatrizCaracteres() :filas_utilizadas(0) { } int FilasUtilizadas(){ return filas_utilizadas; } char Elemento(int fila, int columna){ return matriz_privada[fila].Elemento(columna); } MiVectorCaracteres Fila(int indice_fila){ return matriz_privada[indice_fila]; } void Aniade(MiVectorCaracteres nueva_fila){ if (filas_utilizadas < MAXIMO_FILAS){ matriz_privada[filas_utilizadas] = nueva_fila; filas_utilizadas++; } } }; int main(){ MiMatrizCaracteres frase;
439
cadena_1.Aniade('u'); cadena_1.Aniade('n'); cadena_2.Aniade('d'); cadena_2.Aniade('o'); cadena_2.Aniade('s'); frase.Aniade(cadena_1); frase.Aniade(cadena_2); num_cadenas = frase.FilasUtilizadas(); for (int fila = 0; fila < num_cadenas; fila++){ MiVectorCaracteres vector_fila; vector_fila = frase.Fila(fila); for (int col = 0; col < vector_fila.TotalUtilizados(); col++) cout << vector_fila.Elemento(col); cout << "\n"; } } Si se quiere, puede mantenerse la misma interfaz pública (añadir y devolver filas enteras) pero cambiar la implementación interna de la clase, usando una matriz clásica de dos dimensiones
char matriz_privada[MAXIMO_FILAS][MAXIMO_COLUMNAS]
Clases (Segunda parte) Trabajando con varios objetos → Vectores de objetos. Matrices
440
La elección de una implementación u otra dependerá del tipo de operaciones que vayamos a realizar y si resulta más cómodo o eficiente hacerlo con un tipo u otro. #
El método Fila devuelve una copia de la fila de la matriz. Si modificamos la copia, no se modifica la correspondiente fila de la matriz.
"
MiVectorCaracteres copia; copia = frase.Fila(0); copia.Aniade('o');
!
// NO se modifica la frase!
Ampliación:
Clases (Segunda parte) Trabajando con varios objetos → Vectores de objetos. Matrices
441
Ejemplo. Supongamos que queremos encontrar un carácter. Para devolver la fila y columna en la que se encuentra, podemos definir un struct:
struct ParFilaColumna{ int fila; int columna; }; class MiMatrizCaracteres{ ............. ParFilaColumna PrimeraOcurrencia(char buscado){ bool encontrado = false; ParFilaColumna fila_columna_encontrado = {-1,-1}; int i, j; MiVectorCaracteres fila;
Tal y como se indicó en la página 393, los lenguajes orientados a objetos no suelen trabajar con copias de objetos, sino que lo usual es que los métodos devuelvan referencias a objetos. Por lo tanto, una sentencia análoga a copia.Aniade('o') sí modificaría la fila de la matriz. Esto ocurre porque sólo hay un objeto, aunque es accesible desde muchos sitios y con nombres distintos. Esta forma de trabajar se consigue en C++ usando punteros (pointers) (se verá en el segundo cuatrimestre)
for (i=0; i<filas_utilizadas && !encontrado ; i++){ for (j=0; j<matriz_privada[i].TotalUtilizados() && !encontrado; j++){ fila = matriz_privada[i]; if (fila.Elemento(j) == buscado){ fila_columna_encontrado.fila = i; fila_columna_encontrado.columna = j; encontrado = true; } } } return fila_columna_encontrado; } };
Clases (Segunda parte) Trabajando con varios objetos → Vectores de objetos. Matrices
442
Ejemplo. Definimos la clase matriz de caracteres usando la plantilla estándar vector (vector de vectores). Cambiamos la interfaz pública:
class MiMatrizCaracteres{ private: vector <vector <char>> matriz_privada; public: int FilasUtilizadas(){ return matriz_privada.size(); } char Elemento(int fila, int columna){ return matriz_privada.at(fila).at(columna); // o bien: // return matriz_privada[fila][columna]; } vector <char> Fila (int indice_fila){ return matriz_privada.at(indice_fila); } void Aniade(vector <char> nueva_fila){ matriz_privada.push_back(nueva_fila); } }; int main(){ MiMatrizCaracteres matriz; vector <char> vector_caracteres_STL; vector <char> otro_vector_caracteres_STL; vector_caracteres_STL.push_back('h'); vector_caracteres_STL.push_back('o'); vector_caracteres_STL.push_back('l'); vector_caracteres_STL.push_back('a');
Clases (Segunda parte) Trabajando con varios objetos → Vectores de objetos. Matrices
443
otro_vector_caracteres_STL.push_back('b'); otro_vector_caracteres_STL.push_back('y'); otro_vector_caracteres_STL.push_back('e'); matriz.Aniade(vector_caracteres_STL); matriz.Aniade(otro_vector_caracteres_STL); vector <char> una_fila; una_fila = matriz.Fila(0); una_fila.push_back('!');
// No se modifica la fila de la matriz!
Clases (Segunda parte) Diseño de una solución →
V.3. V.3.1.
444
Diseño de una solución Funciones globales versus métodos
Hemos visto que las funciones pueden definirse: Encapsuladas dentro de una clase (métodos) Globales, fuera de las clases Como norma general, no usaremos funciones globales. Sin embargo, en ocasiones, puede justificarse el uso de éstas: cuando sean funciones auxiliares genéricas que podamos utilizar en muchas clases diferentes, cuando sean funciones definidas para trabajar con tipos básicos predefinidos.
Clases (Segunda parte) Diseño de una solución → Funciones globales versus métodos
445
Usando un método privado
class Punto2D{ private: double abscisa; double ordenada; bool SonIguales(double uno, double otro) { return fabs(uno-otro) <= 1e-6; } public: Punto2D(double abscisaPunto, double ordenadaPunto) :abscisa(abscisaPunto), ordenada(ordenadaPunto) { } double GetAbscisa(){ return abscisa; } double GetOrdenada(){ return ordenada; } bool EsIgual_a (Punto2D otro_punto){ return SonIguales(abscisa, otro_punto.GetAbscisa()) && SonIguales(ordenada, otro_punto.GetOrdenada()); } }; Esta aproximación es correcta, pero ¿y si necesitamos comparar dos doubles en otras clases? Habría que repetir el código del método SonIguales en cada una de estas clases.
Clases (Segunda parte) Diseño de una solución → Funciones globales versus métodos
446
447
#include <iostream> using namespace std;
Usando una función global
#include <iostream> using namespace std; bool SonIguales(double uno, double otro) return fabs(uno-otro) <= 1e-6; }
Clases (Segunda parte) Diseño de una solución → Funciones globales versus métodos
{
class Punto2D{ private: double abscisa; double ordenada; public: Punto2D(double abscisaPunto, double ordenadaPunto) :abscisa(abscisaPunto), ordenada(ordenadaPunto) { } double GetAbscisa(){ return abscisa; } double GetOrdenada(){ return ordenada; } bool EsIgual_a (Punto2D otro_punto){ return SonIguales(abscisa, otro_punto.GetAbscisa()) && SonIguales(ordenada, otro_punto.GetOrdenada()); } }; int main(){ <Aquí también puedo usar directamente SonIguales> }
bool SonIguales(double uno, double otro) return fabs(uno-otro) <= 1e-6; }
{
class TransaccionBancaria{ public: double Importe(){ ............... } .............. }; int main(){ TransaccionBancaria una_transaccion, otra_transaccion; bool son_iguales; ........... son_iguales = SonIguales(una_transaccion.Importe(), otra_transaccion.Importe()); ...........
Otros ejemplos de funciones que podríamos poner globales:
int MaximoComunDivisor (int un_entero, int otro_entero) double Maximo (double un_real, double otro_real) char Transforma_a_Mayuscula (char un_caracter)
Clases (Segunda parte) Diseño de una solución → Funciones globales versus métodos
448
Usando un método público ¿Nos vale poner el método SonIguales como público y así poder usarlo en otros sitios? Poder, se puede, pero no tiene sentido:
class Punto2D{ private: double abscisa; double ordenada; bool SonIguales(double uno, double otro) { return fabs(uno-otro) <= 1e-6; } ...... public: ...... }; class TransaccionBancaria{ private: double importe; public: double Importe(){ return importe; } .............. }; int main(){ TransaccionBancaria una_transaccion, otra_transaccion; Punto2D que_pinta_aqui_un_punto(4.3, 5.8); bool son_iguales; ........... son_iguales = que_pinta_aqui_un_punto.SonIguales( una_transaccion.Importe(), otra_transaccion.Importe());
Clases (Segunda parte) Diseño de una solución → Funciones globales versus métodos
449
Ampliación: Muchos lenguajes de programación prohíben las funciones globales. Sin embargo, proporcionan un mecanismo muy similar, que son las clases estáticas (static class). Éstas son clases que contienen las funciones que nosotros pretendemos utilizar de forma global, pasándoles como parámetros los datos sobre los que deben actuar (al contrario que los métodos, que actúan sobre los datos del objeto y se les pasa como parámetro información adicional). Las clases estáticas tienen la peculiaridad de que el compilador te ofrece una instancia que se llama igual que la clase y el programador no puede crear otras instancias. La diferencia con las funciones globales es que la llamada a la función debe ir precedida del nombre de la clase. Este recurso también está disponible en C++. El uso de clases estáticas, al igual que las funciones globales, debe limitarse a las mismas situaciones que hemos indicado en el tema.
Clases (Segunda parte) Diseño de una solución → Tipos de relaciones entre clases
V.3.2.
450
Tipos de relaciones entre clases
Hasta ahora hemos trabajado con clases aisladas. La resolución de un programa complejo necesita la construcción de muchos objetos. Éstos no funcionan de forma aislada sino que cooperan entre sí. El programador diseñará la solución estableciendo las relaciones (relationships) entre las clases. Estas relaciones se determinan en función de las responsabilidades de las clases. – TrabajadorDatosPersonales se relaciona con Fecha – MiMatrizCaracteres se relaciona con MiVectorCaracteres – Triangulo se relaciona con SegmentoDirigido – CuentaBancaria se relaciona con Cliente – Transaccion se relaciona con CuentaBancaria Las relaciones entre las distintas clases nos indican cómo se comunican los objetos de esas clases entre sí. Una vez realizado el proceso de diseño, se procederá a implementar la solución (en nuestro caso, en C++)
Clases (Segunda parte) Diseño de una solución → Tipos de relaciones entre clases
451
Tipos de relaciones entre clases: Dependencia (Dependency) Establece una relación de uso esporádica. Asociación (Association) Establece una relación estructural o conexión permanente entre objetos (o, al menos, durante gran parte de la vida de éstos) Generalización/Especialización (Generalization/Specialization) Establece una dependencia entre clases a través de una relación de herencia (inheritance) → segundo cuatrimestre La determinación de las clases, sus responsabilidades y relaciones es el primer paso en el diseño de una solución. La implementación en cualquier lenguaje (por ejemplo C++) será siempre posterior.
Clases (Segunda parte) DiseĂąo de una soluciĂłn â&#x2020;&#x2019; RelaciĂłn de dependencia
V.3.3.
452
Establece una relaciĂłn de uso esporĂĄdica. Una clase Servidor S depende de otra clase Cliente C, cuando en algĂşn momento determinado, cualquier objeto de C necesite ejecutar algĂşn mĂŠtodo de un objeto de S. Para indicarlo en UML, se usa una ďŹ&#x201A;echa discontinua:
! !
Situaciones tĂpicas: â&#x20AC;&#x201C; Objetos de S pasados como parĂĄmetros a mĂŠtodos de C.
! &
â&#x20AC;&#x201C; Objetos de S declarados localmente dentro de mĂŠtodos de C.
â&#x20AC;&#x201C; Objetos de S devueltos como parĂĄmetros por mĂŠtodos de C.
"
453
El ejemplo visto en la pĂĄgina 395 muestra que hay una dependencia entre Recta y Punto2D ya que, en alguna ocasiĂłn (cuando se ejecuta el mĂŠtodo Contiene por ejemplo) la recta necesita tener acceso a un punto.
RelaciĂłn de dependencia
Clases (Segunda parte) DiseĂąo de una soluciĂłn â&#x2020;&#x2019; RelaciĂłn de dependencia
ÂżPor quĂŠ esta dependencia? Porque cualquier cambio en S implica un cambio en C. Al revĂŠs, no.
" # $ # % # # # $ # % # # # & # $ %
Clases (Segunda parte) DiseĂąo de una soluciĂłn â&#x2020;&#x2019; RelaciĂłn de asociaciĂłn
V.3.4.
454
RelaciĂłn de asociaciĂłn
Dos clases A y B mantienen una asociaciĂłn cuando los objetos de ambas clases mantienen una relaciĂłn estructural permanente, es decir, que se mantiene durante toda la vida de los objetos (o, al menos, durante un tiempo prolongado) Para indicarlo en UML, se usa una ďŹ&#x201A;echa continua:
Clases (Segunda parte) DiseĂąo de una soluciĂłn â&#x2020;&#x2019; RelaciĂłn de asociaciĂłn
455
Ejemplo. En la pĂĄgina 429 vimos que un objeto de la clase SegmentoDirigido contiene dos objetos de la clase Punto2D. Para que exista un segmento, deben existir siempre los dos puntos. Por tanto, la relaciĂłn entre ambas es de asociaciĂłn: ! !
Situaciones tĂpicas: â&#x20AC;&#x201C; Una clase A que tiene un dato miembro (pĂşblico o privado) de tipo B.
"
! ! #
â&#x20AC;&#x201C; Una clase A que tiene un dato miembro (pĂşblico o privado) de tipo B, y viceversa, B tiene un dato miembro de tipo A. Implica conocimientos mĂĄs avanzados. La asociaciĂłn implica una relaciĂłn mĂĄs fuerte que la dependencia. Al igual que ocurrĂa con la dependencia, cualquier cambio en B afecta a A.
" # $ %
Nota. Observad que tambiĂŠn hay una relaciĂłn de dependencia entre ambas clases ya que, en algĂşn momento determinado, la ejecuciĂłn de alguno de los mĂŠtodos del segmento devuelve un punto. En estos casos, se omite la ďŹ&#x201A;echa que indica la relaciĂłn mĂĄs dĂŠbil (la de dependencia). El diagrama completo se muestra en la siguiente pĂĄgina:
Clases (Segunda parte) DiseĂąo de una soluciĂłn â&#x2020;&#x2019; RelaciĂłn de asociaciĂłn
456
! !
" ! ! # " # $ %
& ! ) & # ' # ( # # # ' # ( # # # ) # ' (
Clases (Segunda parte) DiseĂąo de una soluciĂłn â&#x2020;&#x2019; RelaciĂłn de asociaciĂłn
457
Ejemplo. En la pĂĄgina 431 vimos que un objeto de la clase CuentaBancaria contiene (relaciĂłn estructural) un objeto Fecha. Mientras exista la cuenta, existirĂĄ la fecha de apertura correspondiente. Por tanto, la relaciĂłn se ajusta a una asociaciĂłn.
Clases (Segunda parte) Diseño de una solución → Acoplamiento
V.3.5.
458
Acoplamiento
Clases (Segunda parte) Diseño de una solución → Arquitectura por capas
V.3.6.
459
Arquitectura por capas
Si una clase tiene una relación de dependencia o de asociación con muchas otras, cualquier modificación de éstas puede afectar a la primera. Esto se detecta fácilmente en el diagrama viendo las cajas de las que salen flechas.
Al separar entradas de datos, salida de datos y cómputos, conseguimos que el desarrollo de las clases que realizan los cómputos propios de la aplicación no dependa del desarrollo de las clases dedicadas a las E/S de datos.
Cuando una clase usa (depende de) otras clases, se produce una dependencia entre ellas, también llamada acoplamiento (coupling).
En general, el diseño de una solución pasa por una organización en capas, obteniendo lo que se denomina una arquitectura en varias capas (multi-layer architecture)
Tipos de acoplamiento: Acoplamiento fuerte (Tight coupling) Se produce cuando muchas clases dependen de muchas otras. Cualquier cambio en una obliga a un cambio en las otras. Acoplamiento débil (Loose coupling) Se produce cuando no se da la anterior circunstancia: – Hay bastantes clases que no dependen de ninguna otra. – Cada una de las restantes clases no dependen de un gran número de clases.
En el gráfico siguiente se presenta la arquitectura típica en 3 capas de muchas aplicaciones de gestión. Ampliación: En el diseño de una solución completa, también ha de tenerse en cuenta la organización física de ésta (ordenadores servidores y clientes en los que se distribuirá la aplicación) En este contexto se usa el término arquitectura en varios niveles (multi-tier architecture)
Clases (Segunda parte) DiseĂąo de una soluciĂłn â&#x2020;&#x2019; Arquitectura por capas
460
! " " #
! " $ # %&'( % ')*' + " !
Clases (Segunda parte) DiseĂąo de una soluciĂłn â&#x2020;&#x2019; Casos prĂĄcticos de diseĂąo
V.3.7.
461
Casos prĂĄcticos de diseĂąo
En este tema se han introducido conceptos bĂĄsicos de programaciĂłn con C++, viendo cĂłmo se produce la copia entre objetos, cĂłmo pasar objetos como parĂĄmetros, etc. Una vez ďŹ jados estos conceptos, hemos aumentado el nivel de abstracciĂłn introduciendo los conceptos de cohesiĂłn, relaciones entre clases, etc. En el desarrollo de un proyecto software, el primer paso serĂĄ siempre el diseĂąo abstracto de la soluciĂłn. La implementaciĂłn en un lenguaje concreto serĂĄ posterior. En este apartado veremos algunos casos prĂĄcticos con los pasos a seguir a la hora de plantear la soluciĂłn a un problema.
Clases (Segunda parte) DiseĂąo de una soluciĂłn â&#x2020;&#x2019; Casos prĂĄcticos de diseĂąo
462
V.3.7.1. Matriz de caracteres Queremos construir un sencillo programa que necesitarĂĄ manejar vectores y matrices de caracteres e imprimirlos en pantalla. DeterminaciĂłn de responsabilidades:
â&#x20AC;&#x201C; MiMatrizCaracteres se ocupa de representar una matriz de caracteres, como un vector de MiVectorCaracteres. TendrĂĄ funcionalidades como permitir aĂąadir un vector de caracteres, obtener el tamaĂąo actual, obtener una ďŹ la determinada, etc. Observad que permitimos que cada ďŹ la tenga un nĂşmero distinto de columnas. â&#x20AC;&#x201C; ImpresorVectorCaracteres se ocupa de imprimir un vector de caracteres en el perifĂŠrico de salida de datos por defecto. â&#x20AC;&#x201C; LectorVectorCaracteres para leer datos desde un perifĂŠrico y llenar vectores de caracteres. â&#x20AC;&#x201C; ImpresorMatrizCaracteres se ocupa de imprimir una matriz de caracteres en el perifĂŠrico de salida de datos por defecto. Relaciones entre las clases: clases
463
Por tanto, la relaciĂłn es temporal y se ajusta a una dependencia en la que pasamos como parĂĄmetro al mĂŠtodo de ImpresorVectorCaracteres encargado de imprimir el objeto MiVectorCaracteres sobre el que va a trabajar.
â&#x20AC;&#x201C; MiVectorCaracteres se ocupa de representar un vector de caracteres, con funcionalidades como permitir aĂąadir caracteres, obtener el tamaĂąo actual, etc.
â&#x20AC;&#x201C; ÂżCĂłmo estĂĄn ligadas las ImpresorVectorCaracteres?
Clases (Segunda parte) DiseĂąo de una soluciĂłn â&#x2020;&#x2019; Casos prĂĄcticos de diseĂąo
MiVectorCaracteres
e
Un objeto de ImpresionVectorCaracteres necesitarĂĄ, en algĂşn momento, acceder a los datos de un objeto de MiVectorCaracteres. En otro momento, el mismo objeto ImpresorVectorCaracteres querrĂĄ acceder a los datos de otro objeto de MiVectorCaracteres, para imprimir otro vector distinto.
!
â&#x20AC;&#x201C; De igual forma, un objeto de la clase ImpresorMatrizCaracteres necesitarĂĄ crear localmente un objeto de la clase ImpresorVectorCaracteres cuando ejecute el mĂŠtodo de imprimir. Posteriormente, ya no lo necesitarĂĄ. â&#x20AC;&#x201C; LectorVectorCaracteres tambiĂŠn depende de MiVectorCaracteres ya que, en algĂşn momento, un objeto de la primera clase devolverĂĄ un objeto de la segunda (cuando se ejecute el mĂŠtodo LeeVector()) â&#x20AC;&#x201C; La relaciĂłn entre MiMatrizCaracteres y MiVectorCaracteres es mĂĄs fuerte ya que la primera contiene un vector de MiVectorCaracteres. Es una relaciĂłn de asociaciĂłn. TambiĂŠn hay una relaciĂłn de dependencia ya que la clase MiMatrizCaracteres contendrĂĄ un mĂŠtodo para devolver una ďŹ la determinada, es decir, un objeto de la clase MiVectorCaracteres. En cualquier caso, como es una relaciĂłn mĂĄs dĂŠbil que la asociaciĂłn, Ăşnicamente se mostrarĂĄ esta Ăşltima (realmente una asociaciĂłn es un tipo fuerte de dependencia) El diseĂąo completo de la soluciĂłn quedarĂa:
Clases (Segunda parte) DiseĂąo de una soluciĂłn â&#x2020;&#x2019; Casos prĂĄcticos de diseĂąo
464
!
! !" !
) *!$ ( + , ! . ) *!$ (/ " ! $ $ "
( # $ $ # % &' (
Clases (Segunda parte) DiseĂąo de una soluciĂłn â&#x2020;&#x2019; Casos prĂĄcticos de diseĂąo
465
Una vez hecho el diseĂąo de la soluciĂłn, veamos una posible implementaciĂłn en C++:
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: MiVectorCaracteres() :total_utilizados(0) { } int TotalUtilizados(){ return total_utilizados; } void BorraTodos(){ total_utilizados = 0; } void Aniade(char nuevo){ if (total_utilizados < TAMANIO){ vector_privado[total_utilizados] = nuevo; total_utilizados++; } } char Elemento(int indice){ return vector_privado[indice]; } };
Clases (Segunda parte) Diseño de una solución → Casos prácticos de diseño
466
class MiMatrizCaracteres{ private: static const int TAMANIO_FILAS = 40; MiVectorCaracteres matriz_privada[TAMANIO_FILAS]; //Se crean 40 objetos! int filas_utilizadas; public: MiMatrizCaracteres() :filas_utilizadas(0) { } int FilasUtilizadas(){ return filas_utilizadas; } char Elemento (int fila, int columna){ return matriz_privada[fila].Elemento(columna); } void Aniade(MiVectorCaracteres nueva_fila){ if (filas_utilizadas < TAMANIO_FILAS){ matriz_privada[filas_utilizadas] = nueva_fila; filas_utilizadas++; } } MiVectorCaracteres Fila(int indice_fila){ return matriz_privada[indice_fila]; } }; // Habría que añadir métodos como Modifica, Elimina, etc ... class ImpresorVectorCaracteres{ public: void Imprime(string mensaje, MiVectorCaracteres un_vector){ cout << mensaje << "|"; for (int i=0; i<un_vector.TotalUtilizados(); i++) cout << un_vector.Elemento(i); cout << "|";
Clases (Segunda parte) Diseño de una solución → Casos prácticos de diseño
467
} }; class LectorVectorCaracteres{ // Dentro de esta clase usamos algunos private: // conceptos que se ven en el segundo const char terminador; // cuatrimestre const string nombre_fichero; ifstream lector; // Necesita #include <fstream> public: LectorVectorCaracteres(string nombre_fichero, char terminador_datos) :terminador(terminador_datos), nombre_fichero(nombre_fichero) { } void AbreFichero(){ lector.open(nombre_fichero); } void CierraFichero(){ lector.close(); } MiVectorCaracteres LeeVector(){ MiVectorCaracteres vector_leido; char caracter; caracter = lector.get();
// lector >> caracter; no lee separadores // como los espacios en blanco. // lector.get() sí los lee
while (caracter != terminador){ vector_leido.Aniade(caracter); caracter = lector.get(); } return vector_leido; }
Clases (Segunda parte) Diseño de una solución → Casos prácticos de diseño
468
bool HayDatos(){ lector.peek(); return !lector.fail() && !lector.eof(); } }; class ImpresorMatrizCaracteres{ public: void Imprime(string mensaje_general, string mensaje_por_fila, MiMatrizCaracteres una_matriz){ ImpresorVectorCaracteres output_fila; cout << mensaje_general; for (int i=0; i<una_matriz.FilasUtilizadas(); i++){ output_fila.Imprime(mensaje_por_fila, una_matriz.Fila(i)); } } }; int main(){ ImpresorVectorCaracteres output_vector_caracteres; MiMatrizCaracteres matriz; ImpresorMatrizCaracteres output_matriz; LectorVectorCaracteres input_vectores_caracteres("DatosEntrada.txt"); input_vectores_caracteres.AbreFichero(); while (input_vectores_caracteres.HayDatos()){ MiVectorCaracteres vector(input_vectores_caracteres.LeeVector()); matriz.Aniade(vector); } input_vectores_caracteres.CierraFichero(); output_matriz.Imprime("\nDatos de la matriz: ","\nFila: ", matriz); .........
Clases (Segunda parte) Diseño de una solución → Casos prácticos de diseño
V.3.7.2.
469
Gestión de nóminas
Descripción de requerimientos Una empresa quiere gestionar las nóminas de sus empleados (empezamos a ver este ejemplo en la página 290 y en la relación de problemas III). El cómputo de la nómina se realiza basándose en los siguientes criterios: 1. Hay cuatro tipos de categorías laborales: Operario, Base, Administrativo y Directivo. 2. Se parte de un salario base que depende de la antigüedad del trabajador y de su categoría laboral. Dicho salario base se incrementa con un tanto por ciento igual al número de años trabajados. 3. Los trabajadores tienen complementos en su nómina por el número de horas extraordinarias trabajadas. La hora se paga distinta según la categoría. Además, al complemento que sale al computar el número de horas extraordinarias, se le aplica una subida con un tanto por ciento igual al número de años trabajados. 4. Se almacenan en disco los datos de los trabajadores, tanto los personales, como los del puesto de trabajo (así como las horas de trabajo extraordinarias realizadas por cada uno). 5. También se almacenan en disco las tablas salariales (el saldo base de cada una de las categorías laborales y la cuantía percibida por hora) Diseño de la solución Determinación de responsabilidades: – Fecha se encargará de representar una fecha y proporcionará métodos para obtener, por ejemplo, el número de días pasados
Clases (Segunda parte) Diseño de una solución → Casos prácticos de diseño
470
hasta hoy.
Clases (Segunda parte) Diseño de una solución → Casos prácticos de diseño
471
Relaciones entre las clases:
– Para representar un trabajador, distinguiremos entre sus datos personales y los del puesto de trabajo. Para ello, definiremos las siguiente clases:
– Las clases DatosPersonalesy Datos_del_Puesto tendrán una relación de asociación con Fecha ya que ambas contienen un dato de este tipo.
◦ DatosPersonales para almacenar los datos personales como la fecha de nacimiento, su dirección, etc.
– Todos los métodos de la clase Salario necesitarán acceder a la información suministrada por las tablas salariales por lo que habrá una relación de asociación entre Salario y las clases TablaSalarioBase y TablaSalarioHorasExtraordinarias .
◦ Datos_del_Puesto para almacenar los datos del puesto de trabajo (categoría laboral, fecha de incorporación, etc) ◦ Trabajador será una clase que aglutine la información dada por las dos anteriores. – Las clases TablaSalarioBase y TablaSalarioHorasExtraordinarias se encargarán de almacenar las tablas salariales: la cuantía base de cada categoría laboral y la cuantía a pagar por hora extraordinaria trabajada (según sea la categoría laboral). – La clase Salario se encargará de computar los salarios en función de la política descrita anteriormente. – Los datos históricos se almacenarán en: ◦ Un fichero de clientes, que incluirá los datos personales, del puesto de trabajo y el número de horas extraordinarias trabajadas. La clase LectorTrabajadores tendrá la responsabilidad de leer dichos datos. ◦ Sendos ficheros con las tablas salariales básicas: en un fichero se guardan los cuatro sueldos de cada categoría laboral y en otro fichero las cuatro cantidades que representan el complemento por hora extraordinaria de cada categoría. La clase LectorTablasSalariales tendrá la responsabilidad de leer dichas tablas de los ficheros.
Cuando queramos saber el salario de un trabajador, la clase Salario necesitará (en ese momento y no antes) conocer sus datos laborales (categoría laboral y antigüedad) por lo que habrá una dependencia entre Salario y Datos_del_Puesto. No necesitará conocer sus datos personales, por lo que Salario no dependerá de DatosPersonales. – LectorTrabajadores dependerá de Trabajador ya que la primera, en algún momento, construye objetos de la segunda, leyendo los datos desde un fichero. De la misma forma, LectorTablasSalariales dependerá de TablaSalarioBase y TablaSalarioHorasExtraordinarias . En la siguiente página se muestra el diseño completo de la solución. Se puede obtener este gráfico en el enlace:
http://decsai.ugr.es/jccubero/salario.pdf En las clases de prácticas se verá una posible implementación en C++.
Clases (Segunda parte) Diseño de una solución → Casos prácticos de diseño
472
Clases (Segunda parte) Tratamiento de errores con excepciones →
V.4.
473
Tratamiento de errores con excepciones
Supongamos que se violan las precondiciones de un método ¿Cómo lo notificamos al cliente del correspondiente objeto?
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: ......... void Aniade(char nuevo){ if (total_utilizados < TAMANIO){ vector_privado[total_utilizados] = nuevo; total_utilizados++; } else cout << "No hay componentes suficientes"; } }; int main(){ MiVectorCaracteres vector; vector.Aniade('h'); .......... vector.Aniade('a'); // Si ya no cabe => "No hay componentes suficientes" Ya sabemos que esta solución rompe la norma básica de no mezclar C-E/S
Capa: Acceso a Datos
Capa: Lógica Aplicación
Capa: Presentación
Clases (Segunda parte) Tratamiento de errores con excepciones →
474
Clases (Segunda parte) Tratamiento de errores con excepciones →
El método Aniade ha sido el encargado de tratar el error producido por la
........ if (vector.Aniade('h') == 1) cout << "No hay componentes suficientes"; else{ <sigo la ejecución del programa> };
violación de la precondición. Esto es poco flexible #
Si se detecta un error en un método (como la violación de una precondición) la respuesta (acciones a realizar para tratar el error) debe realizarse fuera de dicho método "
!
Problema: El excesivo anidamiento oscurece el código:
La solución clásica es devolver un código de error (bool, char, entero, enumerado, etc)
int main(){ MiVectorCaracteres vector;
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: ......... int Aniade(char nuevo){ int error = 0; if (total_utilizados < TAMANIO){ vector_privado[total_utilizados] = nuevo; total_utilizados++; } else error = 1; return error; } }; int main(){ MiVectorCaracteres vector;
if (vector.Aniade('h') == 1) cout << "No hay componentes suficientes"; else{ if (vector.Aniade('o') == 1) cout << "No hay componentes suficientes"; else if (vector.Aniade('y') == 1) cout << "No hay componentes suficientes"; else <sigo la ejecución del programa> }; }
475
Clases (Segunda parte) Tratamiento de errores con excepciones →
476
Solución: Utilizar el mecanismo de gestión de excepciones. En PDO una excepción (exception) es un objeto que contiene información que es traspasada desde el sitio en el que ocurre un problema a otro sitio en el que se tratará dicho problema. La idea es la siguiente: Un método genera una excepción con información sobre un error. La generación de la excepción se realiza a través del operador throw El flujo de control sale del método y va directamente a un lugar en el que se gestiona el error. Dicho lugar queda determinado por un bloque try - catch. En C++, una excepción puede ser de cualquier tipo de dato: entero, real, nuestras propias clases, etc. Por ahora, utilizaremos clases de excepción estándar, incluidos en la biblioteca stdexcept, como por ejemplo: Clase logic_error: Se usa para notificar que ha habido un error lógico como por ejemplo la violación de una precondición de un método. Clase runtime_error: Se usa para notificar que ha habido un error de ejecución como por ejemplo una división entera entre cero o un acceso a un fichero inexistente. Hay una sobrecarga de los constructores de dichas clases que acepta una cadena de caracteres. En dicha cadena indicaremos el error que se ha producido. Una vez creado el objeto de excepción se consulta dicha cadena con el método what().
Clases (Segunda parte) Tratamiento de errores con excepciones →
477
#include <stdexcept> class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: ......... void Aniade(char nuevo){ if (total_utilizados < TAMANIO){ vector_privado[total_utilizados] = nuevo; total_utilizados++; } else throw logic_error("No hay componentes suficientes"); // Se crea el objeto de la clase logic_error // El f.control sale del método } }; int main(){ MiVectorCaracteres vector; try{ vector.Aniade('h'); .......... vector.Aniade('a'); // Si ya no cabe => El f.control salta al catch .......... } catch(logic_error excepcion){ cout << "Error lógico: " << excepcion.what(); }
Clases (Segunda parte) Tratamiento de errores con excepciones →
478
Por ahora, el tratamiento del error dentro del catch únicamente consiste en informar con un cout. Si no construimos el bloque try catch y salta una excepción, el programa abortará.
int main(){ MiVectorCaracteres vector; vector.Aniade('h'); .......... vector.Aniade('a');
Clases (Segunda parte) Tratamiento de errores con excepciones →
479
El resto de métodos también pueden generar excepciones:
class MiVectorCaracteres{ ......... void Inserta(int pos_insercion, char valor_nuevo){ if ((total_utilizados < DIM) && (pos_insercion>=0) && (pos_insercion <= total_utilizados)){ for (int i=total_utilizados ; i>pos_insercion ; i--) vector_privado[i] = vector_privado[i-1];
// Si ya no cabe => El programa aborta
vector_privado[pos_insercion] = valor_nuevo; total_utilizados++; } else throw logic_error("Posición de inserción inválida"); } }; int main(){ MiVectorCaracteres vector; try{ vector.Aniade('h'); .......... vector.Inserta(60, 'a'); .......... } catch(logic_error excepcion){ cout << "Error lógico: " << excepcion.what(); } Según sea la excepción que se lance el método what() devolverá Posición de inserción inválida o bien No hay componentes suficientes .
Clases (Segunda parte) Tratamiento de errores con excepciones →
480
Clases (Segunda parte) Tratamiento de errores con excepciones →
481
En el mismo bloque try-catch podemos capturar excepciones generadas por métodos de distintas clases:
¿Y si se generan excepciones de tipos distintos? Hay que añadir el catch correspondiente.
class MiVectorCaracteres{ ......... }; class MiMatrizCaracteres{ ......... void Aniade(MiVectorCaracteres nueva_fila){ if (filas_utilizadas < DIM_FILAS){ matriz_privada[filas_utilizadas] = nueva_fila; filas_utilizadas++; } else throw logic_error("No hay más filas disponibles"); } }; int main(){ MiVectorCaracteres vector; MiMatrizCaracteres matriz;
class LectorVectorCaracteres{ private: const char terminador; const string nombre_fichero; ifstream lector; // Para leer de un fichero public: LectorVectorCaracteres(string nombre_fichero_entrada, char terminador_datos) :terminador(terminador_datos), nombre_fichero(nombre_fichero_entrada) { } void AbreFichero(){ lector.open(nombre_fichero);
try{ vector.Aniade('h'); .......... matriz.Aniade(vector); .......... } catch(logic_error excepcion){ cout << "Error lógico: " << excepcion.what(); }
if (lector.fail()){ string mensaje_error = "Error en la apertura del fichero " + nombre_fichero; throw runtime_error(mensaje_error); } } void CierraFichero(){....} MiVectorCaracteres LeeVector(){....} bool HayDatos(){....} }; int main(){ MiVectorCaracteres vector; MiMatrizCaracteres matriz; const char terminador = '#'; LectorVectorCaracteres input_vector("DatosEntrada.txt", terminador);
482
483
El tratamiento de errores de programaciĂłn usando el mecanismo de las excepciones se basa en asociar el error con un TIPO de excepciĂłn !
' '
( $ ! )
*
& &
% & ' '
% &
% & &
%
#
! " # " # $
' '
% '
Si se omite el catch correspondiente a un tipo y se produce una excepciĂłn de dicho tipo, el programa abortarĂĄ.
& &
% & ' '
% &
Si se produce una excepciĂłn de un tipo, el ďŹ&#x201A;ujo de control entra en el catch que captura el correspondiente tipo. No entra en los otros bloques catch.
% & &
%
A tener en cuenta:
Hay que destacar que cuando se produce una excepciĂłn en un mĂŠtodo, el ďŹ&#x201A;ujo de control va saliendo de todas las llamadas de los mĂŠtodos, hasta llegar a algĂşn sitio (por ahora serĂĄ usualmente en la funciĂłn main) en el que haya una sentencia try-catch. Este proceso se conoce como stack unwinding (limpieza de la pila serĂa una posible traducciĂłn)
try{ input_vector.AbreFichero(); .......... vector.Aniade('h'); .......... matriz.Aniade(vector); .......... } catch(logic_error excepcion){ cout << "Error lĂłgico: " << excepcion.what(); } catch(runtime_error excepcion){ cout << "Error de ejecuciĂłn: " << excepcion.what(); }
"
Clases (Segunda parte) Tratamiento de errores con excepciones â&#x2020;&#x2019;
! " # " # $
Clases (Segunda parte) Tratamiento de errores con excepciones â&#x2020;&#x2019;
Clases (Segunda parte) Tratamiento de errores con excepciones →
484
Consideraciones finales: El uso de excepciones modifica el flujo de control del programa. Un mal programador podría forzar su uso en sustitución de estructuras de control básicas como las condicionales. Siempre reservaremos el uso de excepciones para programar casos excepcionales, como por ejemplo, cuando la violación de las precondiciones de un método pueda tener efectos potencialmente peligrosos (recordar lo visto en la página 342) En C++, una excepción puede ser de cualquier tipo de dato: entero, real, nuestras propias clases, etc. Al crear nuestras propias clases de excepción, podremos construir objetos con más información que un simple string sobre el error producido. Cuando se vea el concepto de Herencia se verá cómo puede capturarse diferentes tipos de excepciones en un único catch. Para ello, tendremos varias clases que heredan de otra clase base. Las excepciones se generarán como se ha visto en este tema, pero se capturarán en un catch de la clase base.
Tema VI
Recursividad Objetivos: Entender el concepto de recursividad. Conocer los fundamentos del diseño de algoritmos recursivos. Comprender la ejecución de algoritmos recursivos. Aprender a realizar trazas de algoritmos recursivos. Comprender las ventajas e inconvenientes de la recursividad.
Este tema ha sido elaborado por Francisco Cortijo y Juan Carlos Cubero
Recursividad El concepto matemático de Recursividad →
VI.1.
486
El concepto matemático de Recursividad
VI.1.1.
Soluciones recursivas
Recursividad El concepto matemático de Recursividad → Soluciones recursivas
487
Ventajas: No es necesario definir la secuencia de pasos exacta para resolver el problema. Podemos considerar que “tenemos resuelto” el problema (de menor tamaño). Desventajas: Tenemos que encontrar una solución recursiva, y, además, podría ser menos eficiente. Ejemplo. Cálculo del factorial con n=3.
"La iteración es humana; la recursividad, divina.".
"Para entender la recursividad es necesario antes entender la recursividad.".
Recursividad (recursion) es la forma en la cual se especifica un proceso basado en su propia definición. Podemos usar recursividad si la solución de un problema está expresada en términos de la resolución del mismo tipo de problema, aunque de menor tamaño y conocemos la solución no-recursiva para un determinado caso base (base case) (o varios).
En Matemáticas se usa este concepto en varios ámbitos:
Demostración por inducción (induction):
– Demostrar para un caso base. – Demostrar para un tamaño n, considerando que está demostrado para un tamaño menor que n. Definición recursiva ⎧ ⎨1 si n = 0 n! = ⎩ n · (n − 1)! si n > 0
Recursividad El concepto matemĂĄtico de Recursividad â&#x2020;&#x2019; Soluciones recursivas
488
VI.1.2.
Recursividad El concepto matemĂĄtico de Recursividad â&#x2020;&#x2019; DiseĂąo de algoritmos recursivos
DiseĂąo de algoritmos recursivos
Debemos diseĂąar: Casos base: Son los casos del problema que se resuelve con un segmento de cĂłdigo sin recursividad. Para que una deďŹ niciĂłn recursiva estĂŠ completamente identiďŹ cada es necesario tener un caso base que no se calcule utilizando casos anteriores y que la divisiĂłn del problema converja a ese caso base.
Siempre debe existir al menos un caso base
El nĂşmero y forma de los casos base son hasta cierto punto arbitrarios. La soluciĂłn serĂĄ mejor cuanto mĂĄs simple y eďŹ ciente resulte el conjunto de casos seleccionados.
489
0! = 1 Casos generales:
En resumen:
Debemos encontrar la soluciĂłn a un problema (o varios) del mismo tipo, pero de menor tamaĂąo.
(n-1)!
ComposiciĂłn:
Una vez encontradas las soluciones a los problemas menores, habrĂĄ que ejecutar un conjunto de pasos adicionales. Estos pasos, junto con las soluciones a los subproblemas, componen la soluciĂłn al problema general que queremos resolver.
n! = n * (n-1)!
Recursividad El concepto matemático de Recursividad → Diseño de algoritmos recursivos
490
Ejercicio. Plantear la solución recursiva al problema de calcular la potencia de un real elevado a un entero positivo xn Caso base: x0 = 1 Caso general: xn−1 Composición: xn = x ∗ xn−1 Ejercicio. Plantear la solución recursiva al problema de sumar los enteros positivos menores que un entero i=n i=1 i i=1
i=1 i=n−1 Caso general: i=1 i i=n−1 Composición: i=n i i=1 i = n + i=1
Caso base:
i=1
Ejercicio. Plantear la solución recursiva al problema de multiplicar dos enteros a ∗ b Casos base: 0 ∗ b = 0, a ∗ 0 = 0 Caso general: a ∗ (b − 1) Composición: a ∗ b = a + a ∗ (b − 1)
Recursividad Funciones recursivas →
VI.2. VI.2.1.
491
Funciones recursivas Definición de funciones recursivas
¿Cómo se expresa en un lenguaje de programación la solución a un problema? Una solución será el valor devuelto por la llamada a una función. n! → Factorial(n) xn → Potencia(x,n) ¿Cómo se traduce a un lenguaje de programación el planteamiento recursivo de una solución? A través de una función que, dentro del código que la define, realiza una o más llamadas a la propia función. Una función que se llama a sí misma se denomina función recursiva (recursive function).
int Factorial (int n) { int resultado; if (n==0) resultado = 1; //Caso base else resultado = n*Factorial(n-1); //Caso general return resultado; } int Factorial (int n) { if (n==0) return 1; else return n*Factorial(n-1); }
//Caso base //Caso general
Recursividad Funciones recursivas â&#x2020;&#x2019; EjecuciĂłn de funciones recursivas
VI.2.2.
492
Recursividad Funciones recursivas â&#x2020;&#x2019; EjecuciĂłn de funciones recursivas
493
EjecuciĂłn de funciones recursivas
En general, en la pila se almacena el marco asociado a las distintas funciones que se van activando. En particular, cada llamada recursiva genera una nueva zona de memoria en la pila independiente del resto de llamadas.
Ejemplo. EjecuciĂłn del factorial con n=3. 1. Dentro de factorial, cada llamada return (n*factorial(n-1)); genera una nueva zona de memoria en la pila, siendo n-1 el correspondiente parĂĄmetro actual para esta zona de memoria y queda pendiente la evaluaciĂłn de la expresiĂłn y la ejecuciĂłn del return. 2. El proceso anterior se repite hasta que la condiciĂłn del caso base se hace cierta.
Se ejecuta la sentencia return 1; Empieza la vuelta atrĂĄs de la recursiĂłn, se evalĂşan las expresiones y se ejecutan los return que estaban pendientes.
Recursividad Funciones recursivas â&#x2020;&#x2019; EjecuciĂłn de funciones recursivas
494
Recursividad Funciones recursivas â&#x2020;&#x2019; EjecuciĂłn de funciones recursivas
495
Recursividad Funciones recursivas â&#x2020;&#x2019; Traza de algoritmos recursivos
VI.2.3.
496
Traza de algoritmos recursivos
Se representan en cascada cada una de las llamadas al mĂłdulo recursivo, asĂ como sus respectivas zonas de memoria y los valores que devuelven.
Caso base: x0 = 1 Caso general: xnâ&#x2C6;&#x2019;1
Ejercicio. Sumar los enteros positivos menores que un entero
Caso base:
i=1
i=1 i=nâ&#x2C6;&#x2019;1 Caso general: i=1 i i=nâ&#x2C6;&#x2019;1 ComposiciĂłn: i=n i i=1 i = n + i=1
Ejercicio. Calcular la potencia de un real elevado a un entero positivo xn
int Potencia(int base, int exponente){ if (exponente == 0) return 1; else return base * Potencia(base, exponente-1); }
497
ComposiciĂłn: xn = x â&#x2C6;&#x2014; xnâ&#x2C6;&#x2019;1
Llamada: factorial(3)
Recursividad Funciones recursivas â&#x2020;&#x2019; Traza de algoritmos recursivos
i=1
int SumaHasta(int tope){ if (tope <= 1) return 1; else return tope + SumaHasta(tope - 1); }
i=n i=1
i
Recursividad Funciones recursivas → Traza de algoritmos recursivos
Ejercicio. Multiplicar dos enteros a ∗ b
498
Recursividad Clases con métodos recursivos →
VI.3.
499
Clases con métodos recursivos
Casos base: 0 ∗ b = 0, a ∗ 0 = 0 Caso general: a ∗ (b − 1) Composición: a ∗ b = a + a ∗ (b − 1)
int Producto(int a, int b){ if (b == 0 || a == 0) return 0; else return a + producto(a, b-1); }
VI.3.1.
Métodos recursivos
El planteamiento y definición de un método recursivo es análogo a las funciones. La diferencia es que todas las llamadas recursivas del método pueden acceder a los datos miembro del objeto. Si una llamada recursiva modifica algún dato miembro, el resto de llamadas se verán afectadas, ya que todas ellas acceden a los mismos datos miembro. Ejercicio. Plantear una solución recursiva al problema de sumar los elementos de un vector y definir un método recursivo que la implemente.
class MiVectorEnteros{ private: static const int DIM = 50; int vector_privado[DIM]; int total_utilizados; public: MiVectorEnteros() :total_utilizados(0) { } int TotalUtilizados(){ return total_utilizados; } void Aniade(int nuevo){ if (total_utilizados < DIM){ vector_privado[total_utilizados] = nuevo; total_utilizados++; } // else // lanzar excepción } char Elemento(int indice){ return vector_privado[indice]; } };
Recursividad Clases con mĂŠtodos recursivos â&#x2020;&#x2019; MĂŠtodos recursivos
class MiVectorEnteros{ private: static const int DIM = 50; int vector_privado[DIM]; int total_utilizados; public: MiVectorEnteros() :total_utilizados(0) { } int TotalUtilizados(){ return total_utilizados; } void Aniade(int nuevo){ if (total_utilizados < DIM){ vector_privado[total_utilizados] = nuevo; total_utilizados++; } // else // lanzar excepciĂłn } char Elemento(int indice){ return vector_privado[indice]; } int SumaHasta(int posicion){ if (posicion == 0) return vector_privado[0]; else return vector_privado[posicion] + SumaHasta(posicion-1); } int SumaTodos(){ return SumaHasta(total_utilizados - 1); } };
500
Recursividad Clases con mĂŠtodos recursivos â&#x2020;&#x2019; MĂŠtodos recursivos
501
! !
" #$
"!#% $ % $
" #% ! $ % ! $
" #% ! $ % ! $
Recursividad Clases con mĂŠtodos recursivos â&#x2020;&#x2019; MĂŠtodos recursivos
502
Ejemplo. Plantear la soluciĂłn recursiva al problema de buscar la primera ocurrencia de una componente de un vector y deďŹ nir un mĂŠtodo que la implemente.
Recursividad Clases con mĂŠtodos recursivos â&#x2020;&#x2019; MĂŠtodos recursivos
"#$
503
! !
Observad que, en este ejemplo, se necesitan dos casos base.
class MiVectorEnteros{ private: static const int DIM = 50; int vector_privado[DIM]; int total_utilizados; public: ....... int PrimeraOcurrenciaEntre(int izda, int dcha, int buscado){ if (izda > dcha) return -1; else if (vector_privado[izda] == buscado) return izda; else return PrimeraOcurrenciaEntre(izda + 1, dcha, buscado); } };
% %
%
Recursividad Clases con mĂŠtodos recursivos â&#x2020;&#x2019; MĂŠtodos recursivos
504
class MiVectorEnteros{ private: static const int DIM = 50; int vector_privado[DIM]; int total_utilizados; public: ....... int PosMayor(int izda, int dcha){ int pos_mayor_anterior;
&
$
!"#
% &
&
$
!"#
% &
if (izda == dcha) return izda; else if (izda < dcha){ pos_mayor_anterior = PosMayor(izda + 1, dcha);
&
$
!"#
% &
if (vector_privado[pos_mayor_anterior] > vector_privado[izda]) return pos_mayor_anterior; else return izda;
&
## &
$
!"#
} else return -1;
&
$
}
' &
505
Ejercicio. Plantear la soluciĂłn recursiva al problema de encontrar el mayor elemento de un vector (entre dos posiciones dadas) y deďŹ nir un mĂŠtodo que la implemente.
Recursividad Clases con mĂŠtodos recursivos â&#x2020;&#x2019; MĂŠtodos recursivos
};
Recursividad Clases con métodos recursivos → Métodos recursivos
506
Ejercicio. Plantear la solución recursiva al problema de buscar con el método de búsqueda binaria la primera ocurrencia de una componente de un vector ordenado y definir un método que la implemente. Recordemos el método de búsqueda binaria:
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: ..................................... int BusquedaBinaria (char buscado){ int izda, dcha, centro;
Recursividad Clases con métodos recursivos → Métodos recursivos
class MiVectorEnteros{ private: static const int DIM = 50; int vector_privado[DIM]; int total_utilizados; public: ....... int BusquedaBinaria(int izda, int dcha, int buscado){ int centro; if (izda > dcha) return -1; else{ centro = (izda + dcha) / 2;
izda = 0; dcha = total_utilizados-1; centro = (izda+dcha)/2;
if (vector_privado[centro] == buscado) return centro; else if (vector_privado[centro] > buscado) return BusquedaBinaria(izda, centro-1, buscado); else return BusquedaBinaria(centro + 1, dcha, buscado);
while (izda <= dcha && vector_privado[centro] != buscado) { if (buscado < vector_privado[centro]) dcha = centro-1; else izda = centro+1; }
};
}
}
centro = (izda+dcha)/2;
if (izda > dcha) return -1; else return centro;
} };
507
Recursividad Clases con mĂŠtodos recursivos â&#x2020;&#x2019; MĂŠtodos recursivos
$%&
508
# #
"
Recursividad Clases con mĂŠtodos recursivos â&#x2020;&#x2019; MĂŠtodos recursivos
#$%
!
" "
'
'
'
!
&
(
509
&
'
'
((
&
'
'
Recursividad Clases con métodos recursivos → Ordenación rápida: Quicksort
VI.3.2.
510
Ordenación rápida: Quicksort
Ideado por Charles Antony Richard Hoare en 1960.
511
} }; Líneas básicas del algoritmo de la función partir:
Este es un método de ordenación que utiliza la recursividad para ir fijando los rangos del vector sobre los que otro método (partir) realizará los intercambios necesarios para ir ordenándolo. Idea: Para ordenar alfabéticamente un montón de exámenes, se hacen dos pilas: en una se van echando los que tienen apellidos menores que un pivote 'M' y en la otra los mayores que 'M'. Se vuelve a repetir el proceso con cada montón, pero cambiando el elemento pivote. En la segunda iteración recursiva, en el primer montón podría escogerse como pivote 'J' y en el segundo 'R'. Si no conocemos a priori el rango de valores, el pivote lo elegimos de forma arbitraria (por ejemplo, el primero que cojamos de la pila)
1. Tomar un elemento arbitrario del vector: pivote 2. Recorrer el vector de izquierda a derecha hasta encontrar un elemento situado en una posición izda tal que v[izda] > pivote 3. Recorrer el vector de derecha a izquierda hasta encontrar otro elemento situado en una posición dcha tal que v[dcha] < pivote 4. Intercambiar los elementos de las casillas izda y dcha Una vez hecho el intercambio tendremos que:
v[izda] < pivote < v[dcha] 5. Repetir hasta que los dos procesos de recorrido se encuentren (izda > dcha). 6. Colocar el pivote en el sitio que le corresponde. 7. Una vez hecho lo anterior, el vector está particionado en dos zonas delimitadas por el pivote. El pivote está ya colocado correctamente en su sitio.
class MiVectorEnteros{ private: int partir(int primero, int ultimo) ....... public: ....... void QuickSort(int inicio, int final){ int pos_pivote; if (inicio < final) { pos_pivote = partir (inicio, final); QuickSort (inicio, pos_pivote - 1); QuickSort (pos_pivote + 1, final); }
Recursividad Clases con métodos recursivos → Ordenación rápida: Quicksort
// Ordena primera mitad // Ordena segunda mitad
Recursividad Clases con mĂŠtodos recursivos â&#x2020;&#x2019; OrdenaciĂłn rĂĄpida: Quicksort
512
Recursividad Clases con mĂŠtodos recursivos â&#x2020;&#x2019; OrdenaciĂłn rĂĄpida: Quicksort
int partir (int primero, int ultimo){ int intercambia; int izda, dcha; int valor_pivote = vector_privado[primero];
izda = primero + 1; dcha = ultimo;
while (izda <= dcha){ while (izda<=dcha && izda++;
// "izda" va a la dcha. // "dcha" va a la izda.
while (izda<=dcha dcha--;
&&
vector_privado[izda]<=valor_pivote)
vector_privado[dcha]>=valor_pivote)
if (izda < dcha) { intercambia = vector_privado[izda]; vector_privado[izda] = vector_privado[dcha]; vector_privado[dcha] = intercambia; dcha--; izda++; }
} intercambia = vector_privado[primero]; vector_privado[primero] = vector_privado[dcha]; vector_privado[dcha] = intercambia;
return dcha; }
513
Recursividad Clases con mĂŠtodos recursivos â&#x2020;&#x2019; OrdenaciĂłn rĂĄpida: Quicksort
514
Recursividad Clases con mĂŠtodos recursivos â&#x2020;&#x2019; OrdenaciĂłn rĂĄpida: Quicksort
515
AmpliaciĂłn:
Quicksort es uno de los mejores algoritmos de ordenaciĂłn. Se analizarĂĄ su eďŹ ciencia en el segundo cuatrimestre. Quicksort puede paralelizarse fĂĄcilmente. Cada CPU se encargarĂa de la resoluciĂłn de una llamada recursiva. Al ďŹ nal basta juntar los subvectores ordenados. Esto es posible porque el pivote calculado en cada momento, es colocado en el lugar que le corresponde.
La elecciĂłn del pivote es muy importante. Las mejoras de Quicksort pasan por desarrollar mecanismos rĂĄpidos de elecciĂłn de buenos pivotes (elementos que dejen mĂĄs o menos el mismo nĂşmero de elementos a la izquierda y derecha)
Algunos applets de demostraciĂłn.
Para tener una idea global:
http://maven.smith.edu/~thiebaut/java/sort/
EjecuciĂłn paso a paso:
http://pages.stern.nyu.edu/~panos/java/Quicksort/ http://math.hws.edu/TMCM/java/xSortLab/
Recursividad Recursividad versus iteración →
VI.4.
516
Recursividad versus iteración
Desventajas de la recursividad: La carga computacional (tiempo-espacio) asociada a una llamada a una función y el retorno a la función que hace la llamada. Algunas soluciones recursivas pueden hacer que la solución para un determinado tamaño del problema se calcule varias veces. Ventajas de la recursividad: La solución recursiva suele ser concisa, legible y elegante. Tal es el caso, por ejemplo, del recorrido de estructuras complejas como árboles, listas, grafos, etc. Se verá en el segundo cuatrimestre. Habrá que analizar en cada caso la conveniencia o no de usar recursividad.
Recursividad Recursividad versus iteración →
517
Recursividad de cola Se dice que una función tiene recursividad de cola (tail recursion) si sólo se produce una llamada recursiva y no se ejecuta posteriormente ningún código. Estas funciones pueden traducirse fácilmente a un algoritmo no recursivo, más eficiente. De hecho, muchos compiladores realizan automáticamente esta conversión. Todos los algoritmos vistos en esta sección, salvo Quicksort, tienen recursividad de cola, y por tanto pueden traducirse fácilmente a un algoritmo iterativo. Por ejemplo:
int Potencia(int base, int exponente){ if (exponente == 0) return 1; else return base * Potencia(base, exponente-1); } int Potencia(int base, int exponente){ int potencia = 1; for (int i = 1; i <= exponente; i++) potencia = potencia * base; return potencia; }
Recursividad Recursividad versus iteración →
518
Ejemplo. Invertir un vector (el subvector entre las posiciones izda y dcha)
Si izda == dcha Solución: Construir un vector con esa única componente si no Calcular la solución al problema de invertir el vector entre las posiciones izda + 1 y dcha Solución: Añadir la componente izda al final de la solución anterior
Recursividad Recursividad versus iteración →
Repetición de cómputos Los algoritmos interesantes serán los que realicen más de una llamada recursiva (como Quicksort), pero debemos evitar la repetición de cómputos. Ejemplo. Sucesión de Fibonacci.
class MiVectorCaracteres{ private: static const int DIM = 50; char vector_privado[DIM]; int total_utilizados; public: ......... MiVectorCaracteres InvierteRecursivo(int izda, int dcha){ MiVectorCaracteres inverso; if (izda == dcha) inverso.Aniade(vector_privado[izda]); else if (izda < dcha){ inverso = InvierteRecursivo(izda+1, dcha); inverso.Aniade(vector_privado[izda]); } return inverso; } }; Este método no tiene recursividad de cola ya que después de hacer la llamada recursiva, el método todavía debe hacer otras operaciones. En cualquier caso, esto no quiere decir que no se podría haber traducido como un algoritmo iterativo.
519
F ib(0) = F ib(1) = 1 F ib(n) = F ib(n − 1) + F ib(n − 2)
Ampliación: La sucesión de Fibonacci tiene muchas aplicaciones. Por ejemplo Fibonacci(n+2) da como resultado el número de subconjuntos de {1, · · · , n} que no contienen enteros consecutivos. En la naturaleza, están presentes en la fórmula que determina cómo se disponen las hojas en los tallos. Hay una revista matemática dedicada exclusivamente a resultados sobre esta sucesión. Para más información, consultad wikipedia (en inglés) y la web de la Enciclopedia de sucesiones enteras .
Recursividad Recursividad versus iteraciĂłn â&#x2020;&#x2019;
520
int Fibonacci (int n){ if ((n == 0) || (n == 1)) return 1; else return Fibonacci(n-1) + Fibonacci(n-2); } CĂĄlculo de Fibonacci(5):
Recursividad Recursividad versus iteraciĂłn â&#x2020;&#x2019;
La soluciĂłn recursiva de Fibonacci repite muchos cĂłmputos, por lo que es muy ineďŹ ciente. La soluciĂłn iterativa es sencilla de implementar y mucho mĂĄs rĂĄpida:
int Fibonacci (int n){ int anterior = 1, pre_anterior = 1; int actual; if ((n == 0) || (n == 1)) actual = 1; else for (int i = 2; i <= n; i++) { actual = anterior + pre_anterior; pre_anterior = anterior; anterior = actual; }
521
return actual; } AmpliaciĂłn: Una forma alternativa de evitar el problema de los cĂłmputos repetidos en recursividad, lo proporcionan las tĂŠcnicas de la programaciĂłn dinĂĄmica (dynamic programming) que, a grosso modo, van guardando adecuadamente los resultados obtenidos previamente.
Recursividad Recursividad versus iteración →
522
Recursividad Recursividad versus iteración →
if (encontrado) return i; else return -1;
Caso base demasiado pequeño Muchos problemas recursivos tienen como caso base un problema de un tamaño reducido. En ocasiones es excesivamente pequeño.
} int BusquedaBinaria(int izda, int dcha, int buscado){ const int umbral = 5; int centro;
Para resolverlo, se suele aplicar un algoritmo iterativo cuando se ha llegado a un caso base suficientemente pequeño. Ejemplo. Búsqueda binaria.
if (dcha - izda + 1 < umbral) return PrimeraOcurrenciaEntre(izda, dcha, buscado); else{ centro = (izda + dcha) / 2;
Aplicamos el algoritmo recursivo en general y el iterativo cuando el tamaño del subvector en el que estoy buscando es pequeño.
Recorrer las componentes con dos apuntadores izda y dcha Si el número de elementos entre izda y dcha es pequeño Buscar el valor con un algoritmo no recursivo en otro caso Colocarse en el centro y comprobar si es el valor buscado Si no lo es Buscar a la izda o a la derecha de centro (realizando la llamada recursiva) según sea el elemento buscado menor o mayor que v[centro] class MiVectorEnteros{ ....... int PrimeraOcurrenciaEntre (int pos_izda, int pos_dcha, char buscado){ int i = pos_izda; bool encontrado = false; while (i <= pos_dcha && !encontrado) if (vector_privado[i] == buscado) encontrado = true; else i++;
523
if (vector_privado[centro] == buscado) return centro; else if (vector_privado[centro] > buscado) return BusquedaBinaria(izda, centro-1, buscado); else return BusquedaBinaria(centro + 1, dcha, buscado); } } }; Nota. El caso base (izda > dcha) de la función ya no es necesario. Nota. dcha - izda + 1 < umbral representa número de elementos menor que un umbral. Si se quiere, por eficiencia, podría cambiarse a dcha - izda < umbral
Recursividad Recursividad versus iteraciĂłn â&#x2020;&#x2019;
524
Ejemplo. Mejoramos QuickSort de forma anĂĄloga:
class MiVectorEnteros{ private: int partir(int primero, int ultimo) ....... public: ....... void Ordena_por_Seleccion_entre(int primero, int ultimo){ int pos_min; for (int izda=primero ; izda<ultimo ; izda++){ pos_min = PosicionMinimoEntre (izda, ultimo); IntercambiaComponentes_en_Posiciones (izda, pos_min); } } void QuickSort(int inicio, int final){ const int umbral = 5; int pos_pivote; if (final - inicio + 1 < umbral) return Ordena_por_Seleccion_entre(inicio, final); else{ if (inicio < final) { pos_pivote = partir (inicio, final); QuickSort (inicio, pos_pivote - 1); // Ordena primera mitad QuickSort (pos_pivote + 1, final); // Ordena segunda mitad } } } };
Recursividad Recursividad versus iteraciĂłn â&#x2020;&#x2019;
525
QuickSort (v, 0, 299) y umbral=100
Recursividad Recursividad versus iteraciĂłn â&#x2020;&#x2019;
526
Recursividad Recursividad versus iteraciĂłn â&#x2020;&#x2019;
527
Problemas complejos con una soluciĂłn recursiva sencilla
Algunos problemas se resuelven directamente con recursividad. Normalmente, tambiĂŠn es posible resolverlos iterativamente, pero puede resultar mĂĄs complejo. Ejemplo. DeďŹ niciĂłn de fractal (fractal) de la Wikipedia:
! " " !" !
Un fractal es un objeto semigeomĂŠtrico cuya estructura bĂĄsica, fragmentada o irregular, se repite a diferentes escalas. A un objeto geomĂŠtrico fractal se le atribuyen las siguientes caracterĂsticas: Es demasiado irregular para ser descrito en tĂŠrminos geomĂŠtricos tradicionales.
! " " !" !
#
# $
! " " !" ! #
%% # $
# ! " " !" !
Es autosimilar.- Su forma es hecha de copias mĂĄs pequeĂąas de la misma ďŹ gura. Las copias son similares al todo: misma forma pero diferente tamaĂąo. ¡¡¡ Se deďŹ ne mediante un simple algoritmo recursivo.
Recursividad Recursividad versus iteración →
Ejemplo. Triángulos de Sierpinski
528
Recursividad Recursividad versus iteración →
529
Ejemplo. Torres de Hanoi. Edouard Lucas, 1883. Se trata de mover un conjunto de discos apilados en un poste a otro poste, con las condiciones siguientes: Se tienen 3 postes (hay versiones para trabajar con más postes) Sólo se puede mover un disco en cada paso. Además, debe ser el disco que está en la parte superior de la pila. Los discos en cualquier poste siempre deben formar una pirámide, es decir, colocados de mayor a menor.
class Sierpinski{ public: void Triangulo(Punto2D A, Punto2D B, Punto2D C, int grado){ PintorFiguras pintor; if (grado == 0) pintor.DibujaTriangulo (A, B, C); else{ SegmentoDirigido segmentoAB(A, B); SegmentoDirigido segmentoBC(B, C); SegmentoDirigido segmentoCA(C, A); Punto2D MedioAB (segmentoAB.Medio()); Punto2D MedioBC (segmentoBC.Medio()); Punto2D MedioCA (segmentoCA.Medio()); Triangulo (A, MedioAB, MedioCA, grado - 1); Triangulo (MedioAB, B, MedioBC, grado - 1); Triangulo (MedioCA, MedioBC, C, grado - 1); } } };
Etiquetando los 3 postes como inicial, intermedio, final, el algoritmo recursivo que lo resuelve sería:
Mover n-1 discos desde inicial hasta temporal usando final como intermedio Mover el disco que queda en inicial hasta final Mover n-1 discos desde intermedio hasta final usando inicial como intermedio
Recursividad Recursividad versus iteración →
530
#include <iostream> using namespace std; void Hanoi (int num_discos, int inicial, int intermedio, int final){ if (num_discos > 0){ Hanoi (num_discos-1, inicial, final, intermedio); cout <<"\nDel poste " << inicial << " al " << final; Hanoi (num_discos-1, intermedio, inicial, final); } } int main(){ int n; // Número de discos a mover cout << "Número de discos: "; cin >> n; Hanoi (n, 1, 2, 3); // mover "n" discos del 1 al 3, // usando el 2 como temporal. }
Recursividad Recursividad versus iteración →
531
Recursividad Recursividad versus iteración →
532
Algunas curiosidades: Applet: http://www.mazeworks.com/hanoi/index.htm Número de movimientos: 2k , siendo k = número de discos. Para 64 discos, moviendo 1 disco por segundo, se tardaría algo menos de 600 mil millones de años (el Universo tiene unos 20 mil millones) También existe una versión iterativa. No son tres líneas, pero no es demasiado complicada. En cualquier caso, el número de movimientos es el mismo.
PRĂ CTICAS
Curso: 2011/2012 Clase: Primero - Grupo: B
Fundamentos de Programaciรณn. Guiรณn de Prรกcticas. Curso 2011/2012
Autor: Juan Carlos Cubero Para cualquier sugerencia o comentario sobre este guión de prácticas, por favor, enviad un e-mail a JC.Cubero@decsai.ugr.es
"Lo que tenemos que aprender a hacer, lo aprendemos haciéndolo". Aristóteles "In theory, there is no difference between theory and practice. But, in practice, there is". Jan L. A. van de Snepscheut "The gap between theory and practice is not as wide in theory as it is in practice". "Theory is when you know something, but it doesn’t work. Practice is when something works, but you don’t know why. Programmers combine theory and practice: Nothing works and they don’t know why".
Guión de Prácticas. Fundamentos de Programación
1
Acceso identificado
Acceso identificado Los alumnos deben registrarse electrónicamente en dos sitios distintos: • En la Universidad de Granada. Los alumnos deben registrarse electrónicamente en la UGR para poder entrar en las aulas de prácticas de la asignatura. Las instrucciones están en:
http://www.ugr.es/informatica/alumnos/correo.htm Se necesita el DNI y el PIN obtenido con la matrícula. El registro electrónico tarda un día (incluso dos) en activarse. Por tanto, realizad el registro al menos tres días antes de la primera sesión de prácticas (que tendrá lugar la segunda semana de clases) Una vez que se tenga cuenta de correo en la UGR, el alumno podrá iniciar sesión en cualquiera de los ordenadores de la Escuela de Informática. Para ello, tendrá que introducir como usuario y contraseña, los mismos que se han elegido para la UGR. Automáticamente, tendrá acceso a un sitio de almacenamiento en los servidores de la Escuela. Esto se explica en la primera sesión de prácticas (página 22) • En la Web del departamento de Ciencias de la Computación e Inteligencia Artificial
https://decsai.ugr.es La mayor parte de los alumnos ya están dados de alta en la web con los datos que hemos obtenido de la matrícula. En cualquier caso, el alumno debe comprobarlo durante la primera semana de clases, dando los siguientes pasos: – Al entrar en el acceso identificado de la web del departamento, introducir como nombre de usuario y como contraseña el DNI sin la letra del final (o el número de pasaporte). – Si puede entrar con éxito, acceda a sus datos personales para cambiar el password y subir una foto reciente. Introducid en los datos de contacto una dirección de e-mail que consulte habitualmente, ya que se enviará asiduamente información sobre la asignatura. No debe elegirse ninguna dirección de Hotmail ya que la UGR clasifica como SPAM muchos correos de dicho servidor. – Puede
encontrar
instrucciones
más
detalladas
en
el
fichero
RegistroWebDECSAI.pdf incluido en http://decsai.ugr.es/jccubero/fp.zip – Si no puede entrar, envíe un e-mail a JC.Cubero@decsai.ugr.es con su número de DNI, Nombre y Apellidos.
Guión de Prácticas. Fundamentos de Programación
2
Acceso identificado
Una vez tenga acceso a la web del departamento, podrá entrar en la asignatura de Fundamentos de Programación y elegir un grupo de prácticas. También podrá acceder al material de la asignatura. El material del primer tema también se encuentra en:
http://decsai.ugr.es/jccubero/fp.zip MUY IMPORTANTE: El alumno nunca deberá dar a conocer a un tercero su nombre de usuario y contraseña, ya que es el responsable del mal uso que se pudiese realizar con su cuenta.
Guión de Prácticas. Fundamentos de Programación
3
Sobre el guión de prácticas
Sobre el guión de prácticas Este guión de prácticas contiene las actividades a realizar por el alumno, tanto de forma presencial (en las aulas de la Escuela de Informática) como no presencial (individual y en grupo). Todas las actividades son obligatorias (incluidas las presenciales en las aulas de prácticas), excepto las marcadas como Actividades de Ampliación. El guión está dividido en sesiones, que corresponden a cada semana lectiva. Las actividades no presenciales de una sesión deben realizarse antes de la correspondiente sesión de prácticas presencial. La primera sesión presencial de prácticas (página 21) tendrá lugar durante la segunda semana de clases, por lo que el alumno deberá realizar con anterioridad las actividades encargadas. Una actividad no presencial recurrente es la resolución de problemas propuestos en las Relaciones de Problemas. Éstas constan de dos tipos de problemas: • Básicos: problemas que se resolverán a lo largo de la asignatura. Del conjunto de problemas básicos, en el guión de prácticas se distinguirá entre: 1. Obligatorios: Deben resolverse en la casa y el alumno deberá defenderlos individualmente (a veces explicándolo a sus compañeros) en las sesiones de prácticas presenciales, durante la primera hora. La defensa será individual y forma parte de la nota final de la asignatura (10 % tal y como se explica en el apartado de evaluación de la asignatura). En la segunda hora, el profesor explicará estos ejercicios a toda la clase. También sacará a la pizarra a algún alumno para que explique alguno de los ejercicios. Los proyectos soluciones a los problemas deberán ser subidos a la cuenta particular de cada alumno en el servidor de la ETS Informática y Telecomunicaciones antes del inicio de la sesión de prácticas presencial en las aulas de la Escuela. Para ello, el alumno debe conectarse a turing.ugr.es usando cualquier programa cliente de ftp que incorpore el protocolo ssh, como por ejemplo winscp (http://winscp.net/). Previamente, el alumno debe tener activada su cuenta en la Universidad de Granada, tal y como se explica en la página 2. Hay que subir la carpeta completa del proyecto solución incluyendo los ficheros sln, vcxproj y cpp. Borrad el resto de ficheros: para ello, puede usarse la macro _limpiar.bat que se encuentra en el material de la asignatura (fichero zip indicado en la página 2 o a través del acceso identificado en la web del departamento) Debe copiarse el fichero _limpiar.bat junto con el fichero limpiar.exe en el directorio del que cuelgan las carpetas de los proyectos soluciones, y ejecutad la macro _limpiar.bat, haciendo doble click sobre ella. Una vez ejecutada la macro, ya pueden subirse a turing las carpetas de los proyectos. En caso de tener algún problema con la conexión a turing, también pueden llevarse los proyectos en un pen-drive el mismo día de las prácticas.
Guión de Prácticas. Fundamentos de Programación
4
Sobre el guión de prácticas
Nota: mientras el profesor corrige los ejercicios a un alumno en la primera hora, los alumnos desarrollarán las actividades presenciales de la correspondiente sesión. Cuando las hayan terminado, se pueden poner a trabajar en los problemas de la próxima sesión. 2. Opcionales: Se recomienda que se resuelvan en casa, pero el profesor no los preguntará en las sesiones de prácticas. En la segunda hora, el profesor explicará estos ejercicios a toda la clase. • Ampliación: problemas cuya solución no se verá, pero que sirven para afianzar conocimientos. El alumno debería intentar resolver por su cuenta un alto porcentaje de éstos. No hay que entregar al profesor la solución de los ejercicios de las distintas sesiones, ya que, durante la segunda hora de prácticas, se proporcionará la solución a los problemas básicos y opcionales propuestos. Es muy importante que el alumno revise estas soluciones y las compare con las que él había diseñado. Las actividades marcadas como Seminario han de hacerse obligatoriamente y siempre serán expuestas por algún alumno elegido aleatoriamente. Para la realización de estas prácticas, se utilizará el entorno de programación Microsoft Visual Studio. En la página 7 se encuentran las instrucciones para su instalación en nuestra casa. En cualquier caso, el alumno puede instalar en su casa cualquier otro compilador, como por ejemplo Code::Blocks → http://www.codeblocks.org/ Muy importante: • La resolución de los problemas y actividades puede hacerse en grupo, pero la defensa durante las sesiones presenciales es individual. • Es muy importante que la asignatura se lleve al día para poder realizar los ejercicios propuestos en estos guiones.
Guión de Prácticas. Fundamentos de Programación
5
Evaluación de la asignatura
Evaluación de la asignatura La nota final en la asignatura se computa de la siguiente forma: • En Febrero se realizará un examen escrito consistente en la resolución de varios problemas de programación relacionados con la materia impartida. La ponderación de este bloque es del 60 %. • La parte práctica se evalúa de la siguiente forma: – Evaluación continua: Durante las sesiones de prácticas, los alumnos deberán defender ante la clase o ante el profesor, los problemas encargados para dicha sesión de prácticas. La ponderación de esta parte es del 10 %. – Exámenes de prácticas: A mediados de Noviembre y a principios de Enero se realizará un examen de prácticas. Cada uno de estos exámenes consistirá en la resolución de un ejercicio de programación que se extraerá de una lista de ejercicios que, previamente, se habrá entregado a los estudiantes. Dichas listas de ejercicios se denominarán Listas de Problemas para el Examen Práctico, para diferenciarlas de las Relaciones de Problemas. Cada uno de los dos exámenes durará una hora y se realizará en las aulas de prácticas de la Escuela de Informática. La ponderación del primer examen es del 10 % y la del segundo del 20 % La realización de las prácticas puede realizarse en grupo, pero la defensa es siempre individual. En Septiembre se guarda la nota de la evaluación continua y sigue valiendo el 10 % de la nota final. No hay ningún examen de recuperación de esta parte. Por otra parte, en Septiembre se realizarán dos exámenes consistentes en la resolución de varios problemas de programación relacionados con la materia impartida. Llamémosle T y P a dichos exámenes. La ponderación de ambos exámenes es 60 % para T y 30 % para P. Sólo para aquellos alumnos que se presentaron a alguna de las partes (escrita/prácticas) en Febrero: • El alumno puede elegir no presentarse a alguna de las parte T o P. En dicho caso, conservaría la nota que hubiese obtenido en Febrero. Si no se presenta al examen T, la nota de T sería la que hubiese sacado en el examen escriTo de Febrero y si no se presenta a P, la nota sería la obtenida en las Prácticas de Febrero. • Si un alumno se presenta a una de las partes (T o P), la nota obtenida en Febrero en dicha parte NO se conserva (es una nueva calificación) por lo que la nota de dicha parte podría bajar.
Guión de Prácticas. Fundamentos de Programación
6
Instalación de Visual Studio en nuestra casa
Instalación de Visual Studio en nuestra casa Conseguir Visual Studio e instalarlo Conseguir Visual Studio Puede encontrarse información sobre la última versión de Visual Studio en:
http://msdn.microsoft.com/es-es/vstudio/default(en-us).aspx http://www.microsoft.com/spain/visualstudio/products/ 2010-editions/ultimate El DVD con la instalación de Visual Studio 2010 Ultimate puede adquirirse en la fotocopiadora de la Escuela por unos 3 euros aproximadamente. La licencia es legal para todo alumno del departamento de Ciencias de la Computación e Inteligencia Artificial, fruto del programa MSDN Academic Alliance entre Microsoft y el departamento. Para más información sobre este programa:
http://www.microsoft.com/spanish/msdn/academico.mspx La principal restricción es que no se pueden desarrollar programas comerciales con esta licencia. Para más información sobre las restricciones de la licencia:
http://msdn.microsoft.com/en-us/academic/bb250608.aspx La versión Ultimate disponible para los alumnos contiene muchas más herramientas de las que se necesitarán en la asignatura. Si se quiere, puede descargarse directamente desde Internet la versión Express de Visual Studio. Esta versión también contiene todas las herramientas y opciones necesarias en esta asignatura, y es la que se usará en las aulas de prácticas:
http://www.microsoft.com/express/Downloads/ Para descargas de versiones anteriores, consultad:
http://msdn.microsoft.com/es-es/express/aa975050.aspx Los requisitos del sistema, para la instalación de Visual Studio Express se encuentran en
http://www.microsoft.com/express/Support/ y para Visual Studio 2010 en:
http://www.microsoft.com/spain/visualstudio/products/ 2010-editions/ultimate Instalar Visual Studio Si se usa el DVD de la fotocopiadora con Visual Studio 2010 Ultimate, se puede instalar la versión completa que incluye todos los lenguajes de Visual Studio como Visual Basic, C#, etc. Al introducir el DVD se lanzará automáticamente el programa de instalación. Selec-
Guión de Prácticas. Fundamentos de Programación
7
Instalación de Visual Studio en nuestra casa
cionamos Instalación Personalizada. Por ahora, instalamos únicamente la parte correspondiente al lenguaje C++ (posteriormente, podemos instalar el resto de lenguajes) En el caso de que instalemos la versión Visual Studio C++ Express, no se instalarán el resto de lenguajes (que, obviamente, no son necesarios para esta asignatura)
Configuración Preparar y acceder a la consola del sistema La consola de Windows (la ventana con fondo negro que aparece al ejecutar el comando cmd.exe, o bien la que sale al ejecutar un proyecto de consola de Visual Studio) no está preparada por defecto para mostrar adecuadamente caracteres latinos como los acentos. Por ejemplo, al ejecutar la sentencia de C++
cout << "Atención" saldrá en la consola un mensaje en la forma
Atenci5/4n Para que podamos ver correctamente dichos caracteres, debemos seguir los siguientes pasos: 1. Cambiar la fuente de la consola a una que acepte caracteres Unicode. En la versión de XP de las aulas ya se ha realizado dicho cambio. En nuestra casa, tendremos que hacer lo siguiente:
Inicio -> Ejecutar -> cmd Una vez que se muestre la consola, hacemos click con la derecha y seleccionamos Predeterminados. Seleccionamos la fuente Lucida Console y aceptamos. 2. Debemos cargar la página de códigos correspondiente al alfabeto latino. Para ello, tenemos dos alternativas: a) Si queremos que la consola siempre cargue la tabla de caracteres latinos, debemos modificar el registro de Windows. Lo abrimos desde
Inicio->Ejecutar->regedit
Nos situamos en la clave
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\ \Control\Nls\CodePage y cambiamos el valor que hubiese dentro de OEMCP y ACP por el de 1252. Esta es
Guión de Prácticas. Fundamentos de Programación
8
Instalación de Visual Studio en nuestra casa
la forma recomendada y la que se ha usado en las aulas de prácticas. Requiere reiniciar el ordenador. Muy Importante: Si se usa otra tabla (distinta a 1252), el sistema operativo podría incluso no arrancar. b) Si queremos hacerlo para una única consola, basta ejecutar el comando
chcp 1252
sobre la consola. El problema es que cada vez que se abre una nueva consola (por ejemplo, como resultado de ejecutar un programa desde Visual C++) hay que realizar este cambio. En nuestro caso, pondríamos (por ejemplo, al inicio del programa, justo después de las declaraciones de las variables) lo siguiente:
system("chcp 1252"); En cualquier caso, remarcamos que esta solución no es necesaria si se adopta la primera, es decir, el cambio del registro de Windows. Configuración del entorno Se recomienda que se usen las mismas opciones de configuración del entorno que las establecidas en las aulas de prácticas. Para ello, deben darse los siguientes pasos. Dentro del fichero
http://decsai.ugr.es/jccubero/fp.zip se encuentra la carpeta Visual Studio\Settings, y dentro el fichero SettingsAulasPracticas_VS2010.vssettings.vssettings Copiadlo localmente, preferiblemente dentro de la carpeta
Mis Documentos\Visual Studio 2010\Settings
Abrid Visual Studio. Seleccionad
Herramientas -> Importar y exportar configuraciones -> Importar la configuración de entorno seleccionada -> Siguiente Si se quiere guardar las configuraciones actuales, elegid la primera opción. Si no se ha trabajado anteriormente con Visual Studio, podemos elegir directamente la segunda opción:
No, sólo importar la nueva configuración, reemplazando la configuración actual -> Siguiente -> Examinar Seleccionamos las opciones guardadas anteriormente en
Mis Documentos\Visual Studio 2010\Settings\ \SettingsAulasPracticas.vssettings
Guión de Prácticas. Fundamentos de Programación
9
Instalación de Visual Studio en nuestra casa
y hacemos click en Siguiente. Aparecen algunas opciones marcadas con un símbolo de admiración, indicando que no se van a importar (pues pueden contener información privada). Aceptamos y terminamos. Cosas a tener en cuenta: • A partir de ahora, si realizamos algún cambio en el entorno o en las opciones de configuración (Herramientas->Opciones), éstas se guardan automáticamente en el archivo
Mis Documentos\Visual Studio 2010\ \Settings\CurrentSettings.vssettings Es decir, el archivo SettingsAulasPracticas.vssettings no se modifica con
los cambios que efectuemos en las opciones del entorno. • Si queremos exportar nuestra configuración para utilizarla en otro sitio, basta con seleccionar
Herramientas -> Importar y exportar configuraciones -> Exportar la configuración de entorno seleccionada y seleccionad el nombre del fichero configuración.
.vssettings en el que queremos guardar la
La plantilla _fp_2010 En el entorno Visual Studio de las aulas de prácticas, se ha incluido una plantilla con las opciones de configuración de compilación más usuales así como un esqueleto del programa principal que incluye la cabecera que usaremos siempre en nuestros programas. Para instalar esta plantilla en nuestros ordenadores personales, descargad de Internet el fichero
http://decsai.ugr.es/jccubero/fp.zip Dentro se encuentra la carpeta Visual Studio\PlantillaFP. Seguid las instrucciones que se encuentran en el fichero Instrucciones.txt (simplemente habrá que cambiar una línea de texto de un fichero y copiar unos ficheros). Para crear un proyecto nuevo en Visual Studio basado en esta plantilla, basta abrir Visual Studio y seleccionar:
Archivo -> Nuevo Proyecto -> Asistentes -> _fp_2010 Seleccionamos una ubicación y tecleamos un nombre para el proyecto.
Guión de Prácticas. Fundamentos de Programación
10
Instalación de Visual Studio en nuestra casa
Otros recursos Dentro del fichero
http://decsai.ugr.es/jccubero/fp.zip se encuentra la carpeta Visual Studio\OtrosRecursos, con los siguientes ficheros: •
StepOver_VS_2010.reg. Es un fichero que añade claves al registro de Windows para que el depurador de Visual Studio no entre a depurar recursos usuales como cout. Para ejecutarlo, basta hacer doble click sobre él (como Administrador del Sistema)
•
_limpiar.bat y _limpiar.exe. El primero es una macro que llama al segundo programa. Ambos ficheros deben estar en el directorio del cual cuelgan nuestras carpetas locales de proyectos de Visual Studio. La macro borra todos los ficheros auxiliares como los ejecutables, los que contienen información de depuración, etc. En nuestra casa ejecutaremos la macro cuando nos estemos quedando sin espacio en el disco duro y antes de subir a turing los proyectos soluciones de las distintas sesiones prácticas.
Guión de Prácticas. Fundamentos de Programación
11
Tabla resumen de accesos directos usados en Visual Studio
Tabla resumen de accesos directos usados en Visual Studio
Ctr-F7 Alt-Ctr-F7 F5 F10 F11 Shift-F11 Shift-F5 Ctr-F5 Ctr-Barra espaciadora F1 F9 Ctr-F10 Ctr-K,Ctr-F Ctr-K,Ctr-D Ctr -
Compilar Generar ejecutable Ejecutar programa (con depuración) Ejecución paso a paso (sin entrar en las funciones) Ejecución paso a paso (entrando en las funciones) Sale de la ejecución paso a paso de la función Detiene ejecución paso a paso Ejecutar programa (sin depuración) Autocompleta el código Ayuda Quita o pone un punto de interrupción Ejecuta instrucciones hasta el cursor y empieza depuración Aplica formato al texto seleccionado Aplica formato al documento Navega por el texto hacia atrás
Guión de Prácticas. Fundamentos de Programación
12
RELACIÓN DE PROBLEMAS I. Introducción a C++
RELACIÓN DE PROBLEMAS I. Introducción a C++ Problemas Básicos 1. Crear un programa que pida un valor de intensidad y resistencia e imprima el voltaje correspondiente, según la Ley de Ohm: voltaje = intensidad * resistencia Finalidad: Ejemplo básico de asignación a una variable del resultado de una expresión. Dificultad Baja. 2. Un banco presenta la siguiente oferta. Si se deposita una cantidad de euros capital durante un año a plazo fijo, se dará un interés dado por la variable interes. Realizad un programa que lea una cantidad capital y un interés interes desde teclado y calcule en una variable total el dinero que se tendrá al cabo de un año, aplicando la fórmula:
total = capital + capital ∗
interes
100 Es importante destacar que el compilador primero evaluará la expresión de la parte derecha de la anterior asignación (usando el valor que tuviese la variable capital) y a continuación ejecutará la asignación, escribiendo el valor resultante de la expresión dentro de la variable total). A continuación, el programa debe imprimir en pantalla el valor de la variable total. Tanto el capital como el interés serán valores reales. Supondremos que el usuario introduce el interés como un valor real entre 0 y 100, es decir, un interés del 5,4 % se introducirá como 5.4. También supondremos que lo introduce correctamente, es decir, que sólo introducirá valores entre 0 y 100. Supongamos que queremos modificar la variable original capital con el nuevo valor de total. ¿Es posible hacerlo directamente en la expresión de arriba? Nota: El operador de división en C++ es / Finalidad: Resolver un problema real sencillo, usando varias sentencias. Dificultad Baja. 3. Queremos realizar un programa para intercambiar los contenidos de dos variables enteras. El programa leerá desde teclado dos variables edad_Pedro y edad_Juan e intercambiará sus valores. A continuación, mostrará en pantalla las variables ya modificadas. El siguiente código no funciona correctamente.
edad_Pedro = edad_Juan; edad_Juan = edad_Pedro;
Guión de Prácticas. Fundamentos de Programación
13
RELACIÓN DE PROBLEMAS I. Introducción a C++
¿Por qué no funciona? Buscad una solución. Finalidad: Entender cómo funciona la asignación entre variables. Dificultad Baja. 4. Cread un programa que nos pida la longitud del radio, calcule el área del círculo y la longitud de la circunferencia correspondientes, y nos muestre los resultados en pantalla. Recordad que: long. circunf = 2πr
área circ = πr 2
Usad el literal 3.1416 a lo largo del código, cuando se necesite multiplicar por π. Una vez hecho el programa, cambiad las apariciones de 3.1416 por 3.14159, recompilad y ejecutad (La parte de compilación y ejecución se realizará cuando se vea en clase de prácticas el entorno de programación). ¿No hubiese sido mejor declarar un dato constante PI con un valor igual a 3.14159, y usar dicho dato donde fuese necesario? Hacedlo tal y como se explica en las transparencias de los apuntes de clase. Cambiad ahora el valor de la constante PI por el de 3.1415927, recompilad y ejecutad. Finalidad: Entender la importancia de las constantes. Dificultad Baja. 5. Realizar un programa que lea los coeficientes reales µ y σ de una función gaussiana (ver definición abajo). A continuación el programa leerá un valor de abscisa x y se imprimirá el valor que toma la función en x 2 x − µ 1 −2 1 σ e gaussiana(x) = √ σ 2π El parámetro µ se conoce como esperanza o media y σ como desviación típica (mean y standard deviation en inglés). Para definir la función matemática e usad la función exp de la biblioteca cmath. En la misma biblioteca está la función sqrt para calcular la raíz cuadrada. Para elevar un número al cuadrado se puede usar la función pow, que se utiliza en la siguiente forma:
pow(base, exponente) En nuestro caso, el exponente es 2 y la base son correctos, usando el applet disponible en
x−µ σ
. Comprobad que los resultados
http://www.danielsoper.com/statcalc/calc54.aspx o bien algunos de los ejemplos de la figura siguiente (observad que el valor de la desviación está elevado al cuadrado):
Guión de Prácticas. Fundamentos de Programación
14
RELACIÓN DE PROBLEMAS I. Introducción a C++
Finalidad: Trabajar con expresiones numéricas más complejas. Dificultad Baja. 6. Las ganancias de un determinado producto se reparten entre el diseñador y los tres fabricantes del mismo. Diseñar un programa que pida la ganancia total de la empresa (los ingresos realizados con la venta del producto) y diga cuanto cobran cada uno de ellos, sabiendo que el diseñador cobra el doble que cada uno de los fabricantes. El dato de entrada será la ganancia total a repartir. Utilizad el tipo double para todas las variables. Importante: No repetid cálculos ya realizados. Finalidad: Entender la importancia de no repetir cómputos para evitar errores de programación. Dificultad Baja. 7. Redactar un algoritmo para calcular la media aritmética muestral y la desviación estándar (o típica) muestral de las alturas de tres personas. Éstos valores serán reales (de tipo double) v u 3 3 u1 X 1X X= xi , σ = t (xi − X)2 3 i=1 3 i=1 X representa la media aritmética y σ la desviación estándar. Para resolver este problema es necesario usar la función sqrt (raíz cuadrada) que se encuentra en la biblioteca cmath. Finalidad: Trabajar con expresiones numéricas y con variables para no repetir cómputos. Dificultad Baja. 8. Realizar un programa que declare las variables x, y y z, les asigne los valores 10, 20 y 30 e intercambien entre sí sus valores de forma que el valor de x pasa a y, el de y pasa a z y el valor de z pasa a x (se pueden declarar variables auxiliares aunque se pide que se use el menor número posible). Finalidad: Mostrar la importancia en el orden de las asignaciones. Dificultad Media. 9. Leed desde teclado tres variables correspondientes a un número de horas, minutos y segundos, respectivamente. Diseñar un algoritmo que calcule las horas, minutos
Guión de Prácticas. Fundamentos de Programación
15
RELACIÓN DE PROBLEMAS I. Introducción a C++
y segundos dentro de su rango correspondiente. Por ejemplo, dadas 10 horas, 119 minutos y 280 segundos, debería dar como resultado 12 horas, 3 minutos y 40 segundos. En el caso de que nos salgan más de 24 horas, daremos también los días correspondientes (pero ya no pasamos a ver los meses, años, etc) Como consejo, utilizad el operador / que cuando trabaja sobre datos enteros, representa la división entera. Para calcular el resto de la división entera, usad el operador %. Finalidad: Trabajar con expresiones numéricas y con variables para no repetir cómputos. Dificultad Media. 10. Realizad el ejercicio del reparto de la ganancia de un producto, pero cambiando el tipo de dato de la ganancia total a int (el resto de variables siguen siendo double) Finalidad: Trabajar con expresiones numéricas que involucren distintos tipos de datos. Dificultad Baja. 11. Realizad el ejercicio del cálculo de la desviación típica, pero cambiando el tipo de dato de las variables xi a int. Nota: Para no tener problemas en la llamada a la función pow (en el caso de que se haya utilizado para implementar el cuadrado de las diferencias de los datos con la media), obligamos a que la base de la potencia sea un real multiplicando por 1.0, por lo que la llamada quedaría en la forma pow(base*1.0, exponente) Finalidad: Trabajar con expresiones numéricas que involucren distintos tipos de datos. Dificultad Baja. 12. Encontrar el valor de la variable resultado después de la ejecución de este código
real1 = 4.0; real2 = 3.0; resultado = real1/real2 - real1*real2; cout << "\nResultado = " << resultado; Considerad los siguientes casos:
float real1, real2; double resultado; y
float real1, real2, resultado;
Guión de Prácticas. Fundamentos de Programación
16
RELACIÓN DE PROBLEMAS I. Introducción a C++ Nota. Si se desea ver el contenido de una variable real con cout, es necesario que antes de hacerlo, se establezca el número de decimales que se quieren mostrar en pantalla. Hacedlo escribiendo la sentencia cout.precision(20);, en cualquier sitio del programa antes de la ejecución de cout << real1 << "," << real2;. Realizad lo mismo pero asignando los siguientes valores:
double real1, real2 real1 = 1234567890.1; real2 = 1234567891.2; Al multiplicar los dos double se obtiene un número real demasiado grande, que podrá ser representado en formato exponencial, pero sólo con 16 cifras significativas, por lo que sólo se puede garantizar un resultado aproximado. Generad otro error lógico al perder precisión en las cifras decimales. Finalidad: Trabajar con tipos distintos en una misma expresión y entender los problemas de precisión. Dificultad Baja. 13. Considerad el siguiente código:
double real, otro_real; real = 2e34; otro_real = real + 1; otro_real = otro_real - real; cout << otro_real; ¿Qué imprime en pantalla? Justificar la respuesta. Finalidad: Entender el problema de la precisión con los reales. Dificultad Media. 14. Diseñar un programa que lea un carácter (supondremos que el usuario introduce una mayúscula), lo pase a minúscula y lo imprima en pantalla. Hacedlo sin usar las funciones toupper ni tolower de la biblioteca cctype. Para ello, debe considerarse la equivalencia en C++ entre los tipos enteros y caracteres. Finalidad: Entender la equivalencia de C++ entre tipos enteros y de carácter. Dificultad Baja. 15. Supongamos el siguiente código:
int entero; char caracter; caracter = '7'; entero = caracter; Guión de Prácticas. Fundamentos de Programación
17
RELACIÓN DE PROBLEMAS I. Introducción a C++ La variable entero almacenará el valor 55 (el orden en la tabla ASCII del carácter '7'). Queremos construir una expresión que devuelva el entero 7, para asignarlo a la variable entero. Formalmente: Supongamos una variable car de tipo carácter que contiene un valor entre '0' y '9'. Construid un programa que obtenga el correspondiente valor entero, se lo asigne a una variable de tipo int llamada entero y lo imprima en pantalla. Por ejemplo, si la variable car contiene '7' queremos asignarle a entero el valor numérico 7. Nota. La comilla simple para representar un literal de carácter es la que hay en el teclado del ordenador debajo de la interrogación ?. Finalidad: Entender la equivalencia de C++ entre tipos enteros y de carácter. Dificultad Baja. 16. Razonar sobre la falsedad o no de las siguientes afirmaciones: a)
'c' es una expresión de caracteres.
b) 4<3 es una expresión numérica. c) (4+3)<5 es una expresión numérica.
cout << a; da como salida la escritura en pantalla de una a. e) ¿Qué realiza cin >> cte, siendo cte una constante entera?
d)
Finalidad: Distinguir entre expresiones de distinto tipo de dato. Dificultad Baja. 17. Escribid una expresión lógica que sea verdadera si una variable de tipo carácter llamada letra es una letra minúscula y falso en otro caso. Escribid una expresión lógica que sea verdadera si una variable de tipo entero llamada edad es menor de 18 o mayor de 65. Escribid una expresión lógica que nos informe cuando un año es bisiesto. Los años bisiestos son aquellos que o bien son divisibles por 4 pero no por 100, o bien son divisibles por 400. Nota: Cuando se imprime por pantalla (con cout) una expresión lógica que es true, se imprime 1. Si es false, se imprime un 0. En el tema 2 veremos la razón. Finalidad: Empezar a trabajar con expresiones lógicas, muy usadas en el tema 2. Dificultad Baja. 18. Indique qué tipo de dato usaría para representar: • Edad de una persona • Producto interior bruto de un país. Consultad:
http://es.wikipedia.org/wiki/Anexo:Pa%C3%ADses_por_PIB_ (nominal)
• La cualidad de que un número entero sea primo o no.
Guión de Prácticas. Fundamentos de Programación
18
RELACIÓN DE PROBLEMAS I. Introducción a C++ • Estado civil (casado, soltero, separado, viudo) • Sexo de una persona (hombre o mujer exclusivamente) Finalidad: Saber elegir adecuadamente un tipo de dato, atendiendo a la información que se quiere representar. Dificultad Media.
Problemas de Ampliación 19. El precio final de un automóvil para un comprador es la suma total del costo del vehículo, del porcentaje de ganancia de dicho vendedor y del I.V.A. Diseñar un algoritmo para obtener el precio final de un automóvil sabiendo que el porcentaje de ganancia de este vendedor es del 20 % y el I.V.A. aplicable es del 16 %. Dificultad Baja. 20. Declarar las variables necesarias y traducir las siguientes fórmulas a expresiones válidas del lenguaje C++. a)
b)
1+
Algunas funciones de
cmath
x3 1+y
1+
1 3
s c)
x2 y
1+
sin h −
1 7
sen(x) −→ sin(x) cos(x) −→ cos(x) xy −→ pow(x, y) ln(x) −→ log(x) ex −→ exp(x)
cos h
2h x 2 e x2
Dificultad Baja. 21. Dos locomotoras parten de puntos distintos avanzando en dirección contraria sobre la misma vía. Se pide redactar un programa para conocer las distancias que habrán recorrido ambas locomotoras antes de que choquen teniendo en cuenta que la primera locomotora viaja a una velocidad constante V1 , que la segunda viaja a una velocidad constante V2 , la fórmula que relaciona velocidad, espacio y tiempo (s = v t) y que el momento en que se producirá el choque viene dado por la fórmula t=
D V1 + V2
Dificultad Baja. 22. El área A de un triángulo se puede calcular a partir del valor de dos de sus lados, a y 1 b, y del ángulo θ que éstos forman entre sí con la fórmula A = ab sin(θ). Construid 2
Guión de Prácticas. Fundamentos de Programación
19
RELACIÓN DE PROBLEMAS I. Introducción a C++
un programa que pida al usuario el valor de los dos lados (en centímetros), el ángulo que éstos forman (en grados), y muestre el valor del área.
Tened en cuenta que el argumento de la función sin va en radianes por lo que habrá que transformar los grados del ángulo en radianes. Dificultad Baja. 23. Escribir un programa que lea un valor entero. Supondremos que el usuario introduce siempre un entero de tres dígitos, como por ejemplo 351. Escribid en pantalla los dígitos separados por tres espacios en blanco. Con el valor anterior la salida sería:
3
5
1
Dificultad Media. 24. Los compiladores utilizan siempre el mismo número de bits para representar un tipo de dato entero (este número puede variar de un compilador a otro). Por ejemplo, 32 bits para un int. Pero, realmente, no se necesitan 32 bits para representar el 6, por ejemplo, ya que bastarían 3 bits: 6 = 1 ∗ 22 + 1 ∗ 21 + 0 ∗ 20 ≡ 110 Se pide crear un programa que lea un entero n, y calcule el mínimo número de dígitos que se necesitan para su representación. Para simplificar los cómputos, suponed que sólo queremos representar valores enteros positivos (incluido el cero). Consejo: se necesitará usar el logaritmo en base 2 y obtener la parte entera de un real (se obtiene tras el truncamiento que se produce al asignar un real a un entero) Dificultad Media.
Guión de Prácticas. Fundamentos de Programación
20
RELACIÓN DE PROBLEMAS I. Introducción a C++
Sesión 1
I Actividades a realizar en casa Actividad: Conseguir login y password. Antes de empezar la primera sesión de prácticas el alumno debe estar registrado en la Universidad, tal y como se indica en:
http://www.ugr.es/informatica/alumnos/correo.htm
De esta forma, obtendremos un login y un password que habrá que introducir al arrancar los ordenadores en las aulas de prácticas. La cuenta tarda 48 horas an activarse, por lo que el registro debe realizarse al menos dos días antes de la primera sesión de prácticas. Actividad: Instalación de Visual Studio. Durante la primera semana de clase, el alumno debería instalar en su casa el compilador Visual Studio. Consultad la sección de Instalación (página 7) de este guión. Actividad: Resolución de problemas. Realizad una lectura rápida de las actividades a realizar durante la próxima sesión de prácticas en las aulas de ordenadores. Resolved los ejercicios siguientes de la relación de problemas I, página 13 (utilizando únicamente papel y lápiz): • Obligatorios: 1, 2, 3, 4, 5, 6 • Opcionales: 7, 8 Actividades de Ampliación Leer el artículo de Norvig: Aprende a programar en diez años
http://loro.sourceforge.net/notes/21-dias.html sobre la dificultad del aprendizaje de una disciplina como la Programación.
Guión de Prácticas. Fundamentos de Programación
21
El Entorno de Programación. Compilación de Programas
I Actividades a realizar en las aulas de ordenadores Durante esta sesión de prácticas, el profesor irá corrigiendo individualmente (a algunos alumnos elegidos aleatoriamente) los ejercicios indicados anteriormente. Mientras tanto, el resto de alumnos deben seguir las instrucciones indicadas en los siguientes apartados. En la última media hora, el profesor explicará los ejercicios.
El Entorno de Programación. Compilación de Programas Arranque del Sistema Operativo Para poder arrancar el SO en las aulas de ordenadores, es necesario obtener el login y password indicados en las actividades a realizar en casa. En la casilla etiquetada como Código, introduciremos fp. Al arrancar el SO, aparecerá una instalación básica de XP con el compilador Microsoft Visual C++. Todo lo que escribamos en la unidad C: se perderá al apagar el ordenador. Por ello, el alumno dispone de un directorio de trabajo en la unidad lógica U:, cuyos contenidos permanecerán durante todo el curso académico. En cualquier caso, es recomendable no saturar el espacio usado ya que, en caso contrario, el compilador no podrá funcionar. El alumno deberá crear el directorio (U:\FP). Si durante la sesión se requiere abrir algún fichero, éste puede encontrarse en la carpeta del Sistema Operativo
H:\CCIA\Grado_FP\ También están accesibles en la web a través de la página web de la asignatura (acceso identificado)
https://decsai.ugr.es
Muy Importante. Los ficheros que se encuentran en la unidad H: están protegidos y no pueden modificarse. Por tanto, habrá que copiarlos a la unidad local U:, dónde ya sí podrán ser modificados. Nota. En el escritorio de XP, se encuentra un acceso directo a la carpeta H:\CCIA\Grado_FP.
El primer programa Copiando el código fuente el directorio H:\ccia\Grado_FP\ProblemasI se encuentra el directorio I_Pitagoras. Copiadlo entero a vuestra carpeta local (dentro de U:\FP).
En
Guión de Prácticas. Fundamentos de Programación
22
El Entorno de Programación. Compilación de Programas
Importante: Siempre hay que copiar localmente las carpetas que aparecen en la unidad H: del departamento ya que están protegidos contra escritura y no se puede trabajar directamente sobre los proyectos incluidos en dichas carpetas. Desde el Explorador de Windows, entrad en la carpeta recién creada en vuestra cuenta:
U:\FP\I_Pitagoras y observad los ficheros que contiene. Cada vez que creemos un programa desde Visual Studio se creará automáticamente una carpeta (en este caso I_Pitagoras) con distintos ficheros necesarios para el compilador Visual Studio. Por ejemplo, podemos encontrar los ficheros
I_Pitagoras.vcproj I_Pitagoras.sln I_Pitagoras.cpp Por ahora, lo único que nos interesa saber es lo siguiente: • Para abrir directamente Visual Studio, basta con hacer doble click sobre los ficheros .vcproj o .sln. Al hacerlo, automáticamente se abrirá Visual Studio y cargará el proyecto que, entre otras cosas, contiene el fichero de código fuente con extensión .cpp. Para abrirlo, haremos doble click sobre él desde el Explorador de Soluciones de Visual Studio. Importante : No debe hacerse doble click desde el Explorador de Windows sobre el fichero con extensión .cpp, ya que, en dicho caso, Visual Studio lo abriría pero no podríamos compilarlo al no encontrarse dentro de un proyecto. Para poder compilar un fichero, éste debe estar incluido en un proyecto
• El fichero realmente interesante es el que tiene la extensión cpp. En nuestro caso es I_Pitagoras.cpp. Estos serán los ficheros sobre los que escribiremos el código de nuestros programas. Este fichero se abrirá automáticamente en el editor de Visual Studio, después de que hallamos cargado el correspondiente fichero I_Pitagoras.sln Haced doble click sobre el fichero I_Pitagoras.sln. Debe aparecer una ventana como la de la figura 1 (el sitio exacto en el que se muestran las distintas ventanas del entorno puede cambiar):
Guión de Prácticas. Fundamentos de Programación
23
El Entorno de Programación. Compilación de Programas
Figura 1: Programa que implementa el algoritmo de Pitágoras
Guión de Prácticas. Fundamentos de Programación
24
El Entorno de Programación. Compilación de Programas
Algunas consideraciones con respecto a la escritura de código en C++ (ver figura 2) • Es bueno que, desde el principio se incluyan comentarios indicando el objetivo del programa y resaltando los aspectos más importantes de la implementación. • Es muy importante una correcta tabulación de los programas. Por ahora, incluiremos todas las sentencias del programa principal con una tabulación. Sólo es necesario incluir la primera; el resto las pone automáticamente el entorno, al pasar a la siguiente línea. • Para facilitar la lectura del código fuente, se deben usar espacios en blanco para separar las variables en la línea en la que van declaradas, así como antes y después del símbolo = en una sentencia de asignación. Dejad también un espacio en blanco antes y después de << y >> en las sentencias que contienen una llamada a cout y cin respectivamente.
Figura 2: Escritura de código
No respetar las normas de escritura de código baja puntos en todos los exámenes y prácticas de la asignatura
Guión de Prácticas. Fundamentos de Programación
25
El Entorno de Programación. Compilación de Programas
Observad la última sentencia
system("pause"); Veamos para qué sirve. Cuando el programa se ejecuta desde el entorno del Visual Studio, automáticamente se abre una ventana de consola para ofrecer resultados y leer datos. Cuando el programa termina de funcionar, automáticamente se cierra esta ventana, por lo que si había algún mensaje escrito en ella, no da tiempo a leerlo. Para resolver este problema, ejecutamos el comando pause de la consola del Sistema Operativo. Este comando hace que se muestre el mensaje en pantalla
Pulse cualquier tecla para continuar La ejecución del programa se detiene y cuando el usuario pulsa cualquier tecla, prosigue la ejecución. La forma de ejecutar un comando cualquiera de la consola, desde dentro de un programa de C++, es a través del recurso system, pasándole como parámetro una cadena de caracteres con el comando que se quiere ejecutar. En nuestro caso sería, tal y como aparece arriba, system("pause");.
Compilación Una vez cargado el programa, pasamos a comprobar si las sentencias escritas son sintácticamente correctas, es decir, pasamos a compilar el programa. Para ello pulsamos Ctr-F7, . o bien sobre el icono Para que el proceso de compilación se realice de forma correcta y se obtenga el programa ejecutable, es necesario que el código fuente no contenga errores sintácticos. Si aparecen errores, es necesario volver a la fase de edición, guardar de nuevo el código fuente y repetir la fase de compilación. Como resultado de la fase de compilación, en la esquina inferior izquierda del entorno debe aparecer un mensaje del tipo:
Generación satisfactoria Una vez compilado el programa, pasamos a generar el ejecutable, I_Pitagoras.exe. Para ello, o bien pulsamos Alt-Ctr-F7 o bien sobre el icono de la barra de herramientas que contiene el texto Volver a generar solución. Finalmente, para ejecutar el pro-
grama pulsamos pulsamos F5 o sobre el icono . Si se quiere ejecutar directamente, basta pulsar F5, sin dar los dos primeros pasos de compilación y generación. Debe aparecer una ventana de comandos del Sistema, en la que se estará ejecutando el programa. La ejecución del programa se detendrá en aquellos puntos del mismo donde
Guión de Prácticas. Fundamentos de Programación
26
El Entorno de Programación. Compilación de Programas
se requiera la interacción del usuario para poder proseguir, es decir, en la operaciones de entrada de datos a través del dispositivo estándar de entrada. En este ejemplo, sería en las dos operaciones cin. En el resto de los casos, la ejecución del programa continuará hasta el final. La introducción de datos mediante la sentencia cin se hace siempre de la misma manera; primero se introduce el valor que se desee y al terminar se pulsa la tecla RETURN. Introducid ahora los valores pedidos en el ejemplo de Pitágoras y comprobad la respuesta del programa. Como hemos indicado anteriormente, en la fase de generación del ejecutable se ha creado un fichero en el Sistema que se llama igual que nuestro fichero pero sustituyendo la extensión ”cpp” por ”exe”, es decir, I_Pitagoras.exe. Este fichero se encuentra en el directorio Debug, dentro de la carpeta I_Pitagoras que habíamos copiado localmente. Para mostrar que el fichero generado es independiente del entorno de programación, hacemos lo siguiente: 1. Cerramos Visual
Studio
2. Abrid una ventana de Mi PC. 3. Situarse en la carpeta dónde se guardó el código fuente. 4. Haced doble click sobre el fichero I_Pitagoras.exe. Nota. En la instalación por defecto de Visual Studio que hagamos en nuestra casa, los ficheros de salida e intermedios necesarios por Visual Studio se almacenan en la misma carpeta del proyecto. Prueba del programa Uno podía pensar que una vez que consigo un fichero ejecutable a partir de mi código fuente, el problema está terminado. Sin embargo esto no es así. Tras el proceso de compilado se requiere una fase de prueba. Dicha fase intenta probar que el algoritmo planteado resuelve el problema propuesto. Para llevar a cabo esta fase, es necesario ejecutar el programa y verificar que los resultados que obtiene son los esperados. Ahora que podemos ver el resultado obtenido por el programa implementado, verifiquemos mediante el siguiente conjunto de pruebas que el programa funciona de forma correcta. lado1 lado2 3 4 1 5 2.7 4.3 1.25 2.75
hip 5 5.099 5.077 3.02
Una vez que el algoritmo supera la fase de prueba, podemos considerar que se ha concluido con la fase inicial del desarrollo del software.
Guión de Prácticas. Fundamentos de Programación
27
El Entorno de Programación. Compilación de Programas
Introducción a la corrección de errores Los errores de compilación Ya hemos visto los pasos necesarios para construir un fichero ejecutable a partir del código fuente. El paso central de este proceso era la fase de compilación. En esta parte de este guión de prácticas aprenderemos a corregir los errores más comunes que impiden una compilación exitosa del fichero fuente. Cargad el proyecto I_Pitagoras.sln (no olvidad que desde el Sistema Operativo hay que seleccionar el fichero con extensión vcproj o sln). En la ventana del Explorador de Soluciones, haced doble click sobre I_Pitagoras.cpp para poder editar el fichero. Quitadle una 'u' a alguna aparición de cout. Intentad compilar. Podemos observar que la compilación no se ha realizado con éxito. Cuando esto sucede, en la parte inferior de la ventana principal, en la pestaña Lista de errores, aparecen los errores que se han encontrado. Aparece una descripción del error, así como otra información, como el número de línea en la que se produjo. Los pasos que debemos seguir para la corrección son los siguientes: 1. Ir a la primera fila de la lista de errores. 2. Leer el mensaje de error e intentar entenderlo. 3. Hacer doble click sobre esa fila con el ratón. Esto nos posiciona sobre la línea en el fichero fuente donde el compilador detectó el error. 4. Comprobar la sintaxis de la sentencia que aparece en esa línea. Si se detecta el error, corregirlo. Si no se detecta el error mirar en la línea anterior, comprobar la sintaxis y repetir el proceso hasta encontrar el error. 5. Después de corregir el posible error, guardamos de nuevo el archivo y volvemos a compilar. Esto lo hacemos aunque aparezcan más errores en la ventana. La razón es que es posible que el resto de los errores sean consecuencia del primer error. 6. Si después de corregir el error aparecen nuevos errores, volver a repetir el proceso desde el paso 1. A veces, el compilador no indica la línea exacta en la que se produce el error, sino alguna posterior. Para comprobarlo, haced lo siguiente: • Comentad la línea de cabecera #include <iostream> desde el principio. El compilador no reconocerá las apariciones de cin o cout. • Quitad un punto y coma al final de alguna sentencia. Dará el error en la línea siguiente.
Guión de Prácticas. Fundamentos de Programación
28
El Entorno de Programación. Compilación de Programas
Para familiarizarnos con los errores más frecuentes y su corrección vamos a realizar el siguiente proceso: a partir del código fuente del ejemplo I_Pitagoras.cpp, iremos introduciendo deliberadamente errores para conocer los mensajes que nos aparecen. A continuación se muestran algunos errores posibles. No deben introducirse todos ellos a la vez, sino que han de probarse por separado. 1. Cambiad algún punto y coma por cualquier otro símbolo 2. Cambiad double por dpuble 3. Cambiad la línea using
namespace std; por using namespace STD;
4. Poned en lugar de iostream, el nombre iotream. 5. Borrad alguno de los paréntesis de la declaración de la función main 6. Introducid algún identificador incorrecto, como por ejemplo cour 7. Usad una variable no declarada. Por ejemplo, en la definición de variables cambiad el nombre a la variable lado1 por el identificador lado11. 8. Borrad alguna de las dobles comillas en una constante de cadena de caracteres, tanto las comillas iniciales como las finales. 9. Borrad alguna de las llaves que delimitan el inicio y final del programa. 10. Borrad la línea using
namespace std; (basta con comentarla con //)
11. Cambiad un comentario iniciado con guientes \\ 12. Cambiad la aparición de lo mismo con cin.
//, cambiando las barras anteriores por las si-
<< en cout por las flechas cambiadas, es decir, >>. Haced
13. Suprimid todo el main. No hace falta borrar el código, basta con comentarlo. 14. Aunque no sea un error, comentad la última linea que contiene system("pause"); para comprobar que el programa se ejecuta correctamente, pero no da tiempo a ver los resultados en la consola. Además de los errores, el compilador puede generar avisos. Estos se muestran en la pestaña Advertencias en la misma ventana de la lista de errores. Estas advertencias indican que algún código puede generar problemas durante la ejecución. Por ejemplo, al usar una variable que todavía no tiene un valor asignado, al intentar asignar un entero grande a un entero chico, etc. Sin embargo, no son errores de compilación, por lo que es posible generar el programa ejecutable correspondiente.
Guión de Prácticas. Fundamentos de Programación
29
El Entorno de Programación. Compilación de Programas
Los errores lógicos y en tiempo de ejecución Aunque el programa compile, esto no significa que sea correcto. Puede producirse una excepción durante la ejecución, de forma que el programa terminará bruscamente (típico error en Windows de Exception Violation Address) o, lo que es peor, dará una salida que no es correcta (error lógico). Sobre el programa I_Pitagoras.cpp, haced lo siguiente: • Cambiad la sentencia
sqrt(lado1*lado1 + lado2*lado2) por: sqrt(lado1*lado2 + lado2*lado2)
Ejecutad introduciendo los lados 2 y 3. El resultado no es correcto, pero no se produce ningún error de compilación ni en ejecución. Es un error lógico. • Para mostrar un error de ejecución, declarad tres variables ENTERAS (tipo int) resultado, numerador y denominador. Asignadle cero a denominador y 7 a numerador. Asignadle a resultado la división de numerador entre denominador. Imprimid el resultado. Al ejecutar el programa, se produce una excepción o error de ejecución al intentar dividir un entero entre cero.
Creación de un programa nuevo En esta sección vamos a empezar a crear nuestros propios programas desde Visual Studio. El primer ejemplo que vamos a implementar corresponde al ejercicio 1 sobre la Ley de Ohm, de la relación de problemas I. Para crear un programa nuevo, abrimos Visual Studio y elegimos
Archivo->Nuevo Proyecto
Si tenemos instalados varios lenguajes (Visual Basic, C#, etc), elegimos C++. Nos aparecen distintos tipos de proyectos, en función de lo que vayamos a crear (una aplicación en Consola, un formulario de Windows, etc). Elegimos Proyecto vacío. En la parte inferior de la ventana, hacemos click sobre Examinar para elegir la carpeta donde colgará el directorio de nuestra aplicación. En nuestro caso, será U:\FP. Finalmente elegimos un nombre para nuestra aplicación. Teclead I_Voltaje y pulsad sobre Aceptar (ver figura 3) Se nos abrirá una ventana con el Explorador de Soluciones. Sobre la carpeta Archivos de código fuente pulsamos click con la derecha y seleccionamos Agregar->Nuevo elemento. Seleccionamos Archivo C++ (.cpp) y le damos un nombre. Podemos darle el mismo nombre que el del proyecto, I_Voltaje (ver figura 4) Llegado a este paso, debemos tener el entorno de Visual Studio, con varias ventanas como las de la figura 5 (la situación exacta de las ventanas puede variar)
Guión de Prácticas. Fundamentos de Programación
30
El Entorno de Programación. Compilación de Programas
Figura 3: Creación de un proyecto nuevo
Figura 4: Añadiendo un fichero de código vacío
Guión de Prácticas. Fundamentos de Programación
31
El Entorno de Programación. Compilación de Programas
Figura 5: Entorno de Visual Studio Ya estamos en condiciones de resolver el problema pedido. Escribimos el código en la ventana correspondiente a la pestaña I_Voltaje.cpp. Habrá que leer desde teclado los valores de intensidad y resistencia y el programa imprimirá en pantalla el voltaje correspondiente. Recordad que compilamos con Ctr-F7 y ejecutamos con F5. Nota. Cuando tenemos varias variables en el código, podemos empezar a escribir el nombre de alguna de ellas y antes de terminar, pulsar Ctr-Barra espaciadora. La ayuda nos mostrará los identificadores disponibles que empiecen por las letras tecleadas. A partir de ahora, cada uno de los ejercicios irá en un proyecto nuevo. Implementad los ejercicios que se habían mandado para esta sesión en este orden: 2, 3, 4, 5, 6. Usad en esta sesión el tipo de proyecto "Proyecto Vacío".
Guión de Prácticas. Fundamentos de Programación
32
Tipos básicos y operadores
Sesión 2
Tipos básicos y operadores I Actividades a realizar en casa Actividad: Seminario. Realizar un resumen de los modificadores de tipo de dato ofrecidos por C++ como por ejemplo long, unsigned, etc. En las transparencias de teoría del primer tema pueden encontrarse enlaces en la Web con la información necesaria. Analizad las ventajas e inconvenientes de poder disponer de este amplio conjunto de tipos de datos. Alguno de los alumnos deberá exponer este trabajo durante las horas de teoría (en el aula grande) en la semana que indique el profesor. Actividad: Resolución de problemas. Resolved los siguientes problemas de la relación I. Por cada uno de ellos, habrá que crear el correspondiente proyecto. Usad el tipo de proyecto "Proyecto Vacío". Recordad que antes del inicio de esta sesión en el aula de ordenadores hay que subir las soluciones a la cuenta personal en turing.ugr.es tal y como se indica en la página 4. • Obligatorios: 10 (Fabricantes con enteros) 11 (Media y desviación con enteros) 12 (Precisión con reales) 13 (Precisión con reales) 14 (Pasar de mayúscula a minúscula) 15 (Pasar de carácter a entero) • Opcionales: 9 (Horas, minutos y segundos) 18 (Tipos de datos para representar información diversa)
Guión de Prácticas. Fundamentos de Programación
33
Tipos básicos y operadores
Actividades de Ampliación Recordad los conceptos de combinación y permutación, que irán apareciendo recurrentemente a lo largo de la carrera. Consultad, por ejemplo
http://www.disfrutalasmatematicas.com/combinatoria/ combinaciones-permutaciones.html para una introducción básica a los conceptos, y
http://club.telepolis.com/ildearanda/index.html como una referencia a las fórmulas que nos proporcionan los cómputos correspondientes. Ejecutad el applet
http://www.disfrutalasmatematicas.com/combinatoria/ combinaciones-permutaciones-calculadora.html con los valores pertinentes para calcular cuántos número distintos se pueden representar utilizando únicamente dos símbolos (0 y 1) en 64 posiciones de memoria. Ejecutad el mismo applet para que muestre en la parte inferior las distintas permutaciones que se pueden conseguir con 2 símbolos (0,1) y 4 posiciones.
Guión de Prácticas. Fundamentos de Programación
34
Configuración del proyecto y gestión de errores
I Actividades a realizar en las aulas de ordenadores Todas las sesiones de prácticas en el aula, a partir de ésta, se desarrollarán de la misma forma: El profesor irá corrigiendo individualmente (a algunos alumnos elegidos aleatoriamente) los ejercicios indicados anteriormente. Mientras tanto, el resto de alumnos deben seguir las instrucciones indicadas en la sesión de prácticas. Cuando terminen, pueden empezar a resolver los problemas planteados para la siguiente sesión. En la segunda hora, el profesor sacará a la pizarra a algunos alumnos para que expliquen algunos problemas de esta sesión, y explicará el resto de ejercicios.
Configuración del proyecto y gestión de errores Los proyectos en Microsoft Visual C++ pueden ser generados con muchas opciones. Por ejemplo, se puede añadir al fichero ejecutable información de depuración, para poder realizar un seguimiento de las instrucciones ejecutadas, o que detecte los problemas para exportar el programa a una plataforma de 64 bits, etc. Estas opciones están disponibles desde el Explorador de Soluciones, pinchando con la izquierda el proyecto y seleccionando Propiedades, o bien directamente desde el menú Proyecto->Propiedades. No vamos a ver ninguna de estas opciones, aunque sí debemos saber cómo influyen en el desarrollo de un programa. Normalmente, los programas se desarrollan en dos fases: 1. En una primera fase, se usa un entorno optimizado de depuración. Esto supone una recarga computacional importante, pero es muy adecuada para detectar errores de programación, como por ejemplo, el usar una variable no asignada previamente. Cuando se trabaja en esta fase, hay que seleccionar
Proyecto -> Propiedades y pinchad sobre Administrador de configuración en la esquina superior derecha. Dentro de Configuración de soluciones activas, seleccionad Debug. Esta es la configuración adecuada en esta primera fase. Para probarlo, copiad la carpeta I_Voltaje del directorio de la asignatura a la unidad local U:\FP. Abrid el proyecto y comentad la sentencia en la que se realiza la lectura de la intensidad
//
cin >> intensidad;
Cuando se llegue a ejecutar la sentencia
voltaje = intensidad * resistencia; el valor de intensidad será basura. Si compilamos el programa con Ctr-F7 podemos apreciar que se genera una advertencia en la lista de errores, indicando que se
Guión de Prácticas. Fundamentos de Programación
35
Configuración del proyecto y gestión de errores está usando la variable intensidad sin haberle asignado previamente un valor. Aún así, podemos generar el fichero .exe y ejecutarlo. Hacedlo, para comprobar el error que se obtiene en tiempo de ejecución. 2. En una segunda fase, cuando ya hemos comprobado que el programa funciona correctamente, se procede a generar el fichero ejecutable (.exe) eliminando la información de depuración (entre otras cosas). Esto hará que el ejecutable obtenido sea bastante más pequeño. Para ello, hay que cambiar la configuración antes de generar el ejecutable. Volvemos a entrar en
Configuración de soluciones activas y seleccionamos Release. Podemos hacerlo directamente desde el botón disponible
en la esquina superior derecha de la barra de herramientas. Si generamos el programa bajo esta última configuración, veremos que al ejecutarlo no se produce un error durante la ejecución. Esto es debido, como ya hemos comentado, a que el compilador ya no pierde el tiempo comprobando estos posibles errores de programación. Por contra, obtendremos un error lógico ya que la variable intensidad contiene basura y al multiplicarla por la variable resistencia el resultado será basura. A lo largo de la asignatura, usaremos siempre la configuración Debug, por lo que algunos errores de programación, como el uso de variables no asignadas previamente, serán detectados por el compilador. Pero debemos tener claro que en en la configuración Release, o bien en la compilación realizada con otros compiladores distintos a Visual Studio, no se producirá ningún error durante la ejecución sino un lamentable error lógico. En el entorno Visual Studio de las aulas de prácticas, se ha incluido una plantilla con las opciones de configuración de compilación más usuales (tanto para la configuración Debug como Release) así como un esqueleto del programa principal que incluye la cabecera que usaremos siempre en nuestros programas. Para crear un programa nuevo basándonos en esta plantilla, basta hacer lo siguiente:
Archivo -> Nuevo Proyecto -> _fp_2010 Seleccionamos una ubicación (usualmente U:\FP) y tecleamos un nombre para el proyecto. A partir de esta sesión usaremos siempre la plantilla _fp_2010 para crear nuestros proyectos. Para instalar esta plantilla en nuestros ordenadores personales, seguid las instrucciones que se encuentran en la página 7
Guión de Prácticas. Fundamentos de Programación
36
Configuración del proyecto y gestión de errores
Ejercicio. Cread un proyecto nuevo llamado Prueba_Tipos. Declarad variables de tipo int, double, float, double, char y bool. Provocad errores de compilación y lógicos al mezclar distintos tipos de datos en una misma expresión. Por ejemplo: – Asignad a un dato un literal fuera de rango, – Forzad un error en ejecución al dividir un entero por cero, – Provocar errores lógicos de desbordamiento con un único tipo de dato, – Provocar errores lógicos de desbordamiento mezclando distintos tipos de dato en una misma expresión, · · · Ejercicio. Copiad a la unidad local la carpeta I_Pitagoras. Cargad el proyecto y cambiad la suma dentro de sqrt por una resta. Ejecutad introduciendo los lados 2 y 3. Como el resultado es la raíz cuadrada de un número negativo, el ordenador no puede hacer los cálculos y muestra -1.#IND. Tal y como se vio en clase de teoría, este es el valor de Indeterminación, representable en los datos de coma flotante (double1 es un tipo de dato en coma flotante). En este caso se ha producido un error lógico, pero en otro lenguaje de programación que no utilizase una representación de reales en coma flotante, podría haberse producido un error fatal durante la ejecución, terminándose de forma brusca el programa. Si sobra tiempo, realizad cualquiera de los ejercicios de ampliación de la Relación de Problemas I de la página 19
1
El valor de indeterminación no puede representarse en un tipo entero
Guión de Prácticas. Fundamentos de Programación
37
RELACIÓN DE PROBLEMAS II. Estructuras de Control
RELACIÓN DE PROBLEMAS II. Estructuras de Control Problemas Básicos 1. Ampliad el ejercicio 7 de la relación de problemas I, para que, una vez calculada la media y la desviación, el programa imprima por cada uno de los valores introducidos previamente, si está por encima o por debajo de la media. Por ejemplo:
33 es menor que su media 48 es mayor o igual que su media ..... Nota. Los valores introducidos son enteros, pero la media y la desviación son reales. Finalidad: Plantear un ejemplo básico con varias estructuras condicionales independientes. Dificultad Baja. 2. Cread un programa que incluya una variable edad en la que guardamos la edad de una persona (como una variable entera) y otra variable ingresos, en la que almacenamos sus ingresos (como un real). Subid sus ingresos en un 5% si es un jubilado con unos ingresos inferiores a 300 euros e imprimid el resultado por pantalla. En caso contrario imprimid el mensaje "No es aplicable la subida". En ambos casos imprimid los ingresos resultantes. Finalidad: Plantear una estructura condicional doble con una expresión lógica compuesta. Dificultad Baja. 3. Realizar un programa en C++ que lea dos valores enteros desde teclado y diga si cualquiera de ellos divide o no (de forma entera) al otro. En este problema no hace falta decir quién divide a quién. Supondremos que los valores leídos desde teclado son ambos distintos de cero y se pueden mezclar cómputos con entradas y salidas. Finalidad: Plantear una estructura condicional doble con una expresión lógica compuesta. Dificultad Baja. 4. Vamos a modificar el ejercicio 3 de la siguiente forma. Queremos leer dos valores enteros desde teclado y, en el caso de que uno cualquiera de ellos divida al otro, el programa nos debe decir quién divide a quién. a) En primer lugar, resolved el ejercicio mezclando entradas, cómputos y salidas de resultados b) En segundo lugar, se pide resolver el ejercicio sin mezclar C/E,S. Para ello, se ofrecen varias alternativas. ¿Cual sería la mejor? Escoged una e implementar la solución.
Guión de Prácticas. Fundamentos de Programación
38
RELACIÓN DE PROBLEMAS II. Estructuras de Control
I)
Utilizar un variable de tipo string de la forma siguiente:
string quien_divide; ............. if (a%b==0) quien_divide = "b divide a a" ; ............. if (quien_divide == "b divide a a") cout << b << " divide a " << a;
Nota. Para poder usar el operador de comparación == entre dos string, hay que incluir la biblioteca string. Si se opta por esta alternativa, el suspenso está garantizado. ¿Por qué? II ) Utilizar dos variables lógicas de la forma siguiente:
bool a_divide_b, b_divide_a; ............. if (a%b==0) a_divide_b = true; ............. if (a_divide_b) cout << a << "divide a " << b; III )
Detectamos si se dividen o no y usamos otras dos variables que me indiquen quien es el dividendo y quien el divisor:
bool se_dividen; int Divdo, Divsor; ............. if (a%b==0){ Divdo = a; ............. if (se_dividen) cout << Dvsor << " divide a " << Dvdo; Completar la solución elegida para contemplar también el caso en el que alguno de los valores introducidos sea cero, en cuyo caso, ninguno divide al otro. Finalidad: Mostrar cómo se comunican los distintos bloques de un programa a través de variables que indican lo que ha sucedido en el bloque anterior. Dificultad Media. 5. Se quiere leer un carácter letra_original desde teclado, y comprobar con una estructura condicional si es una letra mayúscula. En dicho caso, hay que calcular la minúscula correspondiente en una variable llamada letra_convertida. En cualquier otro caso, le asignaremos a letra_convertida el valor que tenga letra_original. Finalmente, imprimiremos en pantalla el valor de letra_convertida. No pueden usarse las funciones tolower ni toupper de la biblioteca cctype.
Guión de Prácticas. Fundamentos de Programación
39
RELACIÓN DE PROBLEMAS II. Estructuras de Control
Se propone una primera solución como sigue:
char letra_convertida, letra_original; const int DISTANCIA_MAY_MIN = 'a'-'A'; cout << "\nIntroduzca una letra --> "; cin >> letra_original; if ((letra_original >= 'A') && (letra_original <= 'Z')){ letra_convertida = letra_original + DISTANCIA_MAY_MIN; cout << letra_convertida; } else{ cout << letra_original << " no es una mayúscula"; } El problema es que dentro de la misma sentencia condicional se realizan cómputos (letra_convertida = letra_original + DISTANCIA_MAY_MIN ) y salidas de resultados en pantalla (cout << letra_convertida;). Para evitarlo, se propone el uso de una variable lógica llamada es_mayuscula, de la siguiente forma:
if ((letra_original >= 'A') && (letra_original <= 'Z')) es_mayuscula = true; if (es_mayuscula) letra_convertida = letra_original + DISTANCIA_MAY_MIN; else letra_convertida = letra_original; cout << "\nEl carácter " << letra_original << " una vez convertido es: " << letra_convertida; Sin embargo, hay un error lógico ya que la variable es_mayuscula se podría quedar sin un valor asignado (en el caso de que la expresión lógica fuese false) El compilador lo detecta y da el aviso al inicio de la sentencia if (es_mayuscula). Comprobadlo para familiarizarnos con este tipo de avisos y proponer una solución. Moraleja: Hay que garantizar la asignación de las variables lógicas, en todos los casos posibles Finalidad: Plantear una condición compuesta y destacar la importancia de incluir un bloque else, para garantizar que siempre se ejecuta el código adecuado, en todas las situaciones posibles. Dificultad Baja. 6. Queremos modificar el ejercicio 5 para leer un carácter letra_original desde teclado y hacer lo siguiente:
Guión de Prácticas. Fundamentos de Programación
40
RELACIÓN DE PROBLEMAS II. Estructuras de Control • Si es una letra mayúscula, almacenaremos en la variable la correspondiente letra minúscula.
letra_convertida
• Si es una letra minúscula, almacenaremos en la variable la correspondiente letra mayúscula.
letra_convertida
• Si es un carácter no alfabético, almacenaremos el mismo carácter en la variable
letra_convertida
Para ello, añadimos una variable nueva es_minuscula para detectar el caso en el que la letra sea una minúscula. Si escribimos el siguiente código
if (letra_original >= 'A') && (letra_original <= 'Z') es_mayuscula = true; else es_minuscula = true; estaremos cometiendo el tipo de error indicado en el ejercicio 5, ya que si le asignamos un valor a una de las variables lógicas, no se lo asignamos a la otra y se queda con basura. Para resolverlo, planteamos la siguiente solución:
if ((letra_original >= 'A') && (letra_original <= 'Z')){ es_mayuscula = true; es_minuscula = false; } else{ es_mayuscula = false; es_minuscula = true; } o si se prefiere, de una forma más concisa:
es_mayuscula = (letra_original >= 'A') && (letra_original <= 'Z'); es_minuscula = !es_mayuscula; En este planteamiento hay un error lógico que se comete de forma bastante habitual, cuando se quiere detectar más de dos situaciones posibles. En la asignación
es_minuscula = !es_mayuscula;
estamos diciendo que una minúscula es todo aquello que no sea una mayúscula. Esto no es correcto, ya que un símbolo como # no es ni mayúscula ni minúscula. Deberíamos haber usado lo siguiente:
es_mayuscula = (letra_original (letra_original es_minuscula = (letra_original (letra_original Guión de Prácticas. Fundamentos de Programación
>= <= >= <=
'A') && 'Z'); 'a') && 'z'); 41
RELACIÓN DE PROBLEMAS II. Estructuras de Control
Completad
el
ejercicio, asignándole el valor correcto a la variable letra_convertida e imprimid en pantalla el valor de letra_convertida. Finalidad: Plantear una estructura condicional anidada. Dificultad Baja. 7. Para salir de una crisis económica, el gobierno decide: • Bajar un 1 % la retención fiscal a los autónomos • Para las rentas de trabajo: – Se mantiene la misma retención fiscal a todos los pensionistas – Subir un 6 % la retención fiscal a las rentas de trabajo inferiores a 20.000 euros – Subir un 8 % la retención fiscal a los casados con rentas de trabajo superiores a 20.000 euros – Subir un 10 % la retención fiscal a los solteros con rentas de trabajo superiores a 20.000 euros Cread un programa que calcule la nueva retención a aplicar, la aplique a los ingresos e imprima el resultado . En este ejercicio no haremos ninguna lectura con cin, sino que declararemos las variables necesarias (por ejemplo retencion_fiscal, es_autonomo, etc) y les asignaremos los valores que se deseen directamente dentro del código. Nota: Cuando se pide subir un x % la retención fiscal, significa que la nueva retención será la antigua más el resultado de realizar el x % sobre la antigua retención. Finalidad: Plantear una estructura condicional anidada. Dificultad Baja. 8. Una compañía aérea establece el precio del billete como sigue: en primer lugar se fija una tarifa base de 150 euros, la misma para todos los destinos. Si el destino está a menos de 200 kilómetros, el precio final es la tarifa inicial. Para destinos a más de 200 Km, se suman 10 céntimos por cada kilómetro de distancia al destino (a partir del Km 200). En una campaña de promoción, se barajan las siguientes alternativas de políticas de descuento: a) Una rebaja del 3 % en el precio final, para destinos a más de 600Km b) Una rebaja del 4 % en el precio final, para destinos a más de 1100Km c) Una rebaja del 5 % si el comprador es cliente previo de la empresa. Cread un programa para que lea el número de kilómetros al destino y si el billete corresponde a un cliente previo de la empresa. Calcular el precio final del billete con las siguientes políticas de descuento: • Aplicando c) de forma adicional a los descuentos a) y b)
Guión de Prácticas. Fundamentos de Programación
42
RELACIÓN DE PROBLEMAS II. Estructuras de Control • Aplicando c) de forma exclusiva con los anteriores, es decir, que si se aplica c), no se aplicaría ni a) ni b) Finalidad: Plantear una estructura condicional anidada. Dificultad Media. 9. En el ejercicio 6 se han usado dos variables lógicas (es_mayuscula, es_minuscula), que nos proporciona la distinción entre 4 casos distintos (VV, VF, FV, FF). Sin embargo, sólo queremos distinguir tres posibilidades, a saber, es mayúscula, es minúscula y no es un carácter alfabético. Para ello, volved a resolver el ejercicio 6 sustituyendo las dos variables lógicas por un tipo enumerado adecuado. Finalidad: Usar el tipo enumerado para detectar cuándo se produce una situación determinada. Dificultad Media. 10. Realizar un programa que lea desde teclado un entero tope e imprima en pantalla todos sus divisores. Para obtener los divisores, basta recorrer todos los enteros menores que el valor introducido y comprobar si lo dividen. A continuación, mejorar el ejercicio obligando al usuario a introducir un entero positivo, usando un filtro con un bucle post test (do while). Finalidad: Plantear un ejemplo sencillo de bucle y de filtro de entrada de datos. Dificultad Baja. 11. Realizar un programa que lea reales desde teclado y calcule cuántos se han introducido y cual es el mínimo de dichos valores (pueden ser positivos o negativos). Se dejará de leer datos cuando el usuario introduzca el valor 0. Realizad la lectura de los reales dentro de un bucle sobre una única variable llamada dato. Es importante controlar los casos extremos, como por ejemplo, que el primer valor leído fuese ya el terminador de entrada (en este caso, el cero). Finalidad: Destacar la importancia de las inicializaciones antes de entrar al bucle. Ejemplo de lectura anticipada. Dificultad Baja. 12. Modifiquemos el ejercicio 2 del capital y los intereses de la primera relación. Supongamos ahora que se quiere reinvertir todo el dinero obtenido (el original C más los intereses producidos) en otro plazo fijo a un año. Y así, sucesivamente. Construid un programa para que lea el capital, el interés y un número de años N , y calcule e imprima todo el dinero obtenido durante cada uno de los N años, suponiendo que todo lo ganado (incluido el capital original C) se reinvierte a plazo fijo durante el siguiente año. El programa debe mostrar una salida del tipo:
Total en el año número 1 = 240 Total en el año número 2 = 288 Total en el año número 3 = 345.6 ......................
Guión de Prácticas. Fundamentos de Programación
43
RELACIÓN DE PROBLEMAS II. Estructuras de Control
Finalidad: Usar una variable acumuladora dentro del cuerpo de un bucle (aparecerá a la izquierda y a la derecha de una asignación). Dificultad Baja. 13. Sobre el mismo ejercicio del capital y los intereses, construid un programa para calcular cuantos años han de pasar hasta llegar a doblar, como mínimo, el capital inicial. Los datos que han de leerse desde teclado son el capital inicial y el interés anual. Finalidad: Usar la variable acumuladora en la misma condición del bucle. Dificultad Baja. 14. Se pide leer un carácter desde teclado, obligando al usuario a que sea una letra mayúscula. Para ello, habrá que usar una estructura repetitiva do while, de forma que si el usuario introduce un carácter que no sea una letra mayúscula, se le volverá a pedir otro carácter. Calculad la minúscula correspondiente e imprimidla en pantalla. No pueden usarse las funciones tolower ni toupperde la biblioteca cctype. Si se quiere, se puede usar como base el proyecto que resolvió el ejercicio 14 de la relación de problemas I. Finalidad: Trabajar con bucles con condiciones compuestas. Dificultad Baja. 15. Un número entero de n dígitos se dice que es narcisista si se puede obtener como la suma de las potencias n-ésimas de cada uno de sus dígitos. Por ejemplo 153 y 8208 son números narcisistas porque 153 = 13 + 53 + 33 y 8208 = 84 + 24 + 04 + 84 . Construir un programa que, dado un número entero positivo, nos indique si el número es o no narcisista. Si se va a usar la función pow, no pueden ser ambos argumentos enteros, por lo que forzaremos a que la base (por ejemplo) sea double, multiplicando por 1.0. Finalidad: Ejercitar los bucles. Dificultad Media. 16. Calcular mediante un programa en C++ la función potencia xn , y la función factorial n! con n un valor entero y x un valor real. No pueden usarse las funciones de la biblioteca cmath. El factorial de un entero n se define de la forma siguiente: 0! = 1 n! = 1 × 2 × 3 × · · · n, ∀n ≥ 1 Finalidad: Trabajar con bucles controlados por contador. Dificultad Baja. n 17. Calcular mediante un programa en C++ el combinatorio con n, m valores m enteros. No pueden usarse las funciones de la biblioteca cmath. El combinatorio de n sobre m (con n ≥ m) es un número entero que se define como sigue: n! n = m m! (n − m)!
Guión de Prácticas. Fundamentos de Programación
44
RELACIÓN DE PROBLEMAS II. Estructuras de Control
Finalidad: Trabajar con bucles controlados por contador. Dificultad Media. 18. Todo lo que se puede hacer con un bucle while se puede hacer con un do while. Lo mismo ocurre al revés. Sin embargo, cada bucle se usa de forma natural en ciertas situaciones. El no hacerlo, nos obligará a escribir más código y éste será más difícil de entender. Para comprobarlo, haced lo siguiente: a) Modificad la solución del ejercicio 10 de forma que el filtro de entrada usado para leer la variable tope, se haga con un bucle pre-test while. b) Modificad la solución del ejercicio 12 sustituyendo el bucle while por un do while. Observad que debemos considerar el caso en el que el número de años leído fuese cero. Finalidad: Enfatizar la necesidad de saber elegir entre un bucle pre-test o un bucle post-test. Dificultad Media. 19. Una empresa que tiene tres sucursales decide llevar la contabilidad de las ventas de sus productos a lo largo de una semana. Para ello registra cada venta con tres números, el identificador de la sucursal (1,2 ó 3), el código del producto (1, 2 ó 3) y el número de unidades vendidas. Diseñar un programa que lea desde el teclado una serie de registros compuestos por sucursal, producto, unidades y diga cuál es la sucursal que más productos ha vendido. La serie de datos termina cuando la sucursal introducida vale -1. Por ejemplo, con la serie de datos
1 2 1 2 1 1 1 1 1 3 2 2 2 2 2 1 3 3 -1
10 4 1 1 2 15 6 20 40
Se puede ver que la sucursal que más productos ha vendido es la número 2 con 41 unidades totales. La salida del programa deberá seguir exactamente el siguiente formato:
SUCURSAL: 2 VENTAS: 41 Para comprobar que el programa funciona correctamente, cread un fichero de texto y re-dirigid la entrada a dicho fichero.
Guión de Prácticas. Fundamentos de Programación
45
RELACIÓN DE PROBLEMAS II. Estructuras de Control
Finalidad: Ver un bucle en el que se leen varios datos en cada iteración, pero sólo uno de ellos se usa como terminador de la entrada. Dificultad Media. 20. Supongamos una serie numérica cuyo término general es: ai = a1 r i−1 Cread un programa que lea desde teclado r, el primer elemento a1 y el tope k y calcule la suma de los primeros k valores de la serie, es decir: i=k X
ai
i=1
Realizad la suma de la serie usando la función pow para el cómputo de cada término ai . Los argumentos de pow no pueden ser ambos enteros, por lo que forzaremos a que la base (por ejemplo) sea double, multiplicando por 1.0. Finalidad: Trabajar con bucles que van acumulando resultados en cada iteración. Dificultad Baja. 21. La serie del ejercicio 20 es una progresión geométrica ya que cada término de la serie queda definido por la siguiente expresión: ai+1 = ai ∗ r Es decir, una progresión geométrica es una secuencia de elementos en la que cada uno de ellos se obtiene multiplicando el anterior por una constante denominada razón o factor de la progresión. Cread un programa que lea desde teclado la razón r, el primer elemento a1 y el tope k y calcule la suma de los primeros k valores de la progresión geométrica. En la solución de este ejercicio NO puede utilizarse la función pow. ¿Qué solución es preferible en términos de eficiencia? ¿ésta o la del ejercicio 20? Finalidad: Trabajar con bucles que aprovechan cómputos realizados en la iteración anterior. Dificultad Baja. 22. Reescribid la solución a los ejercicios 10, 12 usando un bucle for Finalidad: Familiarizarnos con la sintaxis de los bucles for. Dificultad Baja. 23. Diseñar un programa para calcular la suma de los 100 primeros términos de la sucesión siguiente: (−1)i (i2 − 1) ai = 2i No puede usarse la función pow. Hacedlo calculando explícitamente, en cada iteración, el valor (−1)i (usad un bucle for). Posteriormente, resolvedlo calculando dicho valor a partir del calculado en la iteración anterior, es decir, (−1)i−1 .
Guión de Prácticas. Fundamentos de Programación
46
RELACIÓN DE PROBLEMAS II. Estructuras de Control
Finalidad: Enfatizar la conveniencia de aprovechar cómputos realizados en la iteración anterior. Dificultad Media. 24. El método RLE (Run Length Encoding) codifica una secuencia de datos formada por series de valores idénticos consecutivos como una secuencia de parejas de números (valor de la secuencia y número de veces que se repite). Esta codificación es un mecanismo de compresión de datos (zip) sin pérdidas. Se aplica, por ejemplo, para comprimir los ficheros de imágenes en las que hay zonas con los mismos datos (fondo blanco, por ejemplo). Realizar un programa que lea una secuencia de números naturales terminada con un número negativo y la codifique mediante el método RLE. Leed los datos desde un fichero externo, tal y como se explica en la página 60. Entrada: 1 1 1 2 2 2 2 2 3 3 3 3 3 3 5 -1 (tres veces 1, cinco veces 2, seis veces 3, una vez 5) Salida: 31526315 Finalidad: Controlar en una iteración lo que ha pasado en la anterior. Dificultad Media. 25. Sobre la solución del ejercicio 12 de esta relación de problemas, se pide lo siguiente. Supondremos que sólo pueden introducirse intereses enteros (1, 2, 3, etc). Se pide calcular el capital obtenido al término de cada año, pero realizando los cálculos para todos los tipos de interés enteros menores o iguales que el introducido (en pasos de 1). Por ejemplo, si el usuario introduce un interés de 5, y un número de años igual a 3, hay que mostrar el capital ganado al término de cada uno de los tres años a un interés del 1 %; a continuación, lo mismo para un interés del 2 %, y así sucesivamente hasta llegar al 5 %. El programa debe mostrar una salida del tipo:
Cálculos realizados al 1%: Dinero obtenido en el año número 1 = 2020 Dinero obtenido en el año número 2 = 2040.2 Dinero obtenido en el año número 3 = 2060.6 Cálculos realizados al 2%: Dinero obtenido en el año número 1 = 2040 Dinero obtenido en el año número 2 = 2080.8 Dinero obtenido en el año número 3 = 2122.42 ....... Finalidad: Empezar a trabajar con bucles anidados. Dificultad Baja. 26. Cread un programa que ofrezca en pantalla la siguiente salida:
Guión de Prácticas. Fundamentos de Programación
47
RELACIÓN DE PROBLEMAS II. Estructuras de Control
1 2 3 4 5 6
2 3 4 5 6
3 4 5 6 4 5 6 5 6 6
Finalidad: Ejercitar los bucles anidados. Dificultad Baja. 27. Cread un programa que ofrezca en pantalla la siguiente salida:
1 2 3 4 5 6
2 3 4 5 6 7
3 4 5 6 7 8
4 5 6 7 8 9
5 6 6 7 7 8 8 9 9 10 10 11
Finalidad: Ejercitar los bucles anidados. Dificultad Media. 28. Modificad los dos ejercicios anteriores para que los valores inicial (1) y final (6) puedan ser cualesquiera introducidos desde el teclado. Finalidad: Ejercitar los bucles anidados. Dificultad Media. 29. Se dice que un número natural es feliz si cumple que si sumamos los cuadrados de sus dígitos y seguimos el proceso con los resultados obtenidos, finalmente obtenemos uno (1) como resultado. Por ejemplo, el número 203 es un número feliz ya que 22 + 02 + 32 = 13 → 12 + 32 = 10 → 12 + 02 = 1. Se dice que un número es feliz de grado k si se ha podido demostrar que es feliz en un máximo de k iteraciones. Se entiende que una iteración se produce cada vez que se elevan al cuadrado los dígitos del valor actual y se suman. En el ejemplo anterior, 203 es un número feliz de grado 3 (además, es feliz de cualquier grado mayor o igual que 3) Escribir un programa que diga si un número natural n es feliz para un grado k dado de antemano. Tanto n como k son valores introducidos por el usuario. Finalidad: Ejercitar los bucles anidados. Dificultad Media. 30. Escribir un programa que encuentre dos enteros n y m mayores que 1 que verifiquen lo siguiente: m X i2 = n2 i=1
Finalidad: Ejercitar los bucles anidados. Dificultad Media.
Guión de Prácticas. Fundamentos de Programación
48
RELACIÓN DE PROBLEMAS II. Estructuras de Control
Problemas de Ampliación 31. Cread un programa que acepte el número de un año e indique si es bisiesto o no. Un año es bisiesto si es múltiplo de cuatro, pero no de cien. Excepción a la regla anterior son los múltiplos de cuatrocientos que siempre son bisiestos. Son bisiestos: 1600,1996, 2000, 2004. No son bisiestos: 1700, 1800, 1900, 1998, 2002. Dificultad Baja. 32. Realizar un programa para calcular los valores de la función: s 3x + x2 f (x) = 1 − x2 para valores de x enteros en el rango [-3..3]. Dificultad Baja. 33. Realizar un programa para calcular los valores de la función: √ x f (x, y) = 2 y −1 para los valores de (x, y) con x = −50, −48, . . . , 48, 50 y = −40, −39, . . . , 39, 40, es decir queremos mostrar en pantalla los valores de la función en los puntos (−50, 40), (−50, −39), · · · (−50, 40), (−48, 40), (−48, −39), · · · (50, 40) Dificultad Baja. 34. Diseñar un programa que presente una tabla de grados C a grados Fahrenheit ( F=9/5C+32) desde los 0 grados a los 300, con incremento de 20 en 20 grados. Dificultad Baja. 35. Diseñar un programa que lea caracteres desde la entrada y los muestre en pantalla, hasta que se pulsa el '.' y diga cuantos separadores se han leído (espacios en blanco ' ', tabuladores '\t' y caracteres de nueva línea '\n'). Dificultad Baja. 36. Realizar un programa para calcular la suma de los términos de la serie 1 − 1/2 + 1/4 − 1/6 + 1/8 − 1/10 + ... − 1/(2n − 1) + 1/(2n) para un valor n dado. Dificultad Baja.
Guión de Prácticas. Fundamentos de Programación
49
RELACIÓN DE PROBLEMAS II. Estructuras de Control
37. Se decide informatizar el acta de un partido de baloncesto para saber qué equipo es el ganador del partido. El acta contiene una serie de anotaciones formadas por una pareja de números cada una, con el dorsal del jugador y el número de puntos conseguidos teniendo en cuenta que la última anotación es un valor -1. Por ejemplo 1 2 4 1 4 1 2 3 6 2 3 2 5 2 5 1 1 3 -1 El programa deberá indicar si ha ganado el equipo 1 (con los dorsales 1, 2 y 3) o el equipo 2 (dorsales 4, 5 y 6) o han empatado. Por ejemplo, con la entrada anterior, gana el equipo 1. Dificultad Baja. 38. La Unión Europea ha decidido premiar al país que más toneladas de hortalizas exporte a lo largo del año. Se dispone de un registro de transacciones comerciales en el que aparecen tres valores en cada apunte. El primer valor es el indicativo del país (E: España, F: Francia y A: Alemania), el segundo valor es un indicativo de la hortaliza que se ha vendido en una transacción (T: Tomate, P: Patata, E: Espinaca) y el tercer valor indica las toneladas que se han vendido en esa transacción. Diseñar un programa que lea desde el teclado este registro, el cual termina siempre al leer un país con indicativo ’@’, y que diga qué país es el que más hortalizas exporta y las toneladas que exporta. Por ejemplo, con la entrada E T 10 E T 4 E P 1 E P 1 E E 2 F T 15 F T 6 F P 20 A E 40 @ el país que más vende es Francia con un total de 41 toneladas. Dificultad Baja. 39. Diseñar un programa para jugar a adivinar un número. El juego tiene que dar pistas de si el número introducido por el jugador está por encima o por debajo del número introducido. Como reglas de parada considerad a) que haya acertado o b) se haya hartado y decida terminar (escoged cómo se quiere que se especifique esta opción) Realizar el mismo ejercicio pero permitiendo jugar tantas veces como lo desee el jugador. Dificultad Media. 40. Se dice que un número es triangular si se puede poner como la suma de los primeros m valores enteros, para algún valor de m. Por ejemplo, 6 es triangular ya que 6 = 1 + 2 + 3. Una forma de obtener los números triangulares es a través de la fórmula n(n + 1) ∀n ∈ N. Se pide construir un programa que obtenga todos los números 2 triangulares que hay menores que un entero k introducido desde teclado, sin aplicar la fórmula anterior. Dificultad Baja.
Guión de Prácticas. Fundamentos de Programación
50
RELACIÓN DE PROBLEMAS II. Estructuras de Control
41. Escribir un programa que lea una secuencia de números enteros en el rango de 0 a 100 terminada en un número mayor que 100 o menor que 0 y encuentre la subsecuencia de números ordenada, de menor a mayor, de mayor longitud. El programa nos debe decir la posición donde comienza la subsecuencia y su longitud. Por ejemplo, ante la entrada siguiente:
23
25 7
40 45
45 73
73
71 4
9 101
el programa nos debe indicar que la mayor subsecuencia empieza en la posición 3 (en el 7) y tiene longitud 6 (termina en la segunda aparición del 73) Dificultad Media. 42. El algoritmo de la multiplicación rusa es una forma distinta de calcular la multiplicación de dos números enteros n ∗ m. Para ello este algoritmo va multiplicando por 2 el multiplicador m y dividiendo (sin decimales) por dos el multiplicando n hasta que n tome el valor 1 y suma todos aquellos multiplicadores cuyos multiplicandos sean impares. Por ejemplo, para multiplicar 37 y 12 se harían las siguientes iteraciones Iteración Multiplicando 1 37 2 18 3 9 4 4 5 2 6 1
Multiplicador 12 24 48 96 192 384
Con lo que el resultado de multiplicar 37 y 12 sería la suma de los multiplicadores correspondientes a los multiplicandos impares (en negrita), es decir 37*12=12+48+384=444 Cread un programa para leer dos enteros n y m y calcule su producto utilizando este algoritmo. Dificultad Media. 43. Construid un programa para comprobar si las letras de una palabra se encuentran dentro de otro conjunto de palabras. Los datos se leen desde un fichero de la forma siguiente: el fichero contiene, en primer lugar un total de 3 letras que forman la palabra a buscar, por ejemplo f e o. Siempre habrá, exactamente, tres letras. A continuación, el fichero contiene el conjunto de palabras en el que vamos a buscar. El final de cada palabra viene determinado por la aparición del carácter '@', y el final del fichero por el carácter '#'. La búsqueda tendrá las siguientes restricciones: • Deben encontrarse las tres letras • Debe respetarse el orden de aparición. Es decir, si por ejemplo encontramos la 'f' en la segunda palabra, la siguiente letra a buscar 'e' debe estar en una palabra posterior a la segunda.
Guión de Prácticas. Fundamentos de Programación
51
RELACIÓN DE PROBLEMAS II. Estructuras de Control • Una vez encontremos una letra en una palabra, ya no buscaremos más letras en dicha palabra. • No nos planteamos una búsqueda barajando todas las posibilidades, en el sentido de que una vez encontrada una letra, no volveremos a buscarla de nuevo.
f h m c c Entrada: p c h #
e o o o e e o o
o l f f r r s y
a e i r a a @
@ t a @ a @ o @ @ @
<- f <- e <- o
En este caso, sí se encuentra. Dificultad Media. 44. Un número perfecto es aquel que es igual a la suma de todos sus divisores positivos excepto él mismo. El primer número perfecto es el 6 ya que sus divisores son 1, 2 y 3 y 6=1+2+3. Escribir un programa que muestre el mayor número perfecto que sea menor a un número dado por el usuario. Dificultad Media. 45. El número aúreo se conoce desde la Antigüedad griega y aparece en muchos temas de la geometría clásica. La forma más sencilla de definirlo es como el único número positivo φ que cumple que φ2 − φ = 1 y por consiguiente su valor es √ 1+ 5 . Se pueden construir aproximaciones al número aúreo mediante la fórφ= 2 f ib(n + 1) mula an = siendo f ib(n) el término n-ésimo de la suceción de fibonacci f ib(n) que se define como: f ib(0) = 0, f ib(1) = 1 y f ib(n) = f ib(n − 2) + f ib(n − 1) para n ≥ 2 Escribir un programa que calcule el menor valor de n que hace que la aproximación dada por an difiera en menos de δ del número φ, sabiendo que n ≥ 1. La entrada del programa será el valor de δ y la salida el valor de n. Por ejemplo, para un valor de δ = 0,1 el valor de salida es n = 3 Dificultad Media.
Guión de Prácticas. Fundamentos de Programación
52
Estructura condicional
Sesión 3 I Actividades a realizar en casa
Estructura condicional Actividad: Resolución de problemas. Resolved los siguientes problemas de la relación II (página 38). Por cada uno de ellos, habrá que crear el correspondiente proyecto. Usad el tipo de proyecto _fp_2010 • Obligatorios: 2 (Subir ingresos a jubilados) 3 (Ver si dos números se dividen) 4 (Quién divide a quién) 5 (Pasar de mayúscula a minúscula) 6 (Pasar de mayúscula a minúscula y viceversa) 7 (Medidas contra la crisis) • Opcionales: 8 (Tarifa aérea)
Actividades de Ampliación Hojear la página
http://catless.ncl.ac.uk/Risks que publica periódicamente casos reales en los que un mal desarrollo del software ha tenido implicaciones importantes en la sociedad.
Guión de Prácticas. Fundamentos de Programación
53
Estructura condicional
I Actividades a realizar en las aulas de ordenadores En esta sesión empezaremos a usar las estructuras condicionales. En primer lugar, copiamos localmente el proyecto II_Media disponible en la carpeta ProblemasII de la unidad H: de la asignatura, que contiene la solución al ejercicio 1 de la relación de problemas II. Sobre este proyecto, probaremos las nuevas herramientas que vamos a ver a continuación. Es muy importante poner atención a la tabulación correcta de las sentencias, tal y como se indica en las transparencias. Recordad que una tabulación incorrecta supondrá bajar puntos en la primera prueba práctica que se realizará dentro de algunas semanas. Visual Studio ofrece algunas herramientas para agilizar la correcta tabulación: • VS tabula automáticamente el código si así se especifica en:
Herramientas -> Opciones -> Editor de texto -> C/C++ -> Tabulaciones -> Automático El fichero de configuración dicha opción seleccionada.
.settings usado en las aulas de prácticas viene con
• De forma semi-automática, podemos seleccionar un bloque de texto con el ratón y pulsar Ctr-K,Ctr-F. • De forma manual, podemos seleccionar un bloque de texto con el ratón y pulsar sopara disminuir o aumentar la sangría. Las teclas de bre alguno de los iconos acceso directo para estas operaciones son Shift-Tabulador y Tabulador respectivamente. Visual Studio ofrece también un recurso bastante útil para la escritura de código. Cuando nuestros programas contienen muchas líneas de código, nos gustaría ocultar las partes que se han comprobado que funcionan correctamente. Para ello, basta seleccionar con el ratón un bloque de texto y seleccionar con la derecha
Esquematización -> Ocultar selección De esta forma conseguimos que en el margen izquierdo del editor de texto aparezca un símbolo +. Si pinchamos sobre él, se despliega mostrando el código y si pulsamos otra vez, se oculta dicha selección. Hay que destacar que el código que estamos ocultando sigue siendo código del programa, por lo que se compilará y ejecutará como cualquier otro código. Dicho de otra forma, por el hecho de ocultar de esta forma un trozo de código, no lo estamos incluyendo como un comentario.
Guión de Prácticas. Fundamentos de Programación
54
Depuración
Forzad los siguientes errores en tiempo de compilación, para habituarnos a los mensajes de error ofrecidos por el compilador: • Suprimid los paréntesis de alguna de las expresiones lógicas de la sentencia if • Quitad la llave abierta de la sentencia condicional (como únicamente hay una sentencia dentro del if no es necesario poner las llaves, pero añadimos las llaves a cualquier condicional para comprobar el error que se produce al eliminar una de ellas) • Quitad la llave cerrada de la sentencia condicional
Depuración " If debugging is the process of removing bugs, then programming must be the process of putting them in. Edsger Dijkstra (1930/2002) ".
Un depurador de programas (debugger en inglés) permite ir ejecutando un programa sentencia a sentencia (ejecución paso a paso). Además, nos permite ver en cualquier momento el valor de las variables usadas por el programa. El uso de un depurador facilita la localización de errores lógicos en nuestros programas, que de otra forma resultarían bastante difíciles de localizar directamente en el código. "Debuggers don’t remove bugs. They only show them in slow motion".
Cargad el proyecto II_Media. Una vez abierto lo compilamos para que se genere la información de depuración. Dicha información se puede almacenar en distintos sitios. En la plantilla de _fp_2010 se ha seleccionado que sea sobre un fichero con extensión .pdb. Nota. Para más información, id al Explorador de Soluciones, pinchad sobre el proyecto con el click derecho y seleccionad ayuda con F1 sobre:
Proyecto -> Propiedades -> Propiedades de configuración -> C/C++ -> General -> Formato de la información de depuración Si en la anterior opción se eligiese Deshabilitado, no se generaría ninguna información de depuración. La idea básica en la depuración es ir ejecutando el código línea a línea para ver posibles fallos del programa. Para eso, debemos dar los siguientes pasos: 1. Establecer una línea del programa en la que queremos que se pare la ejecución. Lo haremos introduciendo un punto de ruptura o (breakpoint) en dicha línea. Si sospechamos dónde puede estar el error, situaremos el punto de ruptura en dicha línea. En caso contrario, lo situaremos al principio del programa.
Guión de Prácticas. Fundamentos de Programación
55
Depuración
Para establecer un punto de ruptura podemos mover el ratón en la parte más a la izquierda de una línea de código, y pulsar el botón izquierdo en esa posición. La instrucción correspondiente queda marcada en rojo. Si en esa línea ya había un punto de ruptura, entonces será eliminado. También podemos colocar el cursor sobre la instrucción y pulsar F9 . Para eliminar un punto de ruptura, se realiza la misma operación que para incluirlo, sobre la instrucción que actualmente lo tiene. Colocad ahora un punto de ruptura sobre la linea que contiene la primera sentencia condicional if. 2. A continuación le pediremos al depurador que empiece a ejecutar nuestro programa pulsando como siempre F5 . Esto provocará que se ejecuten todas las sentencias que hay justo antes del punto de ruptura. Llegado a este punto, la ejecución se pausa y se muestra en amarillo aquella línea que se va a ejecutar a continuación (en este caso, la que contiene el punto de interrupción) 3. Una vez que tenemos la línea amarilla sobre una instrucción, para proceder a su ejecución, pulsamos F10 . Sólo se ejecuta una sentencia (línea) y se marca en amarillo la siguiente línea, en espera a que volvamos a pulsar F10. Desde la barra de herramientas, podemos pinchar sobre el icono Ejecutad varias líneas de programa pulsando F10. Cuando se ejecute una sentencia cin, habrá que irse a la ventana de la consola e introducir el valor pedido, y una vez hecho, volver a Visual Studio para proseguir la ejecución con F10. 4. Para proseguir la ejecución del programa sin ir paso a paso, basta con pulsar de nuevo F5. Para detener la ejecución (y por tanto también la depuración), seleccionaríamos Shift-F5 , o bien el cuadrado de la barra de herramientas
.
Importante. No debe abortarse la ejecución del programa, cerrando la ventana de la consola (a través de Ctr-C o haciendo click sobre la aspa de la ventana). Esta forma de parar la ejecución es brusca y el compilador podría no liberar adecuadamente algunos recursos utilizados del sistema operativo. 5. Cuando lleguemos a la última línea del programa, la que contiene la llave cerrada del main, al pulsar F10, se abrirá una pestaña nueva sobre un fichero denominado crt0.c. Este fichero es utilizado internamente por Visual Studio como punto de entrada de la aplicación. Para evitar que aparezca este fichero, cuando lleguemos a la última línea del programa, pulsaremos F5 en vez de F10.
Guión de Prácticas. Fundamentos de Programación
56
Depuración
Otros recursos: Este comando, accesible también con Ctr-F10 , ejecuta todas las instrucciones hasta donde se encuentre actualmente el cursor de la ventana de edición. Al llegar a él, se marca en amarillo dicha línea y se queda en espera a que iniciemos una ejecución paso a paso, o a que prosigamos la ejecución ininterrumpida con F5. Se utiliza para saltar determinadas sentencias que no queremos revisar. • Si pulsamos Ctr-F5 en vez de F5, el programa se ejecutará sin procesar la información de depuración, por lo que, por ejemplo, no se parará en ningún punto de interrupción. Además, al ejecutar el programa de este forma, también saldrá un mensaje al final con la leyenda Pulse cualquier tecla para continuar. Este mensaje se añadirá al que nosotros ya habíamos puesto con system("pause"). • Podemos iniciar la ejecución paso a paso desde el principio, sin poner ningún punto de interrupción, pulsando directamente F10. Esto hará que la primera instrucción destacada en amarillo sea la correspondiente a main. Los depuradores de programas permiten consultar los valores que tienen en cada momento algunas de las variables que se utilizan en el programa, para así intentar descubrir dónde puede estar el posible error lógico del programa. EN PRIMER LUGAR, debemos iniciar la depuración introduciendo un punto de interrupción y pulsando F5 (o empezando desde el principio con F10). Automáticamente aparecen las ventanas siguientes: •
Automático: Contiene una lista de las variables involucradas en la sentencia que se va a ejecutar, así como las involucradas en la sentencia anterior
•
Locales: Contiene una lista de las variables definidas en el bloque actual. Por ahora, las que aparecen son las que están definidas dentro de main.
•
Inspección: Contiene variables que explícitamente ha incluido el programador. Ya veremos en temas posteriores en qué situaciones puede resultar útil.
Los valores mostrados para cada variable en las anteriores ventanas, son el valor actual para cada una de ellas. Por valor actual entendemos el valor que tiene dicha variable, antes de que se ejecute la línea marcada en amarillo. El valor de cada variable se irá actualizando de forma automática conforme el programa siga su ejecución. Además, si durante la ejecución interrumpida situamos el cursor sobre alguna variable del código escrito .cpp, aparecerá automáticamente una leyenda informativa con el valor actual de dicha variable. También podemos seleccionar una expresión con el ratón (como cualquier selección de texto) y se mostrará la evaluación de dicha expresión.
Guión de Prácticas. Fundamentos de Programación
57
Depuración
Observación final: El depurador ayuda a encontrar errores al permitir ejecutar las sentencias paso a paso y así comprobar por donde va el flujo de control y ver cómo van cambiando las variables. En cualquier caso, nunca nos cansaremos de repetir que el mejor programador es el que piensa la solución en papel, antes de escribir una sola línea de código en el entorno de programación. "When your code does not behave as expected, do not use the debugger, think".
Guión de Prácticas. Fundamentos de Programación
58
Estructura Repetitiva. Bucles while
Sesión 4 Estructura Repetitiva. Bucles while I Actividades a realizar en casa Actividad: Resolución de problemas. Resolved los siguientes problemas de la relación II. Por cada uno de ellos, habrá que crear el correspondiente proyecto. Usad el tipo de proyecto _fp_2010 Importante: En estos ejercicio se permiten mezclar E/S con cómputos dentro del mismo bucle (ya que todavía no se conocen herramientas para no hacerlo) • Obligatorios: 10 (Divisores de un número) 11 (El mínimo de varios reales leídos desde teclado) 12 (Reinvertir capital e interés un número de años) 13 (Reinvertir capital e interés hasta doblar cantidad inicial) 20 y 21 (Progresión geométrica) • Opcionales: 14 (Filtro entrada mayúscula) 16 (Factorial y potencia) 17 (Combinatorio) 18 (While versus do while)
Guión de Prácticas. Fundamentos de Programación
59
Redireccionando las entradas de cin
I Actividades a realizar en las aulas de ordenadores
Redireccionando las entradas de cin Cada vez que se ejecuta cin, se lee un dato desde el periférico por defecto. Nosotros lo hemos hecho desde el teclado, introduciendo un dato y a continuación un ENTER. Otra forma alternativa es introducir un dato y luego un separador (uno o varios espacios en blanco o un tabulador). Para comprobarlo, copiad localmente la carpeta ProblemasII\II_cin que se encuentra en el directorio de la asignatura. Observad el código del programa y ejecutadlo desde Visual Studio. Para introducir los datos pedidos (un entero y dos caracteres) separadlos por espacios en blanco. Pulsad ENTER al final (una sola vez). Los datos anteriores se pueden poner dentro de un fichero de texto. Dentro del fichero, cada dato hay que separarlo por uno o varios espacios en blanco o tabuladores. Para poder leer los datos del fichero, basta con ejecutar el programa .exe desde una consola del sistema y especificar que la entrada de datos será desde un fichero a través del símbolo de redireccionamiento < (no ha de confundirse con el token << que aparecía en una instrucción cout de C++) Hay que destacar que este redireccionamiento de la entrada lo estamos haciendo en la llamada al ejecutable desde la consola del sistema operativo2 . Para probarlo, seleccionad el fichero II_cin_datos_entrada.txt que se encuentra dentro de la carpeta II_cin y copiadlo dentro de Mis Documentos. Copiad también dentro de Mis Documentos, el fichero ejecutable II_cin.exe que se encuentra en la carpeta II_cin\Debug Abrimos MisDocumentos desde el sistema operativo y seleccionamos con el click derecha del ratón "Abrir Símbolo del Sistema". Introducimos la instrucción siguiente (ver Figura 6):
II_cin.exe < II_cin_datos_entrada.txt Ahora, cada vez que se ejecute una instrucción cin en el programa, se leerá un valor de los presentes en el fichero de texto. Visual Studio también permite redireccionar la entrada de datos desde el propio entorno. Esto resulta muy útil cuando se desea leer datos de un fichero y a la misma vez depurar el programa (introducir puntos de interrupción, ejecución paso a paso, evaluación de variables, etc). Para hacerlo, seleccionad
Proyecto -> Propiedades de .... Propiedades de Configuración -> Depuración Argumentos del comando -> Editar -> 2
También pueden leerse datos de un fichero desde dentro del propio código fuente del programa, pero esto se verá en el segundo cuatrimestre
Guión de Prácticas. Fundamentos de Programación
60
Redireccionando las entradas de cin
Figura 6: Redirección de la entrada desde la consola En el cuadro de texto que aparece al seleccionar Editar, introduciremos el símbolo < seguido del nombre del fichero de texto que contiene los datos(ver Figura 7). Con el anterior ejemplo sería:
< II_cin_datos_entrada.txt Por defecto, el entorno busca el fichero de entrada de datos en la misma carpeta que el proyecto. Si se quiere, puede indicarse otra carpeta distinta, a través de la opción siguiente:
Proyecto/Propiedades Propiedades de Configuración/Depuración Directorio de trabajo/ Cuando ejecutemos el programa, cada ejecución de cin leerá un dato desde el fichero indicado, saltándose previamente todos los espacios en blanco y tabuladores que hubiese previamente. Cuando llegue al final del fichero, la lectura de un valor cargaría el carácter fin de fichero. Cuando llega al final del programa y se ejecuta la instrucción system("pause") el programa se detiene y lee una tecla del dispositivo por defecto, que en este caso es el fichero. Lee la marca de fin de fichero como tecla válida y por tanto no hace la pausa que estábamos esperando. Para solucionarlo, basta con que introduzcamos un punto de ruptura al final del código fuente de nuestro programa. Nota. En nuestra casa, o bien instalamos un programa que permita abrir una consola en el directorio actual, como por ejemplo Open Command Prompt Shell Extension disponible en http://code.kliu.org/cmdopen/ o bien abrimos un símbolo del sistema (Inicio->Ejecutar->cmd) y vamos cambiando de directorio con la orden cd Nota. CodeBlocks también permite, en teoría, la ejecución paso a paso cuando los datos se leen desde un fichero (desde la opción Project-Set program’s arguments). Sin embargo, en la última versión (10.5) esta opción no funciona correctamente bajo Windows.
Guión de Prácticas. Fundamentos de Programación
61
Redireccionando las entradas de cin
Figura 7: Redirecciรณn de la entrada desde el entorno
Guiรณn de Prรกcticas. Fundamentos de Programaciรณn
62
Redireccionando las entradas de cin
Ejercicio. Construid un programa para que vaya leyendo números desde la entrada por defecto y los vaya sumando, hasta que se lea el valor -1. Imprimid el resultado. Realizad la entrada desde un fichero externo que contendrá la serie de valores. Utilizad las herramientas de depuración vistas en la sesión anterior para inspeccionar el valor de las variables usadas en cada iteración del bucle.
Guión de Prácticas. Fundamentos de Programación
63
Estructura Repetitiva: bucles for y bucles anidados
Sesión 5 Estructura Repetitiva: bucles for y bucles anidados I Actividades a realizar en casa Actividad: Resolución de problemas. Resolved los siguientes problemas de la relación II. Por cada uno de ellos, habrá que crear el correspondiente proyecto. Usad el tipo de proyecto _fp_2010. Importante: En estos ejercicio se permiten mezclar E/S con cómputos dentro del mismo bucle (ya que todavía no se conocen herramientas para no hacerlo) Durante la primera media hora de la clase, el alumno debe realizar las actividades indicadas en "Actividades a realizar en las aulas de ordenadores". Durante la hora y media posterior, los alumnos saldrán a explicar las soluciones de los ejercicios. • Obligatorios: 22 (Divisores e interés usando un for) 24 (Método RLE) 15 (Número narcisista) 25 (Interés con anidamiento) 26, 27, 28 (Pintar pirámide y cuadrado) • Opcionales: 21 (Progresión geométrica usando un for) 23 (Serie) 30 (Encontrar enteros)
Guión de Prácticas. Fundamentos de Programación
64
Estructura Repetitiva: bucles for y bucles anidados
I Actividades a realizar en las aulas de ordenadores Entrad en la web del departamento y pinchad sobre el último icono que aparece de acceso a la plataforma GAP. Esta es la plataforma que se usará para subir el fichero cpp el día del examen. Una vez accedamos a ella, estaremos automáticamente dados de alta. Si se quiere, se puede cambiar la contraseña de acceso para que sea distinta a la del departamento, aunque no se recomienda que se haga. Ejercicio. Objetivo: Enfatizar la importancia del correcto diseño de una estructura condicional anidada en un bucle Copiad la carpeta ProblemasII\II_Divisores que se encuentra en el directorio de la asignatura. Cambiad el bloque de código
while (divisor <= tope){ if (tope % divisor == 0) cout << divisor << " es un divisor de " << tope << endl; divisor++; } por el siguiente:
while (divisor <= tope){ if (tope % divisor == 0) cout << divisor << " es un divisor de " << tope << endl; else divisor++; } ¿Qué error se produce? Moraleja: Mucho cuidado con las sentencias condicionales dentro de los bucles: debemos asegurarnos de cambiar apropiadamente las variables involucradas en la condición del bucle, para que, en algún momento, dicha condición sea false Como programadores noveles en C++, podemos confundir el operador = con el operador ==. El resultado puede ser un error lógico en nuestro programa, aunque afortunadamente, puede ser detectado en tiempo de compilación. Vamos a familiarizarnos con el aviso del compilador cuando usamos una sentencia de asignación en una condición. Sobre el ejemplo de los divisores, cambiad la línea en la que aparece la sentencia condicional
if (tope % divisor == 0){ .... por las siguientes
Guión de Prácticas. Fundamentos de Programación
65
Estructura Repetitiva: bucles for y bucles anidados
modulo = tope % divisor if (modulo = 0){ ....
// <- Usamos = en vez de ==
dónde modulo es una variable entera que habrá que declarar previamente. Compilad y observad el aviso que nos da Visual Studio. ¿Qué error lógico se produce y por qué? Para contestar esta pregunta hay que repasar las transparencias de teoría en las que se dice que una asignación es una expresión y se habla sobre la dualidad entre el tipo bool y el tipo
int
Guión de Prácticas. Fundamentos de Programación
66
RELACIÓN DE PROBLEMAS III. Funciones y Clases
RELACIÓN DE PROBLEMAS III. Funciones y Clases Problemas Básicos Problemas sobre funciones 1. Cread una función que calcule el máximo entre tres ción será la siguiente:
double. La cabecera de la fun-
double Max(double un_valor, double otro_valor, double el_tercero) Construid un programa principal que llame a dicha función con los parámetros actuales que se quiera. Finalidad: Familiarizarnos con la definición de funciones, el paso de parámetros y el ámbito de las variables. Dificultad Baja. 2. Cread una función para calcular la suma de una progresión geométrica, tal y como se vio en el ejercicio 21 de la relación de problemas II. Analizad cuáles deben ser los parámetros de dicha función. Finalidad: Diseño de una función sencilla. Dificultad Baja. 3. En el ejercicio 8 de la relación de problemas II (sobre los descuentos a aplicar a una tarifa aérea), se repetía en varias ocasiones un código muy parecido, a saber:
rebaja = rebaja + tarifa_con_km_adicionales * DESCUENTO_CLIENTE_PREVIO/100.0; rebaja = rebaja + tarifa_con_km_adicionales * DESCUENTO_TRAYECTO_MEDIO/100.0; ........... Construid la función apropiada para que no se repita código y re-escribid la solución llamando a dicha función en todos los sitios donde sea necesario. Finalidad: Diseño de una función sencilla. Dificultad Baja. 4. Se pide definir las siguientes funciones y cread un programa principal de ejemplo que las llame: a)
EsMinuscula comprueba si un carácter pasado como parámetro es una minúscula. A dicho parámetro, llamadlo una_letra.
Guión de Prácticas. Fundamentos de Programación
67
RELACIÓN DE PROBLEMAS III. Funciones y Clases
b)
Convierte_a_Mayuscula comprueba si un carácter pasado como parámetro es minúscula (para ello, debe llamar a la función anterior), en cuyo caso lo transforma a mayúscula. Si el carácter no es minúscula debe dejar la letra igual. A dicho parámetro, llamadlo caracter. Esta función hace lo mismo que la función tolower de la biblioteca cctype
Observad que el parámetro una_letra de la función EsMinuscula podría llamarse igual que el parámetro caracter de la otra función. Esto es porque están en ámbitos distintos y para el compilador son dos variables distintas. Haced el cambio y comprobarlo. Finalidad: Entender el ámbito de los parámetros de las funciones, así como las llamadas entre funciones. Dificultad Baja. 5. En el ejercicio 5 de la relación de problemas I (página 14) se vio cómo obtener el valor de ordenada asignado por la función gaussiana, sabiendo el valor de abscisa x. Recordemos que esta función matemática dependía de dos parámetros µ (esperanza) y σ (desviación), y venía definida por: 2 x − µ 1 −2 1 σ e gaussiana(x) = √ σ 2π Cread una función para calcular el valor de la función gaussiana en un punto arbitrario leído desde teclado. Cread un programa principal que llame a la función. Finalidad: Entender la separación de E/S con Cómputos en el contexto de las funciones. Dificultad Baja. 6. Vamos a añadir una función al ejercicio 4 de esta relación de problemas. Se pide implementar la función Transforma, a la que se le pase como parámetro un char y haga lo siguiente: • si el argumento es una letra en mayúscula, devuelve su correspondiente letra en minúscula, • si el argumento es una letra en minúscula, devuelve su correspondiente letra en mayúscula, • si el argumento no es ni una letra mayúscula, ni una letra mayúscula, devuelve el carácter pasado como argumento. Observad que la constante de amplitud
const int AMPLITUD = 'a'-'A'; es necesaria declararla como constante local en ambas funciones. Para no repetir este código, ¿qué podemos hacer? Finalidad: Entender cómo se llaman las funciones entre sí. Dificultad Baja.
Guión de Prácticas. Fundamentos de Programación
68
RELACIÓN DE PROBLEMAS III. Funciones y Clases
Problemas sobre clases 7. Se pide crear una clase Conjunto_3_Valores que represente un conjunto de 3 valores de tipo double, sobre los que se quiere realizar las operaciones de calcular el máximo y la media aritmética. Para ello, haced lo siguiente: a) Definid en primer lugar la clase con tres datos miembro públicos llamados dos y tres. A esta primera versión de la clase, dadle el nombre
uno,
Conjunto_3_Valores_vs1 Añadid un método público Max. Este método tendrá la siguiente cabecera: double Max() Observad que el método Max actúa sobre los datos de la clase (uno, dos y tres) por lo que no tiene parámetros. Observad la diferencia con la cabecera del ejercicio 1 en el que Max era una función que no estaba dentro de ninguna clase.
b) Añadid ahora un método para calcular la media aritmética de los tres valores. c) Cread un main que llame a los métodos anteriores. Comprobad que podemos modificar directamente los datos miembro públicos desde el main, lo cual no es muy recomendable como ya se ha visto en clase de teoría. Para resolver este problema, hacemos lo siguiente: d) Copiad el código de la clase anterior y copiadlo dentro de una nueva clase llamada
Conjunto_3_Valores_vs2
En esta nueva versión, declarad los datos miembro como private, en vez de public. Comprobad que ya no podemos acceder a ellos desde el main, por lo que necesitamos asignarles un valor de otra forma. Para ello, haced lo siguiente: Añadid un constructor a la clase Conjunto_3_Valores_vs2. En dicho constructor, pasaremos los tres valores que habrá que asignárselos a los datos miembro (usad la lista de inicialización, que va precedida de dos puntos). Cread el objeto correspondiente desde el main (el compilador nos obligará a pasar los tres valores en el constructor) II ) Añadid tres métodos (GetPrimero(), GetSegundo(), GetTercero()) que devuelvan los correspondientes datos miembro, para que así pueda consultarse su valor desde el main. I)
e) Supongamos que creamos un objeto con los valores 2, 5, 7. Si ahora quisiéramos cambiar de conjunto de datos y trabajar con 3, 9, 1, por ejemplo, tendríamos que crear otro objeto distinto. Esto es completamente lícito. En cualquier
Guión de Prácticas. Fundamentos de Programación
69
RELACIÓN DE PROBLEMAS III. Funciones y Clases
caso, dependiendo del problema que estemos abordando, podríamos estar interesados en permitir cambiar los datos del objeto (su estado) y así no tener que crear otro objeto distinto. Para ello, debemos proporcionar dicha funcionalidad a la clase. Lo hacemos de tres formas alternativas: • Creamos la clase
Conjunto_3_Valores_vs3
que contendrá el mismo código que Conjunto_3_Valores_vs2 y le añadimos un método SetValores que cambie los tres valores a la vez (como ha cambiado el nombre de la clase, habrá que cambiar el nombre del constructor). Cread un objeto de esta clase desde el main y llamad a sus métodos. En esta solución, asignamos (dentro del constructor) un primer valor a cada uno de los tres datos miembro y posteriormente permitimos su modificación a través del método SetValores. Sólo permitimos la modificación conjunta de los tres datos miembro a la misma vez. • Alternativamente, Creamos la clase
Conjunto_3_Valores_vs4
que contendrá el mismo código que Conjunto_3_Valores_vs3, salvo que suprimimos el constructor de la clase. Modificad convenientemente el main. En esta solución, los tres datos miembro sólo los podemos asignar con el método nuevo SetValores. Sólo permitimos la modificación conjunta de los tres datos miembro a la misma vez. • Alternativamente, creamos la clase
Conjunto_3_Valores_vs5
que contendrá el mismo código que Conjunto_3_Valores_vs4, pero suprimimos el método SetValores, y le añadimos tres métodos nuevos SetPrimero, SetSegundo, SetTercero, para que cambien cada uno de los valores de forma independiente. Cread un objeto de esta clase desde el main y llamad a sus métodos. En esta solución, los tres datos miembro sólo los podemos asignar a través de un método específico para cada uno de ellos. Observad la flexibilidad que me ofrece el concepto de clase. El programador decide cómo quiere que se acceda a los datos miembro: prohibiendo cambiar los datos miembro una vez que se ha construido el objeto (versión 2) o permitiéndolo, bien todos a la vez (versiones 3 y 4) o uno a uno (versión 5). Finalidad: Trabajar con los conceptos básicos de clases. Dificultad Baja. A partir de ahora, todos los ejercicios deben resolverse utilizando únicamente datos miembro privados.
Guión de Prácticas. Fundamentos de Programación
70
RELACIÓN DE PROBLEMAS III. Funciones y Clases 8. Cread una clase llamada ParejaEnteros que represente una pareja de enteros cualesquiera. Añadid un constructor y los métodos que se estimen oportunos para asignarles valores a los enteros y para obtener el valor que éstos tengan. Las operaciones que queremos realizar sobre una pareja son calcular el mayor, el menor, comprobar si se dividen, ver quien es el dividendo y quien el divisor, intercambiarlos y calcular el máximo común divisor. Cread un main que llame a dichos métodos, para comprobar que están bien implementados. ¿Le añadiría a la clase número? ¿Por qué?
ParejaEnteros un método para calcular el factorial de un
Finalidad: Diseñar la interfaz de una clase. Dificultad Baja. 9. Sobre el ejercicio 8 de esta relación de problemas se pide crear un programa principal más completo de la siguiente forma. En el main se leerán dos enteros y a continuación se ofrecerá al usuario un menú con las opciones para realizar cualquiera de las operaciones definidas en la clase ParejaEnteros. También se ofrecerá la posibilidad de imprimir el valor de cada uno de los dos enteros. Una vez ejecutada la operación, se volverá a mostrar el mismo menú, hasta que el usuario decida salir (pulsando la tecla 'X', por ejemplo). Finalidad: Organizar un menú de opciones que se va repitiendo mientras el usuario no decida salir. Dificultad Media. 10. Definid una clase para representar una progresión geométrica, tal y como se definió en el ejercicio 21 de la relación de problemas II. a) Pensad cual debería ser el constructor de la clase y los métodos básicos que la clase debería tener. b) Añadid un método llamado SumaHastaInfinito para que calcule la suma dada por: i=∞ X a1 ai = 1−r i=1 siempre que el valor absoluto de la razón sea menor o igual que 1. c) Añadid un método llamado SumaHasta para que sume los k primeros elementos de la progresión. Para implementar el método SumaHasta, usad el mismo algoritmo (con un bucle for) que se vio como solución del ejercicio 21 de la relación de problemas II. Cread un programa principal que construya la progresión con los valores a1 = 3,5 y r = 4,1 y calcule la suma hasta k = 30. d) Cambiemos ahora la implementación del método SumaHasta. Para ello, en vez de usar un bucle for aplicamos la siguiente fórmula que nos da la sumatoria
Guión de Prácticas. Fundamentos de Programación
71
RELACIÓN DE PROBLEMAS III. Funciones y Clases
aplicando únicamente cinco operaciones: i=k X
i
a1 r = a1
i=1
rk − 1 r−1
Es muy importante remarcar que el programa main no cambia nada. Hemos cambiado la implementación del método SumaHasta y lo hemos podido hacer sin cambiar el main, ya que éste no tenía acceso al código que hay dentro del método. Esto es ocultación de información tal y como se describió en las clases de teoría. Nota. La potenciación (en nuestro ejemplo r k ) es una operación costosa, por lo que hasta podría ser más lenta la versión nueva que la antigua usando un bucle for. Probad distintos valores para ver si hay diferencias significativas. En cualquier caso, lo importante es que mientras no cambiemos la cabecera del método SumaHasta, podemos cambiar su implementación sin tener que cambiar ni una línea de código del main. e) Añadid ahora otro método MultiplicaHasta para que multiplique los k primeros elementos de la progresión, aplicando la fórmula: i=k Y
a1 r i =
p (a1 ak )k
i=1
Finalidad: Enfatizar la importancia de la ocultación de información. Dificultad Baja. 11. Se quiere construir una clase DepositoSimulacion para simular préstamos, ofreciendo la funcionalidad descrita en los ejercicios 12 (reinvierte capital e interés un número de años) y 13 (reinvierte capital e interés hasta obtener el doble de la cantidad inicial) de la relación de problemas II (página 44). Por tanto, la clase debe proporcionar, para un capital y unos intereses dados, métodos para: • calcular el capital que se obtendrá al cabo de un número de años, • calcular el número de años que deben pasar hasta obtener el doble de la cantidad inicial. A la hora de diseñar la clase, tendremos que analizar cuestiones como: a) Si queremos modificar el capital y el interés una vez creado el objeto, b) Si queremos poder modificarlos de forma independiente, c) Si hay alguna restricción a la hora de asignar un valor al capital e interés, d) Si es mejor un método para calcular el número de años hasta obtener el doble de la cantidad inicial, o por el contrario es mejor un método para calcular el número de años hasta obtener una cantidad específica.
Guión de Prácticas. Fundamentos de Programación
72
RELACIÓN DE PROBLEMAS III. Funciones y Clases
Finalidad: Diseñar la interfaz de una clase. Dificultad Baja. 12. Se quiere construir una clase para realizar la funcionalidad descrita en el ejercicio 10 de la relación de problemas I sobre la nómina del fabricante y diseñador (página 16). La clase debe proporcionar, al menos, los métodos SalarioNetoDisenador y SalarioNetoFabricante. El criterio del cálculo del salario es el mismo (el diseñador cobre el doble de cada fabricante) pero además, ahora se le va a aplicar una retención fiscal para el cómputo de los salarios netos. Dicha retención será la misma tanto para el fabricante como para el diseñador. Analizad las siguientes posibilidades y elegid la que crea más adecuada, justificándolo: a) Creamos un dato miembro para almacenar el valor de retención fiscal y pasamos su valor en el constructor. b) Creamos un dato miembro para almacenar su valor y además proporcionamos un método para su modificación. Finalidad: Trabajar con datos miembro constantes. Dificultad Baja. 13. Una empresa quiere gestionar las nóminas de sus empleados. El cómputo de la nómina se realiza en base a los siguientes criterios: a) Hay cuatro tipos de categorías laborales: Operario, Base, Administrativo y Directivo. b) Se parte de un salario base que depende de la antigüedad del trabajador y de su categoría laboral. Para la categoría Operario, el salario base es de 900 euros, 1100 el puesto Base, 1200 los Administrativos y 2000 los Directivos. Dicho salario base se incrementa con un tanto por ciento igual al número de años trabajados. c) Los trabajadores tienen complementos en su nómina por el número de horas extraordinarias trabajadas. La hora se paga distinta según la categoría: 16 euros por hora para los operarios, 23 para el puesto Base, 25 los Administrativos y 30 los Directivos. Además, al complemento que sale al computar el número de horas extraordinarias, se le aplica una subida con un tanto por ciento igual al número de años trabajados. Se pide diseñar la interfaz de una clase (también hay que incluir los datos miembro privados) para poder trabajar con esta información. No se pide implementar la clase, únicamente determinar la interfaz. Finalidad: Diseñar la interfaz de una clase. Dificultad Media. 14. Transformad la solución del ejercicio 9 de esta relación de problemas para que la responsabilidad de la lectura de la opción introducida por el usuario recaiga en una clase a la que llamaremos Menu. Finalidad: Ver una clase específica que gestione la entrada de datos. Dificultad Media.
Guión de Prácticas. Fundamentos de Programación
73
RELACIÓN DE PROBLEMAS III. Funciones y Clases
15. Cread una clase para representar fracciones. El numerador y denominador serán enteros y debe proporcionar un método para normalizar la fracción, es decir, encontrar la fracción irreducible que sea equivalente. Para ello, tendrá que dividir el numerador y denominador por su máximo común divisor. Finalidad: Diseñar la interfaz de una clase. Dificultad Media. 16. En C++, los tipos de datos simples como int, char, double, etc, no son clases. Por tanto, si declaramos una variable real en la forma double real, para calcular el seno de real, llamamos a una función en la forma
double real, resultado; real = 6.5; resultado = sin(real); Estamos interesados en construir una clase llamada MiReal para trabajar con reales al estilo de las clases. Por tanto, para calcular el seno de 6.5, deberíamos hacer algo como lo siguiente:
MiReal real(6.5); double resultado; resultado = real.Seno(); En esta caso, Seno es un método de la clase MiReal que devuelve un tipo double. Construid dicha clase, proporcionando los siguientes métodos:
Valor. Este método debe devolver el valor del real (6.5 en el ejemplo anterior) b) Seno. Para definirlo, llamad a la propia función sin de la biblioteca cmath. Debe devolver un double c) ValorAbsoluto. Devolverá un tipo double que contendrá el valor absoluto del real. No pueden utilizarse las funciones de cmath. a)
Cread un programa principal que llame a dichos métodos. Observad que sería interesante que nuestro código fuese el siguiente:
MiReal real(6.5); MiReal resultado; resultado = real.Seno(); resultado = resultado + real; Es decir, que el método Seno devolviese un tipo MiReal en vez de un tipo double. Aunque esto es posible en C++, por ahora no disponemos de las herramientas necesarias para hacerlo. Por ejemplo, para hacer la asignación entre variables de tipo MiReal se necesita el concepto de sobrecarga del operador de asignación y para
Guión de Prácticas. Fundamentos de Programación
74
RELACIÓN DE PROBLEMAS III. Funciones y Clases realizar la suma de dos variables de tipo MiReal se necesita sobrecargar el operador + (se verá en el segundo cuatrimestre). Nota. Este tipo de clases (MiEntero, MiReal, etc) son proporcionadas por Java de forma standard. En otros lenguajes como SmallTalk o plataformas como .NET (con lenguajes como Visual Basic, C#) no existe la diferencia entre tipos básicos (como int, double, etc) y todo son clases, siguiendo una filosofía orientada a objetos pura. Finalidad: Entender las clases que actúan como envolventes (wrappers) de los tipos básicos. Dificultad Baja.
Guión de Prácticas. Fundamentos de Programación
75
RELACIÓN DE PROBLEMAS III. Funciones y Clases
Problemas de Ampliación 17. Definid funciones aisladas para implementar las soluciones a los siguientes ejercicios de la relación de problemas II: 16 (factorial y combinatorio), 23 (serie), 15 (narcisista), 29 (feliz). Dificultad Baja. 18. Construid la clase MiEntero que será utilizada en la forma siguiente:
MiEntero entero(6); int entero; resultado = entero.Factorial(); Debe proporcionar los siguientes métodos:
Valor. Este método debe devolver el valor del entero (un tipo int). b) Factorial. Usad la solución del ejercicio 16 de la relación de problemas II a)
(página 44). c)
EsNarcisista. Usad la solución del ejercicio 15 de la relación de problemas II (página 44)
Cread un programa principal que llame a dichos métodos. 19. Construid la clase MiCaracter que será utilizada en la forma siguiente:
MiCaracter caracter('a'); char resultado; resultado = entero.GetMayuscula(); Debe proporcionar los siguientes métodos: a) b) c) d) e) f)
Valor. Este método debe devolver el valor del carácter. EsMinuscula. Devuelve un bool. EsMayuscula. Devuelve un bool. EsLetra. Devuelve un bool. GetMayuscula. Devuelve un char correspondiente al carácter convertido a mayúscula. Si el carácter no es minúscula, devuelve el mismo valor del char. GetMinuscula. El recíproco del anterior.
Cread un programa principal que llame a dichos métodos.
Guión de Prácticas. Fundamentos de Programación
76
RELACIÓN DE PROBLEMAS III. Funciones y Clases
20. La sonda Mars Climate Orbiter fue lanzada por la NASA en 1998 y llegó a Marte el 23 de septiembre de 1999. Lamentablemente se estrelló contra el planeta ya que se acercó demasiado. El error principal fue que los equipos que desarrollaron los distintos módulos de la sonda usaron sistemas de medida distintos (el anglosajón y el métrico). Cuando un componente software mandaba unos datos en millas (o libras), otro componente software los interpretaba como si fuesen kilómetros (o Newtons). El problema se habría arreglado si todos hubiesen acordado usar el mismo sistema. En cualquier caso, cada equipo se encuentra más a gusto trabajando en su propio sistema de medida. Por tanto, la solución podría haber pasado por que todos utilizasen una misma clase para representar distancias (idem para fuerzas, presión, etc), utilizando los métodos que les resultasen más cómodos. Para ello, se pide construir la clase Distancia que contendrá métodos como SetKilometros, SetMillas, etc. Internamente se usará un único dato miembro privado llamado kilometros al que se le asignará un valor a través de los métodos anteriores, realizando la conversión oportuna (una milla es 1,609344 kilómetros). La clase también proporcionará métodos como GetKilometros y GetMillas para lo que tendrá que realizar la conversión oportuna (un kilómetro es 0,621371192 millas). Observad que la implementación de la clase podría haber utilizado como dato miembro privado, una variable millas, en vez de kilómetros. Esto se oculta a los usuarios de la clase, que sólo ven los métodos SetKilometros, SetMillas, GetKilometros y GetMillas. Nota. Otro de los fallos del proyecto fue que no se hicieron suficientes pruebas del software antes de su puesta en marcha, lo que podría haber detectado el error. Esto pone de manifiesto la importancia de realizar una batería de pruebas para comprobar el correcto funcionamiento del software en todas las situaciones posibles. Esta parte en el desarrollo de un proyecto se le conoce como pruebas de unidad (unit testing) Finalidad: Trabajar con una clase como una abstracción de un concepto. Dificultad Baja. 21. Se quiere construir una clase para representar la tracción de una bicicleta, es decir, el conjunto de estrella, cadena y piñón. Supondremos que el plato delantero tiene tres estrellas y el trasero tiene 7 piñones. La clase debe proporcionar métodos para cambiar la estrella y el piñón, sabiendo que la estrella avanza o retrocede de 1 en 1, y los piñones cambian a salto de uno o de dos. Una vez hecha la clase y probada con un programa principal, modificadla para que no permita cambiar la marcha (con la estrella o el piñón) cuando haya riesgo de que se rompa la cadena. Este riesgo se produce cuando la marcha a la que queremos cambiar es de la siguiente forma: • Primera estrella y piñón mayor o igual que 5 • Segunda estrella y piñón o bien igual a 1 o bien igual a 7 • Tercera estrella y piñón menor o igual que 4
Guión de Prácticas. Fundamentos de Programación
77
RELACIÓN DE PROBLEMAS III. Funciones y Clases
Finalidad: Diseñar la interfaz de una clase. Trabajar con datos miembro constantes.. Dificultad Media. 22. Construid una clase llamada MedidaAngulo que represente una medida de un ángulo. Al igual que se hizo en el ejercicio 20, la clase aceptará datos que vengan de alguna de las siguientes formas: número de grados con decimales (real); número de radianes (entero); número de segundos (entero); número de grados, minutos y segundos (en un struct que represente estos tres valores) Dificultad Baja. 23. Recuperad el ejercicio 5 de esta relación de problemas sobre la función gaussiana. Ahora estamos interesados en obtener el área que cubre la función en el intervalo [−∞, x]. Dicho valor se conoce como la distribución acumulada (cumulative distribution function) en Zel punto x, abreviado CDF (x). Matemáticamente se calcula x
realizando la integral
gaussiana(x)dx, tal y como puede comprobarse ejecutan−∞
do el applet disponible en:
http://dostat.stat.sc.edu/prototype/calculators/index.php3? dist=Normal También pueden obtenerse un valor aproximado de CDF (x), como por ejemplo, el dado por la siguiente fórmula (ver Wikipedia: Normal distribution) CDF (x) = Área hasta (x) ≈ 1 − gaussiana(x)(b1 t + b2 t2 + b3 t3 + b4 t4 ) dónde: t=
1 1 + b0 x
b0 = 0,2316419 b1 = 0,319381530 b2 = −0,356563782
b3 = 1,781477937 b4 = −1,821255978 b5 = 1,330274429 Para implementarlo, podríamos definir la función siguiente:
double AreaHasta (double esperanza, double desviacion, double x) En vez de trabajar con funciones, planteamos la solución con una clase. La clase se llamará Gaussiana y crearemos objetos pasándole en el constructor la esperanza y la distribución (los parámetros que definen la gaussiana). Le añadiremos métodos para: • Obtener el valor de la función en un punto x cualquiera. • Obtener el área hasta un punto x cualquiera. Dificultad Media.
Guión de Prácticas. Fundamentos de Programación
78
RELACIÓN DE PROBLEMAS III. Funciones y Clases 24. Cread un struct llamado CoordenadasPunto2D para representar un par de valores reales correspondientes a un punto en R2 . Cread ahora una clase llamada Circunferencia. Para establecer el centro, se usará un valor del tipo CoordenadasPunto2D. Añadid métodos para obtener la longitud de la circunferencia y el área del círculo interior. Añadid también un método para saber si la circunferencia contiene a un punto. Recordemos que un punto (x1 , y1 ) está dentro de una circunferencia con centro (x0 , y0 ) y radio r si se verifica que: (x0 − x1 )2 + (y0 − y1 )2 <= r 2 Observad que el valor de π debe ser constante, y el mismo para todos los objetos de la clase Circunferencia. Finalidad: Trabajar con constantes estáticas de tipo double. Dificultad Baja. 25. Implementad la clase del ejercicio 13 de esta relación de problemas. Dificultad Media.
Guión de Prácticas. Fundamentos de Programación
79
Funciones
Sesión 6
Funciones I Actividades a realizar en casa Actividad: Resolución de problemas. Resolved los siguientes ejercicios de la relación de problemas III: • Obligatorios: 1 (Máximo de 3 doubles) 2 (Progresión geométrica) 3 (Tarifa aérea) 4 (Comprobar si una letra es minúscula y pasar a mayúscula) 5 (Gaussiana) • Opcionales: 6 (Mayúscula a minúscula y viceversa)
Actividades de Ampliación Familiarizarse con las webs de referencias de funciones estándar.
http://www.cplusplus.com/reference/clibrary/ http://www.cppreference.com I Actividades a realizar en las aulas de ordenadores Durante la primera media hora de clase, seguid las instrucciones indicadas en el apartado siguiente Depuración de funciones (el otro apartado es para hacerlo en casa), mientras que el profesor corrige individualmente los ejercicios de algunos alumnos. Durante el resto de la sesión, el profesor irá llamando a los alumnos para que expliquen al resto de compañeros los ejercicios de esta sesión.
Guión de Prácticas. Fundamentos de Programación
80
Funciones
Depuración de funciones • Ejecución paso a paso dentro de las funciones. Abrid el proyecto III_Max, disponible en el directorio de Material de la Asignatura. Incluid un punto de interrupción en la primera línea del main. Ejecutad con F5. Si pulsamos F10 ejecutaremos el programa paso a paso, pero cuando llegue a la sentencia que llama a la función Max, el depurador no entrará en el código de dicha función. Para poder hacerlo, habrá que pulsar F11 justo antes de la llamada a la función. Se puede alternar F10 y F11 de forma arbitraria. El icono de acceso directo a esta funcionalidad es
. Probadlo.
Si pulsamos F11 justo en la llave cerrada del main, el depurador entrará en el código de una función especial que se encarga de ejecutar el programa. Para salirnos, o bien pulsamos F5, o bien interrumpimos la ejecución. Para no entrar en este código, cuando lleguemos a la llave cerrada del main, pulsaremos F10 o F5. • Salirse de una función. Si durante la depuración entramos en el código de una función, por ejemplo Max, y queremos ejecutar de golpe el resto de la función Max y proseguir paso a paso a partir de la línea desde la que se llamó a Max, pulsaremos Shift-F11 . • Variables locales. Cuando estemos escribiendo código, la función de autocompletar el nombre de las variables con Ctr-Barra espaciadora también está disponible dentro de las funciones (para las variables locales). Cuando estamos depurando, en la ventana Variables Locales podemos ver el valor de las variables locales de la función activa en ese momento. Obviamente, también aparecen los parámetros formales ya que son variables locales a la función. Comprobadlo ejecutando paso a paso la función Max. Otra forma de ver el valor de una variable local durante la depuración es dejar el cursor del ratón encima de la variable y aparecerá una leyenda en amarillo con el valor que dicha variable tiene en ese momento (al igual que hacíamos con las variables del main) • Ir a definición. Otro recurso interesante del entorno de Visual Studio es el siguiente: situad el cursor sobre la llamada a una función, por ejemplo, sobre la llamada a Max desde el main. Haced click con el ratón derecha y seleccionad Ir a definición (o pulsad sobre ). Automáticamente, el cursor se situará en la definición de la función. Para el icono volver a la línea en la que estábamos, pulsaremos Ctr- (navegación hacia atrás), o bien pincharemos sobre el icono
Guión de Prácticas. Fundamentos de Programación
81
Funciones
• En la ventana Vista de Clases, si pinchamos sobre Funciones y variables globales, podremos ver las funciones definidas dentro del fichero .cpp. Haciendo doble click sobre cualquier función, el cursor se sitúa en el código de la función.
Figura 8: Vista de Clases • Una vez hayamos definido una función, desde cualquier otra función definida con posterioridad, podemos invocarla escribiendo su nombre y la lista de parámetros actuales. Visual Studio permite el uso de Ctr-Barra espaciadora para completar el nombre de la función llamada. Además, saldrá automáticamente una leyenda informativa con los nombres de los parámetros formales.
Guión de Prácticas. Fundamentos de Programación
82
Funciones
Apéndice: Saltar la depuración de ciertas funciones Lo que sigue a continuación es para hacerlo en nuestra casa y no en esta sesión de prácticas. Cuando se pulse F11 sobre una instrucción cin, cout o system el depurador de Visual Studio también entrará en el código fuente de dichos recursos. Para que no lo haga, debemos ejecutar el fichero StepOver_VS_2010.reg (se encuentra en el directorio Otros Recursos Visual Studio dentro del fichero fp.zip que se proporcionó al inicio de la asignatura). Para ello, basta hacer doble click sobre él desde el explorador del Sistema Operativo. Una vez hecho esto, volvemos a cargar Visual Studio. Esta operación sólo es necesario realizarla una vez en nuestro PC (en las aulas de prácticas no es necesario hacerlo). Si no funcionase la ejecución del fichero, hay que cambiar manualmente el registro de Windows, ejecutando Regedit y accediendo a la clave
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\ 10.0\NativeDE\StepOver] y seleccionamos con el click derecho del ratón Nuevo->Valor alfanumérico. Añadimos sendas claves con los nombres NoDepures1 y con los siguientes valores:
NoDepures2 (por ejemplo) y
std\:\:.*=NoStepInto system=NoStepInto
Guión de Prácticas. Fundamentos de Programación
83
Clases
Sesión 7
Clases I Actividades a realizar en casa Actividad: Resolución de problemas. Resolved los siguientes ejercicios de la relación de problemas III: • Obligatorios: 7 (Conjunto de 3 valores) 8 (Pareja de enteros) 10 (Progresión geométrica) • Opcionales: 9 (Pareja de enteros con un menú principal)
I Actividades a realizar en las aulas de ordenadores El profesor irá llamando a los alumnos para que expliquen al resto de compañeros los ejercicios de esta sesión. Si sobra tiempo, los alumnos pueden trabajar sobre los ejercicios del próximo examen.
Guión de Prácticas. Fundamentos de Programación
84
Clases
Sesión 8 I Actividades a realizar en casa Durante la semana del 21 al 25 de Noviembre se realizará el primer examen práctico. Dicho examen se realizará durante las horas de las sesiones prácticas. Para la próxima semana, no hay que resolver ningún problema, pero sí hay que hacer lo siguiente: • Seguid las instrucciones indicadas en el apartado siguiente Inspección de objetos. Esta actividad hay que realizarla en la casa (no más de 10 minutos). • Consultar las soluciones a los siguientes ejercicios (las soluciones están disponibles en la web de la asignatura y en clase de teoría ya se han explicado la mayoría) 23 (Gaussiana) 11 (Interés) 12 (Fabricante) 15 (Fracción) 16 (MiReal) 18 (MiEntero) 13 (Nómina) 14 (Pareja de enteros con el menú principal como una clase)
Guión de Prácticas. Fundamentos de Programación
85
Clases
Inspección de objetos Recuperar la solución del ejercicio 9 disponible en la web de la asignatura (III_ParejaEnteros). Incluid un punto de interrupción dentro del método MCD tal y como se indica en la figura. Ejecutar el programa introduciendo dos enteros cualesquiera y seleccionar la opción de calcular el máximo común divisor. Cuando la ejecución pare al llegar al punto de interrupción, abrir la ventana de inspección de variables locales:
Depurar -> Ventanas -> Variables Locales Podremos ver el valor de las variables locales al método MCD, así como los datos miembro (desplegando el signo + que aparece junto al nombre this) De todas las variables que se muestran, la última en modificarse se mostrará en color rojo.
Guión de Prácticas. Fundamentos de Programación
86
RELACIÓN DE PROBLEMAS IV. Vectores
RELACIÓN DE PROBLEMAS IV. Vectores Importante: Para todos los ejercicios, se ha de diseñar una batería de pruebas. Si se va a leer desde un fichero, para poder leer carácter a carácter incluidos los espacios en blanco, debe realizar la lectura en la forma caracter = cin.get(). Cada vez que se ejecute cin.get() el compilador lee un carácter (incluido el espacio en blanco) desde la entrada de datos por defecto.
Problemas Básicos 1. En clase de teoría se ha visto la siguiente clase MiVectorCaracteres.
class MiVectorCaracteres{ private: static const int TAMANIO = 50; char vector_privado[TAMANIO]; int total_utilizados; public: MiVectorCaracteres() :total_utilizados(0) { } int TotalUtilizados(){ return total_utilizados; } void Aniade(char nuevo){ if (total_utilizados < TAMANIO){ vector_privado[total_utilizados] = nuevo; total_utilizados++; } } char Elemento(int indice){ return vector_privado[indice]; } }; Añadir un método que nos diga si el vector es un palíndromo, es decir, que se lee igual de izquierda a derecha que de derecha a izquierda. Por ejemplo, {'a','b','b','a'} sería un palíndromo. Finalidad: Ejemplo de método que accede a las componentes del vector. Dificultad Baja.
Guión de Prácticas. Fundamentos de Programación
87
RELACIÓN DE PROBLEMAS IV. Vectores 2. Sobre la clase MiVectorCaracteres, añadir un método con la cabecera
void Modifica (int indice_componente, char nuevo) para que modifique la componente con índice indice_componente y ponga en su lugar el valor nuevo. Este método está pensado para modificar una componente ya existente, pero no para añadir componentes nuevas. Por tanto, en el caso de que la componente no esté dentro del rango correcto, el método no modificará nada. Finalidad: Ejemplo de método que modifica el estado del objeto. Gestión de precondiciones. Dificultad Baja. la clase MiVectorCaracteres, añadir un método IntercambiaComponentes para intercambiar dos componentes del vector. Por ejemplo, si el vector contiene {'h','o','l','a'}, después de intercambiar las componentes 1 y 3, se quedaría con {'h','a','l','o'}.
3. Sobre
Añadid otro método para invertir el vector, de forma que si el vector contenía los caracteres {'a','g','t','b','i','o'}, después de llamar al método se quedará con {'o','i','b','t','g','a'}. Para implementar este método, llamad el método anterior IntercambiaComponentes. ¿Qué deberían hacer los métodos si los índices de componentes no son correctos? Imprimir las componentes del vector desde el main, antes y después de llamar a dicho método. Observad que se repite el mismo tipo de código cuando se imprimen las componentes del vector. Ya lo arreglaremos en el tema siguiente. Finalidad: Ejemplo de método que modifica el estado del objeto. Gestión de precondiciones. Dificultad Baja. 4. Sobre la clase MiVectorCaracteres, añadir un método Elimina para eliminar el carácter que se encuentre en una determinada posición, de forma que si el vector contenía los caracteres {'h','o','l','a'}, después de eliminar la componente con índice 2 (la tercera) se quedará con {'h','o','a'}. ¿Qué debería hacer el método si el índice de componente no es correcto? Finalidad: Ejemplo de método que modifica el estado del objeto. Gestión de precondiciones. Dificultad Baja. la clase MiVectorCaracteres, se quiere añadir un método EliminaMayusculas para eliminar todas las mayúsculas. Por ejemplo, después de aplicar dicho método al vector {'S','o','Y',' ','y','O'}, éste debe quedarse con {'o',' ','y'}. Implementar el siguiente algoritmo:
5. Sobre
Recorrer todas las componentes del vector Si la componente es una mayúscula, borrarla
Guión de Prácticas. Fundamentos de Programación
88
RELACIÓN DE PROBLEMAS IV. Vectores
Para borrar la mayúscula, se desplazan una posición a la izquierda todas las componentes que hay a su derecha. Para ello, llamad al método Elimina que se ha definido en el ejercicio 4 de esta relación de problemas. Finalidad: Recorrido sencillo de un vector con dos bucles anidados. Dificultad Baja. 6. El algoritmo del ejercicio 5 es muy ineficiente ya que requiere trasladar muchas veces muchas posiciones. Proponer un algoritmo para resolver eficientemente este problema e implementarlo. Consejo: Una forma de hacerlo es utilizar dos variables, posicion_lectura y posicion_escritura que nos vayan indicando, en cada momento, la componente que se está leyendo y el sitio dónde tiene que escribirse. Finalidad: Modificar un vector a través de dos apuntadores. Dificultad Media.
Problemas de Ampliación 7. Sobre la clase MiVectorCaracteres, añadir un método para eliminar el exceso de caracteres en blanco, es decir, que suprima todas las secuencias de espacios en blanco mayor de 1. Por ejemplo, si el vector original es (' ','a','h',' ',' ',' ','c'), el vector resultante debe ser (' ','a','h',' ','c'). Debe hacerse lo más eficiente posible. Finalidad: Recorrido de las componentes de un vector, en el que hay que recordar lo que ha pasado en la iteración anterior. Dificultad Media. 8. Sobre la clase MiVectorCaracteres, añadir un método EliminaRepetidos que quite los elementos repetidos, de forma que cada componente sólo aparezca una única vez. Por ejemplo, si el vector contiene
{'b','a','a','h','a','a','a','a','c','a','a','a','g'} después de quitar los repetidos, se quedaría como sigue:
{'b','a','h','c','g'}
Implementad los siguientes algoritmos para resolver ester problema: a) Usar un vector local sin_repetidos en el que almacenamos una única aparición de cada componente:
Recorrer todas las componentes del vector original Si la componente NO está en el vector sin_repetidos, añadirla (al vector sin_repetidos) Asignar las componentes de sin_repetidos al vector original b) El problema del algoritmo anterior es que usa un vector local, lo que podría suponer una carga importante de memoria si trabajásemos con vectores grandes. Por lo tanto, vamos a resolver el problema sin usar vectores locales.
Guión de Prácticas. Fundamentos de Programación
89
RELACIÓN DE PROBLEMAS IV. Vectores
Si una componente está repetida, se borrará del vector. Para borrar una componente, podríamos desplazar una posición a la izquierda, todas las componentes que estén a su derecha. El algoritmo quedaría:
Recorrer todas las componentes del vector original Si la componente se encuentra en alguna posición anterior, la eliminamos desplazando hacia la izquierda todas las componentes que hay a su derecha. c) El anterior algoritmo nos obliga a desplazar muchas componentes cada vez que encontremos una repetida. Proponed una alternativa (sin usar vectores locales) para que el número de desplazamientos sea lo menor posible, e implementadla en el método EliminaRepetidos Dificultad Media. 9. Supongamos un fichero en el que hay caracteres cualesquiera. Realizar un programa que vaya leyendo dichos caracteres hasta que se encuentre un punto '.'. Queremos contar el número de veces que aparece cada una de las letras mayúsculas ('A', · · · , 'Z'). Una posibilidad sería leer el carácter, y después de comprobar si es una mayúscula, ejecutar un conjunto de sentencias del tipo:
if (letra == 'A') conteo_caracteres[0] = conteo_caracteres[0] + 1; else if (letra == 'B') conteo_caracteres[1] = conteo_caracteres[1] + 1; else if (letra == 'C') conteo_caracteres[2] = conteo_caracteres[2] + 1; else .... Sin embargo, este código es muy redundante. Proponed una solución e implementarla. Para resolver este ejercicio puede usarse o bien un vector clásico de enteros, o bien un objeto de una clase que construyamos del tipo MiVectorEnteros (en cuyo caso, sustituiríamos los corchetes de acceso a las componentes por el correspondiente método) Finalidad: Diseñar un vector en el que los índices de las componentes representan algo. Dificultad Media. la clase MiVectorCaracteres, añadir un método PosicionCaracteresConsecutivos que calcule la posición donde se en-
10. Sobre
cuentre la primera secuencia de al menos n caracteres consecutivos e iguales a uno que se pase como parámetro al método. Por ejemplo, en el vector de abajo, si n = 3, y el carácter buscado es 'a', entonces dicha posición es la 4.
{'b','a','a','h','a','a','a','a','c','a','a','a','g'} Dificultad Media.
Guión de Prácticas. Fundamentos de Programación
90
RELACIÓN DE PROBLEMAS IV. Vectores
11. Escribir un programa que calcule, almacene en un vector y muestre por pantalla los k primeros términos de la sucesión de Fibonacci de orden n. Tanto n como k serán enteros a leer desde el dispositivo de entrada por defecto. La sucesión de Fibonacci de orden n es una secuencia de números en la que los dos primeros son el 0 y el 1. A partir del tercero, los elementos se calculan como la suma de los n anteriores, si ya hay n elementos disponibles, o la suma de todos los anteriores si hay menos de n elementos disponibles. Por ejemplo, la sucesión de Fibonacci de orden 4 sería la siguiente: 0, 1, 1, 2, 4, 8, 15, 29, . . . El programa debe definir una clase llamada Fibonacci para generar los números de Fibonacci y otra clase del tipo MiVectorEnteros para ir almacenándolos. Dificultad Media.
Guión de Prácticas. Fundamentos de Programación
91
Vectores
Sesión 9
Vectores I Actividades a realizar en casa Actividad: Resolución de problemas. Resolved los siguientes ejercicios de la relación de problemas IV. Entregad un único fichero que contenga la clase MiVectorCaracteres con todos los métodos pedidos en los siguientes ejercicios. • Obligatorios: 1 (Palíndromo) 2 (Modifica componente) 3 (Invierte) 4 (Elimina componente) 5 (Elimina las mayúsculas) • Opcionales: 6 (Elimina las mayúsculas eficientemente)
I Actividades a realizar en las aulas de ordenadores El profesor irá llamando a los alumnos para que expliquen al resto de compañeros los ejercicios de esta sesión.
Guión de Prácticas. Fundamentos de Programación
92
Vectores
Sesión 10 I Actividades a realizar en casa Actividad: Resolución de problemas. Resolved los siguientes ejercicios de la relación de problemas IV: • Obligatorios: 8 (Elimina repetidos) • Opcionales: 7 (Elimina exceso de blancos)
I Actividades a realizar en las aulas de ordenadores El profesor irá llamando individualmente a los alumnos para que defiendan los ejercicios de esta sesión. Mientras tanto, el resto de los alumnos pueden trabajar sobre los ejercicios del próximo examen.
Guión de Prácticas. Fundamentos de Programación
93
RELACIÓN DE PROBLEMAS V. Clases (Segunda parte)
RELACIÓN DE PROBLEMAS V. Clases (Segunda parte) Problemas Básicos 1. A la hora de diseñar una clase debemos poner especial énfasis en intentar que sea lo más reutilizable posible, intentando que su interfaz pública no esté ligada a los detalles de la implementación. Por ejemplo, en el ejercicio 9 de la relación de problemas III se definió una clase para leer una opción del usuario. Para ello, se definió una clase Menu con un método LeeOpcion() que devolvía un char. ¿Y si quisiésemos modificar el criterio de entrada y que la opción fuese un entero en vez de un carácter o que fuesen caracteres distintos? Habría que hacerlo en dos sitios: por un parte, dentro de la clase Menu y por otra parte en el programa main en el que se utiliza. Para evitarlo, podemos definir el método LeeOpcion() para que devuelva algo más genérico que un char, por ejemplo, un enumerado que represente la opción leída. De esta forma, si queremos cambiar la forma en la que se lee la opción (por ejemplo '1' en vez de ’P’, '4' en vez de 'I', etc), bastará modificar la implementación interna de la clase Menu, pero los clientes de dicha clase (en nuestro caso el programa principal main) seguirán utilizándola de la misma forma, a través del enumerado que devuelve el método. Recuperad la solución al ejercicio 9 disponible en la web del departamento (III_ParejaEnterosMenu.cpp) y realizad la transformación pedida. Finalidad: Enfatizar la necesidad de abstraer la interfaz de una clase. Dificultad Baja. 2. En clase de teoría se ha definido la siguiente clase para representar una matriz, en la que cada fila puede tener un número distinto de columnas.
class MiMatrizCaracteres{ private: static const int MAXIMO_FILAS = 40; MiVectorCaracteres matriz_privada[MAXIMO_FILAS]; int filas_utilizadas; public: MiMatrizCaracteres() :filas_utilizadas(0) { } int FilasUtilizadas(){ return filas_utilizadas; } char Elemento(int fila, int columna){ return matriz_privada[fila].Elemento(columna); } Guión de Prácticas. Fundamentos de Programación
94
RELACIÓN DE PROBLEMAS V. Clases (Segunda parte)
};
MiVectorCaracteres Fila(int indice_fila){ return matriz_privada[indice_fila]; } void Aniade(MiVectorCaracteres nueva_fila){ if (filas_utilizadas < MAXIMO_FILAS){ matriz_privada[filas_utilizadas] = nueva_fila; filas_utilizadas++; } }
Cread un programa principal en el que se cree una matriz, se le añadan varias filas y se impriman todos sus caracteres. Para imprimir los caracteres, accederemos en primer lugar a cada fila de la matriz a través del método Fila. Se pide hacerlo de dos formas: a) Imprimir cada fila con un bucle que vaya recorriendo los caracteres de dicha fila a través del método Elemento. b) Imprimir cada fila usando la clase ImpresorVectorCaracteres vista en clase de teoría. Finalidad: Usar matrices desde main. Dificultad Baja. 3. Cread una clase ImpresorMatricesCaracteres para que imprima matrices de caracteres. En la definición de esta clase, utilizad la clase ImpresorVectorCaracteres vista en clase de teoría. Finalidad: Pasar a un método de una clase un parámetro de otra clase. Dificultad Baja. 4. Añadid un método a la clase MiMatrizCaracteres para que pleta en una posición (de fila) determinada. Por ejemplo, c b a l ? Insertar c b a f g h ? ? → (d, d, e, e) → d d e ? ? ? ? ? en la fila 1 f g h
inserte una fila com l ? ? ? i ?
dónde ? representa un valor no utilizado. Se quiere que el módulo sea robusto, por lo que deben comprobarse las precondiciones pertinentes. Finalidad: Pasar a un método de una clase un parámetro de otra clase. Dificultad Baja. 5. Cread la clase MiMatrizRectangularCaracteres modificando apropiadamente la clase MiMatrizCaracteres para obligar a que todas las filas tengan el mismo número de columnas, es decir, que sea una matriz rectangular. Finalidad: Diseñar apropiadamente una clase. Dificultad Baja.
Guión de Prácticas. Fundamentos de Programación
95
RELACIÓN DE PROBLEMAS V. Clases (Segunda parte) 6. Modificad la clase MiMatrizCaracteres para que las filas sean del tipo vector <char> en vez de MiVectorCaracteres. Se pide cambiar la interfaz pública y el dato miembro que representa la matriz, es decir:
vector <vector <char>> matriz_privada; La clase debe proporcionar, como mínimo, los siguientes métodos públicos:
int FilasUtilizadas() int ColumnasUtilizadas() void Aniade(vector <char> fila_nueva) char Elemento(int indice_fila, int indice_columna) vector <char> Fila(int indice_fila) void Intercambia(int indice_una_fila, int indice_otra_fila) Obsérvese que el método Aniade añadirá a la matriz rectangular una fila entera. Esta fila es un objeto de la plantilla vector. Finalidad: Diseñar apropiadamente una clase. Dificultad Baja. 7. Modificad la clase definida en el ejercicio 6 de la siguiente forma. En la interfaz pública seguiremos usando el tipo vector <char>, pero para almacenar y manejar internamente los datos, se usará una matriz clásica de dos dimensiones:
class MiMatrizCaracteres{ private: static const int MAXIMO_FILAS = 50; static const int MAXIMO_COLUMNAS = 40; char matriz_privada[MAXIMO_FILAS][MAXIMO_COLUMNAS]; ...... }; Finalidad: Diseñar apropiadamente una clase. Dificultad Baja. 8. En el ejercicio 15 de la relación de problemas III se creó una clase para representar una fracción. Recuperad la solución disponible en la web de la asignatura. Se pide rediseñar la clase Fraccion para que, en vez de trabajar con dos datos miembro numerador y denominador, trabaje con un único dato miembro que será un objeto del tipo ParejaEnteros (esta clase se vio en el ejercicio 8 de la relación de problemas III). Se pide: • Construid dos constructores para la clase Fraccion: – Uno de ellos se le pasará como parámetros dos enteros que representen el numerador y el denominador (aunque se le pasen dos enteros, hemos indicado que la clase contendrá un único dato miembro privado de tipo ParejaEnteros)
Guión de Prácticas. Fundamentos de Programación
96
RELACIÓN DE PROBLEMAS V. Clases (Segunda parte) – Al otro constructor se le pasará un objeto de la clase ParejaEnteros. • La clase ParejaEnteros debe proporcionar el método máximo común divisor.
MCD para calcular el
• Añadir también dos métodos para sumar y multiplicar dos fracciones. La declaración será de la forma
class Fraccion{ ...... Fraccion Suma(Fraccion otra_fraccion) }; • Cread un programa principal que cree varios objetos fracciones y ejecute sus métodos. Finalidad: Declarar un objeto como dato miembro de otro objeto y pasar como parámetro un objeto de la misma clase. Dificultad Baja. 9. Cread una clase LectorVectorCaracteres que lea los datos de un vector de caracteres (de un objeto de la clase MiVectorCaracteres). Finalidad: Ver un método de una clase que devuelve un objeto de otra clase. Dificultad Baja.
Problemas sobre Excepciones 10. Como ya se ha comentado en clase de teoría, si se produce la violación de las precondiciones de un método podemos hacer varias cosas: • El método no hará nada o devolverá un valor indeterminado. El caso típico sería el método Elemento de la clase MiVectorCaracteres. Si se le pasa un índice incorrecto, devolvería cualquier valor. • El método lanza una excepción para notificar el error producido. El caso típico sería el método Inserta de la clase MiVectorCaracteres. Si se le pasa un índice incorrecto, el método lanza una excepción. Nota: Si se estima oportuno, el mismo método anterior Elemento podría lanzar una excepción, aunque al ser un método muy usado, optamos por aumentar la eficiencia no comprobando que el índice sea correcto. En la relación de problemas IV se trabajó con la clase MiVectorCaracteres. Analizar los métodos que deberían lanzar una excepción y programarlos, incluyendo un programa principal que capture dichas excepciones. Incluir, al menos, los métodos
Guión de Prácticas. Fundamentos de Programación
97
RELACIÓN DE PROBLEMAS V. Clases (Segunda parte)
Aniade, TotalUtilizados, Modifica, Elemento y los que resolvían los ejercicios 1 (Palíndromo), 3 (Invierte), 4 (Elimina) y 5 (Elimina mayúsculas) de la relación de problemas IV. Las excepciones que se lancen deben ser de la biblioteca estándar stdexcept. Finalidad: Lanzar excepciones para notificación de errores. Dificultad Baja. 11. Modificad la solución del ejercicio 8 de esta relación de problemas para que incluya el lanzamiento de excepciones en aquellos sitios dónde haya que hacerlo. Finalidad: Lanzar excepciones para notificación de errores. Dificultad Baja.
Problemas de Ampliación 12. Sobre la clase MiVectorCaracteres, añadidle un método que compruebe si todos los elementos del vector están en un segundo vector. Debe respetarse el orden en el que aparecen los caracteres, pero no tienen por qué estar consecutivos. Por ejemplo, el vector {'f','e'} se encuentra dentro del vector {'c','o','f','r','e','s'}. La cabecera del método será:
int PrimeraOcurrenciaDebil (MiVectorCaracteres otro_vector) Observad que al método se le pasa como parámetro un objeto de la misma clase a la que pertenece dicho método y devuelve la posición de la primera ocurrencia (-1 si no está). Finalidad: Pasar a un método de una clase un parámetro de la propia clase. Dificultad Baja. 13. En el ejercicio 12 se veía si todos los caracteres de una subcadena estaban en otra. Haced lo mismo pero obligando a que los caracteres se encuentren en posiciones consecutivas. Por ejemplo, el vector {'f','f','e'} se encuentra dentro de {'e','c','o','f','f','f','e','s'} (a partir de la posición 4), pero no dentro de {'c','o','f','f','s','e'} Finalidad: Pasar a un método de una clase un parámetro de la propia clase.. Dificultad Media. 14. Se quiere calcular la moda de un vector de caracteres, es decir, el carácter que más veces se repite. Por ejemplo, si el vector fuese
{'l','o','s',' ','d','o','s',' ','c','o','f','r','e','s'} los caracteres que más se repiten son 'o' y 's' con un total de 3 apariciones. La moda sería cualquiera de ellos, por ejemplo, el primero encontrado 'o'. El método devolverá un struct del tipo:
Guión de Prácticas. Fundamentos de Programación
98
RELACIÓN DE PROBLEMAS V. Clases (Segunda parte)
struct ParCaracterEntero{ char caracter; int entero; } en el que el campo caracter contendrá el carácter en cuestión ('o') y el campo entero el conteo de la moda (3). 15. Modificad el ejercicio 24 de la relación de problemas III (creación de la clase Circunferencia) para que use una clase Punto2D en vez del struct
CoordenadasPunto2D
Finalidad: Declarar un objeto como dato miembro de otro objeto. Dificultad Baja. 16. Supongamos
una
matriz
rectangular
M
(usad
la
clase
MiMatrizRectangularCaracteres vista en el ejercicio 5 de esta relación
de problemas). Añadir métodos para: a) Sumar aquellos valores Mij de la matriz M que verifiquen que i + j sea igual a: k X
h2
h=1
para algún valor de k ≥ 1. b) Comprobar si es igual a otra matriz rectangular. c) Obtener la traspuesta de la matriz. d) Comprobar si es simétrica. Hacedlo primero calculando la traspuesta de la matriz y viendo si es igual a su simétrica, usando los métodos de los ejercicios anteriores. Hacedlo también comprobando directamente si cada componente es igual a su simétrica y parando el recorrido en cuanto encuentre una componente que no lo verifique.
Guión de Prácticas. Fundamentos de Programación
99
RELACIÓN DE PROBLEMAS V. Clases (Segunda parte)
Sesión 11 I Actividades a realizar en casa Para la primera semana de vuelta de vacaciones de Navidad (del 9 al 13 de Enero), no hay que resolver ningún problema, pero sí hay que consultar las soluciones a los siguientes ejercicios (las soluciones están disponibles en la web de la asignatura). El profesor explicará estos ejercicios durante la sesión de prácticas a la vuelta de Navidad. Se recomienda que: • Se intente plantear la solución de los ejercicios antes de ver la solución • Se consulten las soluciones antes de empezar a resolver los ejercicios del segundo examen práctico.
1 (Clase menú de opciones con un enumerado) 2 (Imprimir matrices) 3 (Imprimir matrices con una clase específica) 4 (Insertar una fila en una matriz) 5 (Matriz Rectangular) 8 (Fracción con pareja de enteros) Durante la semana del 23 al 27 de Enero se realizará el segundo examen práctico. Dicho examen se realizará durante las horas de las sesiones prácticas.
Guión de Prácticas. Fundamentos de Programación
100
RELACIÓN DE PROBLEMAS VI. Recursividad
RELACIÓN DE PROBLEMAS VI. Recursividad Problemas Básicos 1. Definir una función recursiva para sumar los dígitos de un entero positivo. 2. Cread sendas funciones recursivas para: a) Calcular la división entera entre dos números enteros positivos. b) Calcular el resto de la división entera de dos números enteros positivos. Pasad como parámetros a cada función los dos enteros, y suponed que en la llamada siempre pasarán números positivos (es la precondición de la función) En la definición de las funciones no pueden utilizarse los operadores de división, multiplicación ni módulo. 3. Sobre la clase MiVectorCaracteres, definir un método recursivo que nos diga si es un palíndromo, es decir, que se lee igual de izquierda a derecha que de derecha a izquierda. Por ejemplo, {'a','b','b','a'} sería un palíndromo. Pasad al método como parámetros las posiciones izquierda y derecha sobre las que trabajará. 4. Definir una función recursiva a la que se le pasen dos valores enteros n y k y devuelva como resultado el valor k-ésimo comenzando por la derecha del número n. Por ejemplo para n = 427 y k = 3 el resultado sería 4. Si k es mayor que el número de dígitos de n la función devolverá el valor 0. Por ejemplo, para n = 23 y k = 5 el resultado es 0. 5. Definid una función recursiva con prototipo
int MCD(int un_entero, int otro_entero) para que calcule el máximo común divisor entre dos enteros, aplicando el algoritmo de Euclides. 6. Cread una función recursiva con la siguiente cabecera
bool ContieneDesdeInicio(string cadena_grande, string cadena_peque) que compruebe si la cadena cadena_peque se encuentra al inicio de la cadena cadena_grande (desde la posición cero)
Guión de Prácticas. Fundamentos de Programación
101
RELACIĂ&#x201C;N DE PROBLEMAS VI. Recursividad
Problemas de AmpliaciĂłn 7. Cread una funciĂłn recursiva con la siguiente cabecera
bool Contiene(string cadena_grande, string cadena_peque) que compruebe si la cadena cadena_peque se encuentra dentro de la cadena cadena_grande. En la definiciĂłn de esta funciĂłn, se puede llamar a la funciĂłn ContieneDesdeInicio del ejercicio 6 8. Definir una funciĂłn recursiva que acepte un __int64 y devuelva un vector de caracteres con los dĂgitos del mismo incluyendo los puntos de separaciĂłn cada tres cifras decimales. Por ejemplo, para el entero 23456789, la funciĂłn devolverĂa el vector de caracteres 23.456.789. 9. Definir una funciĂłn recursiva para construir un vector de 0 y 1 con la representaciĂłn en binario de un entero. En la pĂĄgina web
http://es.wikipedia.org/wiki/Sistema_binario
se puede consultar cĂłmo pasar un nĂşmero en decimal a binario. (el mismo procedimiento para pasar un decimal a binario se aplica sobre cualquier otra base menor o igual que 10). El bit mĂĄs significativo (el de la mayor potencia de 2) debe ir en la posiciĂłn 0 del vector, mientras que el menos significativo (el correspondiente a 20 ) debe ir en la Ăşltima posiciĂłn del vector. La funciĂłn debe tener la siguiente cabecera:
MiVectorEnteros PasaBinario(int n) Analizad la eficiencia de esta funciĂłn. 10. Sobre la clase MiVectorCaracteres, definir un mĂŠtodo recursivo que devuelva la diferencia entre el nĂşmero de letras mayĂşsculas y el nĂşmero de letras minĂşsculas que hay entres dos posiciones del vector. Por ejemplo, si el vector tuviese los caracteres Hay UnAS LeTRas DESpuEs, la diferencia entre mayĂşsculas y minĂşsculas entre las posiciones 4 y 15 serĂa 6 - 4 = 2. 11. El mĂŠtodo de bisecciĂłn usado para calcular de forma aproximada el punto de corte de una funciĂłn f con el eje de abscisas (la raĂz de la funciĂłn) se puede enunciar como sigue: Sea f (x) una funciĂłn real, estrictamente monĂłtona en [i, d], donde f (i) y f (d) tienen distinto signo y sea una constante real pequeĂąa (del orden de 10â&#x2C6;&#x2019;4 , por ejemplo). a) Calcular m, el punto medio entre i y d,
GuiĂłn de PrĂĄcticas. Fundamentos de ProgramaciĂłn
102
RELACIĂ&#x201C;N DE PROBLEMAS VI. Recursividad b) Si |i â&#x2C6;&#x2019; d| < , terminar. c) Si f (m) tiene igual signo que f (i), repetir considerando el intervalo [m, d] d) Si f (m) tiene igual signo que f (d), repetir considerando el intervalo [i, m] Implemente una funciĂłn recursiva que reciba como datos de entrada los extremos i, d del intervalo, asĂ como el valor de y devuelva la raĂz de la funciĂłn. Se supone que la funciĂłn f(x) ya estĂĄ definida con anterioridad. 12. Realizar una funciĂłn recursiva para multiplicar dos enteros segĂşn el mĂŠtodo de multiplicaciĂłn rusa (ver ejercicio 42 de la relaciĂłn de problemas II de Estructuras de Control -pĂĄgina 51-)
GuiĂłn de PrĂĄcticas. Fundamentos de ProgramaciĂłn
103
Recursividad
Sesión 12
Recursividad I Actividades a realizar en casa Actividad: Resolución de problemas. Resolved los siguientes ejercicios de la relación de problemas VI: • Obligatorios: 1 (Sumar dígitos) 2 (División entera) 3 (Palíndromo) 4 (k-ésimo) 5 (Algoritmo de Euclides) • Opcionales: 6 (Contiene subcadena desde inicio) 7 (Contiene subcadena)
I Actividades a realizar en las aulas de ordenadores El profesor irá llamando a los alumnos para que expliquen al resto de compañeros los ejercicios de esta sesión.
Guión de Prácticas. Fundamentos de Programación
104
Proyecto final
Sesión 13
Proyecto final I Actividades a realizar en casa Esta sesión es de ampliación. No hay que entregar nada y no entra como materia de examen. Corresponde a lo visto en la última semana de clase. Descargad el fichero V_Salario.zip disponible en la web del departamento y descomprimirlo en el ordenador. Abrir el diagrama de clases UML (salario.pdf) que se encuentra dentro de la carpeta del proyecto. Dicho diagrama también está disponible en
http://decsai.ugr.es/jccubero/salario.pdf
Estudiar la solución propuesta, haciendo especial énfasis en: • Ver que la solución propuesta implementa el diagrama de clase UML. • Ver cómo se comunican los objetos a través de los métodos de las clases. Dentro de la definición de algunas clases hay muchos detalles de manejo de ficheros o de cadenas de caracteres que no son necesarios entender. Lo importante es entender cómo se manejan los objetos de dichas clases (la interfaz pública de las clases) • Ver cómo se realiza la gestión de errores con throw y try-catch. Finalidad: Trabajar con un programa que requiere la intervención de varios objetos. Dificultad Media.
Guión de Prácticas. Fundamentos de Programación
105
EXAMENES
Curso: 2011/2012 Clase: Primero - Grupo: B
´ FUNDAMENTOS DE PROGRAMACION ´ EXAMEN PRACTICO 1 (Noviembre 2011) ¡Importante! No se corregir´a ninguna pr´actica que no cumpla escrupulosamente las normas que aparecen a continuaci´on
Descripci´on de la pr´actica La pr´actica consiste en la realizaci´on de uno de los problema que se describen m´as adelante. El problema concreto que habr´a que resolver el d´ıa del examen, se dar´a a conocer al inicio de la prueba. El tiempo que se tendr´a el d´ıa del examen es de 1 hora. La resoluci´on de todos los problemas se har´a en ISO C++. Para la resoluci´on s´olo se podr´an utilizar los conocimientos adquiridos en los temas 1 y 2. En la evaluaci´on de las pr´acticas se tendr´a en cuenta, adem´as de la correcci´on de la soluci´on propuesta, el estilo de programaci´on, el uso correcto de espacios y tabuladores, as´ı como la claridad del c´odigo fuente. La ejecuci´on de los programas por parte del profesor se har´a de forma autom´atica, leyendo los datos desde un fichero de validaci´on y comprobando que la salida es correcta. En cualquier caso, el alumno puede comprobar que el programa funciona correctamente introduciendo los datos desde un fichero o desde el teclado. Recordemos que para leer los datos de un fichero hay que ejecutar el programa desde una consola de sistema y redirigir la entrada de datos en la forma: problema.exe < validacion.txt, donde validacion.txt es el nombre del fichero de validaci´on. Si usa Visual Studio, tambi´en puede ejecutarlo y depurarlo desde el mismo entorno, indicando el nombre del fichero de entrada y el directorio en el que se encuentra. Para ello, hay que irse a la ventana del Explorador de Soluciones y hacer click sobre el nombre del proyecto (no sobre el fichero cpp), y ahora seleccionar desde el men´u principal: Proyecto/Propiedades Propiedades de Configuraci´ on/Depuraci´ on Argumentos del comando/Editar/
En esta opci´on hay que introducir el car´acter < seguido del nombre del fichero de validaci´on.
C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
Indicaremos la carpeta en la que se encuentra el fichero de validaci´on, a trav´es de la opci´on siguiente: Proyecto/Propiedades Propiedades de Configuraci´ on/Depuraci´ on Directorio de trabajo/
En aquellos ejercicios en los que la entrada de datos requiere introducir muchos valores, se han preparado varios ficheros de validaci´on. En la p´agina web de la asignatura (Material de la Asignatura → Examen) se pueden encontrar estos ficheros de validaci´on. En los enunciados de los ejercicios encontrar´a ejemplos de validaci´on con un formato similar al siguiente: Entrada: DDDIDDIIDA Salida: NO SE ABRE En el apartado Entrada se describe el formato en el que el programa espera encontrar los datos en el fichero de validaci´on. En la Salida, se especifica la salida que el programa debe dar. Es importante remarcar que la salida del programa debe aparecer sola en una l´ınea de texto siguiendo EXACTAMENTE el formato especificado.
Instrucciones a seguir el d´ıa del examen Para iniciar sesi´on en las aulas de pr´acticas, tendr´a que introducir su identificador de usuario y contrase˜na como siempre. En la casilla C´odigo, deber´a introducir el que se le diga el mismo d´ıa del examen. Con este c´odigo s´olo tendr´a acceso Internet a la p´agina de la asignatura. No tendr´a disponible su unidad en red U: ni los puertos USB. Al inicio del examen, se le indicar´a el problema que ha de resolver. Tendr´a que bajar el fichero problema.zip que se encuentra en la web de la asignatura (Material de la Asignatura → Examen) a la carpeta local Mis Documentos. Para copiar el fichero desde la web, hay que pinchar con el bot´on derecha del rat´on y seleccionar Guardar destino como. Una vez copiado el fichero en la carpeta Mis Documentos, pinchad con el bot´on derecha del rat´on y seleccionad Extraer todo, para poder extraer los archivos incluidos y abrir el fichero de soluci´on correspondiente. Una vez abierto el proyecto, debe modificar los datos que aparecen en la cabecera del fichero problema.cpp. En concreto, su nombre, el nombre del ordenador en que se sienta y el nombre del problema que aparece entre corchetes en la p´agina de los enunciados. C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
La entrega de la pr´actica se har´a durante la hora que dura el examen. Para ello, se usar´a el sistema de gesti´on autom´atica de pr´acticas (GAP) accesible a partir de la p´agina web de la asignatura (Men´u Principal → GAP). Una vez dentro de GAP habr´a que seleccionar Pr´ acticas -> Entregar -> Examinar Se abrir´a un cuadro de di´alogo en el que buscar´a el fichero problema.cpp. Una vez seleccionado, debe pulsar sobre Entregar pr´ actica Cualquier intento de entregar la pr´actica fuera de la hora del examen conllevar´a el suspenso autom´atico. Importante: El u´ nico fichero que se debe subir a GAP es el fichero problema.cpp. No debe subirse ning´un otro fichero (como por ejemplo los ficheros con extensi´on .vcproj o sln asociados a los proyectos en Visual Studio). La pr´actica se puede entregar tantas veces como se quiera durante el examen. El sistema GAP guarda la u´ ltima entrega. De hecho, se recomienda que se entregue varias veces a lo largo del examen, ya que si el ordenador se quedara colgado, habr´ıa que reiniciarlo y se perder´ıa toda la informaci´on. Tiempo de examen: 1 HORA
C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
1. [primos] Realizar un programa que compruebe si dos enteros le´ıdos desde el teclado son primos relativos, es decir, que no exista ning´un valor que divida de forma entera a ambos. El programa s´olo debe dar por salida el mensaje “SI” (en el caso de que sean primos relativos) o “NO” (en otro caso). Entrada: 2 1246 Salida: NO Entrada: 11 27 Salida: SI 2. [elr] El m´etodo RLE (Run Length Encoding) codifica una secuencia de datos formada por series de valores id´enticos consecutivos como una secuencia de parejas de n´umeros (valor de la secuencia y n´umero de veces que se repite). Realizar un programa que lea una secuencia de n´umeros naturales terminada con un n´umero negativo que se supone codificada con el m´etodo RLE y obtenga la secuencia decodificada. Entrada: 3 1 5 2 6 3 1 5 -1 (tres veces 1, cinco veces 2, seis veces 3, una vez 5) Salida: 111222223333335 3. [nicomaco] Consideremos la siguiente propiedad descubierta por Nic´omaco entre los siglos I y II. Con el primer impar, se obtiene el primer cubo; sumando los dos siguientes impares, se obtiene el segundo cubo; sumando los tres siguientes, se obtiene el tercer cubo, etc. Por ejemplo: 13 = 1 = 1 23 = 3 + 5 = 8 33 = 7 + 9 + 11 = 27 43 = 13 + 15 + 17 + 19 = 64 Escribir un programa que calcule el cubo de un n´umero natural dado, aplicando el anterior m´etodo (no podr´a usarse ninguna funci´on de la biblioteca cmath). Entrada: 4 Salida: 64 4. [quebrado] Dada una fracci´on expresada como numerador y denominador, se desea reducirla a su forma can´onica, es decir, de forma que el numerador y el denominador sean primos relativos. Entrada: 100 25 Salida: 41 Entrada: 56 32 Salida: 74 C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
5. [adn] Un laboratorio de an´alisis gen´etico ha descubierto una secuencia en los genes (ACGTAAATTGGCAA...) que predispone a una enfermedad hereditaria y que se identifica como “AGA” siempre que no vaya seguida inmediatamente de una secuencia inhibidora identificada como “TT”. Se pide construir un programa que lea una secuencia gen´etica (terminada con el car´acter ’.’) y nos diga si tiene esa predisposici´on o no. En este ejercicio no puede emplearse la biblioteca cstring Entrada: ACGTAAATTGAGATTGCAA. Salida: NO Entrada: ACGTAAATTGAGAATTGCAA. Salida: SI 6. [feliz] Se dice que un n´umero natural es feliz si cumple que si sumamos los cuadrados de sus d´ıgitos y seguimos el proceso con los resultados obtenidos, finalmente obtenemos uno (1) como resultado. Por ejemplo, el n´umero 203 es un n´umero feliz ya que 22 +02 +32 = 13 → 12 +32 = 10 → 12 +02 = 1. Se dice que un n´umero es feliz de grado k si se ha podido demostrar que es feliz en un m´aximo de k iteraciones. Se entiende que una iteraci´on se produce cada vez que se elevan al cuadrado los d´ıgitos del valor actual y se suman. En el ejemplo anterior, 203 es un n´umero feliz de grado 3 (adem´as, es feliz de cualquier grado mayor o igual que 3). Otros ejemplos de n´umeros felices son: 7 es feliz con cualquier valor de k mayor o igual a 5. 404 es feliz con cualquier valor de k mayor o igual a 4. 383 es feliz con cualquier valor de k mayor o igual a 4. Ejemplos de n´umeros no felices para cualquier valor de k: 4, 37, 58, 113 Se pide lo siguiente: Leer un valor entero n que representar´a el valor que queremos comprobar si es feliz o no. Leer un valor entero Gradok que representar´a el n´umero m´aximo de iteraciones que vamos a permitir. El programa nos dir´a si n es un n´umero feliz de grado Gradok. La salida ser´a SI en el caso afirmativo y NO en caso contrario. Entrada: 7 6 Salida: SI Entrada: 4 8 Salida: NO C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
7. [raiz] Escribir un programa para calcular la ra´Ĺz en´esima de un n´umero real X mediante el m´etodo de bisecci´on con una precisi´on deseada expresada como un n´umero real (p.e. 0.001). Para ello se propone examinar el intervalo [1, X] y comprobar si alguno de los dos valores extremos del mismo, elevados a n coincide con X (aceptando un error de precisi´on dado por ), en cuyo caso se habr´Ĺa terminado. En caso contrario, habr´Ĺa que dividir el intervalo en dos partes tomando un punto central y elegir un subintervalo en funci´on de si el punto central elevado a n es mayor o menor que X. El programa termina cuando uno de los extremos elevado a n coincide con X (aceptando un error de precisi´on dado por ), en cuyo caso se muestra dicho extremo, o cuando la amplitud del intervalo sea menor que la precisi´on , en cuyo caso se muestra el punto medio del intervalo. Se aconseja utilizar la funci´on pow de la biblioteca cmath (pow(x, y) = xy ). Entrada (ra´Ĺz cuadrada de 4 con precisi´on de 0.00001): Salida:
4 2 0.00001 2
Entrada (ra´Ĺz cuarta de 123 con precisi´on de 0.00001): 123 4 0.00001 Salida: 3.33025 8. [secuencia] Escribir un programa que lea una secuencia de n´umeros enteros en el rango de 0 a 100 terminada en un n´umero mayor que 100 o menor que 0 y encuentre la subsecuencia de n´umeros ordenada, de menor a mayor, de mayor longitud, dentro dicha secuencia. El programa dar´a como salida la posici´on donde comienza la subsecuencia y a continuaci´on su longitud. En el siguiente ejemplo, la mayor secuencia creciente es 7 40 45 45 73 73 que empez´o en la posici´on 3 y tiene una longitud de 6. Entrada: 23 25 7 40 45 45 73 73 71 4 9 101 Salida: 36
C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
´ FUNDAMENTOS DE PROGRAMACION ´ EXAMEN PRACTICO II (Enero 2012) Normas generales !Importante! No se corregir´a ning´un examen que no cumpla escrupulosamente las normas que aparecen a continuaci´on: 1. El examen consiste en la realizaci´on de un programa que resuelva el problema que se describe en las siguientes p´aginas. La resoluci´on del problema se implementar´a en ISO C++98. 2. En la p´agina web de la asignatura se encuentran ficheros de validaci´on que contienen datos para validar nuestros programas. Su uso nos permite que, en lugar de introducir los datos por teclado, se carguen de un fichero, con lo que la prueba de los programas es m´as r´apida. Recordemos que para leer los datos de un fichero hay que ejecutar el programa desde una consola de sistema y redirigir la entrada de datos en la forma: problema.exe < validacion.txt, donde validacion.txt es el nombre del fichero de validaci´on. Si usa Visual Studio, tambi´en puede ejecutarlo y depurarlo desde el mismo entorno, indicando el nombre del fichero de entrada y el directorio en el que se encuentra. Para ello, hay que seleccionar: Proyecto/Propiedades de .... Propiedades de Configuraci´ on/Depuraci´ on Argumentos del comando/Editar/
En esta casilla hay que introducir el car´acter < seguido del nombre del fichero de validaci´on. Indicaremos la carpeta en la que se encuentra el fichero de validaci´on, a trav´es de la opci´on siguiente: Proyecto/Propiedades de .... Propiedades de Configuraci´ on/Depuraci´ on Directorio de trabajo/
3. Debe incluir la descripci´on de los algoritmos implementados. 4. Muy Importante: Para poder realizar una bater´ıa autom´atica de validaciones, en el programa no deben etiquetarse las entradas de datos con instrucciones cout del tipo Por favor, introduzca datos. Las u´ nicas sentencias cout que el programa contendr´a ser´an las correspondientes a la salida del programa, indicada expl´ıcitamente en el enunciado de cada ejercicio. C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
5. En la evaluaci´on de las pr´acticas se tendr´a en cuenta, adem´as de la correcci´on de la soluci´on propuesta, el estilo de programaci´on, la claridad del c´odigo y la calidad del dise˜no utilizado. 6. El fichero debe llamarse obligatoriamente problema2.cpp. Tiempo de examen: 1 HORA Y 20 MINUTOS
C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
1. [similitud] Vamos a definir una medida de similitud entre dos matrices de caracteres cuadradas n × n, M1 y M2 , de la siguiente forma: S(M1 , M2 ) = cI + cP , donde cI es el n´umero de caracteres que est´an a la vez en M1 y en M2 (es decir |M1 ∩ M2 |) y cP es el n´umero de posiciones (i, j) (i = 0, · · · , n − 1 y j = 0, · · · , n−1) en las que M1 y M2 tienen el mismo car´acter. Por ejemplo, dadas las siguientes matrices: M1 =
x z
w k
M2 =
z q
w f
entonces S(M1 , M2 ) = 2 + 1 = 3, ya que tienen dos caracteres en com´un (z y w) y una posici´on (i = 0 y j = 1) en la que ambas matrices tienen el mismo caracter (w). Realizar un programa que dadas dos matrices cuadradas de caracteres, A n × n y B m × m con m ≥ n, determine la posici´on de comienzo, (ic , jc ), de la submatriz n × n de B que tiene mayor similitud con A. Por ejemplo, dadas las siguientes matrices A y B, respectivamente:
a b c d
a e c h
b f d i
c a e a
b d f b
la submatriz de B con mayor similitud con A comienza en la posici´on (0, 2) (cI = 4 y cP = 2). Los datos de entrada al programa se deben dar en el siguiente orden: valor de n, datos de la matriz A (primero la fila 0, luego la fila 1, etc.), valor de m y datos de la matriz B. Restricciones del problema: Debe implementar una clase “MatrizCuadrada”. Dicha clase debe incorporar la funcionalidad necesaria para resolver el problema. C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
No se admitir´a que la entrada o la salida de datos se realicen dentro de la clase que incorpora la funcionalidad para resolver este problema. Esas operaciones se resolver´an fuera de la clase. Ejemplo de fichero de validaci´on: 2 a c 4 a e c h
b d b f d i
c a e a
b d f b
Salida del programa: (ic , jc ). 0 2
C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
2. [MatrizSerie] Desarrolle un programa para sumar aquellos valores Mij de una matriz rectangular n × m de enteros M (todas las filas tienen el mismo n´umero de columnas) que verifiquen que i + j (i = 0, · · · , n − 1 y j = 0, · · · , m − 1) sea igual a: k X
h2 ,
h=1
para alg´un valor de k ≥ 1. Por ejemplo, dada la siguiente matriz: 1 3 5 1 16
5 22 0 20 11
32 8 10 17 0
5 4 9 8 2
entonces esta suma ser´ıa 5(0, 1)+3(1, 0)+9(2, 3)+17(3, 2)+11(4, 1) = 45. Los datos de entrada al programa se deben dar en el siguiente orden: n´umero de filas, n´umero de columnas y datos de la matriz (primero la fila 0, luego la fila 1, etc.). Restricciones del problema: a) Debe implementar una clase “MatrizRectangular”. Dicha clase debe incorporar la funcionalidad necesaria para resolver el problema. b) No se admitir´a que la entrada o la salida de datos se realice dentro de la clase que incorpora la funcionalidad para resolver este problema. Esas operaciones se resolver´an fuera de la clase. Ejemplo de fichero de validaci´on: 5 4 1 5 3 22 5 0 1 20 16 11
32 8 10 17 0
5 4 9 8 2
Salida del programa: La suma de los elementos de la matriz que cumplen la restricci´on especificada. 45
C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
3. [subcadena] Desarrolle un programa para localizar una cadena de caracteres dentro de otra. El programa leer´a dos cadenas desde la entrada est´andar, buscar´a la primera en la segunda, y escribir´a en la salida est´andar la posici´on donde se encuentra (en caso de encontrarla) o un mensaje indicando que no se ha localizado. Un ejemplo es el siguiente: Cadena 1: mundo. Cadena 2: Hola mumumundo. Salida: 5 Tenga en cuenta que no podr´a usar ning´un m´etodo de la clase string salvo el acceso a cada uno de los caracteres de las cadenas y al tama˜no de las cadenas. En particular, no se puede utilizar el m´etodo find. El programa debe leer una entrada con el siguiente formato: a) (Cadena 1). Una primera cadena de caracteres de caracteres que finaliza con el car´acter ’.’ (punto). b) (Cadena 2). Otra cadena con las mismas caracter´ısticas que la primera. Restricciones del problema: a) Debe implementar una clase “CadenaCaracteres”. Dicha clase debe incorporar la funcionalidad necesaria para resolver el problema. b) No se permite declarar vectores fuera de la clase. c) Para poder leer car´acter a car´acter incluidos los espacios en blanco, debe realizar la lectura en la forma caracter = cin.get(). Cada vez que se ejecute cin.get() el compilador lee un car´acter (incluido el espacio en blanco) desde la entrada de datos por defecto. d) No se admitir´a que la entrada o la salida de datos se realice dentro de la clase que incorpora la funcionalidad para resolver este problema. Esas operaciones se resolver´an fuera de la clase. Ejemplo de fichero de validaci´on: mundo. Hola mumumundo.
Salida del programa: La posici´on de comienzo de Cadena 1 en Cadena 2 si se encuentra. 5 En el caso de que Cadena 1 no se encuentre en Cadena 2 se imprimir´a el mensaje NO ESTA
C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
4. [clasificador] Un clasificador asigna una etiqueta a una entidad, y esa etiqueta lo hace miembro de una categor´ıa. Una entidad que va a ser etiquetada se modeliza mediante los valores de una serie de rasgos. Por ejemplo, si las entidades a etiquetar son personas, cada persona podr´ıa representarse mediante los valores de los rasgos altura y peso. Escribir un programa que etiquete un conjunto de datos n dimensionales usando dos categor´ıas: (A y B) empleando un criterio de m´ınima distancia. El programa recibir´a, por este orden cuatro n´umeros enteros: el n´umero de rasgos (n), el n´umero de ejemplos de la categor´ıa A, el n´umero de ejemplos de la categor´ıa B, y el n´umero de entidades a etiquetar. A continuaci´on recibir´a, en orden, los ejemplos A, los ejemplos de B y las entidades a etiquetar. La regla que se emplear´a para etiquetar una entidad X consiste en asignar a X la etiqueta de la categor´ıa m´as cercana. En lugar de medir la distancia de una entidad a una categor´ıa, se escoge un representante de cada categor´ıa y se mide la distancia entre la entidad a etiquetar y el representante de la categor´ıa. Un representante poco discutido es la media: el representante medio de la categor´ıa c, al que llamaremos µc tiene n rasgos y cada uno de ellos, µc i (i = 1, 2, . . . , n) es el valor medio del rasgo i, que se calcula como se indica en la ecuaci´on 1, usando los Nc ejemplos de la categor´ıa c (a los que llamaremos c1 , c2 , . . . , cNc ). µc i =
Nc X
ck i
(1)
k=1
Entonces, una vez calculados los representantes de las categor´ıas, puede calcularse la etiqueta de cada entidad X como se indica en la ecuaci´on 2: (
Etiqueta(X) =
A B
si D(X, µA ) ≤ D(X, µB ) si D(X, µA ) > D(X, µB )
(2)
donde la distancia D se calcula: D(X, µc ) =
n X
(Xi − µc i )2
(3)
i=1
donde el sub´ındice i hace referencia, de nuevo, al rasgo i-´esimo. En l´ıneas generales el programa: a) Leer´a y almacenar´a todos los datos (ejemplos, entidades a etiquetar y representantes) en memoria. b) Calcular´a µA y µB usando los ejemplos proporcionados.
C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
c) Para cada entidad a etiquetar X calcular´a D2 (X, µA ) y D2 (X, µB ) y mostrar´a X y la etiqueta asignada. Restricciones del problema: a) El programa debe guardar en memoria todos los ejemplos y entidades. Supondremos que los rasgos est´an cuantificados por valores num´ericos de tipo double, para mayor generalidad. b) Emplear, al menos, la clase “Clasificador” y dotarla de la funcionalidad necesaria. c) No se admitir´a que la entrada o la salida de datos se realice dentro de la clase que incorpora la funcionalidad para resolver este problema. Esas operaciones se resolver´an fuera de la clase. Ejemplo de fichero de validaci´on: 3 0 -2 1 0 -1 6 4 6 5 1 -3 8 10 -6 4
5 4 6 -1 -2 -2 -1 0 0 2 1 1 0 5 5 7 4 6 5 4 6 1 1 -2 0 7 2 15 8 -3 0 4 4
Salida del programa: media de las categor´ıas, las entidades clasificadas y la etiqueta asignada a cada una. -0.40 0.00 -0.40 A 5.25 5.50 5.00 B 1 1 1 A -3 -2 0 A 8 7 2 B 10 15 8 B -6 -3 0 A 4 4 4 B
C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
5. [limpia] Escribir un programa que lea un texto y elimine todos los espacios que contiene. El usuario introducir´a el texto original por la entrada est´andar de una manera natural: escribir´a una serie de l´ıneas (terminadas todas ellas con la pulsaci´on de la tecla ENTER) y finalizar´a la entrada de texto con una l´ınea vac´ıa (pulsando u´ nicamente ENTER). Entonces el programa mostrar´a en la salida est´andar el mismo texto introducido, l´ınea a l´ınea, pero sin espacios. Restricciones del problema: a) No puede emplearse la clase string b) Para la lectura del texto se emplear´a la estrategia de leer cacr´acter a car´acter, de la forma caracter = cin.get(). Cada vez que se ejecuta cin.get() se lee de la entrada est´andar un s´olo car´acter (inclu´ıdo el espacio en blanco y el salto de l´ınea) y el car´acter le´ıdo es devuelto. Deber´a implementarse, al menos, la clase “Texto”, que modeliza una colecci´on de vectores de caracteres. Los caracteres introducidos se guardar´an en un objeto de esta clase y todo el procesamiento se realizar´a sobre este objeto. No se admitir´a que la entrada o la salida de datos se realice dentro de la clase que incorpora la funcionalidad para resolver este problema. Esas operaciones se resolver´an fuera de la clase. Ejemplo de fichero de validaci´on: En un lugar no no ha mucho un
de la Mancha de cuyo nombre[ENTER] quiero acordarme, [ENTER] tiempo que viv´ ıa [ENTER] hidalgo de los de lanza [ENTER] en astillero, [ENTER] adarga antigua, roc´ ın flaco y [ENTER] galgo corredor. [ENTER] [ENTER]
Salida del programa: El texto introducido sin espacios: EnunlugardelaManchadecuyonombre[ENTER] noquieroacordarme,[ENTER] nohamuchotiempoqueviv´ ıa[ENTER] unhidalgodelosdelanza[ENTER] enastillero,[ENTER] adargaantigua,roc´ ınflacoy[ENTER] galgocorredor.[ENTER] [ENTER]
C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
6. [liga] Escribir un programa que lea los resultados de los partidos disputados en una liga y presente ordenadamente la clasificaci´on final, en base a los puntos obtenidos por los equipos participantes. Si la liga consta de N equipos, y se juega a dos vueltas, cada vuelta tiene N − 1 jornadas, por lo que en toda la liga se disputar´an 2 (N − 1) jornadas y en total se jugar´an N (N − 1) partidos, de manera que cada pareja de equipos se enfrenta dos veces. Las puntuaciones se asignan de la manera tradicional: en caso de empate, 1 punto para cada equipo, y en otro caso, 3 puntos al ganador y 0 al perdedor. El programa recibir´a el n´umero de equipos que forman la liga (N ) y a continuaci´on los resultados de los N (N − 1) partidos. Cada resultado se expresa como una pareja de enteros que indican los goles marcados y encajados, tomando como referencia al equipo local. Cada equipo juega contra los dem´as una vez como local y otra como visitante. Por comodidad, los dos resultados entre cualquiera pareja de equipos se indican de manera consecutiva en la misma l´ınea. Implementar, al menos, la clase “Liga” y dotarla de la funcionalidad necesaria. Ejemplo de fichero de validaci´on: 4 0 3 4 0 2 3
2 1 2 0 1 0
4 1 1 0 0 0
1 0 1 0 0 2
El primer n´umero indica el n´umero de equipos: la liga est´a formada por 4 equipos. Los resultados de los 4 · 3 = 12 partidos se proporcionan en 2 · (4 − 1) = 6 l´ıneas de manera que reflejan los resultados que se escriben usualmente en tablas como la siguiente: 1
1 2 3 4
2 3 0-2 3-1 4-1 0-0 1-0 0-0 1-1 0-0 0-2
4 4-2 2-1 3-0
Salida del programa: Una l´ınea por cada equipo, indicando los puntos conseguidos, los goles a favor, en contra, y la diferencia total de goles, en orden decreciente de puntuaci´on:
C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
Equipo Equipo Equipo Equipo
2 3 1 4
= = = =
12 puntos, favor = 8, contra = 2, 11 puntos, favor = 7, contra = 3, 9 puntos, favor = 9, contra = 11, 2 puntos, favor = 4, contra = 12,
dif dif dif dif
= = = =
6 4 -2 -8
C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
7. [permutacion] Una permutaci´on de un conjunto de enteros, C = {1, · · · , n}, tiene l lecturas, si para leer sus elementos en orden creciente (de izquierda a derecha) tenemos que recorrer la permutaci´on l veces. Por ejemplo, la siguiente permutaci´on del conjunto {1, . . . , 9}: 491825367 necesita 4 lecturas. En la primera obtendr´ıamos 1, 2 y 3. En la segunda 4, 5, 6 y 7. En la tercera 8. Y finalmente, en la cuarta 9. Implementa un programa que lea un conjunto de enteros y que muestre el n´umero de lecturas de dicha permutaci´on. Los datos de entrada al programa se deben dar en el siguiente formato: a) El cardinal n del conjunto C. b) La permutaci´on: una secuencia de n´umeros enteros distintos entre 1 y n. Restricciones del problema: a) Debe implementar una clase “Permutaci´on”. Dicha clase debe incorporar la funcionalidad necesaria para resolver el problema. b) No se permite declarar vectores fuera de la clase “Permutaci´on”. c) No se admitir´a que la entrada o la salida de datos se realice dentro de la clase que incorpora la funcionalidad para resolver este problema. Esas operaciones se resolver´an fuera de la clase. d) Observe que, por definici´on, en un “conjunto” de n´umeros no existen elementos repetidos. Ejemplo de fichero de validaci´on: 9 4 9 1 8 2 5 3 6 7
Salida del programa: El n´umero de lecturas de la permutaci´on. 4
C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
8. [biblioteca] Se desea gestionar una biblioteca. Para cada libro, se desea almacenar un identificador num´erico (´unico) y el a˜no de su publicaci´on u´ nicamente. Debe construir un programa que lea una serie de datos de libros y que los muestre ordenados por a˜no de publicaci´on (de menor a mayor). En el caso de que haya varios libros de un mismo a˜no, se deben ordenar por el identificador, pero esta vez de mayor a menor. Por ejemplo, a continuaci´on tenemos una serie de parejas de n´umeros. Cada una de ellas corresponde a un libro y es de la forma (identificador,a˜no): {(5, 2006), (8, 2000), (30, 2001), (25, 2000), (11, 2005), (21, 2005), (7, 2005), (63, 2006), (27, 2006)} El resultado de la ordenaci´on de nuestro programa deber´ıa ser el siguiente: {(25, 2000), (8, 2000), (30, 2001), (21, 2005), (11, 2005), (7, 2005), (63, 2006), (27, 2006), (5, 2006)} Los datos de entrada al programa se proporcionan con el siguiente formato: a) N´umero de libros. b) Secuencia de pares con los identificadores y a˜no de publicaci´on. Restricciones del problema: a) Debe implementar una clase “Biblioteca”. Dicha clase debe incorporar la funcionalidad necesaria para resolver el problema. b) No se permite declarar vectores fuera de la clase. c) No se admitir´a que la entrada o la salida de datos se realice dentro de la clase que incorpora la funcionalidad para resolver este problema. Esas operaciones se resolver´an fuera de la clase. Ejemplo de fichero de validaci´on: 9 5 2006 8 2000 30 2001 25 2000 11 2005 21 2005 7 2005 63 2006 27 2006 C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
Salida del programa: Los libros ordenados seg´un el criterio anterior. 25 2000 8 2000 30 2001 21 2005 11 2005 7 2005 63 2006 27 2006 5 2006
C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
9. [maximin] Se pide construir un programa que lea una matriz rectangular de enteros y que indique si existe alg´un elemento que sea m´aximo de su fila y m´ınimo de su columna a la vez. En caso afirmativo, el programa mostrar´a la posici´on (fila y columna) de dicho elemento. Si existen varios podemos devolver uno cualquiera. Si no hay ning´un elemento que cumpla la condici´on el programa mostrar´a la posici´on -1 -1 Por ejemplo, dada la matriz M (3 × 4), 4 2 7
9 18 13
5 5 2 2 2 5
el valor 9 que est´a en la posici´on (0, 1) es m´aximo de su fila y m´ınimo de su columna (podr´ıa haber m´as). Los datos de entrada al programa se deben dar en el siguiente orden: a) N´umero de filas de la matriz. b) N´umero de columnas de la matriz. c) Los elementos de la matriz. Restricciones del problema: a) Debe implementar una clase “MatrizRectangular”. Dicha clase debe incorporar la funcionalidad necesaria para resolver el problema. b) No se admitir´a que la entrada o la salida de datos se realice dentro de la clase que incorpora la funcionalidad para resolver este problema. Esas operaciones se resolver´an fuera de la clase. Ejemplo de fichero de validaci´on: 3 4 4 9 5 5 2 18 2 2 7 13 2 5
Salida del programa: Dos enteros que representan la fila y columna del elemento buscado. 0 1
C/ Periodista Daniel Saucedo Aranda s/n, ETSI Inform´ atica, 18071 Granada, Tlf.: +34.958.244019, Fax: +34.958.243317
Fundamentos de Programaci´ on. Convocatoria de Febrero Curso 2010/2011 8 de Febrero de 2011
Tiempo: dos horas y media. IMPORTANTE: Los algoritmos han de ir correctamente explicados. 1. (2.5 puntos) El m´etodo de bisecci´ on usado para calcular de forma aproximada el punto de corte de una funci´ on f con el eje de abscisas (la ra´Ĺz de la funci´ on) se puede enunciar como sigue: Sea f (x) una funci´ on real, estrictamente mon´otona en [i, d], donde f (i) y f (d) tienen distinto signo y sea una constante real pequeË&#x153; na (del orden de 10â&#x2C6;&#x2019;4 , por ejemplo). a) Calcular m, el punto medio entre i y d, b) Si |i â&#x2C6;&#x2019; d| < , terminar. c) Si f (m) tiene igual signo que f (i), repetir considerando el intervalo [m, d] d ) Si f (m) tiene igual signo que f (d), repetir considerando el intervalo [i, m] Implemente una funci´ on recursiva que reciba como datos de entrada los extremos i, d del intervalo, as´Ĺ como el valor de y devuelva la ra´Ĺz de la funci´ on. Se supone que se dispone de la funci´ on f(x) ya implementada en C++. 2. (2 puntos) Desarrolle un programa para localizar una cadena dentro de otra. El programa leer´a dos cadenas desde la entrada est´ andar, buscar´ a la primera en la segunda, y escribir´a en la salida est´andar la posici´ on donde se encuentra -en caso de encontrarla- o un mensaje indicando que no se ha localizado. Un ejemplo de ejecuci´ on es el siguiente: Introduzca la primera cadena: mundo Introduzca la segunda cadena: Hola mumumundo "mundo" Se encuentra en la posici´ on 5 de "Hola mundo" Tenga en cuenta que no podr´ a usar ning´ un m´etodo de la clase string salvo el acceso a cada uno de los caracteres de las cadenas y al tamaË&#x153; no de las cadenas. En particular, no se puede utilizar el m´etodo find. 3. Defina la clase MatrizEnteros para poder trabajar con una matriz de enteros, de forma que todas las filas tengan el mismo n´ umero de columnas. a) (0.25 puntos) Definir un m´etodo para aË&#x153; nadir una fila completa y otro m´etodo para acceder a una componente i, j de la matriz. b) (1.75 puntos) Definir un m´etodo para ejemplo,  3 1 4 5 ďŁ 9 0 4 3 ? ? ? ?
insertar una fila completa en una posici´on (de fila) determinada. Por   6 Insertar 3 2  â&#x2020;&#x2019; (8, 5, 0, 1, 2) â&#x2020;&#x2019; ďŁ 8 ? en la fila 1 9
1 5 0
4 0 4
5 1 3
 6 2  2
d´ onde ? representa un valor no utilizado. Se quiere que el m´odulo sea robusto, por lo que deben comprobarse las precondiciones pertinentes. c) (1.5 puntos) Definir un m´etodo que construya y devuelva una matriz con los datos traspuestos de la primera, es decir, matriz traspuestaij = matriz originalji d ) (2 puntos) Definir un m´etodo para sumar aquellos valores Mij de la matriz M que verifiquen que i + j sea igual a: k X h2 h=1
para alg´ un valor de k â&#x2030;Ľ 1.
Fundamentos de Programaci´ on. Convocatoria de Septiembre. Curso 2010/2011
Recuperaci´ on de la parte pr´ actica de Febrero (40 % de la nota final) Tiempo: una hora y media. IMPORTANTE: Los algoritmos han de ir correctamente explicados. Se pide construir un programa que lea una matriz y que calcule la posici´on de aquel elemento que sea el mayor de entre los m´ınimos de cada fila. El programa mostrar´a la posici´on de dicho elemento (fila y columna). Por ejemplo, dada la matriz M (3 × 4), 9 2 7
7 18 9
4 2 1
5 12 5
el m´ aximo entre 4, 2 y 1 (los m´ınimos de cada fila) es 4 que se encuentra en la posici´on (0, 2). Los datos de entrada al programa se deben dar en el siguiente orden: 1. N´ umero de filas de la matriz. 2. N´ umero de columnas de la matriz. 3. Los elementos de la matriz. Restricciones del problema: 1. Debe implementar una clase “Matriz”. Dicha clase debe incorporar la funcionalidad necesaria para resolver el problema. 2. No se admitir´ a que la entrada o la salida de datos se realice dentro de la clase que incorpora la funcionalidad para resolver este problema. Esas operaciones se resolver´an fuera de la clase. Ejemplo de fichero de validaci´ on: 3 4 9 2 7
7 18 9
4 2 1
5 12 5
Salida del programa: Dos enteros que representan la fila y columna del elemento buscado: 0 2
Fundamentos de Programaci´ on. Convocatoria de Septiembre. Curso 2010/2011
Recuperaci´ on de la parte escrita de Febrero (60 % de la nota final) Tiempo: dos horas y media. IMPORTANTE: Los algoritmos han de ir correctamente explicados. 1. (2 puntos) Definir una funci´ on recursiva a la que se le pasen dos valores de tipo int n y k y devuelva como resultado la suma de todos los k d´ıgitos menos significativos de n. Por ejemplo para n = 61427 y k = 3 el resultado es 7 + 2 + 4 = 13, y para n = 23 y k = 4 el resultado es 2 + 3 = 5 . 2. Implemente la clase Texto para representar una colecci´on de cadenas de caracteres (string). Esta clase debe implementar la siguiente funcionalidad p´ ublica: Bloque 1 (2 puntos). Tareas generales. Escribir m´etodos para: 1) consultar cu´antas cadenas contiene, 2) obtener la cadena i-´esima, 3) a˜ nadir una nueva cadena, 4) insertar una cadena en la posici´on i-´esima, y 5) eliminar la cadena i-´esima. Para todas estas funciones, 0 ≤ i < n donde n es el n´ umero actual de cadenas. Bloque 2 (2 puntos). Tareas de “limpieza”. Escribir m´etodos para: 1) eliminar los “blancos” iniciales de todas las cadenas, 2) eliminar los “blancos” finales de todas las cadenas, y 3) eliminar los “blancos” iniciales y finales de todas las cadenas. Bloque 3 (2 puntos). Tareas de reorganizaci´ on. Puede ser interesante reordenar las cadenas que componen la colecci´ on en un momento dado. En lugar de los criterios cl´asicos (orden lexicogr´afico, longitud, . . . ) proponemos emplear otro criterio, basado en lo que llamaremos ´ındice de ocupaci´ on de una cadena. Un ´ındice de ocupaci´ on es un n´ umero real entre 0.0 y 1.0, que indica cu´antas casillas de la cadena contienen espacios en blanco. El valor 1.0 indica que no hay ninguna casilla en blanco y el valor 0.0 que todas son blancos. El alumno debe escribir un m´etodo que modifique el objeto Texto de manera que reorganice las cadenas de mayor a menor ´ındice de ocupaci´ on. Notas: a) En todos los casos se deber´ an comprobar y tratar las situaciones posibles de error. b) Se pueden implementar tantos m´etodos/funciones como considere oportuno para conseguir la soluci´ on optima. ´ 3. (2 puntos) Desarrolle un programa que lea una secuencia de n´ umeros enteros en el rango de 0 a 100 terminada en un n´ umero mayor que 100 o menor que 0 y encuentre la subsecuencia de n´ umeros creciente (ordenada de menor a mayor), de mayor longitud, dentro de dicha secuencia. Supondremos que dos valores iguales consecutivos forman parte de la misma subsecuencia creciente. El programa nos debe decir la posici´on (empezando desde 1) donde comienza la subsecuencia y su longitud. Entrada: Salida:
23 23 7 45 45 45 73 73 71 4 9 101 POSICION = 3 LONGITUD = 6