AnteriorPosterior

9b. Funciones (2)

  Curso: Introducción a C++

9.6. Modificar el valor de un parámetro

Como ya hemos visto, cuando un dato se recibe como parámetro, los cambios que hagamos en su valor no se reflejan al salir de la función. Esto se debe a que realmente se trabaja sobre "una copia" de ese dato, no sobre el dato original. Esa forma de trabajar se llama "pasar parámetros por valor".

Pero en alguna ocasión nos puede interesar que sí se pueda modificar el valor de un parámetro, lo que llamaremos "pasar un parámetro por referencia". Hay dos casos especialmente frecuentes:

  • Cuando tengamos que devolver más de un valor. Sabemos cómo hacer que una función devuelva un resultado, pero si tiene que devolver dos (por ejemplo, las dos soluciones de una ecuación de segundo grado), esto no se puede conseguir de la forma convencional. Hay dos alternativas para conseguirlo: la "mala" es devolver un array que contenga ambos valores; la "buena" es que esos dos datos se devuelvan como parámetros modificados.
  • También puede ocurrir que estemos pasando datos muy grandes como parámetros (un array o un struct de gran tamaño, por ejemplo). En ese caso, el pasar los datos "por referencia" hace que el programa sea ligeramente más rápido, porque evitamos hacer una copia de esos datos de gran tamaño. A cambio, existe el riesgo de modificar los datos sin querer.

La forma de indicar que un parámetro es "por referencia" (que queremos permitir que su valor se pueda modificar) es incluir un símbolo "&" entre el tipo de la variable y el nombre de la variable:

// Introducción a C++, Nacho Cabanes
// Ejemplo 09.10:
// Modificar el valor de un parámetro (estilo C++)
 
#include <iostream>
using namespace std;
 
void duplica(int & x) 
{
    x = x * 2;
}
 
int main() 
{
    int n = 5;
    cout << "n vale " << n << endl;
    duplica(n);
    cout << "Ahora n vale " << n << endl;
 
    return 0;   
}
 

En cuanto al "sitio" en que se coloca ese símbolo, hay gente que prefiere escribir junto al tipo de datos: "int& x", hay quien prefiere que esté junto a la variable: "int &x" y hay quien prefiere dejar un espacio entre medias: "int & x". En los tres casos el comportamiento será el mismo.

En ocasiones, podemos encontrar fuentes que pasan parámetros por referencia usando la sintaxis de C, que es más enrevesada que la de C++. En C, el símbolo "&" se usa en la llamada a la función, mientras que en la cabecera de ésta se usa un "asterisco" (*) y en el cuerpo de la función se debe escribir también el nombre de la variable precedido por un asterisco (las razones exactas las veremos más adelante, cuando hablemos de memoria dinámica):

// Introducción a C++, Nacho Cabanes
// Ejemplo 09.10b:
// Modificar el valor de un parámetro (estilo C)
 
#include <iostream>
using namespace std;
 
void duplica(int *x) 
{
    *x = *x * 2;
}
 
int main() 
{
    int n = 5;
    cout << "n vale " << n << endl;
    duplica(&n);
    cout << "Ahora n vale " << n << endl;
 
    return 0;   
}
 

Para duplicar un valor no hace falta pasar parámetros por referencia, sino que nos habría bastado con devolver el nuevo valor. Un ejemplo real en el que sí se necesitarían parámetros por referencia es el de intercambiar los valores de dos variables. Se podría conseguir así:

// Introducción a C++, Nacho Cabanes
// Ejemplo 09.11:
// Intercambiar el valor de dos parámetros (estilo C++)
 
#include <iostream>
using namespace std;
 
void intercambia(int & x, int & y) 
{
    int auxiliar;
    auxiliar = x;
    x = y;
    y = auxiliar ;
}
 
int main() 
{
    int a = 5;
    int b = 12;
    cout << "a es " << a << " y b es "
        << b << endl;
 
    intercambia(a, b);
    cout << "Ahora a es " << a << " y b es "
        << b << endl;
 
    return 0;
}
 

