Ejemplos¶
En los siguientes ejemplos vamos a utilizar todas las herramientas de programación vistas hasta el momento:
Entrada y salida de datos
Control de flujo condicional
Control de flujo iterativo
Saltos incondicionales
Funciones
Un procedimiento que se observa con frecuencia en el programador novel es desarrollar casi al mismo tiempo todo el código de un programa, como si de una estructura monolítica se tratara. Esta estrategia conduce normalmente a un tortuoso proceso iterativo de corrección de errores antes de obtener, en el mejor de los casos, un código satisfactorio.
Como alternativa, en los ejemplos que siguen se propondrá un proceso de construcción del programa consistente en:
Para cada función diferente de
main()
:Se programa la función
Se diseña un test para esa función, dentro del
main()
Una vez validada, se borra o comenta el código del test
Se programa la función
main()
Se prueba el programa para diferentes casos
De esta forma, antes de su uso definitivo dentro de main()
o
por parte de otra función, tendremos depurada a nivel sintáctico
y semántico todas y cada una de las funciones.
Calculadora aritmética de enteros¶
Descripción del programa¶
La idea es implementar un programa que realice las siguientes tareas:
Solicite dos operandos enteros al usuario
Solicite el operador aritmético. Si el operador no es válido, se lanzará un mensaje de error y se volverá a solicitar hasta que el usuario introduzca un operador correcto.
Mostrará por pantalla el resultado.
Preguntará al usuario si desea realizar otro cálculo. En caso afirmativo se volverá al punto 1. En caso contrario, finalizará el programa.
Diseño del programa¶
Existen múltiples opciones para resolver el problema. Otras alternativas a las aquí planteadas pueden ser igualmente válidas.
Comenzaremos por programar el código correspondiente a los datos de entrada.
Introducción de los operandos¶
Vamos a introducir los dos operandos usando una única función. Como
necesitamos devolver dos valores, no es posible hacerlo (con los
conocimientos actuales) mediante return
, puesto que con return
solo podemos devolver una única variable.
La solución es pasar los operandos como referencias.
La función tendrá prototipo:
void introduce_operandos(int&, int&)
Una posible implementación es:
void introduce_operandos(int& num1, int& num2)
{
cout << "Primer operando: ";
cin >> num1;
cout << "Segundo operando: ";
cin >> num2;
}
Incluso para una función tan simple como la anterior, es importante probar su corrección, tanto sintáctica como semántica.
Para ello, diseñamos la siguiente prueba en la función main()
:
#include <iostream>
using namespace std;
void introduce_operandos(int& num1, int& num2);
int main()
{
// TEST FUNCION introduce_operandos()
int num1, num2;
introduce_operandos(num1, num2);
cout << num1 << " " << num2 << endl;
}
void introduce_operandos(int& num1, int& num2)
{
cout << "Primer operando: ";
cin >> num1;
cout << "Segundo operando: ";
cin >> num2;
}
Edita, compila y ejecuta el código
Como puede observarse, la prueba consiste únicamente en llamar a la función y visualizar los valores devueltos.
Introducción del operador¶
Para representar los operadores vamos a utilizar los caracteres
+
, -
, *
, /
y %
.
Solicitaremos el operador con una función con prototipo:
char introduce_operador();
La función no tiene argumentos y devuelve el operador a través de una
variable tipo char
. Esta función garantizará que el operador
devuelto es válido.
Una posible implementación es la siguiente:
char introduce_operador()
{
char op;
bool error;
do
{
cout << "Introduzca el operador: ";
cin >> op;
error = false;
switch (op)
{
case '+':
case '-':
case '*':
case '/':
case '%':
break;
default:
cout << "ERROR: El operador introducido no es valido.\n";
error = true;
break;
}
}while (error);
return op;
}
El test en main()
podría ser el siguiente:
#include <iostream>
using namespace std;
void introduce_operandos(int& num1, int& num2);
char introduce_operador();
int main()
{
// TEST FUNCION introduce_operandos()
// int num1, num2;
// introduce_operandos(num1, num2);
// cout << num1 << " " << num2 << endl;
// TEST FUNCION introduce_operador()
char op = introduce_operador();
cout << op << endl;
}
void introduce_operandos(int& num1, int& num2)
{
...
}
char introduce_operador()
{
...
}
Edita, compila y ejecuta el código
Se han comentado las sentencias del test anterior, realizándose un test similar. Para comprobar bien la función, el alumno debería repetir la ejecución del programa introduciendo los 5 operadores y algunos caracteres no válidos.
Implementación de la calculadora¶
Para la calculadora usaremos una función con prototipo:
int calculadora(int, int, char);
que recibe los operandos y el operador y devuelve el resultado.
Una posible implementación es la siguiente:
int calculadora(int x, int y, char op)
{
int resultado;
switch (op)
{
case '+':
resultado = x + y;
break;
case '-':
resultado = x - y;
break;
case '*':
resultado = x*y;
break;
case '/':
if (y != 0)
resultado = x/y;
else
{
cout << "ERROR: División por 0.\n";
exit(EXIT_FAILURE);
}
break;
case '%':
resultado = x % y;
break;
default:
cout << "ERROR: El operador utilizado no es valido.\n";
exit(EXIT_FAILURE);
}
return resultado;
}
Nótese que se ha utilizado la función exit()
para dar por
finalizado el programa si se dan dos situaciones excepcionales:
Una división por 0
Uso de un operador no válido
Los lenguajes de programación contemplan para este tipo de casos manejadores de excepciones, herramientas que permiten programáticamente reconducir el error sin necesidad de interrumpir la ejecución del programa. Dejamos estos instrumentos para un curso más avanzado.
Por otro lado, podría argumentarse que nuestro programa ya
filtra en la función introduce_operador()
los casos
en los que el usuario introduce un carácter inválido. Sin
embargo, no debe olvidarse que las funciones se caracterizan
por ser reutilizables y, quizás, en otro programa de aplicación,
ese filtro no exista y, de esta forma, nuestra función
calculadora()
seguirá siendo robusta ante errores de
este tipo.
Advertencia
La función exit()
, dependiendo del compilador, suele estar
incluida en la biblioteca iostream
. Si no es el caso, bastaría incluir
el archivo de cabecera de la biblioteca <cstdlib>
.
Pasemos a la batería de pruebas para la función
calculadora(int, int, char)
:
#include <iostream>
using namespace std;
void introduce_operandos(int& num1, int& num2);
char introduce_operador();
int calculadora(int, int, char);
int main()
{
// TEST FUNCION introduce_operandos()
// int num1, num2;
// introduce_operandos(num1, num2);
// cout << num1 << " " << num2 << endl;
// TEST FUNCION introduce_operador()
// char op = introduce_operador();
// cout << op << endl;
// TEST FUNCION calculadora()
cout << calculadora(13, 5, '+') << endl;
cout << calculadora(13, 5, '-') << endl;
cout << calculadora(13, 5, '*') << endl;
cout << calculadora(13, 5, '/') << endl;
cout << calculadora(13, 5, '%') << endl;
cout << calculadora(13, 5, '&') << endl; // exit()
cout << calculadora(13, 0, '/') << endl; // exit()
}
int calculadora(int x, int y, char op)
{
...
}
void introduce_operandos(int& num1, int& num2)
{
...
}
char introduce_operador()
{
...
}
Edita, compila y ejecuta el código
En este caso, probamos los distintos operadores con los valores
escogidos a voleo 13
y 5
. También probamos que se detectan los dos
posibles errores. En el ejemplo, con el operador erróneo
&
se ejecutará exit()
y, por tanto, no llegará
a comprobarse la división por 0. Para probar
este último error, basta comentar la línea anterior y
volver a ejecutar el programa.
Implementación de main()
¶
Una vez testadas las tres funciones, es el momento de ponerlas a trabajar conjuntamente. Nótese que puede reutilizarse parte del código usado para las pruebas.
La función main()
podría tener el siguiente aspecto:
int main()
{
int num1, num2;
introduce_operandos(num1, num2);
char op = introduce_operador();
cout << num1 << op << num2 << "=" << calculadora(num1, num2, op) << endl;
}
Edita, compila y ejecuta el código
Por último, tenemos que incorporar la opción de que el usuario
pueda realizar más cálculos. Para ello, vamos a utilizar una
variable tipo char
que recoja una respuesta s
o
S
ante la pregunta Desea realizar una nueva operacion?
.
La estructura do-while
es la idónea para resolver este patrón
habitual en muchos programas.
char respuesta;
do
{
...
cout << "\nDesea realizar una nueva operacion?\n"
<< "Conteste s o S para continuar.\n"
<< "Respuesta: ";
cin >> respuesta;
} while (respuesta == 's' || respuesta == 'S');
Lo único que falta es integrar el código actual de main()
sustituyendo a los puntos ...
.
#include <iostream>
using namespace std;
void introduce_operandos(int& num1, int& num2);
char introduce_operador();
int calculadora(int, int, char);
int main()
{
char respuesta;
do
{
int num1, num2;
introduce_operandos(num1, num2);
char op = introduce_operador();
cout << num1 << op << num2 << "=" << calculadora(num1, num2, op) << endl;
cout << "\nDesea realizar una nueva operacion?\n"
<< "Conteste s o S para continuar.\n"
<< "Respuesta: ";
cin >> respuesta;
} while (respuesta == 's' || respuesta == 'S');
}
int calculadora(int x, int y, char op)
{
...
}
void introduce_operandos(int& num1, int& num2)
{
...
}
char introduce_operador()
{
...
}
Simulación de la multiplicación entera hardware¶
Descripción del programa¶
La idea es implementar el programa de multiplicación hardware visto en los ejemplos del tema de Control de flujo iterativo, pero ahora usando funciones. Para ello:
Se solicitará dos operandos enteros al usuario
Se calculará el producto usando el algoritmo de multiplicación hardware
Se mostrará por pantalla el resultado.
Se preguntará al usuario si desea realizar otro cálculo. En caso afirmativo se volverá al punto 1. En caso contrario, finalizará el programa.
Puede observarse que los puntos 1, 2 y 4 son prácticamente idénticos a los del ejemplo anterior. Es importante que el alumno reconozca patrones en los diferentes problemas que se le presentan: «mismos perros con distinto collar».
Diseño del programa¶
Introducción de los operandos¶
Aquí tenemos un claro ejemplo de reutilización de código. La función
void introduce_operandos(int&, int&)
del ejemplo anterior nos sirve,
no necesitamos ninguna modificación. Además, ya la hemos sometido a un test
de pruebas.
void introduce_operandos(int& num1, int& num2)
{
cout << "Primer operando: ";
cin >> num1;
cout << "Segundo operando: ";
cin >> num2;
}
Algoritmo de la multiplicación hardware¶
Este algoritmo lo encapsularemos usando una función con prototipo:
int producto_hardware(int, int);
que recibirá los dos operandos y devuelve su producto.
Una posible implementación es la siguiente:
int producto_hardware(int x, int y)
{
int producto = 0;
int signo = 1;
if (x != 0 && y != 0) // Si alguno es 0 no hacemos nada, producto=0.
{
// La variable contador cuenta los cambios de signo realizados. Si solo
// se realiza un cambio, el producto será negativo.
int contador = 0;
if (x < 0)
{
x *= -1;
++contador;
}
if (y < 0)
{
y *= -1;
++contador;
}
if (contador == 1)
signo = -1;
while (y)
{
if (y % 2) // Es impar
{
producto += x;
y = y - 1;
}
else
{
x = x*2;
y = y/2;
}
}
}
return signo*producto;
}
Nótese que este código es básicamente un corta y pega de la versión sin funciones. No obstante, se ha introducido una variante para el cálculo del signo del resultado. Es una muestra de que las soluciones a un problema pueden tener múltiples variantes.
Pasemos a la batería de pruebas para la función
int producto_hardware(int, int)
:
#include <iostream>
using namespace std;
void introduce_operandos(int&, int&);
int producto_hardware(int, int);
int main()
{
// DOS PARES
cout << producto_hardware(8, 4) << endl;
cout << producto_hardware(4, 8) << endl;
// DOS IMPARES
cout << producto_hardware(3, 5) << endl;
cout << producto_hardware(5, 3) << endl;
// PAR IMPAR
cout << producto_hardware(8, 3) << endl;
cout << producto_hardware(3, 8) << endl;
// COMPROBANDO SIGNOS
cout << producto_hardware(5, -3) << endl;
cout << producto_hardware(-5, 3) << endl;
cout << producto_hardware(-5, -3) << endl;
//COMPROBANDO OPERANDOS 0
cout << producto_hardware(0, 3) << endl;
cout << producto_hardware(3, 0) << endl;
cout << producto_hardware(-3, 0) << endl;
cout << producto_hardware(0, -3) << endl;
cout << producto_hardware(0, 0) << endl;
}
int producto_hardware(int x, int y)
{
...
}
void introduce_operandos(int& num1, int& num2)
{
...
}
Como puede observarse se han probado toda una serie de situaciones para las que quizás el algoritmo pudiese estar mal implementado.
Implementación de main()
¶
De nuevo, destacar que la función main()
sigue un patrón similar a la
del ejemplo de la calculadora.
Una posible implementación sería la siguiente:
#include <iostream>
using namespace std;
void introduce_operandos(int&, int&);
int producto_hardware(int, int);
int main()
{
char respuesta;
do
{
int num1, num2;
introduce_operandos(num1, num2);
cout << num1 << '*' << num2 << "=" << producto_hardware(num1, num2) << endl;
cout << "\nDesea realizar una nueva operacion?\n"
<< "Conteste s o S para continuar.\n"
<< "Respuesta: ";
cin >> respuesta;
} while (respuesta == 's' || respuesta == 'S');
}
int producto_hardware(int x, int y)
{
...
}
void introduce_operandos(int& num1, int& num2)
{
...
}
Cálculo de la raíz cuadrada mediante el método de bisección¶
Descripción del problema¶
La idea es partir de un rango \([inf,sup]\) inicial de valores entre los que sepamos que debe encontrarse la raíz cuadrada.
En función del valor inicial \(x\) del que queremos calcular su raíz cuadrada, podemos establecer los siguientes casos para determinar el rango inicial \([inf,sup]\):
Si \(x>1\), el rango inicial será \([1,x]\)
Si \(x<1\), el rango inicial será \([0,1]\)
Si el valor inicial \(x\) es \(0\) ó \(1\) se dará la respuesta obvia sin hacer ningún cálculo.
Para dar por finalizado el proceso de aproximación iterativo se utilizará una tolerancia \(\epsilon\) suficientemente pequeña.
En cada iteración se ensayará con el valor \(\sqrt{x} \simeq y=(inf+sup)/2\).
Si \(|y*y-x|<\epsilon\) se dará por finalizado el bucle y el valor \(y \simeq \sqrt{x}\) será el valor aproximado buscado.
En caso contrario,
Si \(y*y-x > 0\) se actualizará \(sup = y \,\) (nos hemos pasado de largo)
Si \(y*y-x < 0\) se actualizará \(inf = y \,\) (nos hemos quedado cortos)
Descripción del programa¶
El programa realizará los siguientes pasos:
Solicitará un valor para la tolerancia, exigiendo que sea un real positivo.
Solicitará un número real del que se calculará su raíz cuadrada.
Se calculará la raíz cuadrada o finalizará el programa lanzando un mensaje de error si el número es negativo.
Se mostrará por pantalla el resultado.
Diseño del programa¶
Introducción de la tolerancia y el valor \(x\)¶
Utilizamos una función con prototipo:
double pide_tolerancia();
Para garantizar que la tolerancia sea positiva utilizaremos
el patrón habitual consistente en un bucle do-while
.
Una posible implementación es la que sigue:
double pide_tolerancia()
{
double tol;
do
{
cout << "Tolerancia:";
cin >> tol;
if (tol <= 0)
cout << "\nERROR: La tolerancia debe ser positiva.\n";
}while (tol <= 0);
return tol;
}
Y para probarla bastaría con introducir desde teclado un valor negativo, un valor 0 y un valor positivo verificando que la salida es la correcta en cada caso.
#include <iostream>
using namespace std;
double pide_tolerancia();
int main()
{
cout << pide_tolerancia() << endl;
}
double pide_tolerancia()
{
...
}
Edita, compila y ejecuta el código
Para introducir el valor \(x\) la solución es prácticamente idéntica a la
anterior con excepción del bucle do-while
.
double pide_valor()
{
double x;
cout << "x: ";
cin >> x;
return x;
}
Edita, compila y ejecuta el código
Obviamos reflejar el program de prueba que no tiene ningún misterio.
Cálculo de la raíz cuadrada por bisección¶
Para resolver el algoritmo proponemos una función con prototipo:
double sqrt_biseccion(double x, double tol);
El primer argumento será el valor y el segundo la tolerancia.
Dado que, en el caso general, se realizará al menos una iteración,
lo lógico es utilizar un bucle do-while
.
Si el usuario usa un valor inapropiado para alguno de los parámetros se lanzará un mensaje de error y se dará por finalizado el programa. Para ello nos ayudamos de la función auxiliar con prototipo:
void manejo_errores(double, double)
que permite aligerar el código de la función sqrt_biseccion()
.
Una posible implementación es la que sigue:
void manejo_errores(double x, double tol)
{
if (x < 0)
{
cout << "ERROR: no esta definida la raiz cuadrada de reales negativos.\n";
exit(EXIT_FAILURE);
}
if (tol <= 0)
{
cout << "ERROR: la tolerancia debe ser un valor positivo.\n";
exit(EXIT_FAILURE);
}
}
Una batería de pruebas podría es:
#include <iostream>
#include <cmath>
using namespace std;
...
void manejo_errores(double x, double tol);
int main()
{
// cout << pide_tolerancia() << endl;
// cout << pide_valor() << endl;
manejo_errores(1., 0.01); // Ok
manejo_errores(-1., 0.01); // exit() debido a x<0
manejo_errores(1., -1.); // exit() debido a tol<0
manejo_errores(1., 0); // exit() debido a tol=0
}
...
Una posible implementación del algoritmo de bisección para la raíz cuadrada es la que sigue:
double sqrt_biseccion(double x, double tol)
{
manejo_errores(x, tol);
double y;
if (x == 0)
y = 0.;
else if (x == 1)
y = 1.;
else
{
double inf = 1., sup = x;
if (x < 1.)
{
inf = 0.;
sup = 1.;
}
bool salida = false;
do
{
y = (inf + sup)/2.;
double dif = y*y - x;
if (fabs(dif) < tol)
salida = true;
else
if (dif > 0.)
sup = y;
else
inf = y;
}while (!salida);
}
return y;
}
La batería de pruebas debe verificar diferentes aspectos:
La precisión obtenida se adecua a la tolerancia establecida
Funciona bien para valores superiores e inferiores a 1
Da el resultado esperado para los casos especiales 0 y 1
Genera los mensajes de error cuando los parámetros de entrada son incorrectos (aunque esto en principio ya se ha comprobado).
Una posible batería para la función sqrt_biseccion()
es la que sigue:
#include <iostream>
#include <cmath>
using namespace std;
double pide_tolerancia();
double pide_valor();
double sqrt_biseccion(double, double);
int main()
{
// cout << pide_tolerancia() << endl;
// cout << pide_valor() << endl;
double tol = 1.e-3;
double x = 1000.;
double y = sqrt_biseccion(x, tol);
cout.precision(20);
// Probamos para x=1000 y tol=1.e-3
cout << "sqrt(" << x << ")=" << y << endl;
if (fabs(y*y-x) >= tol)
cout << "ERROR: "<< fabs(y*y-x) << " no cumple la tolerancia " << tol << endl;
// Probamos para x=1000 y tol=1.e-9
tol = 1.e-9;
y = sqrt_biseccion(x, tol);
cout << "sqrt(" << x << ")=" << y << endl;
if (fabs(y*y-x) >= tol)
cout << "ERROR: "<< fabs(y*y-x) << " no cumple la tolerancia " << tol << endl;
// Probamos para x=0.5 y tol=1.e-9
x = 0.5;
y = sqrt_biseccion(x, tol);
cout << "sqrt(" << x << ")=" << y << endl;
if (fabs(y*y-x) >= tol)
cout << "ERROR: "<< fabs(y*y-x) << " no cumple la tolerancia " << tol << endl;
// Probamos los casos especiales 0 y 1
y = sqrt_biseccion(0., tol);
cout << "sqrt(" << x << ")=" << y << endl;
if (y != 0)
cout << "ERROR: El resultado debe ser 0.\n";
y = sqrt_biseccion(1., tol);
cout << "sqrt(" << x << ")=" << y << endl;
if (y != 1)
cout << "ERROR: El resultado debe ser 1.\n";
// Probamos un valor negativo en la x
sqrt_biseccion(-1, tol); // exit()
sqrt_biseccion(x, 0); // exit()
}
void manejo_errores(double x, double tol)
{
...
}
double sqrt_biseccion(double x, double tol)
{
...
}
double pide_valor()
{
...
}
double pide_tolerancia()
{
...
}
Edita, compila y ejecuta el código
Nótese que, aunque la batería parece prolija, construirla es fácil, ya que son bloques prácticamente idénticos, editados mediante copiar y pegar.
También puede observarse que no se ha realizado una declaración explícita
para la función manejo_errores()
. En este caso, no es necesario puesto
la definición se encuentra en el código antes que la llamada que de ella
se hace en la función sqrt_biseccion()
. Si intercambiamos el orden
de estas dos funciones,
entonces el enlazador generaría un mensaje de error.
Implementación de main()
¶
Reutilizando parte del código de los test de pruebas, la función
main()
se programa de forma sencilla:
int main()
{
double tol = pide_tolerancia();
double x = pide_valor();
double y = sqrt_biseccion(x, tol);
cout.precision(20);
cout << "sqrt(" << x << ")=" << y << endl;
}
Variante del problema¶
El método de bisección propuesto genera en cada iteración un intervalo \([inf,sup]\) cada vez más pequeño. Por ello, tarde o temprano, en ausencia de una tolerancia como criterio de parada, el valor \(y\) convergerá a uno de los límites. Una posible solución sin usar el parámetro tolerancia sería:
double sqrt_biseccion(double x)
{
manejo_errores(x, 1);
double y;
if (x == 0)
y = 0.;
else if (x == 1)
y = 1.;
else
{
double inf = 1., sup = x;
if (x < 1.)
{
inf = 0.;
sup = 1.;
}
bool salida = false;
do
{
y = (inf + sup)/2.;
if (y == inf || y == sup) // CONVERGENCIA
{
salida = true;
double dif1 = inf*inf - x;
double dif2 = sup*sup - x;
if (fabs(dif1) < fabs(dif2))
y = inf;
else
y = sup;
}
else
{
double dif = y*y - x;
if (dif > 0.)
sup = y;
else if (dif < 0.)
inf = y;
else // VALOR EXACTO
salida = true;
}
}while (!salida);
}
return y;
}
Edita, compila y ejecuta el código
La convergencia se produce porque llega un momento, cuando \(inf \simeq sup\), en que no existe la representación binaria exacta para el valor \((inf + sup)/2\), y el resultado acaba siendo uno de los dos valores. Por tanto, el valor de convergencia será bien \(y = inf\) bien \(y = sup\). Para decidir cual de los dos escoger basta utilizar aquel cuyo cuadrado esté más cerca del valor de partida \(x\).
En la solución mostrada en el enlace observe como coexisten sin problema las dos versiones del algoritmo de bisección, debido a que C++ soporta la sobrecarga.
Véase que la versión sin tolerancias da el mismo resultado
(aunque de forma menos eficiente) que la versión sqrt()
de la biblioteca cmath
.