07. Apuntadores: accediendo a funciones II

Publicado por

Ha llegado el momento de utilizar esos apuntadores a función. Una de las utilidades más importantes que se le puede dar a esta clase de apuntadores es que permiten a nuestro código ser más legible y que no tengamos que llenarlo de “casos especiales” para realizar ciertas tareas. Por otra parte, también se utilizan para implementar los famosos callbacks; estos son funciones que no son llamadas de manera explícita por el programador, sino que las llama el propio sistema en el momento adecuado.

Comenzaremos conociendo una forma más conveniente de utilizar los apuntadores a función, de ahí aprenderemos a crear arreglos que almacenen funciones y cómo usar funciones como parámetros de otras funciones; para finalizar, trabajaremos en unos ejemplos para aplicar los conocimientos adquiridos.


Definiendo un tipo para un apuntador a función

Como vimos, no es muy complicado crear una variable que almacene la dirección de una función; pero ¿qué sucede cuando se quieren crear más apuntadores para el mismo tipo de función? Deberemos escribir la ya conocida sintaxis una y otra vez y, para casos donde el prototipo de una función sea algo extenso, esto puede convertirse en algo aburrido.

El caso anterior es algo muy frecuente en el área de las estructuras. Has de recordar que en C no basta con escribir el identificador de nuestra estructura a fin de crear varias variables de este tipo, habrá que colocar también esa palabra reservada de struct junto con el identificador. ¿Cuál es la solución que surge ante este problema? Definir un nuevo tipo de dato haciendo uso de typedef.

Recordemos cómo se utiliza este especificador:

// Sintaxis
typedef <tipo dato original> <nuevo nombre>;

// Algunos ejemplos
typedef int entero;
typedef struct n Nodo;

Como vemos, typedef permite que le demos otro nombre a un tipo de dato ya existente. Su uso nos vendrá muy bien para no tener que lidiar con largas declaraciones de apuntadores a función; lo único que necesitamos saber es cómo se usa en este caso.

Igual que en los ejemplos, la palabra typedef vendrá primero, seguido de la declaración de nuestro apuntador (como lo hacíamos normalmente). El nombre de este nuevo tipo será exactamente el mismo que utilicemos como identificador del apuntador, veamos unos ejemplos para ver la diferencia:

// 1. Un apuntador a función normal
void (*nuevoApuntador)();

// 2. Declaración de un nuevo tipo
typedef void (*nuevoApuntador)();

Como habíamos dicho, el primer ejemplo solo requiere le asignemos la dirección de una función y listo; si deseamos crear más apuntadores del mismo tipo, habrá que volver a escribir la misma declaración pero, claro, con distinto identificador. El segundo ejemplo nos facilita las cosas convirtiendo a nuestra declaración en un tipo de dato, solo habremos de utilizar el identificador como ese “tipo de dato” para crear uno o varios apuntadores, vamos a crear algunos:

// Definimos un tipo de dato para crear apuntadores que se refieren
// a una función que recibe dos "float" y regresa igual un "float".
typedef float (*Operacion)(float, float);

// Utilizamos el tipo de dato anterior. Cada uno de estos nuevos apuntadores
// podrá almacenar la dirección de una función con las características ya
// mencionadas anteriormente.
Operacion suma;
Operacion resta;
Operacion multiplicacion;
Operacion division;

¿Sencillo, no? Continuemos entonces con la siguiente parte.


Arreglos de funciones y funciones que reciben funciones

Si tendremos varios apuntadores que se refieren a un mismo tipo de función, valdría la pena agruparlas en un arreglo. Veamos las dos formas en que podemos declarar e inicializar estos grupos, primero sin typedef y luego usando a este último.

// Primera forma (sin typedef).
float (*operacion[4])(float, float);

// Inicializamos el arreglo con funciones que suponemos ya existen.
operacion[0] = suma;
operacion[1] = resta;
operacion[2] = multiplicacion;
operacion[3] = division;

// Segunda forma (con typedef).
typedef float (*Operacion)(float, float);
Operacion arregloOperaciones[4];

// Inicializamos el arreglo.
arregloOperaciones[0] = suma;
arregloOperaciones[1] = resta;
arregloOperaciones[2] = multiplicacion;
arregloOperaciones[3] = division;