Y con la sintaxis de C quedaría así:

// Introducción a C++, Nacho Cabanes
// Ejemplo 09.11b:
// Intercambiar el valor de dos parámetros (estilo C)
 
#include <iostream>
using namespace std;
 
void intercambia(int *x, int *y) 
{
    int auxiliar;
    auxiliar = *x;
    *x = *y;
    *y = auxiliar ;
}
 
int main() 
{
    int a = 5;
    int b = 12;
    cout << "a es " << a << " y b es "
        << b << endl;
 
    intercambia(&a, &b);
    cout << "Ahora a es " << a << " y b es "
        << b << endl;
 
    return 0;
}
 

Ejercicios propuestos:

  • (9.6.1) Crear un programa que resuelva ecuaciones de segundo grado, del tipo ax2 + bx + c = 0 El usuario deberá introducir los valores de a, b y c. Pista: la solución se calcula con
    x = +- raíz (b2 – 4·a·c) / 2·a Como hay dos soluciones x1 y x2, deberás crear una función que use parámetros por referencia para devolver sus valores. Si alguna de las soluciones no existe, devolverás un valor prefijado: -9999. Si no sabes cómo calcular raíces cuadradas, puedes aplazar este ejercicio hasta leer el siguiente apartado.

9.7. Algunas funciones útiles

En la bilioteca estándar de C++ existen muchas funciones ya credas, que nos pueden resultar útiles. Vamos a comentar algunas de ellas.

9.7.1. Funciones matemáticas

Dentro del fichero de cabecera "math.h" tenemos acceso a muchas funciones matemáticas predefinidas en C, como:

  • acos(x): Arco coseno
  • asin(x): Arco seno
  • atan(x): Arco tangente
  • atan2(y,x): Arco tangente de y/x (por si x o y son 0)
  • ceil(x): El valor entero superior a x y más cercano a él
  • cos(x): Coseno
  • cosh(x): Coseno hiperbólico
  • exp(x): Exponencial de x (e elevado a x)
  • fabs(x): Valor absoluto
  • floor(x): El mayor valor entero que es menor que x
  • fmod(x,y): Resto de la división x/y
  • log(x): Logaritmo natural (o neperiano, en base "e")
  • log10(x): Logaritmo en base 10
  • pow(x,y): x elevado a y
  • sin(x): Seno
  • sinh(x): Seno hiperbólico
  • sqrt(x): Raíz cuadrada
  • tan(x): Tangente
  • tanh(x): Tangente hiperbólica

    (todos ellos usan parámetros X e Y de tipo "double")

    y una serie de constantes como

    M_E, el número "e", con un valor de 2.71828...
    M_PI, el número "Pi", 3.14159...

    La mayoría de ellas son específicas para ciertos problemas matemáticos, especialmente si interviene la trigonometría o si hay que usar logaritmos o exponenciales. Pero vamos a destacar las que sí pueden resultar útiles en situaciones más variadas:

La raiz cuadrada de 4 se calcularía haciendo x = sqrt(4);
La potencia: para elevar 2 al cubo haríamos y = pow(2, 3);
El valor absoluto: si queremos trabajar sólo con números positivos usaríamos n = fabs(x);

Así se podría calcular el coseño de un ángulo:

// Introducción a C++, Nacho Cabanes
// Ejemplo 09.12:
// Funciones matemáticas: coseno
 
#include <iostream>
#include <cmath>
 
using namespace std;
 
int main() 
{
    float anguloGrados = 45;
 
    float PI = 3.14159265;
    float anguloRadianes = anguloGrados * PI / 180;
    cout << "El coseño de 45 grados es " 
        << cos(anguloRadianes) << endl;
 
    return 0;
}
 

