20 Funciones III: más cosas

Aún quedan algunas cosas interesantes por explicar sobre las funciones en C++.

Parámetros con valores por defecto

En algunas funciones suele suceder que para ciertos parámetros se repiten frecuentemente los mismos valores cada vez que necesitamos invocarlas.

Por ejemplo, podemos tener una función que calcule la velocidad final de un cuerpo en caída libre. Para hacer el cálculo necesitaremos algunos parámetros: velocidad inicial, altura...

h = vo·t + 1/2·g·t2
vf = vo + g·t
   donde:
   h es altura, o espacio recorrido
   g es la fuerza de la gravedad, o aceleración
   v0 es la velocidad inicial
   vf es la velocidad final y
   t es el tiempo

La fuerza de la gravedad también es un parámetro que hay que tener en cuenta, aunque podemos considerar que casi siempre nuestros cálculos se referirán a caídas que tendrán lugar en el planeta Tierra, por lo que podemos estar tentados de considerar la gravedad como una constante. Sin embargo, C++ nos permite tomar una solución intermedia. Podemos hacer que la fuerza de la gravedad sea uno de los parámetros de la función, y darle como valor por defecto el que tiene en la Tierra. De este modo, cuando no proporcionemos un valor para ese parámetro en una llamada, se tomará el valor por defecto, y en caso contrario, se usará el valor proporcionado.

Durante el programa, cuando se llama a la función incluyendo valores para esos parámetros opcionales, funcionará como cualquiera de las funciones que hemos usado hasta ahora. Pero si se omiten todos o algunos de estos parámetros la función trabajará con los valores por defecto para esos parámetros que hemos definido.

Por ejemplo:

#include <iostream>
#include <cmath>

using namespace std;

double VelocidadFinal(double h, double v0=0.0, double g=9.8);

int main() {
   cout << "Velocidad final en caida libre desde 100 metros,\n" <<
      "partiendo del reposo en la Tierra" <<
      VelocidadFinal(100) << "m/s" << endl;
   cout << "Velocidad final en caida libre desde 100 metros,\n" <<
      "con una velocidad inicial de 10m/s en la Tierra" <<
      VelocidadFinal(100, 10) << "m/s" << endl;
   cout << "Velocidad final en caida libre desde 100 metros,\n" <<
      "partiendo del reposo en la Luna" <<
      VelocidadFinal(100, 0, 1.6) << "m/s" << endl;
   cout << "Velocidad final en caida libre desde 100 metros,\n" <<
      "con una velocidad inical de 10m/s en la Luna" <<
      VelocidadFinal(100, 10, 1.6) << "m/s" << endl;
   return 0;
}

double VelocidadFinal(double h, double v0, double g) {
   double t = (-v0+sqrt(v0*v0 + 2*g*h))/g;
   return v0 + g*t;
}

Nota: En este programa hemos usado la función sqrt. Se trata de una función ANSI C, que está declarada en el fichero de cabecera math, y que, evidentemente, sirve para calcular raíces cuadradas.

La salida de este programa será:

Velocidad final en caida libre desde 100 metros,
partiendo del reposo en la Tierra 44.2719 m/s
Velocidad final en caida libre desde 100 metros,
con una velocidad inicial de 10m/s en la Tierra 45.3872 m/s
Velocidad final en caida libre desde 100 metros,
partiendo del reposo en la Luna 17.8885 m/s
Velocidad final en caida libre desde 100 metros,
con una velocidad inical de 10m/s en la Luna 20.4939 m/s

La primera llamada a la función "VelocidadFinal" dará como salida la velocidad final para una caída libre desde 100 metros de altura. Como no hemos indicado ningún valor para la velocidad inicial, se tomará el valor por defecto de 0 m/s. Y como tampoco hemos indicado un valor para la gravedad, se tomará el valor por defecto, correspondiente a la fuerza de la gravedad en la Tierra.