¿Cuál de las dos te agrada más?
Para poder hacer uso de los elementos de un arreglo de este tipo, es muy sencillo:

// Basándonos en la segunda forma, llamamos a la función almacenada
// en la primera posición. Suponemos que existen dos float "a" y "b".
float resultado = arregloOperaciones[0](a, b);

Ahora, si deseamos que una función pueda hacer uso de otra función dentro de su bloque de instrucciones, vendría bien si le pasamos la función que queremos por medio de uno de sus parámetros. Para poder realizar esto es muy fácil y, de igual forma, contamos con dos versiones:

// Primera forma.
void unaFuncion(void (*funPtr)(int)) {
  // Haz algo
}

// Segunda forma.
typedef void (*PtrParametro)(int);

void unaFuncion(PtrParametro p) {
  // Haz algo
}

La decisión de cuál elegir, es tuya. Aún así, es muy notorio cómo typedef hace que nuestro código sea más limpio y legible para nosotros y los demás.

A estas funciones que son pasadas como parámetros son las que reciben el nombre de callbacks. Como puedes notar, nosotros no estamos haciendo la llamada a la función de forma explícita, la función que la recibe como parámetro se encargará de llamarla de acuerdo a lo que esté expresado en su bloque de instrucciones.


Aplicaciones de apuntadores a función

Un uso que podríamos darles a estos apuntadores es para cambiar la forma en que podemos seleccionar lo que nuestro programa debería hacer. Imagina que tenemos nuestro código y que hay una parte donde, dependiendo de ciertas condiciones, se tiene que llamar a una función en particular. Una primera implementación podría ser la siguiente:

if (condicionA) {
  llamarFuncion1();
}
else if (condicionB) {
  llamarFuncion2();
}
else if (condicionC) {
  llamarFuncion3();
}
else {
  llamarFuncion4();
}

Lo anterior, si fueran muchas condiciones, hace al código algo difícil de mantener. Otra forma en que podríamos realizar esto es haciendo uso de un arreglo de apuntadores y modificar un poco la lógica. Podríamos por ejemplo revisar cierto valor y, en base a este, llamar a la función correspondiente por medio de su índice:

typedef void (*FuncionPtr)();
FuncionPtr arregloPtr[] = {funcion1, funcion2, funcion3, funcion4};</code></pre>

// Se codifica algo para elegir un "indice" adecuado y llamar
// a la función deseada.
arregloPtr[indice]();

Lo anterior simplifica y hace más legible lo que tratamos de lograr. Un ejemplo muy en particular es cuando creamos una “máquina de estado finito”, podríamos hacer que en cada estado nuestro apuntador señale a la función adecuada y, una vez hecho esto, tener una sola línea al final que haga la llamada a la función elegida.

Otro uso es si tenemos una función, por ejemplo de ordenamiento, y necesitamos indicarle de qué forma queremos que ordene al grupo de elementos que le vamos a pasar (pudiendo ser ascendente o descendente). Un caso existente es el de la función qsort(), puedes ver de qué trata aquí. Ahí podrás apreciar que uno de sus parámetros es precisamente un apuntador a una función que recibe las dos entidades a comparar y regresará un valor entero (dependiendo de cómo resultó dicha comparación).


Para terminar

Como notarás, los usos que se le pueden dar a los apuntadores a función pueden llegar a ser muy útiles y solo basta practicar un poco con ellos para dominarlos con el fin de mejorar nuestros programas. Lo que hemos visto aquí es un poco de lo mucho que se puede lograr, por eso, te invito a que investigues más aplicaciones a fin de que el tema quede mejor comprendido.

Espero que todo esto te haya ayudado a comprender mejor a este tipo de apuntadores y que comiences a usarlos en tus siguientes proyectos. La siguiente nota será la final de esta temporada y hablará de forma general sobre todo lo que hemos visto durante la serie. Ten en cuenta que cualquier comentario o sugerencia que tengas es más que bienvenida en los comentarios.

Nos vemos en la próxima, ¡hasta entonces!

2 comments

  1. Estoy siguiendo esté pequeño Gran curso y simplemente te quería dar las gracias… me está viniendo de fábula para entender mejor conceptos que había estudiando por mi cuenta, pero no sabía muy bien como aplicarlos o que hacer.

    Lo dicho muchas gracias, está todo explicado de maravilla.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *