Funciones

Las funciones se declaran y se definen exactamente igual que en C, y, al igual que en éste, se puede utilizar prototipo (prototype).
Un prototipo es un modelo limitado de una entidad más completa que aparecerá después. En el caso de funciones, la función es la entidad completa que vendrá después, y la declaración de dicha función es el prototipo. El prototipo da un modelo de la interface a la función. Veamos un ejemplo:

# include <iostream.h>

void do_stuff (int wings, float feet, char eyes);

main()
{
   int arm = 2;
   float foot = 1000.0;
   char lookers = 2;

   do_stuff (3, 12.0, 4);
   do_stuff (arm, foot, lookers);
}

void do_stuff (int wings, float feet, char eyes);
{
   cout << "There are " << wings << "wings." << '\n';
   cout << "There are " << feet << "feet. " << '\n';
   cout << "There are " << int(eyes) << "eyes." << '\n';
}


La salida de este programa será:

There are 3 wings.
There are 12 feet.
There are 4 eyes.

There are 2 wings.
There are 1000 feet.
There are 2 eyes.

Cada llamada a la función do_stuff() debe verificar:

Nótese que cuando llamamos a la función, la comprobación de tipo la hace el compilador basándose en el prototipo (en la declaración) puesto que la función todavía no ha sido definida.
Los nombres de variables que aparecen en el prototipo son opcionales y actúan casi como comentarios al lector del programa, ya que son completamente ignorados por el compilador.

Tipos compatibles

Son compatibles cualquiera de los tipos simples (definidos en C++) que pueden ser convertidos de uno a otro de manera significativa. Por ejemplo, si llamamos con un entero a una función que está esperando un número real como parámetro, el sistema lo convertirá automáticamente, sin mencionarlo al usuario. Esto también es cierto de float a char, o de char a int.
En cambio, si pasamos un puntero a un entero a una función que estaba esperando un entero, no habrá conversión de tipo, ya que son dos variables completamente distintas. De la misma forma, un tipo definido por el usuario (estructura o clase) no puede ser convertido automáticamente a un long float, a un array o incluso a otra estructura o clase diferente, porque son tipos incompatibles y no puede realizarse la conversión de manera significativa.
Sin embargo, el tipo devuelto por la función, void en el ejemplo anterior, debe ser compatible con el tipo que se espera que devuelva en la función de llamada, o el compilador dará un warning.

Pequeños cambios

Volvamos al ejemplo anterior. Veamos qué sucede cuando hacemos pequeños cambios:
El uso de prototipos no supone coste alguno en tiempo ni en velocidad de ejecución. El prototipo se verifica durante la compilación. Ralentiza ésta, debido a que es una comprobación extra que el compilador debe hacer, pero es despreciable el tiempo que necesita. El prototipo sólo alarga el tamaño del código.

Void como argumento de una función

Una función sin lista de argumentos como

void func ();

significa en C que no se ha declarado el tipo de la lista de argumentos que recibe la función, por lo que el compilador no producirá errores respecto al uso impropio de los argumentos. Cuando en C se declara una función que no tiene argumentos se utiliza el tipo void:

void func (void);

En C++, ambas expresiones son equivalentes.

Funciones a las que se pasan punteros

Cuando se llama a una función, todos los parámetros con los que la llamamos son copiados y pasados a la función. Esto significa que si la función cambia el valor de los parámetros, sólo lo hace dentro de la función. Por ejemplo: #include <iostream.h>

void change_values(int a,int b)
{
 a=4;
 b=5;

}
main() {
 int a;
 int b;

 a=1;
 b=2;

 change_values(a,b);

 cout << "A is " << a <<",B is" << b <<'\n';
}

La salida de programa es: A is 1, B is 2
La llamada a la función no ha cambiado el valor de las variables que se le han pasado. La función cambia las copias de lo que se le ha pasado. Para cambiar los valores no debemos pasar datos a la función, sino punteros a los datos. Para hacer esto, utilizamos el operador &, que da la dirección de una variable:
#include <iostream.h>

void change_values(int *a,int *b)
{
 *a=4;
 *b=5;

}
main() {
 int a;
 int b;

 a=1;
 b=2;

 change_values(&a,&b);

 cout << "A is " << a <<",B is" << b <<'\n';
}