En la segunda llamada hemos indicado explícitamente un valor para el segundo parámetro, dejando sin especificar el tercero. Como en el caso anterior, se tomará el valor por defecto para la fuerza de la gravedad, que es 9.8.

Este método tiene algunas limitaciones, por otra parte, muy lógicas:

  • Sólo los últimos argumentos de las funciones pueden tener valores por defecto.
  • De estos, sólo los últimos argumentos pueden ser omitidos en una llamada.
  • Cada uno de los valores por defecto debe especificarse bien en los prototipos, o bien en las declaraciones, pero no en ambos.

En la tercera y cuarta llamadas hemos tenido que especificar los tres parámetros, a pesar de que en la tercera el valor de la velocidad inicial es cero. Si sólo especificamos dos parámetros, el programa interpretará que el que falta es el último, y no el segundo. El compilador no puede adivinar qué parámetro queremos omitir, por eso es necesario aplicar reglas extrictas.

Cuando se declaren valores de parámetros por defecto en prototipos, no es necesario indicar el nombre de los parámetros.

Por ejemplo:

void funcion(int = 1); // Legal
void funcion1(int a, int b=0, int c=1); // Legal
void funcion2(int a=1, int b, int c); // Ilegal
void funcion3(int, int, int=1); // Legal
...
void funcion3(int a, int b=3, int c) // Legal
{
}

Los argumentos por defecto empiezan a asignarse empezando por el último.

int funcion1(int a, int b=0, int c=1);
...
funcion1(12, 10); // Legal, el valor para "c" es 1
funcion1(12); // Legal, los valores para "b" y "c" son 0 y 1
funcion1(); // Ilegal, el valor para "a" es obligatorio

Funciones con número de argumentos variable

También es posible crear funciones con un número indeterminado de argumentos. Para ello declararemos los parámetros conocidos del modo normal, debe existir al menos un parámetro de este tipo. Los parámetros desconocidos se sustituyen por tres puntos (...), del siguiente modo:

<tipo_valor_retorno> <identificador>(<lista_parámetros_conocidos>, ...); 

Los parámetros se pasan usando la pila, (esto es siempre así con todos los parámetros, pero normalmente no tendremos que prestar atención a este hecho). Además es el programador el responsable de decidir el tipo de cada argumento, lo cual limita algo el uso de esta forma de pasar parámetros.

Para hacer más fácil la vida de los programadores, se incluyen algunas macros en el fichero de cabecera "cstdarg", estas macros permiten manejar más fácilmente las listas de argumentos desconocidos.

Tipos

En el fichero de cabecera "cstdarg" de define el tipo va_list:

va_list

Será necesario declarar una variable de este tipo para tener acceso a la lista de parámetros.

Macros

También se definen tres macros: va_start, va_arg y va_end.

void va_start(va_list ap, <ultimo>);

Ajusta el valor de "ap" para que apunte al primer parámetro de la lista. <ultimo> es el identificador del último parámetro fijo antes de comenzar la lista de parámetros desconocidos.

<tipo> va_arg(va_list ap, <tipo>);

Devuelve el siguiente valor de la lista de parámetros, "ap" debe ser la misma variable que se actualizó previamente con va_start, <tipo> es el tipo del parámetro que se tomará de la lista.

void va_end(va_list va);

Permite a la función retornar normalmente, restaurando el estado de la pila, esto es necesario porque algunas de las macros anteriores pueden modificarla, haciendo que el programa termine anormalmente.

Leer la lista de parámetros

<tipo> funcion(<tipo> <id1> [, <tipo> <id2>...], ...)
{
   va_list ar; // Declarar una variable para manejar la lista

   va_start(ar, <idn>); // <idn> debe ser el nombre del último
                        // parámetro antes de ...
   <tipo> <arg>; // <arg> es una variable para recoger
                 // un parámetro
   while((<arg> = va_arg(ar, <tipo>)) != 0) {
      // <tipo> debe ser el mismo que es de <arg>
      // Manejar <arg>
   }
   va_end(ar); // Normalizar la pila
}