Ejercicios propuestos:

  • (9.7.1.1) Crear un programa que halle cualquier raíz de un número. El usuario deberá indicar el número (por ejemplo, 2) y el índice de la raiz (por ejemplo, 3 para la raíz cúbica). Pista: hallar la raíz cúbica de 2 es lo mismo que elevar 2 a 1/3.
  • (9.7.1.2) Crear un programa que muestre el seno de los ángulos de 30 grados, 45 grados, 60 grados y 90 grados. Cuidado: la función "sin" espera que se le indique el ángulo en radianes, no en grados. Tendrás que recordar que 180 grados es lo mismo que Pi radianes (con Pi = 3,1415926535). Puedes crearte una función auxiliar que convierta de grados a radianes.

9.7.2. Números aleatorios

En un programa de gestión o una utilidad que nos ayuda a administrar un sistema, no es habitual que podamos permitir que las cosas ocurran al azar. Pero los juegos se encuentran muchas veces entre los ejercicios de programación más completos, y para un juego sí suele ser conveniente que haya algo de azar, para que una partida no sea exactamente igual a la anterior.

Generar números al azar ("números aleatorios") usando C no es difícil. Si nos ceñimos al estándar ANSI C, tenemos una función llamada "rand()", que nos devuelve un número entero entre 0 y el valor más alto que pueda tener un número entero en nuestro sistema. Generalmente, nos interesarán números mucho más pequeños (por ejemplo, del 1 al 100), por lo que "recortaremos" usando la operación módulo ("%", el resto de la división).

Vamos a verlo con algún ejemplo:

Para obtener un número del 0 al 9 haríamos x = rand() % 10;
Para obtener un número del 0 al 29 haríamos x = rand() % 30;
Para obtener un número del 10 al 29 haríamos x = rand() % 20 + 10;
Para obtener un número del 1 al 100 haríamos x = rand() % 100 + 1;
Para obtener un número del 50 al 60 haríamos x = rand() % 11 + 50;
Para obtener un número del 101 al 199 haríamos x = rand() % 100 + 101;

Pero todavía nos queda un detalle para que los números aleatorios que obtengamos sean "razonables": los números que genera un ordenador no son realmente al azar, sino "pseudo-aleatorios", cada uno calculado a partir del siguiente. Podemos elegir cual queremos que sea el primer número de esa serie (la "semilla"), pero si usamos uno prefijado, los números que se generarán serán siempre los mismos. Por eso, será conveniente que el primer número se base en el reloj interno del ordenador: como es casi imposible que el programa se ponga en marcha dos días exactamente a la misma hora (incluyendo milésimas de segundo), la serie de números al azar que obtengamos será distinta cada vez.

La "semilla" la indicamos con "srand", y si queremos basarnos en el reloj interno del ordenador, lo que haremos será srand(time(0)); antes de hacer ninguna llamada a "rand()".

Para usar "rand()" y "srand()", deberíamos añadir otro fichero a nuestra lista de "includes", el llamado "stdlib":

#include <cstdlib>

Si además queremos que la semilla se tome a partir del reloj interno del ordenador (que es lo más razonable), deberemos incluir también "time":

#include <ctime>

Vamos a ver un ejemplo, que muestre en pantalla un número al azar entre 1 y 10:

// Introducción a C++, Nacho Cabanes
// Ejemplo 09.13:
// Obtener un número al azar
 
#include <iostream>
#include <cstdlib>
#include <ctime>
 
using namespace std;
 
int main() 
{
    int n;
    srand(time(0));
    n = rand() % 10 + 1;
    cout << "Un número entre 1 y 10: " <<  n << endl;
 
    return 0;
}
 

Ejercicios propuestos:

  • (9.7.2.1) Crea un programa que escriba varias veces "Hola" (entre 5 y 10 veces, al azar).
  • (9.7.2.2) Crear un programa que genere un número al azar entre 1 y 100. El usuario tendrá 6 oportunidades para acertarlo.
  • (9.7.2.3) Crea un programa que muestre un "fondo estrellado" en pantalla: mostrará 24 líneas, cada una de las cuales contendrá entre 1 y 78 espacios (al azar) seguidos por un asterisco ("*").