Ahora la salida del programa es:
A is 4, B is 5
La función main pasa la dirección de a y b, por lo que a la función change_values se le pasa una copia de las direcciones. Utilizando las direcciones de a y b, la función puede acceder a los datos directamente. Funciones a las que se pasan arrays

Funciones a las que se pasan arrays

Supongamos que queremos pasar un array a una función. La solución es pasar punteros. El nombre de un array es un puntero al primer elemento del array. Así, si hemos definido int taco[10] como un array de diez enteros, taco será un puntero al array. Por ejemplo, sea guacamole un array de 6 enteros: 6, 63, 112, 18, -52, 16. guacamole es simplemente un puntero al primer elemento. Entonces, *guacamole equivale al valor 6. Si tratamos de acceder al elemento guacamole[6], estamos accediendo a lo que haya después del último elemento del array. Los resultados son inesperados.
Veamos un ejemplo de cómo pasar un array a una función:
# include <iostream.h>

void eat_at_joes(int guacamole[])
{
  guacamole[0] = 1;
  guacamole[1] = 2;
  guacamole[2] = 3;
}

main()
{
  int taco[3];
  int nacho[2];

  eat_at_joes(taco);
  eat_at_joes(nacho);
}

Al definir la función, no hemos puesto ningún número entre los corchetes. Esto significa que estamos permitiendo que un array de cualquier tamaño pueda ser pasado a la función. En la función main, hemos declarado dos arrays de enteros, taco y nacho. Primero pasamos taco a la función, que en realidad es la direccion del array taco. Ahora bien, el segundo array, nacho, tiene sólo dos elementos. Cuando la función trate de acceder al tercer elemento del array, que realmente no existe. Ya que los arrays son muchas variables almacenadas una detrás de otra en la memoria del ordenador, tratará y accederá a la variable almacenada despueés del segundo elemento del array, que no existe.
Además, el programa compilará y correrá sin mensajes de error. Esto sucedía ya en C, que permite leer y escribir en la memoria del ordenador sin saber si realmente hay variables allí. Se deben evitar casos como el del ejemplo, porque, o bien el programa correrá sin mensajes de error, o bien casca.
Una forma de evitarlo es añadiendo un segundo parámetro que indique el tamaño del array:
# include <iostream.h>

void eat_at_joes(int guacamole[], int size)
{
  if (size > 0)
    guacamole[0] = 1;
  if (size > 1)
    guacamole[1] = 2;
  if (size > 2)
    guacamole[2] = 3;
}

main()
{
  int taco[3];
  int nacho[2];

  eat_at_joes(taco,3);
  eat_at_joes(nacho,2);
}

Llamada por referencia

Funciones a las que se pasan referencias

Esta forma de llamada a una función es nueva en C++, no es válida para C. La referencia ya ha sido explicada en el primer capítulo, pero su utilidad principal la veremos aquí. El paso por referencia permite pasar una variable a una función y devolver los cambios que la función ha hecho al programa principal. El mismo efecto se consigue pasando un puntero a la función. Veamos un ejemplo:

# include <iostream.h>

void fiddle(int in1, int &in2);

main()
{
  int count = 7, index = 12;
  cout << "Los valores son " << count << '\t' << index << '\n';

  fiddle (count, index);

  cout << "Los valores son " << count << '\t' << index << '\n';

}

void fiddle (int in1, int &in2)
{
in1 = in1 + 100;
in2 = in2 + 100;
  cout << "Los valores son " << int1 << '\t' << int2 << '\n';

}

La salida de este programa es:

Los valores son 7 12.
Los valores son 107 112.
Los valores son 7 112.

Observamos que:

Polimorfismo

Polimorfismo

En C++ es posible declarar dos funciones diferentes que tengan el mismo nombre. Las funciones deben diferir en la lista de argumentos, bien en el número de variables que se pasan a la función, bien en el tipo de argumentos que recibe. Así, por ejemplo, se puede definir una función que trabaje, bien con enteros, bien con strings; sólo hay que definir dos funciones separadas con el mismo nombre:

  #include <iostream.h>
  void burrito(int nacho)
  {
   cout <<"You sent an int! "<<'\n';
  }
  void burrito(char foo)   {
   cout <<"You sent a char!"<<'\n';
  }
  main()   {
   burrito (42);
   burrito ('A');
   burrito (452.2);
  }