Es necesario diseñar un sistema que permita determinar cuál es el último valor de la lista de parámetros, de modo que no queden parámetros por procesar o que no se procesen más de la cuenta.

Una forma es hacer que el último valor de la lista de parámetros en la llamada a la función sea un 0, (o de forma más general, un valor conocido).

También puede usarse uno de los parámetros conocidos para pasar a la función la cuenta de los parámetros desconocidos.

Además de esto, es necesario que el programador conozca el tipo de cada parámetro, para así poder leerlos adecuadamente. Una forma es que todos los parámetros sean del mismo tipo. Otra, que se use un mecanismo como el de la función "printf", donde analizando el primer parámetro se pueden deducir el tipo de todos los demás. Este último sistema tiene la ventaja de que también sirve para saber el número de parámetros.

Ejemplos:

#include <iostream>
#include <cstdarg>
using namespace std;

void funcion(int a, ...);

int main() {
   funcion(1, "cadena 1", 0);
   funcion(1, "cadena 1", "cadena 2", "cadena 3", 0);
   funcion(1, 0);

   return 0;
}

void funcion(int a, ...) {
   va_list p;
   va_start(p, a);
   char *arg;

   while ((arg = va_arg(p, char*))) {
      cout << arg << " ";
   }
   va_end(p);
   cout << endl;
}

Otro Ejemplo, este usando un sistema análogo al de "printf":

#include <iostream>
#include <cstring>
#include <cstdarg>
using namespace std;

void funcion(char *formato, ...);

int main() {
   funcion("ciic", "Hola", 12, 34, "Adios");
   funcion("ccci", "Uno", "Dos", "Tres", 4);
   funcion("i", 1);

   return 0;
}

void funcion(char *formato, ...) {
   va_list p;
   char *szarg;
   int iarg;
   int i;

   va_start(p, formato);
   /* analizamos la cadena de formato para saber el número y
      tipo de cada parámetro */
   for(i = 0; i < strlen(formato); i++) {
      switch(formato[i]) {
         case 'c': /* Cadena de caracteres */
            szarg = va_arg(p, char*);
            cout << szarg << " ";
            break;
         case 'i': /* Entero */
            iarg = va_arg(p, int);
            cout << iarg << " ";
            break;
      }
   }
   va_end(p);
   cout << endl;
}

Argumentos de main

Muy a menudo necesitamos especificar valores u opciones a nuestros programas cuando los ejecutamos desde la línea de comandos.

Por ejemplo, si hacemos un programa que copie ficheros, del tipo del "copy" de MS-DOS, necesitaremos especificar el nombre del archivo de origen y el de destino.

Hasta ahora siempre hemos usado la función main sin parámetros, sin embargo, como veremos ahora, se pueden pasar argumentos a nuestros programas a través de los parámetros de la función main.

Para tener acceso a los argumentos de la línea de comandos hay que declararlos en la función main, la manera de hacerlo puede ser una de las siguientes:

int main(int argc, char *argv[]);
int main(int argc, char **argv);

Que como sabemos son equivalentes.

El primer parámetro, "argc" (argument counter), es el número de argumentos que se han especificado en la línea de comandos. El segundo, "argv", (argument values) es un array de cadenas que contiene los argumentos especificados en la línea de comandos.

Por ejemplo, si nuestro programa se llama "programa", y lo ejecutamos con la siguiente línea de comandos:

programa arg1 arg2 arg3 arg4 

argc valdrá 5, ya que el nombre del programa también se cuenta como un argumento.

argv[] contendrá la siguiente lista: "C:\programasc\programa", "arg1", "arg2", "arg3" y "arg4".

Ejemplo:

#include <iostream>
using namespace std;

int main(int argc, char **argv) {
   for(int i = 0; i < argc; i++)
      cout << argv[i] << " ";
   cout << endl;
   return 0;
}

Funciones inline

