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():

    1. Se programa la función

    2. Se diseña un test para esa función, dentro del main()

    3. 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:

  1. Solicite dos operandos enteros al usuario

  2. 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.

  3. Mostrará por pantalla el resultado.

  4. 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:

  1. Una división por 0

  2. 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()
{
   ...
}

Edita, compila y ejecuta el código

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:

  1. Se solicitará dos operandos enteros al usuario

  2. Se calculará el producto usando el algoritmo de multiplicación hardware

  3. Se mostrará por pantalla el resultado.

  4. 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.

Edita, compila y ejecuta el código

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)
{
   ...
}

Edita, compila y ejecuta el código

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:

  1. Solicitará un valor para la tolerancia, exigiendo que sea un real positivo.

  2. Solicitará un número real del que se calculará su raíz cuadrada.

  3. Se calculará la raíz cuadrada o finalizará el programa lanzando un mensaje de error si el número es negativo.

  4. 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;
}

Edita, compila y ejecuta el código

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.