En la primera llamada a la función burrito, se le pasa un entero, por tanto se llama a la primera copia de la función burrito. La segunda vez, el argumento es un carácter, por tanto se utiliza la segunda definición, aquella que utiliza un carácter. Ahora bien, la tercera llamada utiliza un número real, y no existe una definición de la función para este caso. El compilador no puede decidir usar una o la otra. La salida del programa es un mensaje de error:
Error occured at line 17:
call of overloaded 'burrito' is ambiguous.

La solución pasa por convertir la variable con la que llamamos a la función a uno de los tipos para los cual burrito está definida. Por ejemplo, si sustituimos la tercera llamada del programa anterior por:
   burrito ((int)452.2);
la salida será:
You sent an int!
You sent a char!
You sent an int!
  
Parámetros por defecto

Parámetros por defecto

Es una forma de indicar qué valor debe ser pasado a una función en el caso en que en la llamada no se pase nada, o se pasen menos argumentos de los definidos. Un ejemplo de definición de una función que tiene parámetros por defecto en su lista de argumentos es:

void funcion (int y = 2)

En este caso, estamos definiendo un valor, 2, que tomará la variable y en caso de que no se pase nada en la llamada a la función:

funcion ();

Aunque no se ha pasado ningún argumento, la función trabajará. Como no se ha pasado ningún valor, y toma el valor 2, y la función se ejecuta. Si quisiéramos pasar un valor, se haría como en un caso normal:

funcion (231);

Ahora y toma el valor 231.
En el caso de que la función tenga una lista con varios argumentos, los que tengan valores por defecto deben ir al final:

void funcion2 (int var1, int var2, int var3 = 0, int var4 = 0)

es la forma correcta de hacerlo. En cambio:

void funcion2 (int var1, int var2 = 0, int var3 = 0, int var4)

no es correcto. La razón es que si quisiéramos llamar a esta función con tres argumentos, el compilador no distinguiría si los valores que pasamos corresponden a var1, var2 y var3 o a var1, var3 y var4.
Resumiendo, hemos de seguir las siguientes reglas al declarar funciones con parámetros por defecto:

Veamos un ejemplo:

# include <iostream.h>

int volumen (int longitud, int anchura = 2, int altura = 3);

main()
{
   int x = 10, y = 12, z = 15;

   cout << "Volumen =" << volumen (x,y,z) << '\n';
   cout << "Volumen =" << volumen (x,y) << '\n';
   cout << "Volumen =" << volumen (x) << '\n';

   cout << "Volumen =" << volumen (x,7) << '\n';
   cout << "Volumen =" << volumen (5,5,5) << '\n';
}

int volumen (int longitud, int anchura, int altura)
{
  return longitud * anchura * altura;
}

El resultado de la ejecución de este programa será:

  Volumen = 1800.
  Volumen = 360.
  Volumen = 60.
  Volumen = 210.
  Volumen = 125.

En este programa:

Número variable de argumentos

Número variable de argumentos

En ocasiones, podemos querer escribir una función que use un número variable de argumentos como parámetros. La función printf es un buen ejemplo de esto. C tiene tres macros definidas en la cabecera estándar stdarg.h que permiten utilizar un número variable de argumentos. También se pueden usar en C++, pero para ello necesitamos eliminar la comprobación de tipo que el compilador hace con todas las funciones en C++. Para ello se utilizan tres puntos en la lista de argumentos de la función. Veamos un ejemplo:

# include <iostream.h>

# include <stdarg.h>

void display_var(int number, ...);

main()
{
   int index = 5;
   int one = 1, two = 2;

   display_var (one, index);
   display_var (3, index, index + two, index + one);
   display_var (two, 7 ,3);
}

void display_var (int number, ...);
{
   va_list param_pt;
   va_start(param_pt, number);

   cout << "The parameters are " ;
   for (int index = 0; index < number; index ++)
     cout << va_arg(param_pt, int) << " " ;
   cout << '\n';
  va_end (param_pt); }


La salida de este programa será:

The parameters are 5.
The parameters are 5 7 6.
The parameters are 7 3.

Observamos que:

Estudiemos la definición de la función:

Un programa bien diseñado necesita algunas funciones en las que el tipo de argumentos no está completamente especificado. La sobrecarga de funciones y el uso de parámetros por defecto deben ser usados para resolver estas situaciones. Sólo cuando el número de argumentos y el tipo de argumentos varían se debe utilizar las funciones con número variable de argumentos. Naturalmente, el uso de un número variable de argumentos en una función conduce a un código oscuro y apenas debe ser usado.