Cuando usamos el nombre de una función, indicando valores para sus argumentos, dentro de un programa, decimos que llamamos o invocamos a esa función. Esto quiere decir que el procesador guarda la dirección actual, "salta" a la dirección donde comienza el código de la función, la ejecuta, recupera la dirección guardada previamente, y retorna al punto desde el que fue llamada.

Esto es cierto para las funciones que hemos usado hasta ahora, pero hay un tipo especial de funciones que trabajan de otro modo. En lugar de existir una única copia de la función dentro del código, si se declara una función como inline, lo que se hace es insertar el código de la función, en el lugar (y cada vez) que sea llamada.

Sintaxis:

inline <tipo> <nombre_de_funcion>(<lista_de_parámetros>);

Esto tiene la ventaja de que la ejecución es más rápida, pero por contra, el programa generado es más grande. Se debe evitar el uso de funciones inline cuando éstas son de gran tamaño, aunque con funciones pequeñas puede ser recomendable, ya que pueden producir programas más rápidos. Su uso es frecuente cuando las funciones tienen código en ensamblador, ya que en estos casos la optimización es mucho mayor.

En algunos casos, si la función es demasiado larga, el compilador puede decidir no insertar la función, sino simplemente llamarla. El uso de inline no es por lo tanto una obligación para el compilador, sino simplemente una recomendación.

Aparentemente, una función inline se comportará como cualquier otra función. De hecho, es incluso posible obtener un puntero a una función declarada inline.

Nota: inline es exclusivo de C++, y no está disponible en C.

Ejemplos:

#include <iostream>
using namespace std;

inline int mayor(int a, int b) {
   if(a > b) return a;
   return b;
}

int main() {
   cout << "El mayor de 12,32 es " << mayor(12,32) << endl;
   cout << "El mayor de 6,21 es " << mayor(6,21) << endl;
   cout << "El mayor de 14,34 es " << mayor(14,34) << endl;

   return 0;
}

Punteros a funciones

Tanto en C como en C++ se pueden declarar punteros a funciones.

Sintaxis:

<tipo> (*<identificador>)(<lista_de_parámetros>);

De esta forma se declara un puntero a una función que devuelve un valor de tipo <tipo> y acepta la lista de parámetros especificada. Es muy importante usar los paréntesis para agrupar el asterisco con el identificador, ya que de otro modo estaríamos declarando una función que devuelve un puntero al tipo especificado y que admite la lista de parámetros indicada.

No tiene sentido declarar variables de tipo función, es decir, la sintaxis indicada, prescindiendo del '*' lo que realmente declara es un prototipo, y no es posible asignarle un valor a un prototipo, como se puede hacer con los punteros, sino que únicamente podremos definir la función.

Ejemplos:

int (*pfuncion1)(); (1)
void (*pfuncion2)(int); (2)
float *(*pfuncion3)(char*, int); (3)
void (*pfuncion4)(void (*)(int)); (4)
int (*pfuncion5[10])(int); (5)

El ejemplo 1 declara un puntero, "pfuncion1" a una función que devuelve un int y no acepta parámetros.

El ejemplo 2 declara un puntero, "pfuncion2" a una función que no devuelve valor y que acepta un parámetro de tipo int.

El ejemplo 3 a una función que devuelve un puntero a float y admite dos parámetros: un puntero a char y un int.

El 4, declara una función "pfuncion4" que no devuelve valor y acepta un parámetro. Ese parámetro debe ser un puntero a una función que tampoco devuelve valor y admite como parámetro un int.

El 5 declara un array de punteros a función, cada una de ellas devuelve un int y admite como parámetro un int.

Este otro ejemplo:

int *(pfuncionx)();

Equivale a:

int *pfuncionx();

Que, claramente, es una declaración de un prototipo de una función que devuelve un puntero a int y no admite parámetros.

Utilidad de los punteros a funciones

La utilidad de los punteros a funciones se manifiesta sobre todo cuando se personalizan ciertas funciones de biblioteca. Podemos por ejemplo, diseñar una función de biblioteca que admita como parámetro una función, que debe crear el usuario (en este caso otro programador), para que la función de biblioteca complete su funcionamiento.