9.8. Recursividad

Una función recursiva es aquella que se define a partir de ella misma. Dentro de las matemáticas tenemos varios ejemplos. Uno clásico es el "factorial de un número":

n! = n · (n-1) · (n-2) · ... · 3 · 2 · 1

(por ejemplo, el factorial de 4 es 4 · 3 · 2 · 1 = 24)

Si pensamos que el factorial de n-1 es

(n-1)! = (n-1) · (n-2) · (n-3) · ... · 3 · 2 · 1

Entonces podemos escribir el factorial de un número a partir del factorial del siguiente número:

n! = n · (n-1)!

Esta es la definición recursiva del factorial. Esto, programando, se haría:

// Introducción a C++, Nacho Cabanes
// Ejemplo 09.14:
// Funciones recursivas: factorial
 
#include <iostream>
using namespace std;
 
long fact(int n) 
{
    if (n==1)               // Aseguramos que termine
        return 1;
 
    return n * fact (n-1);  // Si no es 1, sigue la recursión
}
 
int main() 
{
    int num;
    cout << "Introduzca un número entero: ";
    cin >> num;
    cout << "Su factorial es: " << fact(num) << endl;
 
    return 0;
}
 

Dos consideraciones importantes:

  • Atención a la primera parte de la función recursiva: es MUY IMPORTANTE comprobar que hay salida de la función, para que nuestro programa no se quede dando vueltas todo el tiempo y deje el ordenador (o la tarea actual) "colgado".
  • Los factoriales crecen rápidamente, así que no conviene poner números grandes: el factorial de 16 es 2.004.189.184, luego a partir de 17 podemos obtener resultados erróneos, según sea el tamaño de los números enteros en nuestro sistema.

¿Qué utilidad tiene esto? Pues más de la que parece: muchos problemas complicados se pueden expresar a partir de otro más sencillo. En muchos de esos casos, ese problema se podrá expresar de forma recursiva. Más adelante veremos algún otro ejemplo.

Ejercicios propuestos:

  • (9.8.1) Crear una función que calcule el valor de elevar un número entero a otro número entero (por ejemplo, 5 elevado a 3 = 53 = 5 ·5 · 5 = 125). Esta función se debe crear de forma recursiva.
  • (9.8.2) Como alternativa, crear una función que calcule el valor de elevar un número entero a otro número entero de forma NO recursiva (lo que llamaremos "de forma iterativa"), usando la orden "for".
  • (9.8.3) Crear un programa que emplee recursividad para calcular un número de la serie Fibonacci (en la que los dos primeros elementos valen 1, y para los restantes, cada elemento es la suma de los dos anteriores).
  • (9.8.4) Crear un programa que emplee recursividad para calcular la suma de los elementos de un vector.
  • (9.8.5) Crear un programa que emplee recursividad para calcular el mayor de los elementos de un vector.
  • (9.8.6) Crear un programa que emplee recursividad para dar la vuelta a una cadena de caracteres (por ejemplo, a partir de "Hola" devolvería "aloH").
  • (9.8.7) Crear, tanto de forma recursiva como de forma iterativa, una función diga si una cadena de caracteres es simétrica (un palíndromo). Por ejemplo, "DABALEARROZALAZORRAELABAD" es un palíndromo.
  • (9.8.8) Crear un programa que encuentre el máximo común divisor de dos números usando el algoritmo de Euclides : Dados dos números enteros positivos m y n, tal que m > n, para encontrar su máximo común divisor, es decir, el mayor entero positivo que divide a ambos: - Dividir m por n para obtener el resto r (0 = r < n) ; - Si r = 0, el MCD es n.; - Si no, el máximo común divisor es MCD(n,r).

Actualizado el: 24-09-2013 14:12

AnteriorPosterior