Docsity
Docsity

Prepara tus exámenes
Prepara tus exámenes

Prepara tus exámenes y mejora tus resultados gracias a la gran cantidad de recursos disponibles en Docsity


Consigue puntos base para descargar
Consigue puntos base para descargar

Gana puntos ayudando a otros estudiantes o consíguelos activando un Plan Premium


Orientación Universidad
Orientación Universidad

Apuntes de manejo basico del C++, Apuntes de Diseño Industrial

Asignatura: Fundamentos de Informática, Profesor: Ignacio Ignacio, Carrera: Doble Grado en Ingeniería Mecánica e Ingeniería en Diseño Industrial y Desarrollo del Producto, Universidad: UMA

Tipo: Apuntes

2016/2017

Subido el 15/05/2017

shiskatchegg
shiskatchegg 🇪🇸

4

(1)

1 documento

1 / 117

Documentos relacionados


Vista previa parcial del texto

¡Descarga Apuntes de manejo basico del C++ y más Apuntes en PDF de Diseño Industrial solo en Docsity! C++ básico mediante ejemplos Fundamentos de programación para ingenieros Julio Garralón Ruiz Departamento de Lenguajes y Ciencias de la Computación Universidad de Málaga España 6 de octubre de 2016 CC Attribution-NonCommercial-ShareAlike (CC BY-NC-SA) This license lets others remix, tweak, and build upon your work non-commercially, as long as they credit you and license their new creations under identical terms. To see a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/deed.en_EN or send a request to: Creative Commons, 171 Second Street, Suite 300 San Francisco, California 94105, USA. You are free to: Share Copy and redistribute the material in any medium or format. Adapt Remix, transform, and build upon the material. The licensor cannot revoke these freedoms as long as you follow the license terms. Under the following terms: Attribution – You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. Non commercial – You may not use the material for commercial purposes. ShareAlilke – If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original. No additional restrictions – You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. Notices: You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation. No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material. ii Índice de listados Uso de la biblioteca de entrada/salida iostream . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Caracteres nueva línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Definición de variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Asignaciones simples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Inicialización de variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Definición de constantes simbólicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Cálculos simples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 División de tipo real . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Uso de la biblioteca matemática cmath . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Mensajes de entrada de datos al usuario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Entrada de datos de usuario de tipo entero . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Entrada de datos de usuario de tipo real . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Uso básico de funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Desbordes en operaciones aritméticas con naturales . . . . . . . . . . . . . . . . . . . . . . . . . 13 Salida de números con la biblioteca iomanip . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 Concatenación de caracteres con el tipo string . . . . . . . . . . . . . . . . . . . . . . . . . . 14 Uso de cin.get para leer blancos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 Operadores de incremento y decremento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 Uso del operador de asignación aritmética += . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 Uso del operador de asignación aritmética *= . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Uso de la biblioteca matemática cmath . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 Uso de struct para agrupar variables de tipo simple . . . . . . . . . . . . . . . . . . . . . . . 20 Uso de registros como parámetros a funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 Identificación de unidades léxicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 Control de errores aritméticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 Definición y uso de funciones lógicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 Bloques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 Sentencia if doble . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 Sentencia if simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 Anidamientos de sentencias if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 Selecciones múltiples con elses anidados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 La sentencia if else múltiple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 Decisiones con una función lógica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 Un programa completo con funciones simples . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 La sentencia switch básica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 Casos múltiples en la sentencia switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 El operador condicional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 Bucle while simple controlado por centinela . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 Bucle while simple controlado por contador . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 Sentencias de selección en el cuerpo de un bucle . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 Bucle for simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 Resultados intermedios de un bucle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 Bucle do-while simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 Bucle anidado simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 v Bucles anidados acoplados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 Funciones con parámetros de entrada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 Parámetros de salida en procedimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 Parámetros de entrada/salida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 Parámetro real promocionado al tipo del parámetro formal . . . . . . . . . . . . . . . . . . . . 60 Parámetro real truncado al tipo del parámetro formal . . . . . . . . . . . . . . . . . . . . . . . 61 Uso básico de registros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 Errores de rango al acceder a arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 Comprobación de errores de rango en arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 Uso básico de arrays modernos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 Matrices cuadradas de longitud fija . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 Arrays bidimensionales de caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 Anidamiento de un array dentro de un registro . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 Anidamiento de un array incompleto usando marcas . . . . . . . . . . . . . . . . . . . . . . . . 84 Anidamiento de un array de registros dentro de otro registro . . . . . . . . . . . . . . . . . . . 88 vi Prólogo Estos apuntes son simplemente una serie de ejemplos que presentan los conceptos fundamentales del lenguaje de programación de propósito general C++. Estos conceptos incluyen el concepto de variable, asignación, sentencias condicionales, bucles, subprogramas y uso de estructuras de datos mediante arrays y registros. Están pensados para alumnos de primer curso de los distintos grados de informática e ingenierías donde se impartan asignaturas de programación. Las posibilidades de C++ como lenguaje orientado a objetos no se presentan aquí. Se recomienda editar y ejecutar los ejemplos en un ordenador, y una vez entendidos, hacer los ejercicios en el orden en que se proponen, ya que se han seleccionado de forma que presentan los conceptos de programación con dificultad gradual creciente. Málaga, 4 de octubre de 2016 Julio Garralón Ruiz vii 1.3. Salida de datos en pantalla El texto literal dentro de un programa C++ se escribe entre comillas, bien simples, cuando son caracteres individuales, o bien dobles, cuando son cadenas de caracteres de longitud variable. El siguiente ejemplo (incompleto) lo ilustra. int main() { incomplete cout << "This is text."; cout << "In C++, text is a character string."; cout << "The following are single characters."; cout << 'a'; // an ASCII letter cout << '7'; // an ASCII digit cout << '+'; // a special ASCII character cout << ';'; // only the ; inside the quotes is printed cout << 'H' <<'o' <<'l' <<'a'; // several items within a single cout } El punto y coma es la unidad léxica que determina el final de una sentencia. El uso de las sentencias cout y cin necesita incluir la definición de la biblioteca estándar de entra- da/salida iostream mediante la directiva del compilador include, la cual se sustituye por el código fuente que incluye todas las definiciones de la biblioteca. #include <iostream> // allows the use of cout using namespace std; // search iostream in standard folders int main() { cout << "This is text."; cout << "In C++, text is a character string."; cout << "The following are single characters."; cout << 'a'; // an ASCII letter cout << '7'; // an ASCII digit cout << '+'; // a special ASCII character cout << ';'; // only the ; inside the quotes is printed cout << 'H' <<'o' <<'l' <<'a'; // several items within a single cout } Las líneas que empiezan con el símbolo # no forman parte de C++, son directivas del preproce- sador de C++, y por tanto no llegan a ser compiladas, sino que desaparecen justo antes de la compilación. Los espacios en blanco (' '), tabuladores ('\t'), y saltos de línea ('\n' o bien endl) también hay que imprimirlos explícitamente con la sentencia cout. #include <iostream> using namespace std; int main() { cout << "This is text." <<endl; cout << "In C++, text is a character string." <<endl; cout << "The following are single characters." <<endl; 2 cout << 'a' <<' '; cout << '7' <<' '; cout << '+' <<' '; cout << ';' <<' ' <<endl; cout << 'H' <<'o' <<'l' <<'a' <<endl; } Los números literales no se escriben entre comillas. El siguiente ejemplo incluye números enteros y reales. Los números reales se pueden escribir tanto en coma fija como en coma flotante. #include <iostream> using namespace std; int main() { // poor output: cluttered numbers on screen cout << 12456 << -3413; // integer numbers cout << 12.56 << -0.0004; // real numbers in fixed point format cout << 1.3e25 << -0.4e35; // huge numbers in floating point format cout << 1.3e-15; // close to zero number in floating point format // better: separated with whitespaces cout << 12456 <<' '<< -3413 <<' '<<12.56 <<' '<<-0.0004 <<' ' << 1.3e25 <<' '<< -0.4e35 <<' '<<d1.3e-15 <<endl; } Diferentes tipos de datos se pueden mezclar en una sola sentencia cout. #include <iostream> using namespace std; int main() { // mixing textual and numerical output cout << "Pi number: " <<3.1415926535 <<endl << "E number: " <<2.7182818284 <<endl; cout << '2'<<'+'<<'2'<< '=' <<'4' <<endl; } R Las sentencias se pueden dividir en varios líneas para mejorar su legibilidad, siempre que las unidades léxicas se mantengan indivisibles. Ejercicios 1. Escribe un programa que muestre en pantalla tu nombre completo. Después amplíalo para escribir también tu edad de dos formas, (1) como caracteres, y (2) como un número. 2. Escribe un programa que imprima un pequeño cuadrado en pan- talla de 4×4 asteriscos. Después amplíalo para que pinte, al lado, el triángulo que aparece a la derecha usando los espacios en blanco que sean necesarios. **** * **** ** **** *** **** **** 3 1.4. La Memoria 1.4.1. Variables y la sentencia de asignación Una variable es una posición de memoria principal identificada con un nombre simbólico que le asigna el programador. Dicho identificador debe elegirse de forma que recuerde fácilmente su contenido. La sentencia de asignación es la sentencia más importante de los lenguajes de propósito general como C++, ya que representa el movimiento de datos en memoria. Su sintaxis utiliza el operador de asignación, que es el símbolo =: variable_izquierda = valor_derecha; La sentencia de asignación evalúa la expresión de la derecha (rvalue) y almacena el resultado en la variable de la izquierda (lvalue). E ¡Cuidado! El contenido previo de la variable de la izquierda se pierde tras la asignación. El almacenamiento de valores en variables, ya sean literales o calculados, se realiza con la sentencia de asignación, tal como pretende el siguiente ejemplo. #include <iostream> using namespace std; int main() { incomplete counter_1 = 0; counter_2 = 0; radius_1 = 0.5; radius_2 = 1.75; } Todas las variables deben ser definidas antes de ser utilizadas indicando su tipo delante de su identi- ficador. Las variables que almacenan números enteros se definen con la palabra reservada int, y las que almacenan números reales con la palabra reservada float. #include <iostream> using namespace std; int main() { int counter_1, counter_2; // integer numbers definition float radius_1, radius_2; // real numbers definition counter_1 = 0; counter_2 = 0; radius_1 = 1.0; radius_2 = 0.5; } R Se pueden definir todas las variables que el programador desee, prácticamente no hay límite, pero es conveniente mantener el código limpio de definiciones de variables innecesarias. 4 counter = counter + 1; // incremento en 1 x = x * x; // elevar al cuadrado R Recuerda que el operador = no es el operador de igualdad, sino un símbolo que representa una transferencia de datos en memoria. El operador / puede representar la división entera si sus 2 operandos son números enteros, o la división real cuando al menos uno de sus 2 operandos es un dato de tipo real. #include <iostream> using namespace std; int main() { const float Pi = 3.1415926535; const float EarthRadius = 6371; // kilometers float volume, surface; surface = 4 * Pi * EarthRadius * EarthRadius; volume = 4.0/3.0 * Pi * EarthRadius * EarthRadius * EarthRadius; cout <<"Earth surface: " <<surface <<endl; cout <<"Earth volume: " <<volume <<endl; } Otro operador aritmético muy habitual es el módulo, representado por el símbolo%. Produce como resultado el resto de la división entera, mientras que / devuelve el cociente de la división entera (siempre y cuando los dos operandos sean de tipo entero): 1 / 2 vale 0 1% 2 vale 1 Ejemplos útiles para extraer los dígitos de un número: x% 10 devuelve sólo las unidades de x x% 100 devuelve sólo las decenas de x x% 1000 devuelve sólo las centenas de x x / 10 elimina las unidades de x x / 100 elimina las decenas de x x / 1000 elimina las centenas de x En las expresiones pueden aparecer l lamadas a funciones matemáticas si se incluye la definición de la biblioteca matemática estándar de C cmath (véase el apéndice E). El siguiente ejemplo pretende calcular las soluciones reales de una ecuación de segundo grado. #include <iostream> // cout, endl #include <cmath> // sqrt using namespace std; int main() { float a, b, c; // 2nd. degree equation coefficients float root1, root2; // roots to be calculated float discr; // intermediate result a = 1.0; b = 2.0; c = -3.0; discr = b*b - 4*a*c; 7 root1 = -b + sqrt(discr) / 2*a; flawed root2 = -b - sqrt(discr) / 2*a; flawed cout <<"Root 1: " <<root1 <<endl; cout <<"Root 2: " <<root2 <<endl; } Cuidado con la precedencia de operadores. Las expresiones correctas son del ejemplo anterior son: root1 = (-b + sqrt(discr)) / (2*a); root2 = (-b - sqrt(discr)) / (2*a); 1.6. Entrada de datos del usuario Un programa C++ no es muy útil si su salida siempre es la misma. Para darle mayor utilidad debe utilizar datos de usuario introducidos en tiempo de ejecución. Se puede leer de teclado números, caracteres individuales o cadenas de caracteres. El siguiente ejemplo ilustra la entrada de una cadena de caracteres en una variable textual de tipo string. #include <iostream> using namespace std; int main() { incomplete string first_name, last_name; cin >>last_name; cin >>first_name; cout <<"Your full name is " <<first_name <<' ' <<last_name <<endl; } E ¡Los contenidos previos de las variables especificadas en una sentencia cin se pierden! Siempre es conveniente avisar al usuario qué dato debe introducir (el usuario no tiene por qué conocer las instrucciones que ejecuta un programa). #include <iostream> using namespace std; int main() { string first_name, last_name; cout <<"Your last name: "; // requesting input cin >>last_name; // accepting (or reading) input cout <<"Your first name: "; // requesting more input cin >>first_name; // accepting (or reading) input cout <<"Your full name is " <<first_name <<' ' <<last_name <<endl; } E En tiempo de ejecución el dato tecleado no llega al espacio de memoria del programa, es decir, a sus variables, hasta que el usuario pulsa la tecla Return. Además, en general todos los caracteres blancos anteriores se ignoran. Ejemplo con datos de usuario de tipo entero. 8 #include <iostream> using namespace std; int main() { int a, b, result; cout <<"Enter two integer numbers to be operated: "; cin >>a >>b; // two inputs with a single sentence result = a + b; cout <<"Their sum is "<<result <<endl; result = a * b; cout <<"And their product is "<<result <<endl; } Ejemplo con datos de usuario de tipo real. #include <iostream> using namespace std; int main() { float a, b, c; // 2nd. degree equation coefficients float root1, root2; // roots to be calculated float discr; // intermediate result cout <<"Coefficients of 2nd. degree equation: "; cin >>a >>b >>c; discr = b*b - 4*a*c; root1 = (-b + sqrt(discr)) / (2*a); root2 = (-b - sqrt(discr)) / (2*a); cout <<"Root 1: " <<root1 <<endl; cout <<"Root 2: " <<root2 <<endl; } El último ejemplo puede fallar. ¿Por qué? Ejercicios 1. Escribe un programa que escriba en pantalla la tabla de multiplicar del 1 al 10 de un número entero n elegido por el usuario. Usa el carácter tabulador '\t' para alinear los números en columnas. Por ejemplo, si el usuario desea la tabla del 12, la salida debe tener un formato similar al siguiente: 12 x 1 = 12 12 x 2 = 24 12 x 3 = 36 ... 2. Escribe un programa que lea dos números enteros de teclado y los sume, los reste, los multiplique, y calcule tanto el cociente como el resto de su división entera. 3. Escribe un programa que, utilizando solo dos variables, calcule la suma de 5 números enteros leídos por teclado e imprima el resultado final. 4. La aceleración de caída g en la superficie de un planeta se calcula con la fórmula g = GM r2 , 9 cout <<"Their sum is "<<added <<"and their product "<<multiplied <<endl; } Ejercicios 1. Escribe las interfaces de entrada-salida apropiadas para funciones que calculen: (1) el máximo de tres números reales; (2) la distancia entre dos puntos del plano; (3) el número de dígitos de un número entero; (4) cuántos números son mayores que cero de tres dados; (5) cuántas vocales hay en cuatro caracteres de entrada; y (6) el valor real de un número racional (que consta de un numerador y un denominador). 2. Reescribe el ejercicio 4 de la sección anterior que calcula el peso de un objeto en diferentes planetas definiendo una función que calcula el peso de un objeto en un planeta genérico cualquiera. El programa debe realizar una llamada a esta función para cada planeta. Toda la entrada-salida de datos debe realizarse en el programa principal. 3. Reescribe el ejercicio 5 que calcula la caída de un objeto utilizando una función que realice el cálculo del peso. Llama a esta función desde el principal dos veces, cada una con valores de altura diferentes que se leen de teclado. Toda la entrada-salida de datos debe realizarse en el programa principal. 4. Reescribe el ejercicio 6 que calcula la fuerza de atracción entre dos cargas eléctricas utilizando una función que realice el cálculo. Toda la entrada-salida de datos debe realizarse en el programa principal. 5. Escribe una función que calcule la distancia euclídea entre dos puntos del plano definidos por sus coordenadas 2D: (x1, y1) y (x2, y2) según la fórmula: d = √ (x2 − x1)2 + (y2 − y1)2. El programa principal leerá de teclado las coordenadas de dos puntos y llamará a la función 3 veces para calcular y sacar en pantalla: (1) la distancia entre ellos; (2) la distancia entre el primer punto y el origen; y (3) la distancia entre el segundo punto y el origen. 1.8. Tipos de datos escalares El tipo de un dato determina el dominio de posibles valores que puede tomar, y las operaciones que se pueden realizar con él. C++ dispone de los tipos de datos escalares predefinidos siguientes, algunos de los cuales difieren simplemente en la extensión de su dominio (véase la sección C del apéndice para más información): Números enteros: short, int, long, unsigned. Números reales: float, double. Caracteres individuales: char. 12 Cadenas de caracteres: string. Valores lógicos o booleanos: bool. Aparte de los operadores específicos para cada uno de ellos, todos los datos escalares pueden asignarse con el operador de asignación =, y comparados con los operadores relacionales <, <=, >, >=, y de igualdad ==, !=. Además los números enteros y los caracteres individuales son ordinales, esto es, cada valor tiene exac- tamente un sucesor y un predecesor, excepto el primero y el último. Estos tipos de datos son adecuados para las comparaciones de igualdad, a diferencia de los números reales cuyo resultado depende de la precisión de la máquina. Todos los datos de tipos predefinidos pueden mostrarse en pantalla y ser leídos de teclado directamente con las sentencias cout y cin de la biblioteca de entrada/salida estándar (aunque los valores mostrados para los datos de tipo bool serán siemplemente el valor 0 si el dato vale false y 1 si vale true). 1.8.1. Números enteros Los números naturales se definen con la palabra reservada unsigned o con la combinación unsigned int. No pueden contener números negativos por lo que no son adecuados cuando se van a realizar restas u otras operaciones que puedan producir resultados negativos. #include <iostream> using namespace std; int main() { unsigned number1, number2, result; cout <<"Enter 2 naturals to be substracted: "; cin >>number1 >>number2; result = number1 - number2; flawed cout <<"Difference: " <<result <<endl; } Los números enteros definidos con la palabra reservada int sí pueden contener números negativos y pueden utilizarse con todos los operadores aritméticos básicos {+, -, *, /,% }. En realidad, short y long son modificadores de tipo que especifican un número de bytes menor o mayor respectivamente que el número de bytes reservado para un dato de tipo int (véase la sección C del apéndice para más detalles). 1.8.2. Números reales En C++ los números reales pueden tener diferente precisión y diferente dominios según el número de palabras de memoria utilizadas para almacenarlos: Precisión simple: una palabra de memoria, float. Precisión doble: dos palabras, double. Precisión extendida: cuatro palabras, long double. Los operadores espećficos son los aritméticos reales +, -, * and /. Los números reales se pueden escribir en coma fija, como 12.34, -7.567 or .54, o en coma flotante, como en 1.5e2 (= 1,5× 102) or 7.5e-7 (= 7,5× 10−7). Sin embargo, internamente se almacenan siempre en coma flotante. 13 La formato de la salida en pantalla se puede cambiar utilizando el manipulador setprecision de la sentencia cout definido en la biblioteca de extensión de la entrada/salida iomanip, tal como ilustra el siguiente ejemplo. #include <iostream> // cout, cin, endl #include <iomanip> // fixed, setprecision() using namespace std; int main() { const float GravitationalK = 6.67384e-11; // m3 kg^-1 s^-2 // by default, large or small numbers are shown with floating format cout <<"Gravitational Constant (floating point format):\n" <<"By default:\t" <<GravitationalK <<endl; // and you can change the number of significant digits printed on screen cout <<"Precision 4:\t" <<setprecision(4) <<GravitationalK <<endl; // fixed point format can also be used... cout <<"Gravitational Constant (fixed point format):\n" <<fixed; cout <<"By default:\t" <<GravitationalK <<endl; // or with arbitrary precision (counting from the decimal point cout <<"Precision 2:\t" <<setprecision(2) <<GravitationalK <<endl <<"Precision 16:\t" <<setprecision(16) <<GravitationalK <<endl; } La salida del ejemplo debe ser muy parecida a la siguiente: Gravitational Constant (floating point format): By default: 6.67384e-11 Precision 4: 6.674e-11 Gravitational Constant (fixed point format): By default: 0.0000 Precision 2: 0.00 Precision 16: 0.0000000000667384 1.8.3. Caracteres Cada carácter, bien individual, o bien formando parte de una cadena de caracteres, se almacena como un entero que ocupa un byte que coincide con su código ASCII. Las variables de caracteres individuales se definen con la palabra reservada char, y las cadenas con el tipo string de la biblioteca estándar de C++ string. Las cadenas de caracteres son tipos compuestos, no escalares, pero esta biblioteca de C++ permite hacer muchas operaciones con ellas como si fueran datos individuales. Tanto los caracteres individuales como las cadenas pueden compararse con los operadores relacionales y de igualdad utilizando para ello la ordenación de la tabla ASCII. Además, las cadenas pueden utilizar el operador + para concatenar caracteres. Los literales caracteres individuales se escriben entre comillas simples, mientras que las cadenas entre comillas dobles. #include <iostream> using namespace std; int main() { char a, b, c; 14 Precedencia Operadores Asociatividad más alta () izquierda a derecha ++ −− derecha a izquierda * /% izquierda a derecha + - izquierda a derecha más baja = += -= *= /=%= derecha a izquierda Tabla 1.2: Reglas de precedencia de los operadores aritméticos. last2 = last; last = fib; fib += last2; cout <<fib <<' '; last2 = last; last = fib; fib += last2; cout <<fib <<' '; last2 = last; last = fib; fib += last2; cout <<fib <<' '; last2 = last; last = fib; fib += last2; cout <<fib <<' '; last2 = last; last = fib; fib += last2; cout <<fib <<"...\n"; // ... endlessly ... } Y el siguiente el de asignación multiplicación. #include <iostream> using namespace std; int main() { float x, ratio; cout <<"Starting term of a geometric series: "; cin >>x; cout <<"Ratio: "; cin >>ratio; cout <<x <<' '; x *= ratio; cout <<x <<' '; x *= ratio; cout <<x <<' '; x *= ratio; cout <<x <<' '; x *= ratio; cout <<x <<"...\n"; } 1.9.3. Precedencia de operadores Las reglas de precedencia aritmética en C++ se resumen en las siguientes: Los operadores multiplicativos son más prioritarios que los aditivos. La tabla 1.9.3 los ordena de mayor a menor precedencia. En el mismo nivel de precedencia, y a excepción de los operadores de asignación, se evalúan de izquierda a derecha. Los operadores de asignación son los menos prioritarios. Los siguientes ejemplos se muestran con sus expresiones equivalentes usando paréntesis: a /= a - b / c + d ≡ a = (a / ((a - (b / c)) + d)) b *= a / b + c % d ≡ b = (b * ((a / b) + (c % d))) a - b /= c % d → error 17 1.9.4. Compatibilidad de tipos En general, cuando se mezclan tipos incompatibles en una expresión, se producen errores semánticos. No obstante, C++ permite mezclar algunos tipos si puede tener sentido. A continuación se dan ejemplos útiles. Conversión implícita. Promoción a número real. float money, price_per_hour; money = hours * price_per_hour; // hours promociona a float Conversión implícita. Conversión a número entero, se pierde precisión. int hours; hours = money / price_per_hour; // domina tipo de la variable destino Conversión implícita. A división entera o real. float num_real; int num_entero; num_real = 1/2; // = 0.0 num_real = 1.0/2; // = 0.5 num_entero = 1.0/2.0; // = 0, domina tipo de la variable destino Conversión explícita. Obtención del código ASCII de un carácter. char a; cout <<"El codigo ASCII de " <<a <<" es " <<int(a) <<endl; Conversiones explícitas. A división entera. float time_in_minutes; int hh, mm; hh = int(time_in_minutes) / 60; mm = int(time_in_minutes) % 60; E El tipo de la variable izquierda de una asignación no puede cambiarse, ni siquiera temporal- mente. 1.9.5. La biblioteca matemática La biblioteca matemática estándar de C cmath, permite hacer llamadas a funciones trigonométricas, calcular logaritmos, exponenciales, o potencias reales, entre otras. El siguiente ejemplo muestra como calcular el seno y coseno del ángulo doble. #include <iostream> #include <cmath> // cos(), sin() using namespace std; int main() { const double Pi = 4*atan(1.0); double degrees, radians, sine2, cosine2, angle2; cout <<"Enter an angle in degrees: "; cin >>degrees; radians = degrees*Pi/180; sine2 = 2*sin(radians)*cos(radians); cosine2 = cos(radians)*cos(radians) - sin(radians)*sin(radians); cout <<"The sine of the double angle is " <<sine2 18 <<" and the cosine " <<cosine2 <<endl; } Ejercicios 1. Escribe el resultado que produciría cada una de las siguientes sentencias, teniendo en cuenta su definición inicializada: int d=2, v=50, n=10, t=5; string cadena="Hola"; (1) n / t + 3 (2) v / t + n - 10 * d (3) v + n / t * d (4) v + n / t + d (5) d / 57 (6) 18 / -t (7) v % 3 (8) cadena + ", "+ "que tal!" (9) cadena + '.' (10) d *= n + 1 2. Escribe un programa que calcule la media de 5 números enteros leídos de teclado. 3. Escribe un programa que lea de teclado 4 caracteres y muestre en pantalla sus códigos ASCII. 4. Usando los operados de asignación, escribe un programa que calcule y muestre en pantalla los fac- toriales de los 10 primeros números naturales. Después amplía tu programa para incluir factoriales números naturales mayores hasta obtener errores de desborde. 5. Escribe un programa que lea de teclado un número de 5 dígitos como si fuera un solo número entero e imprima en pantalla la suma de sus dígitos. (Sugerencia: utiliza los operadores resto y cociente de la división entera.) 6. Escribe un programa que lea de teclado un número de segundos arbitrariamente grande y responda con el tiempo equivalente en formato hh:mm:ss. Por ejemplo, si se introduce 12345 segundos, la salida debe ser 3:25:45. 7. Diseña una función que devuelva un valor aproximado del número e según la fórmula: e = ( 1 + 1 x )x , donde x es un número real de entrada a la función. El programa principal debe leer x de teclado y llamar a la función 3 veces con valores de entrada x/100, x, y 100x y escribir en pantalla esas 3 apro- ximaciones al número e. Utiliza el tipo double y el manipulador setprecision(ndigitos) para escribir los valores en pantalla controlando el número de dígitos significativos. Utiliza el manipulador setprecision(ndigitos) de la biblioteca estándar iomanip si quieres mejorar la precisión mostrada en pantalla con la sentencia cout. 1.10. Registros simples Además de variables, el programador puede definir tipos nuevos, no predefinidos, los cuales suelen ser tipos compuestos. Un uso muy simple y útil son registros (struct en C++) que almacenan varias variables de tipo simple. Para ello, utilizamos la palabra reservada typedef que nos permite primero definir el tipo, y una vez definido el tipo, podemos definir las variables. Por ejemplo, los números racionales no existen en C++, pero se pueden definir agrupando en un registro el numerador y denominador de una fracción. 19 (1) √ x21 + x22 (2) −b− √ b2 − 4ac 2a (3) (a, b) + (x, y) (4) |a log10 x+ b ln x| (5) cos a+ i sen b (6) d1 + n2e 2. Escribe un programa que escriba en pantalla la figura de la derecha. * * * * * * * * * * * * 3. Escribe un programa que lea de teclado un número de 5 dígitos como si fuera un solo número entero e imprima sus dígitos separados por blancos. Después, amplíalo para construir aritméticamente un número entero con los mismos dígitos, pero invertidos. ¿Qué ocurre si el número no es de 5 cifras? 4. Diseña una función que calcule y devuelva el índice de masa corporal de un humano según la fórmula: IMC = peso altura2 , donde el peso en kilogramos y la altura en metros son parámetros de entrada a la función. Llama a la función 3 veces desde un programa principal que lea de teclado el peso y altura de una persona que quiere adelgazar. La primera llamada utilizará el peso real introducido, la segunda 5 kilos menos, y la tercera 10 kilos menos. (Nota: se asume que el IMC de una persona sana suele estar entre 18.5 y 24.9). 5. Define un tipo TPunto2D y utilízalo para realizar este ejercicio. Escribe una función con los parámetros de entrada necesarios que calcule la pendiente de una línea que pasa por 2 puntos (x1, y1) y (x2, y2), la cual está dada por la ecuación m = y2 − y1 x2 − x1 . Llamando a la anterior función, escribe un programa que calcule el valor de la pendiente de una línea que conecta dos puntos introducidos por teclado, y las pendientes de las líneas que conectan cada uno de los puntos con el origen. Diseña después una función que reciba como parámetros la pendiente m y las coordenadas de un punto del plano, (x0, y0), y escriba en pantalla la ecuación de la línea que los une con el formato: y = m(x− x0) + y0, donde x e y son simplemente caracteres que representan las coordenadas de un punto genérico. Llama a esta función desde el programa anterior para sacar los resultados en pantalla. 6. Diseña una función que calcule el punto medio de una línea que conecta 2 puntos del plano (x1, y1) y (x2, y2) según la fórmula: Pm = ( x1 + x2 2 , y1 + y2 2 ) . Pruébala con un programa principal que llame a la función 4 veces con los pares de puntos de la tabla adjunta. Después realiza el ejercicio definiendo un tipo nuevo para almace- nar las coordenadas de un punto del plano, TPunto2D, simplifi- cando así la interfaz de la función. (3,7) (8,12) (2,10) (12,6) (2,3) (2,1) (4,3) (2,3) 22 7. Según la serie de MacLaurin la función seno se puede aproximar por: Seno(x) ≈= x− x 3 3! + x5 5! − x7 7! + ..., donde x es el ángulo en radianes. La serie será más precisa cuanto mayor sea el número de términos. Escribe dos funciones Seno2(x) y Seno4(x) que calculen el seno de un ángulo con 2 y 4 términos respectivamente. Compara su precisión llamándolas desde un programa principal para calcular los senos de los ángulos π/6, π/4, π/3, π/2, π y 2π. Llama también a la función sin(x) de la biblioteca matemática estándar para los mismos ángulos y compara los resultados. 23 24 Categoría Operadores Al mismo nivel de precedencia Prioridad () izquierda a derecha Unarios ++ -- + - ! derecha a izquierda Mutiplicativos * /% izquierda a derecha Aditivos + - izquierda a derecha Entrada/salida >> << izquierda a derecha Relacionales «= »= izquierda a derecha Igualdad == != izquierda a derecha lógico and && izquierda a derecha lógico or || izquierda a derecha Operador condicional ?: derecha a izquierda Asignaciones = += -= *= /= %= derecha a izquierda Tabla 2.1: Operadores agrupados por precedencia decreciente. Cuidado con la precedencia de los operadores lógicos. La tabla 2.1 incluye la precedencia de estos operadores. El resultado de una expresión lógica también se puede almacenar en una variable (de tipo bool). // float x, y; int a, b; bool ok; ok = 23.0*x <= sqrt(y*y) || a>b && ok ok = 23.0*x + sqrt(y*y) || a>b && ok En el segundo de los ejemplos anteriores hay un error de tipo semántico ¿cuál?. En el primero, sin errores, el orden de evaluación podría ser: (1) 23.0*x (2) y*y (3) sqrt() (4) <= (5) a>b (6) && (7) || (8) = A estas sentencias de decisión, además de las sentencias de repetición que veremos en el siguiente capítulo, se las denomina sentencias de control porque se basan en una condición de control. El funcio- namiento básico es: R Si en tiempo de ejecución la condición de control es verdadera, hay ejecución de la sentencia (o grupo de ellas), si no, no se ejecuta (o se ejecuta otro grupo diferente). Funciones lógicas Las condiciones más elaboradas conviene encapsularlas en funciones que devuelven un resultado de tipo bool. Las llamadas a estas funciones también pueden hacer el papel de condición de control. El siguiente ejemplo muestra primero la definición de la función y en el principal aparece la llamada, la cual valdrá true o false. 27 #include <iostream> using namespace std; bool IsEven(int a) { return a%2 == 0; } int main() incomplete { int number; bool it_is_even; ... it_is_even = IsEven(number); ... } Evaluación en cortocircuito La evaluación de las expresiones lógicas compuestas se evalúan por orden de izquierda a derecha, y termina prematuramente cuando se puede determinar el resultado final sin necesidad de evaluar el resto de la expresión. Esto es útil en expresiones que contienen varias subexpresiones enlazadas con el operador and o con el operador or. En los ejemplos siguientes se muestran en color más claro las subexpresiones que no llegan a evaluarse por no ser necesario. La primera termina valiendo false, y la segunda true. // float a=1, b=-1, c; a>=0 && b>=0 && b*b>4*a*c && sqrt(b*b-4*a*c)>0 // char c1=’e’, c2=’B’, c3=’5’; c1<’a’ || c1>’z’ || c2<’Z’ || c2>’A’ || c1>c2 || c2>c3 Ejercicios 1. Escribe funciones lógicas que devuelvan true cuando... a) un número sea múltiplo de 5. b) dos números reales se diferencien en menos del valor 10−12. c) un carácter sea una letra, ya sea minúscula o mayúscula. d) un carácter sea alfanumérico. e) un carácter sea una vocal, ya sea minúscula o mayúscula. f ) un carácter sea un dígito hexadecimal. g) un carácter sea imprimible, es decir, que no sea un blanco (espacio, tabulador, nueva línea o retorno de carro) ni un carácter de control (los 32 primeros caracteres ASCII). 2. Escribe una función lógica que devuelva true cuando un número entero sea par, y false cuando sea impar. 3. Escribe una función lógica que devuelva true cuando 2 números reales no difieran en más de una tolerancia dada arbitrariamente pequeña. 28 2.2. Bloques y sentencias compuestas En C++ las sentencias se pueden agrupar entre llaves { } sin que ello altere la ejecución secuencial del programa. #include <iostream> using namespace std; int main() { // definitions const float Pi=3.141592654; float radius, area; { cout << " Radius: "; cin >> radius; } { area = Pi*radius*radius; cout << " Area: " << area << endl; } } Los bloques se pueden utilizar en cualquier sitio de un programa C++, pero su mayor utilidad es para agrupar las sentencias que forman las sentencias de control compuestas, las cuales sí alteran la ejecución secuencial habitual de un programa. 2.3. Sentencias de selección Hay tres tipos de sentencias de selección: a) La selección simple ejecuta opcionalmente un grupo de sentencias. b) La selección doble ejecuta un grupo de sentencias de dos posibles. c) La selección múltiple ejecuta un grupo de sentencias entre varias posibles seleccionada según el valor que tome una única expresión tipo ordinal. C++ proporciona tres mecanismos de selección: (1) la sentencia if, la más general; (2) la sentencia switch; y (3) el operador condicional ?:. 2.3.1. La sentencia if Ejemplo de selección doble. #include <iostream> #include <cmath> // sqroot using namespace std; int main() { float a, b, c; // 2nd. degree equation coefficients float root1, root2; // roots to be calculated float discr; // intermediate result 29 E Presta atención a la indentación, que consiste en sangrar el código fuente varios espacios hacia el interior de forma consistente a lo largo de todo un programa para mejorar su legibilidad. 2.3.3. Sentencias if múltiples Las selecciones múltiples se pueden construir anidando sentencias if en las partes else. #include <iostream> using namespace std; int main() { int number; cout <<"Enter an integer number: "; cin >>number; if (number < 10) { cout <<"The number has only one digit\n"; } else { if (number < 100) { cout <<"The number has two digits\n"; } else { if (number < 1000) { cout <<"The number has three digits\n"; } else { if (number < 10000) { cout <<"The number has four digits\n"; } else { cout <<"The number has more than four digits\n"; } } } } } No obstante, es más habitual escribir los anidamientos sin escribir las llaves de las partes else. #include <iostream> using namespace std; int main() { int number; cout <<"Enter an integer number: "; cin >>number; if (number < 10) { cout <<"The number has only one digit\n"; } else if (number < 100) { cout <<"The number has two digits\n"; } else if (number < 1000) { 32 cout <<"The number has three digits\n"; } else if (number < 10000) { cout <<"The number has four digits\n"; } else { cout <<"The number has more than four digits\n"; } } Ejercicios 1. Escribe un programa que responda cuál es el mayor y el menor de una serie de cinco números leídos de teclado. 2. Escribe un programa que lea 5 números enteros positivos y responda cuántos son pares. 3. Suponga que el nivel de grado de los estudiantes que no han terminado la universidad se determina con base a la siguiente tabla: Número de créditos obtenidos Grado Menor que 32 Primer año 32 a 63 Segundo año 64 a 95 Tercer año 96 ó más Último año Utilizando esta información, escriba un programa que acepte el número de créditos que ha acumu- lado un estudiante y determine en qué año del grado se encuentra. 4. Escribe una función lógica que devuelva true para años bisiestos y false para los que no lo sean. Un año es bisiesto si cumple una de las dos siguientes condiciones: El año es divisible por 400. El año es divisible por 4, pero no por 100. Por ejemplo, el año 1996 es bisiesto, el 2000 y el 2004 también, pero el año 1900, 1995 y el 2100 no lo son. Pruébala con un programa que determine si un año leído de teclado es bisiesto. 5. Una empresa maneja códigos numéricos de 4 dígitos con el formato POOC siguiente: El primero P es el código de una provincia. El segundo y tercero OO el código de operación. El último C el dígito de control, el cual debe coincidir con el resto de la división entera de OO por P. Utilizando el operador módulo% y cociente / de la división entera de C++, escriba un programa que lea de teclado un número de cuatro dígitos, y después extraiga e imprima en pantalla las tres partes separadas. Por ejemplo, para la entrada 1234, la salida sería: Codigo de provincia: 1 Codigo de operacion: 23 Digito de control: 4 El programa debe dar el mensaje de error adecuado cuando el número leído no tenga cuatro dígitos, o cuando el dígito de control C no coincide con el resto de dividir el código de operación por el código de provincia. 33 2.4. Ejemplos con funciones El siguiente ejemplo utiliza una función lógica que encapsula las condiciones para determinar si un alumno cumple unos requisitos mínimos para calcular su nota final. #include <iostream> #include <iomanip> // output format: fixed, setw, setprecision using namespace std; /* * Find out whether minimum grades are met. */ bool Minimums(float test1, float test2, float test3, float prac1, float prac2, float prac3, float exam) { const float MinGrade = 1.0; const float MinExam = 2.5; bool ok; if (exam>=MinExam) { if (test1>=MinGrade && test2>=MinGrade && test3>=MinGrade && prac1>=MinGrade && prac2>=MinGrade && prac3>=MinGrade) { ok = true; } else { ok = false; } } else { ok = false; } return ok; } int main() { const float ExamWeigh = 0.6; const float TestWeigh = 0.2; const float PracWeigh = 0.2; float test1, test2, test3, tests; float practice1, practice2, practice3, practice; float exam, final; cout<<"3 tests grades (0-10): "; cin >>test1 >>test2 >>test3; cout<<"3 practices grades (0-10): "; cin >>practice1 >>practice2 >>practice3; cout<<"Final exam grade (0-10): "; cin >>exam; tests = (test1 + test2 + test3) / 3; practice = (practice1 + practice2 + practice3) / 3; if (Minimums(test1, test2, test3, practice1, practice2, practice3, exam)) { final = exam*ExamWeigh + tests*TestWeigh + practice*PracWeigh; } else { final = exam*ExamWeigh; 34 case 'A': case 'E': case 'I': case 'O': case 'U': cout <<"A vowel\n"; break; case 'B': case 'C': case 'D': case 'F': case 'G': case 'H': case 'J': case 'K': case 'L': case 'M': case 'N': case 'P': case 'Q': case 'S': case 'T': case 'V': case 'W': case 'X': case 'Y': case 'Z': cout <<"A consonant\n"; break; default: cout <<"Not a letter\n"; } } R Recuerda que sólo expresiones de tipo ordinal pueden utilizarse como la expresión de selección de una sentencia switch. 2.4.2. El operador condicional El operador condicional permite hacer asignaciones abreviadas basadas en una condición evitando el uso de la sentencia if. Trata de averiguar su funcionamiento con el siguiente ejemplo que calcula la resistencia equivalente de dos resistores que pueden estar en serie o en paralelo. #include <iostream> using namespace std; main() { float r1, r2, r; char configuration; cout << "Enter two resistor values: "; cin >> r1 >> r2; cout << "Series or parallel (s/p): "; cin >> configuration; // any other answer different from 's' or 'p' will be considered 's' r = configuration=='p' || configuration=='P' ? r1*r2/(r1+r2) : r1+r2; cout << "Equivalent resistance: " << r << endl; } Como muestraba la tabla 2.1, este operador tiene una precedencia muy baja, lo que evita a menudo el uso de paréntesis. Ejercicios de repaso 1. Escribe una función que halle el determinante de una matriz 2× 2: D = ∣∣∣∣ m11 m12m21 m22 ∣∣∣∣ = m11m22 −m12m21 Con ayuda de esta función, escribe un programa que acepte como entrada los coeficientes a, b, c, d, e, f , de un sistema de ecuaciones lineales 2× 2,{ ax+ by = c dx+ ey = f, 37 y después calcule sus soluciones con la regla de Cramer : x = ∣∣∣∣ c bf e ∣∣∣∣∣∣∣∣ a bd e ∣∣∣∣ y = ∣∣∣∣ a cd f ∣∣∣∣∣∣∣∣ a bd e ∣∣∣∣ Prueba el programa con dos ecuaciones linealmente independientes, es decir, que cumplan que ae 6= bd. Después pruébalo con dos ecuaciones linealmentes dependientes (ae = bd), y modifica tu programa para dar al usuario el mensaje de error adecuado cuando no haya solución. 2. Escribe una función que calcule la resistencia que ofrece un conductor cilíndrico a la corriente eléctrica de acuerdo con la fórmula R = ρ l A , donde ρ es su resistividad, l la longitud del conductor, y A el área de su sección transversal. Con ayuda de esta función, escribe un programa que lea de teclado los datos de un con- ductor cilíndrico fabricado con uno de los 4 materiales que se muestran en la tabla adjunta. Los datos a leer serán su letra inicial (c, a, p, o h); su resistividad; su longitud; y su sección transversal. Utiliza una sentencia switch para discriminar los 4 posibles casos según el material. Material ρ cobre 16,8 · 10−9 aluminio 25,4 · 10−9 plata 15,9 · 10−9 hierro 97,1 · 10−9 3. Codifica un programa que se comporte como una calculadora simple que realice las 5 operaciones básicas con números enteros. El usuario primero elegirá una de las 5 posibles operaciones tecleando el carácter correspondiente +, -, *, /, o%, y después introducirá los 2 operandos. Una posible ejecución sería: Operacion (+ - * / \%): / Operando 1: 5 Operando 2: 2 Resultado: 2 4. ¿Cómo escribirías con el operador condicional una asignación a una variable h, el valor de otra variable a sólo si se cumple la condición (a>b)? 5. Teniendo en cuenta que la asociatividad del operador condicional es de derecha a izquierda, reescribe la siguiente asignación condicional anidada con una sentencia if equivalente. h= (a > b) ? (b < c) ? a : b : b; 6. Define una función de tipo carácter, LetraDNI(...), que devuelva la letra de control de un DNI español. La letra correspondiente se obtiene con el resto de la división entera del número del DNI por 23, el cual es el número de orden (del 0 a 22) de la siguiente lista de letras: T, R, W, A, G, M, Y, U, P, D, X, B, N, J, Z, S, Q, V, H, L, C, K, y E. Por ejemplo, al dividir el número 12345678 por 23, se obtiene el resto 14, al cual le corresponde la letra Z, por lo que el DNI completo sería 12345678Z. Realiza un programa que lea por teclado el número de un DNI y responda con el DNI completo correspondiente. 7. Escribe un programa que resuelva completamente una ecuación de segundo grado ax2 + bx+ c = 0, incluyendo las soluciones complejas si fuera el caso. Los tres coeficientes a, b y c se leerán de teclado. Por ejemplo, para la ecuación x2 − 3x+ 2 = 0, la entrada y salida serían: 38 Coeficientes: 1 -3 2 Soluciones reales: 2, 1 Si el coeficiente cuadrático a fuera 0, debe calcularse una única solución real. Y si tanto el coeficiente cuadrático como el lineal b fueran 0, se debe responder con el mensaje adecuado. En el caso de raíces comlejas, hay que calcular por separado las partes real e imaginaria, dando la solución en el formato a + i b. Por ejemplo, para x2 − 2x+ 5 = 0, la ejecución sería: Coeficientes: 1 -2 5 Soluciones complejas conjugadas: 1 +i 2, 1 -i 2 Si alguna de las partes, real o imaginaria, son cero, no debería mostrarse esa parte: Coeficientes: 1 0 1 Soluciones complejas conjugadas: +i 1, -i 1 39 } E Hay que asegurarse de que la condición de control tomará en algún momento el valor false, si no, sería un bucle infinito y colgaría el programa. Los bucles while suelen ser bucles que no pueden calcular el número de repeticiones en el momento justo en que empiezan. Se dicen que son bucles no-deterministas, y a veces controlados por centinela. En el ejemplo anterior el centinela era el valor cero. No obstante, la flexibilidad de la sentencia while también permite utilizarse para bucles determinis- tas, que son aquéllos con un número de repeticiones que se puede calcular al empezar el bucle. También se dice que son bucles controlados por contador. La naturaleza del problema a resolver es el que determina cuándo un bucle es determinista y cuándo no. A veces, como el ejemplo anterior, se puede utilizar ambas alternativas modificando ligeramente el código. #include <iostream> using namespace std; int main() { const int HowMany=5; int number, counter=0, total=0; cout <<"Enter "<<HowMany<<" integer numbers to add: "; cin >>number; counter++; total += number; while (counter < HowMany) { cout <<"Another one ("<<HowMany-counter<<" left): "; cin >>number; counter++; total += number; } cout <<"Total is " <<total <<endl; } Dentro de un bucle puede ir cualquier tipo de sentencia, por ejemplo sentencias de decisión, tal como muestra el siguiente ejemplo, que selecciona los números pares para ser sumados. #include <iostream> using namespace std; int main() { int number, total=0; cout <<"Even numbers to add (0 to finish): "; cin >>number; total = number; while (number!=0) { cout <<"Another one to add (0 finish): "; cin >>number; if (number%2 == 0) { // never trust on the user total += number; } } cout <<"Total of even numbers is " <<total <<endl; 42 } Ejercicios 1. Escribe un programa que lea números enteros de teclado hasta introducir el valor 0 y muestre en pantalla el mayor y el menor de todos ellos. 2. Escribe un programa que lea números enteros de teclado hasta introducir el valor 0 y muestre en pantalla la media aritmética de los números pares y la media aritmética de los números impares de forma separada. 3. Escribe un programa que lea un texto de teclado, carácter a carácter, hasta introducir un punto. El programa debe llevar la cuenta del número de comas, letras, vocales y el número total de caracteres a la entrada. Los resultados se imprimirán en pantalla al final. 4. Utilizando sumas y restas sucesivas, escribe un programa que realice la división entera entre dos números enteros, sacando en pantalla tanto el cociente como el resto. 3.2. El bucle controlado por contador for Un contador no es más una variable que incrementa o decrementa su valor, muy frecuentemente dentro un bucle. La condición de control de los bucles for es un expresión que vigila el valor de un contador hasta que llega a un valor final, momento en el que termina el bucle. Esa expresión suele ser un simple contador que se llama variable de control del bucle. Aunque un bucle while puede estar controlado por un contador, el bucle for dispone de una sintaxis más apropiada para ello. La primera línea de un bucle for contiene la inicialización de la variable de control, que se realiza una sola vez justo antes de que comience el bucle; la propia condición de control, que se evalúa justo antes de cada iteración y permite iterar mientras se evalúe a true; y la sentencia de actualización de la variable de control, que se realiza justo de después de cada iteración. Observa la sintaxis en el siguiente ejemplo que cuenta el número de apuestas diferentes dentro de una quiniela de fútbol española. #include <iostream> using namespace std; int main() { const unsigned NumMatches=14; unsigned i, n1s=0, nXs=0, n2s=0, invalids=0; char result; // to hold '1', 'X', '2' cout <<"Bet on "<<NumMatches<<" football matches (1/X/2)\n"; for (i=1; i<=NumMatches; i++) { cout <<"Match number " <<i <<": "; cin >> result; switch (result) { case '1': n1s++; break; case 'x': case 'X': nXs++; break; case '2': n2s++; break; default: invalids++; 43 } } if (n1s>0) { cout <<n1s <<" home win(s)\n"; } if (nXs>0) { cout <<nXs <<" tie(s)\n"; } if (n2s>0) { cout <<n2s <<" visitor win(s)\n"; } if (invalids>0) { cout <<invalids <<" invalid result(s)\n"; } } R La variable de control puede utilizarse dentro del cuerpo del bucle cuantes veces se desee, pero no debe ser modificada dentro del cuerpo del bucle. En los bucles es muy útil aprovechar cálculos intermedios de iteraciones anteriores, tal como ilustra el siguiente ejemplo que calcula la serie geométrica ∞∑ k=o rk = 1 + r + r2 + r3 + r4... = 11− r #include <iostream> using namespace std; int main() { float series=0, term=1, ratio; unsigned n; cout <<"Number of geometric terms: "; cin >>n; cout <<"Ratio: "; cin >>ratio; for (unsigned i=0; i<n; i++) { series += term; // update current value term *= ratio; // prepare next term } cout <<"Approximated value: " <<series <<endl; } Nótese que se podría haber llamado a la función pow() de la biblioteca matemática estándar, pero esto hubiera aumentado el coste computacional en cada iteración. R Desde el momento en que se usan bucles, cobra importancia el coste computacional, por lo que hay que prestar atención al número de operaciones que se realizan en cada iteración. No obstante, es preferible un código claro y bien estructurado que un código “demasiado optimizado”. Otra observación del ejemplo anterior es la definición de la variable de control, que se puede hacer en la propia sentencia for, en la parte de su inicialización, pero hay que tener presente lo siguiente: E Fuera de la sentencia for, la variable de control no existe si se define dentro de ella. Ejercicios 1. Escribe un programa que acepte 2 números enteros y saque en pantalla todos los números in- termedios, incluidos ellos mismos. Después, modifica el programa para mostrar sólo los números impares. 44 #include <iostream> using namespace std; int main() { unsigned i, j, width, height; cout <<"Width and height of rectangle: "; cin >>width >>height; for (i=0; i<height; i++) { for (j=0; j<width; j++) { cout <<'*'; } cout <<endl; } } Salida para un tamaño 5× 8: ******** ******** ******** ******** ******** Si el usuario introduce una anchura w y una altura h, ¿cuántas veces se ejecuta la sentencia que pinta un solo asterisco? Los anidamientos pueden ser muy flexibles cuando las acciones del bucle interno utilizan valores intermedios calculados en el bucle exterior. El siguiente ejemplo aprovecha esa flexibilidad utilizando el valor de la variable de control del bucle externo en la condición de control del bucle interno. #include <iostream> using namespace std; int main() { unsigned i, j, height; cout <<"Height of triangle: "; cin >>height; for (i=1; i<=height; i++) { for (j=1; j<=i; j++) { cout <<'*'; } cout <<endl; } } Output for height 5: * ** *** **** ***** Ahora, si el usuario introduce una anchura w y una altura h, ¿cuántas veces se ejecuta la sentencia que pinta un solo asterisco? Ejercicios 1. Escribe un programa que muestre en pantalla los subíndices del triángulo inferior izquierda de un matriz bidimensional, sin incluir la diagonal principal, tal como se muestra a la derecha para una matriz 5× 5. (2,1) (3,1) (3,2) (4,1) (4,2) (4,3) (5,1) (5,2) (5,3) (5,4) 2. Escribe un programa que muestre en pantalla la letra C con una longitud característica. Por ejemplo, para la longitud 5, el programa mostrará la figura ajunta de la derecha. CCCCC C C C CCCCC 47 3. Escribe un programa que muestre en pantalla una montaña de números de una altura dada. La figura de la derecha muestra la montaña para una altura igual a 5. Ayudándote del operador módulo%, amplía el programa para alinear también montañas de una altura mayor que 9. 1 121 12321 1234321 123454312 Ejercicios de repaso 1. Escriba un programa que invierta aritméticamente los dígitos de un número positivo entero con número variable de cifras. Por ejemplo, si se introduce el número 8735, debe mostrar el número 5378; y si se introduce el 123456, se mostrará 654321. 2. Con ayuda de una función que halle el máximo común divisor de 2 números, escribe un programa que lea de teclado el numerador y denominador de un número racional y escriba en pantalla el mismo número racional su forma conónica. Puedes utilizar un tipo definido TRacional para agrupar el numerador y denominador en un solo registro. 3. Escribe una función que halle el mínimo común múltiplo de 2 números. Un programa principal leerá de teclado el numerador y denominador de dos números racionales para compararlos, para lo cual utilizará la función anterior para reducirlos a común denominador. Puedes utilizar un tipo definido TRacional para agrupar el numerador y denominador en un solo registro. 4. Escribe un programa que cuente el número de palabras que hay en un texto que se introduce por teclado y acaba en un punto. Se considerará palabra cualquier secuencia de letras y dígitos, y podrá aparecer cualquier otro carácter imprimible entre ellas. 5. Realiza un programa que lea valores de resistencias electrónicas (en ohmios, Ω) conectadas bien en paralelo 1 1/R1 + 1/R2 + 1/R3 + ... o bien en serie R1 +R2 +R3 + ... El usario elegirá la configuración escribiendo la letra 'p' o la letra 's'; después aceptará el número de resistores, n; y después de hacer los cálculos necesarios responderá con el valor de la resistencia equivalente. 6. La biblioteca estándar cstdlib proporciona un generador de números enteros aleatorios, rand(), generados en el rango [0, RAND_MAX] (RAND_MAX es un valor grande constante definido en la propia biblioteca). El generador no es más que una lista de números que, para sea realmente aleatoria, debe empezar a su vez en una posición aleatoria o semilla, porque si no, siempre generaría los mismos primeros números. Para ello, el generador suele inicializarse con una sentencia que dependa de un valor aleatorio, como es el momento en el que se ejecuta el programa: srand(time(NULL)); //#include <ctime> Usando las sentencias comentadas y un bucle do-while escribe un programa que genere un número aleatorio entre 1 y 100 (utiliza el operador módulo% para asegurarte de que está en ese rango). A continuación el usuario, sin ver cuál es el número oculto, debe introducir números hasta que acierte. Al final el programa debe indicar el número de intentos realizado. 7. Sin usar funciones para calcular potencias o factoriales, ya sean estándares o definidas por el programador, escribe las siguientes funciones para calcular su valor en un punto x según la serie de MacLaurin dada. El número de términos de la serie vendrá determinado por la diferencia entre los dos últimos términos, la cual no excederá de una tolerancia dada, por ejemplo 10−12. Comprueba antes que el punto x se encuentra en el intervalo adecuado cuando éste no sea infinito. (Pista: utiliza en cada iteración del bucle el término calculado en la iteración previa.) 48 1 1− x = 1 + x+ x 2 + x3 + x4 + . . . for − 1 < x < 1 1 1 + x = 1− x+ x 2 − x3 + x4 − . . . for − 1 < x < 1 ln(1 + x) = x− x2/2 + x3/3− x4/4 + . . . for − 1 < x ≤ 1 arctan(x) = x− x3/3 + x5/5− x7/7 + . . . for − 1 < x < 1 exp(x) = 1 + x+ x2/2! + x3/3! + x4/4! + . . . for −∞ < x <∞ cos(x) = 1− x2/2! + x4/4!− x6/6! + . . . for −∞ < x <∞ sin(x) = x− x3/3! + x5/5!− x7/7! + . . . for −∞ < x <∞ 8. Diseña una función que calcule el coeficiente binomial Cmn = n! m!(n−m)! donde n y m son 2 números enteros y cumplen que n ≥ m. Después diseña otra función que, dados 2 números reales a y b y un número entero positivo n, calcule la fórmula binomial: (a+ b)n = n∑ k=0 ( n k ) an−kbk = ( n 0 ) anb0 + ( n 1 ) an−1b1 + ( n 2 ) an−2b2 + · · ·+ ( n n ) a0bn, Desde un programa principal que lea de teclado valores para a, b y n, llama a ésta última función con estos valores e imprime el resultado. ¿Notas alguna deficiencia para valores moderadamente grandes de n? Si es así, ¿qué crees que está pasando? 9. Sea p la probabilidad (entre 0 y 1) de que un evento X ocurra una sola vez. La probabilidad de que dicho evento ocurra i veces en n experimentos sigue la fórmula Pr(Xi) = ( n i ) pi(1− p)n−i, Escribe una función que calcule la potencia entera de un número real. Después, con ayuda de esta función, escribe otra que, dada la probabilidad p de que ocurra un evento una vez, y un número de experimentos n, calcule la probabilidad de que el evento ocurra i veces mediante la fórmula anterior. Escribe un programa completo para probarlo todo. 10. Escribe un programa que determine el valor de π mediante el algoritmo de Gauss-Legendre, en el cual se inicializa a0 = 1, b0 = 1/ √ 2, t0 = 1/4, p0 = 1, y se itera mediante: ai+1 = ai + bi 2 bi+1 = √ aibi ti+1 = ti − pi(ai − ai+1)2 pi+1 = 2pi Finalmente, π ≈ (ai + bi) 2 4ti . 49 l lamadas Flujo del controlInvocador(...) { ... Subprograma(...) ... Subprograma(...) ... Subprograma(...) ... } // definicion Subprograma(...) { ... } llama retorna Figura 4.1: Concepto l lamada-retorno. float MeanGrade() // function definition { float grade1 = 3.0; float grade2 = 2.0; float grade3 = 8.5; return grade1*0.2 + grade2*0.2 + grade3*0.6; } int main() { ... global_grade = MeanGrade(); // first call ... if (MeanGrade() > 5.0) { // second call ... } ... cout <<MeanGrade(); // third call ... } Pero observa que no hay parámetros de entrada lo que hace que la expansión de código sea muy rígida. La parametrización del mismo subprograma permite realizar el mismo cálculo sobre diferentes datos. float MeanGrade(float grade1, float grade2, float grade3) // definition { return grade1*0.2 + grade2*0.2 + grade3*0.6; } int main() { float exam1, exam2, exam3; ... for (int i=0; i<NumberOfStudents; i++) { ... cin >>exam1 >>exam2 >>exam3; // user input data ... // input parameters change at each iteratio 52 cout <<MeanGrade(exam1, exam2, exam3); ... } ... } 4.2. Parámetros y procedimientos Los parámetros que aparecen en cada llamada son los argumentos reales, mientras que los correspon- dientes en la cabecera de un subprograma son los parámetros formales o fictíceos. En el ejemplo anterior, grade1, grade2, grade3 son los parámetros formales, mientras que exam1, exam2, exam3 son los parámetros reales correspondientes. La cabecera de un subprograma contiene la lista de parámetros formales donde se especifica el número de parámetros necesario y el tipo de cada uno de ellos. Cada llamada debe contener el mismo número de parámetros especificado en esta lista, y cada uno del tipo indicado. Es el orden en esta lista el que establece el vínculo entre el parámetro real de cada llamada y el correspondiente formal de la definición, y no los identificadores utilizados. Por otro lado, en general, todo (sub)programa tiene datos de salida y datos de entrada. Es fundamental especificar para cada parámetro si es un dato de entrada o es un dato de salida. Hasta ahora hemos definido un tipo de subprogramas, las funciones, que sólo tienen parámetros de entrada, y la salida era el valor devuelto por la sentencia return, simulando de alguna manera el comportamiento de las funciones matemáticas que son univaluadas. El tipo de este valor devuelto también debe especificarse como tipo de la propia función en su cabecera. Los parámetros reales de entrada pueden ser cualquier expresión del mismo tipo que el corres- pondiente parámetro formal siempre que sean del mismo tipo o compatible. Además, la expresión donde aparece la llamada debe ser compatible con el tipo del valor que devuelve la función. Por otro lado, deben proporcionar información válida al subprograma. Pero en programación es habitual la necesidad de devolver más de un valor de salida, lo que no se puede hacer con la sentencia return. A veces incluso no es necesario devolver ningún valor de salida, por ejemplo cuando un subprograma simplemente imprime cálculos en pantalla. Por estas razones los subprogramas también admiten parámetros de salida, que en la llamada van a ser variables que van a contener resultados al final de la ejecución del subprograma. Los parámetros reales de salida deben ser variables, nunca valores constantes o u otras expresiones, aunque sean del mismo tipo. No es necesario que estén inicializadas con ningún valor. Habiendo parámetros de salida, ya no es estrictamente necesaria la sentencia return ni especificar ningún tipo para el propio subprograma, lo cual se hará con la palabra reservada void al principio en su cabecera sustituyendo al tipo que se especificaba para las funciones. A este tipo de subprogramas que no representan ningún valor se les denomina procedimientos. Las dos siguientes funciones convierten temperaturas de/a grados celsius a/de grados fahrenheit. Utilizan el mecanismo de salida de la sentencia return. Nota el tipo double al frente de la definición de cada función. 53 double ToCelsius(double fahrenheit) { return 5.0/9.0*(fahrenheit-32); } double ToFahrenheit(double celsius) { return celsius*9.0/5.0 + 32; } El mecanismo de salida return hace posible llamadas dentro de expresiones de tipo compatible: temp = ToCelsius(temp0) - (alt - alt0) * 6.4; Pero el mismo ejemplo se puede realizar con procedimientos que no devuelven nada como subprogra- ma, sino que utilizan parámetros de salida. Las llamadas ahora van a ser distintas. void ToCelsius(double fahrenheit, // input parameter double &celsius) // output parameter { celsius = 5.0/9.0*(fahrenheit-32); } void ToFahrenheit(double celsius, // input parameter double &fahrenheit) // output parameter { fahrenheit = celsius*9.0/5.0 + 32; } Observa el “tipo” void en la cabecera de cada subprograma, y el símbolo & que precede a los pa- rámetros de salida. Ahora la llamada no puede aparecer dentro de ninguna expresión ya que no de- vuelve nada. Son los parámetros de salida los que se utilizan después de que termine la llamada. ToCelsius(temp0, temp1); //temp1 will hold the result temp = temp1 - (alt - alt0) * 6.4; R Las llamadas a procedimientos no pueden aparecer dentro de expresiones porque no devuelven nada, tal como indica su “tipo” void (que en inglés significa vacío). Recuerda que los parámetros reales de salida de las llamadas deben referenciar variables, posiciones de memoria que puedan albergar valores de salida. Las siguientes llamadas a los anteriores procedimientos son ilegales: ToCelsius(temp0, temp*2); //temp*2 no es una posicion de memoria ToCelsius(temp0, 12.3); //12.3 no puede contener otros valores Las siguientes sin embargo son legales: ToCelsius(temp0-100.0, temp); //temp si es una variable ToCelsius(77.0, temp); //y puede contener resultados ¿Es posible que haya parámetros que proporcionen información válida a un subprograma a la vez que almacenen algún resultado de salida? La respuesta es afirmativa, son los parámetros de entrada/salida. En general pueden verse como parámetros que actualizan su valor dentro del subprograma. Los parámetros de entrada/salida deben tener valores inicializados correctamente antes de la llamada y ser capaces de almacenar un resultado de salida. Por eso, al igual que los parámetros reales de salida, deben ser variables. 54 // Observa el punto y coma al final del prototipo! void Subprograma_A( ... ); // el ambito de Subprograma_A empieza aqui void Subprograma_C( ... ); // y el de Subprograma_C aqui int main() { // main sigue sin poderse llamar desde ningun otro subprograma, // sin embargo, puede llamar a Subprograma_A y Subprograma_C, // pero no a Subprograma_B. ... } void Subprograma_A( ... ) { // Subprograma_C puede ser llamado, pero no Subprograma_B. ... } void Subprograma_B( ... ) { // Se puede llamar a Subprograma_A y a Subprograma_C. ... } void Subprograma_C( ... ) { // Se puede llamar a Subprograma_A y a Subprograma_B. ... } Figura 4.3: Uso de prototipos en C++. 4.3.1. Ámbitos de subprogramas En cuanto a los ámbitos de subprogramas, en C/C++ es fácil decidir cuándo un subprograma es accesible, ya que no es necesario aplicar nunca la regla de visibilidad por la siguiente restricción: En C/C++ no se puede anidar subprogramas. Todos, incluido main, tienen alcance de archivo. La figura 4.2 ilustra la accesibilidad de 4 subprogramas definidos en un programa C++, incluyendo el principal. Por otro lado, los ámbitos de subprogramas pueden anticiparse utilizando los llamados prototipos. El prototipo de un subprograma replica su cabecera, constituida por su tipo, su identificador y su lista de parámetros formales. Sirve para adelantar y extender su ámbito. La figura 4.3 ilustra su uso. El uso de prototipos en engorroso porque hay que mantener replicadas la cabecera del subprograma y el prototipo, y si se ordenan correctamente los subprogramas no es necesario utilizarlos, a no ser que se utilice recursividad indirecta, que es una técnica de programación que no se explica en estos apuntes. 4.3.2. Ámbitos de datos y efectos laterales En C/C++ se distinguen datos locales, los que tienen alcance de bloque, y datos globales, que tienen alcance de archivo porque se definen fuera de cualquier bloque, fuera incluso de los cuerpos de los 57 int a; void Subprogram(int b) { double c; ... if (...) { float d; ... } ... } ... // end of file a scope b scope c scope d scope Figura 4.4: Solapamiento de ámbitos. subprogramas. Una diferencia importante entre ellos es que los datos locales solo existen en la memoria cuando se está ejecutando el bloque que los contiene, por eso en C/C++ también se les llama automáticos. Pero los datos globales existen durante toda la ejecución del programa, y se dice que son estáticos. La figura 4.4 muestra los ámbitos de varias variables. En este caso no hay que aplicar la regla de visibilidad porque todas tienen nombres diferentes y son visibles en su ámbito. La figura 4.5 sin embargo muestra un escenario donde las variables más internas ocultan a las más externas con el mismo identificador. Es una buena técnica de programación limitar los ámbitos de las variables a las zonas de código donde se van a utilizar, evitando así accesos en bloques ajenos a su definición que puedan modificar su contenido de forma accidental. Pero el problema potencial realmente grave ocurre cuando definimos variables globales, ya que tienen alcance de archivo y cualquier subprograma puede acceder y modificar su contenido, dando lugar a efectos laterales que pueden provocar errores muy difíciles de localizar. La utilidad de las variables globales es histórica2 y su uso se limita a otros escenarios de programación donde las variables locales no son la solución. R Aunque no están prohibidas en C/C++, el uso de variables globales debe ser evitado siempre que sea posible. Ejercicio 1. Responde a las siguientes preguntas relacionadas con el código fuente adjunto. 2Cuando se empezaba a programar con lenguajes de alto nivel en los años 70 todos los datos eran globales. 58 int a; void Subprogram(int a) { double a; ... if (...) { float a; ... a = 0; ... } a = 1; ... } ... // end of file int a int a double a float a Figura 4.5: Ocultación de variables. 1 float x; 2 3 void SubA(float &y) 4 { 5 double x; 6 x = 2.5; 7 if (x > y) { 8 y = 0; 9 } 10 SubB(a); 11 } 12 13 void SubB(int b) 14 { 15 double a; 16 a = x*b; 17 if (a > b) { 18 bool b; 19 b = true; 20 } 21 cout <<"b=" <<b <<endl; 22 SubA(b); 23 } 24 25 int main() 26 { 27 cout <<"x="; cin >>x; 28 SubB(x); 29 SubA(x); 30 cout <<"a=" <<a <<endl; 31 } a) Especifica qué clase de alcance (de archivo o de bloque) tienen los ámbitos de cada una de las siguientes definiciones y qué rangos de líneas ocupan: float x; int a; double x; int b; bool b; void SubA(...) void SubB(...) b) Especifica el rango de líneas e identificadores involucrados en la aplicación de la regla de vi- sibilidad si ésta ha lugar en algún punto. c) Especifica las líneas donde se produzcan acce- sos ilegales a variables. d) Especifica las líneas donde aparezcan llamadas a subprogramas ilegales. e) Especifica las líneas donde ocurran efectos la- terales o potencialmente pueden ocurrir. 59 4.6. Diseño modular El término diseño modular o diseño estructurado hace referencia a R técnicas de diseño de software que separan la funcionalidad de un programa en módulos indepen- dientes, intercambiables y reutilizables, de tal manera que cada uno contiene todo lo necesario para ejecutar sólo un aspecto de la funcionalidad deseada. Una de las principales razones que causó la crisis del software de los años 60 fue la ausencia de lengua- jes de alto nivel que permitieran escribir grandes programas organizándolos con pequeños subprogramas, lo que llevó a las conocidas deficiencias para reutilizar código, localizar errores o mejorar su legibilidad. Al diseñar módulos o subprogramas, lo más importante son las interfaces de entrada/salida de cada uno de ellos. Si no son interfaces claras, hay alta probabilidad de que la tarea no esté bien definida, o quizá de que no hay oportunidad para modularizar. R Lo primero y más importante al escribir un subprograma es definir claramente su interfaz de entrada/salida. Muy relacionado con la definición de la interfaz es el hecho de que un subprograma debe realizar una tarea bien definida, es decir, el subprograma debe tener una alta cohesión. R Trata de dividir el código en tareas bien definidas, y evita la repetición de código. Además, las interfaces deben ser lo más pequeñas posibles, siempre que contengan toda la informa- ción necesaria, y no más, para realizar su función. El acoplamiento entre módulos indica la fuerza de interconexión entre las distintas unidades de un programa. Este acoplamiento debe reducirse al mínimo para favorecer la reutilización de código y el mantenimiento general del programa. R Diseña subprogramas con una alta cohesión y reduce al mínimo el acoplamiento entre ellos. Otro concepto relacionado es el ocultamiento de la información. Los detalles de la implementación de un subprograma no deben ser visibles fuera de él, de forma que pueda ser invocado teniendo en cuenta simplemente cuál es la información mínima que necesita de entrada y cuál la salida. Cualquier resultado intermedio debe ser interno al subprograma y tratado con ayuda de variables locales. De hecho, la implementación de un subprograma (los detalles) debe poder cambiarse sin necesidad de cambiar su interfaz de entrada/salida si ésta está bien definida. Se logra así lo que se llama la separación de la especificación de la implementación. 4.6.1. Diseño ascendente y descendente Abordar el diseño modular de un problema de programación moderadamente grande se puede hacer de dos formas. Una de ellas es empezar por los detalles de las subtareas más claras que son independientes y/o tienen poca dependencia del resto. En este caso estaríamos diseñando de abajo hacia arriba, lo que se llama diseño ascendente. El otro enfoque, más común, especialmente con grandes proyectos de programación, es empezar por los módulos principales, sin detenerse en los detalles menos significativos al principio. Una vez diseñadas sus interfaces y su implementación a groso modo, se refinan, posiblemente subdividiendo en más módulos, hasta llegar a los detalles finales. De esta forma estamos diseñando de arriba hacia abajo, lo que se conoce como diseño descendente. El uso de diagramas de llamadas como el de la figura 4.7 ayuda a aplicar el diseño modular. Estos diagramas simplemente resaltan todas las llamadas a subprogramas que contiene un programa. 62 Main Addition MaxCD Multiplic Reduce Compare MinCM Figura 4.7: Diagrama de llamadas. Ejercicios de repaso 1. Escribe una función lógica, EsPrimo, que determine si un número natural es primo. Llama a esta función desde otra, EscribirPrimos, que escriba en pantalla todos los números naturales primos que haya en un rango dado. Un programa principal llamará a EscribirPrimos para escribir en pantalla los números primos que haya entre los primeros 1000 números naturales. Despúes amplíalo para que muestre todos los números primos que haya en un rango especificado por el usuario. 2. Dos números son amigos cuando cada uno coincide con la suma de los divisores propios del otro (un número no es divisor propio de sí mismo). Escribe una función, SumaDivisores, que calcule la suma de sus divisores propios (utiliza una función lógica para decidir si un divisor es primo). Después escribe otra función lógica, Amigos, que acepte como entrada 2 números enteros y devuelva true o false según sean o no amigos. Por ejemplo, son números amigos el 220 y el 284 porque los divisores del primero son 1, 2, 4, 5, 10, 11, 20, 22, 44, 55 y 110 que suman 284, y los divisores del segundo son 1, 2, 4, 71 y 142, que suman 220. Amplía después el programa para que saque en pantalla todos los números que son amigos dentro de un rango dado que el usuario introduce por teclado. Para ello tienes que generar con un bucle anidado todas las posibles combinaciones de 2 números naturales en ese rango. 3. Escribe un procedimiento que reciba como parámetros de entrada la parte real y la parte imaginaria de un número complejo y lo pase a forma polar devolviéndolo en 2 parámetros de salida que contengan el módulo y argumento correspondientes. Asegúrate de que el argumento queda en el rango [0, 2π]. Escribe un programa principal que lea de teclado repetitivamente números complejos (dos números reales cada uno) e imprima en pantalla el módulo y argumento de cada uno hasta que se introduzca el número complejo (0,0). 4. Utilizando el diseño descendente, programa el proceso de ortogonalización de Gram-Schmidt para vectores 3D. Para ello, sigue estos pasos: a) Define el tipo TVector3D para almacenar las coordenadas (x, y, z) de un vector 3D (también podrías hacerlo utilizando 3 variables reales independientes). b) Define las interfaces de e/s de los siguientes módulos independientes: Leer3D Leer un vector 3D de teclado. Escribir3D Escribir un vector 3D en pantalla. PEscalar3D Producto escalar de 2 vectores 3D. 63 Diferencia3D Diferencia de 2 vectores 3D. Suma3D Suma de vectores 3D. Escalado3D Multiplicación de los componentes de un vector 3D por un scalar común. c) Implementa y comprueba uno por uno los subprogramas previamente definidos con un pro- grama principal simple. d) Utilizando 2 de los subprogramas anteriores, define e implementa un subprograma que calcule la proyección de un vector 3D ~v sobre otro ~u según la fórmula: Proj(~v, ~u) = ~v · ~u ~u · ~u ~u e) Define e implementa el proceso de ortogonalización de Gram-Schmidt en un subprograma que tome 3 vectores 3D linealmente independientes como entrada, ~v1, ~v2, ~v3, y genere 3 vectores 3D ortogonales ~u1, ~u2, ~u3 como salida según las fórmulas: ~u1 = ~v1 ~u2 = ~v2 − Proj(~v2, ~u1) ~u3 = ~v3 − Proj(~v3, ~u1)− Proj(~v3, ~u2) f ) Finalmente, utilizando los subprogramas anteriores, escribe un programa principal que acepte 3 vectores linealmente independientes por teclado e imprima en pantalla 3 vectores ortogonales que generen el mismo espacio 3D mediante el proceso de Gram-Schmidt. Soluciones al ejercicio 1 de la sección 4.3 (a) float x; Alcance de archivo, líneas: 1–31 float y; Alcance de bloque, líneas: 3–11 double x; Alcance de bloque, líneas: 5–11 int b; Alcance de bloque, líneas: 13–23 double a; Alcance de bloque, líneas: 15–23 bool b; Alcance de bloque, líneas: 18–20 void SubA(...); Alcance de archivo, líneas: 3–31 void SubB(...); Alcance de archivo, líneas: 13–31 (b) double x oculta a float x desde la línea 5 a la línea 11. bool b oculta a int b desde la línea 18 a la línea 20. (c) El identificador a de la línea 30 no referencia ninguna variable. (d) La llamada a SubB en la línea 10 de SubA es ilegal, SubB() no es accesible. Y el argumento x de la llamada SubA(x) es ilegal porque no es de tipo int (es un argumento de salida). (e) La lectura de teclado cin >>x en la línea 27 es un efecto lateral, y la llamada Sub(x) en la línea 28 es un efecto lateral potencial (podrá modificarse la variable global x). 64 TPersonid int name string address string day month year TFecha place_birth string country string El identificador del registro se utiliza para referirnos a él como un todo, mientras que para acceder a cada campo utilizamos operador '.' entre el identificador del registro y el del campo concreto. El siguiente ejemplo muestra cómo pasar todo el registro a un subprograma en un único parámetro, y cómo acceder a sus campos individualmente. void WriteDate(const TDate &date) { cout <<date.day<<'/'<<date.month<<'/'<<date.year; } R Un campo de un registro se puede usar exactamente de la misma forma que una variable individual del mismo tipo. Las siguientes operaciones con registros completos son legales en C++: Initialización con valores para cada campo. Asignación de todo el registro en una sola sentencia. Paso como parámetro, tanto de entrada como de salida. Uso como valor de retorno de una función. El siguiente ejemplo ilustra todas estas operaciones. #include <iostream> using namespace std; typedef struct { unsigned day, month, year; } TDate; // new record type void ReadDate(TDate &date) // output parameter: pass by reference { bool ok; do { cin >>date.day>>date.month>>date.year; if (date.month <= 12) { switch (date.month) { case 1: case 3: case 5: case 7: case 8: case 10: case 12: ok = date.day>0 && date.day<=31; break; case 4: case 6: case 9: case 11: ok = date.day>0 && date.day<=30; break; 67 case 2: ok = date.day>0 && date.day<=28; break; default: // case 0 ok = false; } if (!ok) cout <<"Invalid day or month. Another date (dd mm yy): "; } else { ok = false; cout <<"The month must be between 1 and 12. Another one (dd mm yy): "; } } while (!ok); } void WriteDate(const TDate &date) // input parameter: pass by constant ref. { cout <<date.day<<'/'<<date.month<<'/'<<date.year; } // compares two dates (notice the return type) TDate MoreRecent(const TDate &d1, const TDate &d2) { TDate recent; if (d1.year>d2.year || d1.year==d2.year && d1.month>d2.month || d1.year==d2.year && d1.month==d2.month && d1.year>d2.year) { recent = d1; // a whole record assignment } else { recent = d2; // a whole record assignment } return recent; // returning as a function value } int main() { const TDate UnixBirth = {1, 1, 1970}; // whole record initialization TDate date1, date2, later; cout <<"Comparing dates with Unix birth 1/1/1970 (dd mm yy): "; ReadDate(date1); cout <<"Introduce another one (dd mm yy): "; ReadDate(date2); later = MoreRecent(date1,date2); // assigning a whole returned record WriteDate(MoreRecent(UnixBirth,later)); // passing a whole returned record cout <<" is more recent." <<endl; } Como los tipos registro no están predefinidos en C++, las siguientes operaciones son ilegales en C++ mientras no sean programadas: Valores literales no pueden asignarse fuera de la definición del registro: date = {2, 3, 1987}; 68 Salida en pantalla de todo el registro como si fuera una sola variable: cout >>date; Lectura de teclado como si fuera una sola variable: cin <<date; Uso de operadores relacionales o aritméticos sobre registros completos: if (date1 <date2) ... ... date1 + date2... Ejercicios 1. Define un tipo, TComplejo, para almacenar 4 números reales que representan (1) la parte real, (2) la parte imaginaria, (3) el módulo, y (4) el argumento principal de un número complejo (a, b). Utilizando este tipo, escribe los dos siguientes procedimientos y llámalos desde un programa prin- cipal: LeerComplejo. Lee un número complejo de teclado. Un parámetro de entrada de tipo lógico especificará si se deben leer las partes real e imaginaria, o bien el módulo y argumento. EscribirComplejo. Escribe un número complejo en pantalla. Un parámetro de entrada de tipo lógico indicará si se muestran las partes real e imaginaria, o bien el módulo y argumento. 2. Utiliza el tipo TComplejo para los parámetros de entrada/salida de los dos siguientes subprogra- mas: ModificaModArg. Actualiza el módulo ρ y el argumento principal φ de un número complejo a partir de sus partes real e imaginaria (a, b) según las fórmulas: ρ = √ a2 + b2, φ = tan−1 b a . ModificaReIm. Actualiza las partes real e imaginaria (a, b) de un número complejo a partir de su módulo ρ y su argumento principal φ según las fórmulas: a = ρ cos(φ), b = ρ sin(φ). Comprueba su funcionamiento con un programa principal que lea de teclado las partes real e imaginaria de un número complejo y calcule su módulo y argumento. Después vuelve a calcular las partes real e imaginaria a partir del módulo y argumento calculados para compararlas con los valores originales leídos de teclado. 3. Utilizando los subprogramas de los ejercicios anteriores, escribe funciones para calcular la suma y producto de dos números complejos. Escribe un programa completo adecuado para probarlo todo. 5.3. Arrays Un array es un colección homogénea de datos, esto es, todos del mismo tipo, como por ejemplo listas de números, de cadenas de caracteres, o incluso de registros. El tipo de cada componente es el tipo base, y el tipo de todo el array es el tipo array. Todos los datos del array comparten el mismo nombre, la variable array, pero los componentes no tienen nombre específico, sino que se identifican de forma única por medio de su posición en el array, la cual se llama índice. Los índices son obligatoriamente números naturales, y en C++ el primer componente siempre tiene el índice 0, tal como se ilustra en la figura 5.2. Además del tipo base, la otra característica fundamental de un array es su tamaño, es decir, su número de componentes. Dependiendo del momento en que se pueda especificar, hay dos clases de arrays, a saber: 69 La función at() de los arrays modernos del estándar 2011 tiene la capacidad de capturar errores de rango automáticamente, pero requiere el uso de excepciones, las cuales no se explican en estos apuntes. En estos apuntes siempre utilizaremos el acceso tradicional con el operador []. Por otra parte, los arrays estáticos no suelen estar completamente llenos con valores válidos, sino que sólo una parte del array físico está siendo utilizada. Es decir, la longitud física no tiene por qué coincidir con la longitud lógica, la cual puede variar en tiempo de ejecución. Recuerda: R Siempre que se escriba una expresión entre corchetes [] para acceder a elementos de un array hay que asegurarse que este valor siempre estará en el rango adecuado, tanto físico como lógico. Ejercicios 1. Sin utilizar bucles, escribe un programa que lea 4 números de teclado y los almacene en un array. Después muestra el contenido del array en el orden de sus posiciones. A continuación, rótalos una posición de forma que cada uno quede en la posición siguiente a la suya, y el último quede en la primera posición. Finalmente, vuelve a mostrar en orden los elementos del array con los contenidos ya rotados. 2. Realiza el ejercicio anterior con bucles utilizando la variable de control para obtener los índices. 3. Sin utilizar bucles, escribe un programa que lea 4 pares de caracteres y se almacenen alternativa- mente en dos arrays de 4 posiciones, es decir, el primero tecleado, el tercero, el quinto y el séptimo en uno de los arrays, y el resto en el otro. Después compara las posiciones iguales componente a componente de ambos arrays con el fin de escribir en pantalla los mayores de cada par introducido. 4. Modifica el ejercicio anterior de forma que las comparaciones y la salida en pantalla se realicen con un bucle. 5. Usando bucles, escribe un programa que lea 4 números de teclado en un array y los copie uno a uno en otro array. Saca en pantalla el contenido del segundo. 6. Utilizando bucles, escribe un programa que lea 5 números reales y los guarde en un array. Después calcula las siguientes fórmulas y escribe los resultados en pantalla: S = 5∑ k=1 xk C = 5∑ k=1 x2k M = máx1≤k≤5 |xk| Asignación completa de arrays La característica más importante de los arryas estáticos modernos del estándar 2011 es la capacidad de copiar un array completo en otro con una sola asignación. La copia es física, de forma que se copia tanto las posiciones válidas como las que están sin usar del array. Esta operación crucial, que está prohibida en los arrays tradicionales, permite a su vez el paso de arrays completos como parámetros, y ser devueltos como único valor de una función. El siguiente ejemplo explota estas posibilidades. 72 typedef array <float, 3> TVector3D; ... // prototipo de la funcion TVector3D Adicion3D(const TVector3D v, const TVector3D w); // llamada al subprograma const TVector3D I = {{ 1, 0, 0 }}; TVector3D a, b, suma; ... b = I; // asignacion completa de un array suma = Adicion3D(a, b); // valor devuelto por una funcion ... Sin embargo, tanto el paso por valor de un array como su devolución como valor de una función puede ser costosa computacionalmente si el array es muy grande. Para estos casos es preferible el paso de arrays por referencia, tanto si es de salida como si es de entrada. En este último caso se debe pasar por referencia constante. La alternativa más eficiente al ejemplo anterior sería: // prototipo del subprograma void Adicion3D(const TVector3D &v, const TVector3D &w, TVector &result); ... // llamada Adicion3D(b, c, suma); Arrays incompletos Ya hemos mencionado que es muy habitual que no se conozca en tiempo de compilación1 el tamaño realmente necesario de un array, por eso se suele sobreestimar y usar sólo una parte del array, de forma que los arrays están incompletos. Es decir, la longitud lógica normalmente es menor que la longitud física. El siguiente array de enteros muestra esta situación: longitud lógica = 7 longitud física = 10 125 -15 -98 267 -16 -95 153 - - - 0 1 2 3 4 5 6 7 8 9 En estos casos es fundamental guardar en alguna variable la longitud lógica del array, y es habitual pasar como dos parámetros independientes el array incompleto y su longitud, ya sea de entrada ya sea de salida: // prototipos void UtilizarNotas(const TMarks &notas, unsigned longitud); // de entrada void ModificarNotas(TMarks &notas, unsigned &longitud); // de salida // llamadas UtilizarNotas(notas, cuantas); // TMarks notas; unsigned cuantas ModificarNotas(notas, cuantas); // TMarks notas; unsigned cuantas Otra solución es usar un tipo registro para almacenar cada array con su longitud lógica, permitiendo su paso a subprogramas como parámetro único. En este caso estamos anidando un tipo compuesto, el array, dentro de otro tipo compuesto, el registro. En la sección 5.6 se verán ejemplos usando esta estrategia. A continuación vemos un ejemplo pasando la longitud como un parámetro aparte. 1Cuando el programador edita el programa. 73 #include <iostream> #include <array> using namespace std; // array type of students grades const unsigned MaxStudents = 100; typedef array <float, MaxStudents> TGrades; void ReadGrades(TGrades &grades, unsigned &n) { unsigned i=0; cout <<"How many students: "; cin >>n; do { i++; cout <<"Grade student " <<i <<": "; cin >> grades[i-1]; } while (i<n); } void WriteGrades(const TGrades &grades, const unsigned n, const float threshold) { unsigned i=0; for (i=0; i<n; i++) { if (grades[i]>=threshold) { cout <<"Student " <<i <<": " <<grades[i] <<endl; } } } float MeanGrades(const TGrades &grades, const unsigned n) { float sum=0; unsigned i=0; do { sum += grades[i++]; } while (i<n); return sum/n; } int main() { TGrades marks; float minimum; unsigned howmany; ReadGrades(marks, howmany); cout <<"The mean grade is " <<MeanGrades(marks, howmany) <<endl; cout <<"\nMinimum grade to pass: "; cin >>minimum; WriteGrades(marks, howmany, minimum); } 74 #include <iostream> #include <array> using namespace std; const unsigned Length = 3; // physical length typedef array <float,Length> TVector; typedef array <TVector,Length> TMatrix; void WriteMat(const TMatrix &m) { int i,j; for (i=0; i<Length; i++) { for (j=0; j<Length; j++) { cout << m[i][j] << ' '; } cout <<endl; } } TMatrix AddMat(const TMatrix &a, const TMatrix &b) { TMatrix c; int i, j; for (i=0; i<Length; i++) { for (j=0; j<Length; j++) { c[i][j] = a[i][j] + b[i][j]; } } return c; } int main() { TMatrix m1 = {{ {{1,2,3}}, {{4,5,6}}, {{1,1,1}} }}; TMatrix m2 = {{ {{1,0,0}}, {{1,1,1}}, {{0,1,0}} }}; TMatrix ms; cout <<"First matrix:\n"; WriteMat(m1); cout <<"Second matrix:\n"; WriteMat(m2); cout <<"Their sum:\n"; ms = AddMat(m1, m2); WriteMat(ms); } El tipo base de una matriz también puede ser char. Estudia el siguiente programa que almacena una partida de ajedrez en una matriz de caracteres 8 × 8. Cada casilla contiene un carácter que representa una pieza del juego o la casilla vacía. El programa simplemente pide al usuario movimientos, y muestra a continuación el estado actual de la partida. #include <iostream> #include <array> using namespace std; typedef array <char,8> TRow; 77 typedef array <TRow,8> TChessboard; typedef struct { unsigned row, col; } TSquare; // White pieces are uppercase, black pieces lowercase const char WKing = 'K'; const char BKing = 'k'; const char WQueen = 'Q'; const char BQueen = 'q'; const char WRook = 'R'; const char BRook = 'r'; const char WBishop = 'B'; const char BBishop = 'b'; const char WKnight = 'N'; const char BKnight = 'n'; const char WPawn = 'P'; const char BPawn = 'p'; const char None = ' '; void ShowMatch(const TChessboard &match) { int i,j; // first column numbering line cout <<' '; for (j=0; j<8; j++) { cout <<' ' << j+1; } cout <<endl; for (i=0; i<8; i++) { cout <<i+1 <<' '; // line number for (j=0; j<8; j++) { cout <<match[i][j] <<' '; } cout <<i+1 <<endl; // another line number } cout <<' '; // last column numbering line for (j=0; j<8; j++) { cout <<' ' << j+1; } cout <<endl; } void ShowHorizLine(unsigned ncells, bool borders) { string chunk = borders ? "|---" : " ---"; cout <<" "; for (unsigned i=0; i<ncells; i++) { cout <<chunk; } if (borders) cout <<'|'; cout <<endl; } void ShowNumbersLine() { cout <<" "; for (unsigned j=1; j<=8; j++) { cout <<" " << j <<' '; } cout <<endl; 78 } void ShowChessboard(const TChessboard &match) { int i,j; // first column numbering line ShowNumbersLine(); ShowHorizLine(8,false); for (i=0; i<8; i++) { cout <<' ' <<i+1 <<" |"; // line number for (j=0; j<8; j++) { cout <<' ' <<match[i][j] <<" |"; } cout <<' ' <<i+1 <<endl; // another line number ShowHorizLine(8, false); } cout <<' '; // last numbering line ShowNumbersLine(); } bool ExistsSquare(const TSquare square) { return square.row>0 && square.row<=8 && square.col>0 && square.col<=8; } void MovePiece(TChessboard &match) { TSquare from, to; cout <<"From square (row column)? "; cin >>from.row >>from.col; if (!ExistsSquare(from)) { cout <<"Square ("<<from.row<<','<<from.col<<") does not exists!\n"; } else if (match[from.row-1][from.col-1]==None) { cout <<"In square ("<<from.row<<','<<from.col<<") there is nothing!\n"; } else { cout <<"To square (row column)? "; cin >>to.row >>to.col; if (!ExistsSquare(to)) { cout <<"Square ("<<to.row<<','<<to.col<<") does not exists!\n"; } else { match[to.row-1][to.col-1] = match[from.row-1][from.col-1]; match[from.row-1][from.col-1] = None; } } } int main() { TChessboard mymatch = {{ {{ BRook, BKnight,BBishop,BQueen, BKing, BBishop,BKnight,BRook }}, {{ BPawn, BPawn, BPawn, BPawn, BPawn, BPawn, BPawn, BPawn }}, {{ None, None, None, None, None, None, None, None }}, 79 5.6. Anidamientos Puesto que C++ no impone restricciones en cuanto al tipo base de un array, tenemos una gran flexibilidad para definir complejas estructuras de datos utilizando anidamientos de arrays y registros. A continuación ilustramos las más habituales. Anidamiento dentro de un registro Una de las estructuras más simples son registros anidados dentro de otros registros. Utilizando el símil de relaciones entre los componentes de una familia, el acceso a un campo individual se logra especificando todos sus registros ascendientes con el operador punto ‘.’. La sintaxis de acceso sería: padre.hijo.nieto.biznieto.tataranieto · · · El siguiente subprograma, por ejemplo, usa el tipo TDate anidado dentro del tipo TPerson de la sección 5.2 para imprimir en pantalla el día, mes y año de nacimiento de una persona. void WriteDate(const TPerson &p) { cout <<p.birth.day<<'/'<<p.birth.month<<'/'<<p.birth.year; } También podemos anidar arrays dentro de un registro. La sintaxis de acceso a un componente del array seguirá la sintaxis: registro.arraydedatos[indice] El siguiente programa extiende el de la sección 5.3 que almacenaba las notas de estudiantes, pero esta vez el número de estudiantes, el cual determina la longitud lógica del array, se almacena junto al array en un registro, de forma que ya no es necesario pasar esa longitud como parámetro independiente a los subprogramas. Nota que para evitar copias en memoria innecesarias, los arrays de entrada se pasan por referencia constante. #include <iostream> #include <array> using namespace std; // array type const unsigned MaxStudents = 100; typedef array <float, MaxStudents> TMarks; // array with actual length typedef struct { TMarks marks; unsigned length; } TGrades; void ReadGrades(TGrades &grades) { unsigned n, i=0; cout <<"How many students: "; cin >>n; do { i++; cout <<"Grade student " <<i <<": "; 82 cin >> grades.marks[i-1]; } while (i<n); grades.length = n; } float MeanGrade(const TGrades &grades) { float sum=0; unsigned i; for (i=0; i<grades.length; i++) { sum += grades.marks[i]; } return sum/grades.length; } float MaxGrade(const TGrades &grades) { float maximum=0; unsigned i; for (i=0; i<grades.length; i++) { if (grades.marks[i]>maximum) maximum = grades.marks[i]; } return maximum; } int main() { TGrades grades; ReadGrades(grades); cout <<"The mean grade is " <<MeanGrade(grades) <<endl; cout <<"The maximum grade is " <<MaxGrade(grades) <<endl; } Anidamientos dentro de un array Una estructura muy habitual son los arrays de registros. Por ejemplo, un array unidimensional de datos personales. 23415678X 234182378M 14415678G 86715678J 78675678P ... 56116678Z Saturnino Hurraca Restituto Elisenda Eufemia ... Indalina Diaz Saenz Lopez Perez Gonzalez ... Martinez 9/6/1955 28/12/2009 12/10/2007 3/8/2001 18/8/1965 ... 2/6/1988 La definición del registro anidado debe hacerse antes que la definición del registro o array anfitrión: typedef struct { int day, month, year; } TDate; typedef struct { string id; string first_name, last_name; TDate birth; 83 } TPerson; const unsigned NPeople = 1000; // la longitud fisica typedef array <TPerson, NPeople> TPeople; // el tipo array TPeople plantilla; // el array La sintaxis de acceso a campos de un registro individual dentro de un array es: miarray[indice].campo R Como norma general para una escritura correcta de accesos a estructuras de datos arbitrariamente complejas, después de un dato de tipo array se escribirá el operador corchete [], y después de un dato de tipo registro se escribirá el nombre de uno de sus campos. El siguiente procedimiento accede a campos de registros contiguos de un array. Observa que es respon- sabilidad de la llamada que la longitud lógica howmany sea correcta. void WritePeopleNames(const TPeople &everyone, const unsigned howmany) { for (unsigned i=0; i<howmany; i++) { cout <<everyone[i].first_name <<' ' <<everyone[i].last_name <<endl; } } Cuando se trabaja con arrays incompletos, las componentes vacías se pueden manejar con estrategias muy diversas. Dos estrategias habituales son: Mantener las componentes contiguas, lo que implica que hay que guardar la longitud lógica del array en algún sitio. Esta es la estrategia que hemos venido utilizando hasta ahora. Permitir que las componentes vacías estén mezcladas con las que contienen valores válidos, en cuyo caso ya no tiene sentido la longitud lógica, y los bucles que recorren el array deben utilizar como límite la longitud física. En este caso, las componentes vacías deben marcarse de alguna manera con algún valor especial que contengan, valor que servirá para detectarlas con la sentencia de selección adecuada dentro de los bucles. El siguiente programa utiliza esta segunda estrategia, y los componentes vacíos se marcan con la cadena vacía "" en el campo identificador de cada registro. Contabilizando los registros no marcados también se puede determinar el número de elementos del array. #include <iostream> #include <array> using namespace std; // individual data type typedef struct { int day, month, year; } TDate; typedef struct { string id; string first_name, last_name; TDate birth; } TPerson; // the array of data type const unsigned MaxPeople = 10; 84 * * Unused records are marked with an empty identificator "", * and can be mixed among the used ones. */ int main() { TPerson another, who; string id; unsigned where; // the order of initialized data must conform the order of definition TPeople everyone = {{ // data { "1A", "Saturnino", "Diaz", { 9/ 6/1955} }, { "", "Noname", "Noone", { 9/ 6/1955} }, //empty { "2B", "Hurraca", "Saenz", {28,12,2009} }, { "3C", "Restituto","Lopez", {12,10,2007} }, { "4D", "Elisenda", "Perez", { 3, 8,2001} }, { "", "Anyone", "Someone", { 3, 8,2001} }, // empty { "5E", "Eufemia", "Gonzalez", {18, 8,1965} }, { "6F", "Indalina", "Martinez", { 2, 6,1988} } }}; cout <<"Everyone includes " <<NPeople(everyone)<<" people:\n"; WritePeople(everyone); // accept data of a new person cout <<"\nA new person to insert\n"; ReadPerson(another); // try to insert the new data if (!InsertPerson(everyone, another)) { cout <<"Sorry! Not enough space for a new person\n"; } else { cout <<"Ok, inserted\n"; } // search and remove a person cout <<"\nIdentity of person to remove: "; cin >>id; if (SearchPerson(everyone, id, who, where)) { cout <<"\nThe following person is to be removed:\n"; WritePerson(who); RemovePerson(everyone, where); } else { cout <<"Sorry! Identificator "<<id<<" not found\n"; } cout <<"\nNow everyone includes:\n"; WritePeople(everyone); } Anidamiento de un array de registros dentro de otro registro El siguiente programa es el mismo ejemplo que el anterior, pero utilizando la estrategia de mantener contiguos los registros válidos. Además, la longitud lógica se almacena anidando ésta junto al array de datos en otro registro mayor que contiene la estructura de datos completa. Ahora la sintaxis de acceso a 87 uno de los campos de un registro individual sería: estructura.arraydedatos[indice].campo Si queremos referenciar un registro individual completo, simplemente omitimos el último operador punto ‘.’ y el nombre del campo individual: estructura.arraydedatos[indice] #include <iostream> #include <array> using namespace std; // individual data type typedef struct { int day, month, year; } TDate; typedef struct { string id; string first_name, last_name; TDate birth; } TPerson; // the array of data type const unsigned MaxPeople = 10; typedef array <TPerson, MaxPeople> TData; // the whole type typedef struct { unsigned npeople; TData data; } TPeople; void ReadPerson(TPerson &p) { cout <<"Id: "; cin >>p.id; // no spaces inbetween cout <<"First name: "; cin >>ws; // skip last user <enter> from keyboard buffer getline(cin, p.first_name); // read until \n cout <<"Last name: "; // no need to remove last \n, already read by last getline getline(cin, p.last_name); // read until \n cout <<"Date of birth (dd mm yyyy): "; cin >>p.birth.day>>p.birth.month>>p.birth.year; } void WritePerson(const TPerson &p) { cout <<"Id: " <<p.id <<endl <<"First name: " <<p.first_name <<endl <<"Last name: " <<p.last_name <<endl <<"Date of birth (dd/mm/yyyy): " <<p.birth.day<<'/'<<p.birth.month<<'/'<<p.birth.year <<endl; } 88 void WritePerson1Line(const TPerson &p) // without labels, in 1 line { cout <<p.id <<", " <<p.first_name <<", " <<p.last_name <<", " <<p.birth.day<<'/'<<p.birth.month<<'/'<<p.birth.year<<endl; } void WritePeople(const TPeople &people) { unsigned i; for (i=0; i<people.npeople; i++) { WritePerson1Line(people.data[i]); } } bool SearchPerson(const TPeople &people, const string wanted, TPerson &who, unsigned &position) // return true if found, false otherwise { bool found=false; unsigned i=0; while (!found && i<people.npeople) { // strings can be compared with primtive operators if (people.data[i].id == wanted) { who = people.data[i]; position = i; found = true; } else { i++; } } return found; } bool InsertPerson(TPeople &people, const TPerson &person) // return true if there has been enough space, false otherwise { bool inserted; // the logical length cannot exceed the available physical space if (people.npeople < MaxPeople) { people.data[people.npeople] = person; people.npeople++; // another record is busy inserted = true; } else { inserted = false; } return inserted; } 89
Docsity logo



Copyright © 2024 Ladybird Srl - Via Leonardo da Vinci 16, 10126, Torino, Italy - VAT 10816460017 - All rights reserved