Este es el caso de la función qsort, declarada en cstdlib. Si nos fijamos en su prototipo:

void qsort(void *base, size_t nmemb, size_t tamanyo,
   int (*comparar)(const void *, const void *));

Vemos que el cuarto parámetro es un puntero a una función comparar que devuelve un int y admite dos parámetros de tipo puntero genérico.

Esto permite a la biblioteca cstdlib definir una función para ordenar arrays independientemente de su tipo, ya que para comparar elementos del array se usa una función definida por el usuario, y qsort puede invocarla después.

Asignación de punteros a funciones

Una vez declarado uno de estos punteros, se comporta como una variable cualquiera, podemos por lo tanto, usarlo como parámetro en funciones, o asignarle valores, por supuesto, del mismo tipo.

int funcion();
...
   int (*pf1)();  // Puntero a función sin argumentos
                  // que devuelve un int.
   pf1 = funcion; // Asignamos al puntero pf1 la
                  // función "funcion"
...
int funcion() {
   return 1;
}

La asignación es tan simple como asignar el nombre de la función.

Nota: Aunque muchos compiladores lo admiten, no es recomendable aplicar el operador de dirección (&) al nombre de la función
pf1 = &funcion;.
La forma propuesta en el ejemplo es la recomendable.

Llamadas a través de un puntero a función

Para invocar a la función usando el puntero, sólo hay que usar el identificador del puntero como si se tratase de una función. En realidad, el puntero se comporta exactamente igual que un "alias" de la función a la que apunta.

int x = pf1();

De este modo, llamamos a la función "funcion" previamente asignada a *pf1.

Ejemplo completo:

#include <iostream>
using namespace std;

int Muestra1();
int Muestra2();
int Muestra3();
int Muestra4();

int main() {
   int (*pf1)();
   // Puntero a función sin argumentos que devuelve un int.
   int num;

   do {
      cout << "Introduce un número entre 1 y 4, "
           << "0 para salir: ";
      cin >> num;
      if(num >= 1 && num <=4) {
         switch(num) {
            case 1:
               pf1 = Muestra1;
               break;
            case 2:
               pf1 = Muestra2;
               break;
            case 3:
               pf1 = Muestra3;
               break;
            case 4:
               pf1 = Muestra4;
               break;
         }
         pf1();
      }
   } while(num != 0);

   return 0;
}

int Muestra1() {
   cout << "Muestra 1" << endl;
   return 1;
}

int Muestra2() {
   cout << "Muestra 2" << endl;
   return 2;
}

int Muestra3() {
   cout << "Muestra 3" << endl;
   return 3;
}

int Muestra4() {
   cout << "Muestra 4" << endl;
   return 4;
}

Otro ejemplo:

#include <iostream>
using namespace std;

int Fun1(int);
int Fun2(int);
int Fun3(int);
int Fun4(int);

int main() {
   int (*pf1[4])(int);  // Array de punteros a función con un
                        // argumento int que devuelven un int.
   int num;
   int valores;

   pf1[0] = Fun1;
   pf1[1] = Fun2;
   pf1[2] = Fun3;
   pf1[3] = Fun4;
   do {
      cout << "Introduce un número entre 1 y 4, "
           << "0 para salir: ";
      cin >> num;
      if(num >= 1 && num <=4) {
         cout << "Introduce un número entre 1 y 10: ";
         cin >> valores;
         if(valores > 0 && valores < 11)
            pf1[num-1](valores);
      }
   } while(num != 0);

   return 0;
}

int Fun1(int v) {
   while(v--) cout << "Muestra 1" << endl;
   return 1;
}

int Fun2(int v) {
   while(v--) cout << "Muestra 2" << endl;
   return 2;
}

int Fun3(int v) {
   while(v--) cout << "Muestra 3" << endl;
   return 3;
}

int Fun4(int v) {
   while(v--) cout << "Muestra 4" << endl;
   return 4;
}

Palabras reservadas usadas en este capítulo

inline.