Lecturas Recomendadas¶
Introducción¶
Genericidad y Callbacks (Punteros a Funciones)¶
En capítulos anteriores, exploramos cómo los punteros nos permiten referenciar y manipular datos en el heap de manera dinámica. Sin embargo, en C, las instrucciones ejecutables de una función también residen en la memoria virtual (en el segmento de código o de texto del espacio de direcciones virtual del proceso).
Un puntero a función almacena la dirección de memoria de una función ejecutable, lo que nos permite invocarla dinámicamente y pasar comportamiento como argumento a otras funciones. Este mecanismo se conoce como callback y es el pilar para construir código genérico y extensible en C.
Desarrollo¶
Punteros a Funciones: Sintaxis y Declaración¶
La sintaxis para declarar un puntero a función en C puede parecer compleja al principio, ya que requiere agrupar el operador de indirección con el nombre del puntero para diferenciarlo de una función que retorna un puntero.
Declaración Básica¶
La estructura general de una declaración es:
tipo_retorno (*nombre_puntero)(tipo_parametro1, tipo_parametro2, ...);El paréntesis alrededor de
*nombre_punteroes obligatorio. Si se omite, se declara una función normal que retorna un puntero del tipo especificado.
int *funcion(int, int); // Declaración de una función que retorna un puntero a
int (int *)
int (*puntero)(int, int); // Declaración de un puntero a una función que retorna
intAsignación e Invocación¶
Asignar una función a un puntero es directo: solo se utiliza el nombre de la función (que decae en su dirección de instrucción en el segmento de código virtual del proceso).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22#include <stdio.h> int sumar(int a, int b) { return a + b; } int main(void) { // Declaración int (*operacion)(int, int); // Asignación operacion = sumar; // O bien: operacion = &sumar; // Invocación (ambas sintaxis son válidas bajo el estándar) int res1 = operacion(5, 3); // Invocación directa (sintaxis recomendada por legibilidad) int res2 = (*operacion)(5, 3); // Invocación explícita desreferenciando el puntero printf("Resultados: %d, %d\n", res1, res2); return 0; }
Simplificación con typedef¶
Para evitar declarar firmas de punteros complejas repetidamente, es una buena
práctica de ingeniería de software definir alias de tipos utilizando typedef
(regla Regla 0x3004h: Utilizá typedef para definir tipos de estructuras con el sufijo _t):
1 2 3 4 5 6// Definimos el alias 'operacion_fn' para representar punteros a funciones (int, int) -> int typedef int (*operacion_fn)(int, int); // Ahora la declaración es simple: operacion_fn mi_operacion = sumar;
Ejercicios de Autoevaluación (Sintaxis y Declaración)¶
Callbacks: Comportamiento como Parámetro¶
Un callback es una función que se pasa a otra función como argumento para ser ejecutada (“llamada de vuelta”) bajo ciertas condiciones o flujos de control.
Ejemplo Clásico: qsort() de la Biblioteca Estándar¶
El ejemplo por excelencia de genericidad y callbacks es qsort (definida en
<stdlib.h>), la cual implementa el algoritmo de ordenamiento rápido QuickSort
de forma genérica para cualquier tipo de arreglo:
void qsort(void *base, size_t nmemb, size_t size,
int (*comparar)(const void *, const void *));Para ordenar un arreglo, le proveemos a qsort:
void *base: Dirección del primer elemento.size_t nmemb: Cantidad de elementos del arreglo.size_t size: Tamaño en bytes de cada elemento (ej:sizeof(int)).comparar: Un callback de comparación que acepta dos punteros constantes genéricosconst void *y retorna un entero:Menor a 0 si el primer elemento es menor al segundo.
0 si son iguales.
Mayor a 0 si el primer elemento es mayor al segundo.
Implementación Completa con qsort()¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct { char nombre[30]; int edad; } Persona; // Callback para ordenar enteros de menor a mayor int comparar_enteros(const void *a, const void *b) { // 1. Casteamos los punteros genéricos const void* al tipo real const int* const int *ptr_a = (const int *)a; const int *ptr_b = (const int *)b; // 2. Desreferenciamos y comparamos if (*ptr_a < *ptr_b) { return -1; } if (*ptr_a > *ptr_b) { return 1; } return 0; } // Callback para ordenar personas por edad int comparar_personas_edad(const void *a, const void *b) { const Persona *p1 = (const Persona *)a; const Persona *p2 = (const Persona *)b; if (p1->edad < p2->edad) return -1; if (p1->edad > p2->edad) return 1; return 0; } int main(void) { int numeros[] = {34, 12, 5, 90, 45}; size_t cant_nums = sizeof(numeros) / sizeof(numeros[0]); qsort(numeros, cant_nums, sizeof(int), comparar_enteros); printf("Numeros ordenados: "); for (size_t i = 0; i < cant_nums; i++) { printf("%d ", numeros[i]); } printf("\n"); Persona grupo[] = { {"Carlos", 30}, {"Ana", 25}, {"Beatriz", 28} }; size_t cant_personas = sizeof(grupo) / sizeof(Persona); qsort(grupo, cant_personas, sizeof(Persona), comparar_personas_edad); printf("\nPersonas ordenadas por edad:\n"); for (size_t i = 0; i < cant_personas; i++) { printf("- %s (%d anos)\n", grupo[i].nombre, grupo[i].edad); } return 0; }
Program 1:Uso de qsort con callbacks para ordenar enteros y structs en C
Ejercicios de Autoevaluación (Callbacks y qsort)¶
Genericidad en C mediante void *¶
Dado que C carece de tipos genéricos en tiempo de compilación (como templates
de C++ o genéricos de Java), la genericidad se simula a bajo nivel utilizando
punteros genéricos void *.
Reglas de Oro para Trabajar con void *¶
No se puede desreferenciar directamente un
void *: Como el compilador no sabe cuántos bytes ocupa el tipo apuntado, la expresión*p_voidgenera un error de compilación. Siempre debés realizar un casteo explícito a un puntero del tipo real antes de acceder a la memoria.No se permite aritmética de punteros sobre
void *: Sumar o restar a unvoid *genera comportamiento indefinido o errores, ya que la escala de incremento depende del tamaño del tipo. Nuevamente, debés castear previamente a un tipo con tamaño conocido (ej:char *para aritmética byte a byte).
Diseñando una Función Genérica de Búsqueda Lineal¶
Para consolidar estos conceptos, implementemos nuestro propio algoritmo genérico de búsqueda lineal, capaz de buscar en cualquier tipo de arreglo utilizando un callback de comparación:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54#include <stdio.h> #include <stddef.h> #include <string.h> // Definición de tipo para el callback de comparación typedef int (*comparar_fn)(const void *, const void *); // Función genérica de búsqueda lineal // Retorna la dirección del elemento si se encuentra, o NULL si no está. const void *buscar_generico(const void *base, size_t nmemb, size_t size, const void *clave, comparar_fn cmp) { if (base == NULL || clave == NULL || cmp == NULL) { return NULL; } // Convertimos a const char* para poder realizar aritmética de punteros byte a byte const char *ptr = (const char *)base; for (size_t i = 0; i < nmemb; i++) { // Calculamos la dirección del elemento i-ésimo: base + i * size const void *elemento = ptr + (i * size); // Invocamos el callback de comparación if (cmp(elemento, clave) == 0) { return elemento; // Encontrado } } return NULL; // No encontrado } // Callback para buscar en enteros int cmp_enteros(const void *a, const void *b) { int val_a = *(const int *)a; int val_b = *(const int *)b; if (val_a < val_b) return -1; if (val_a > val_b) return 1; return 0; } int main(void) { int arr[] = {10, 20, 30, 40, 50}; size_t n = sizeof(arr) / sizeof(arr[0]); int clave = 30; const void *resultado = buscar_generico(arr, n, sizeof(int), &clave, cmp_enteros); if (resultado != NULL) { printf("El numero %d fue encontrado en la posicion: %ld\n", clave, ((const int *)resultado - arr)); } else { printf("El numero %d no fue encontrado.\n", clave); } return 0; }
Program 2:Implementación de búsqueda lineal genérica en C usando callbacks y void*
Ejercicios de Autoevaluación (Genericidad y void*)¶
Ejercicios de Autoevaluación¶
Solution to Exercise 1
double *procesar(double, double);: Declara un prototipo de función ordinario llamadoprocesarque recibe dos parámetros de tipodoubley retorna un puntero a double (double *).double (*procesar)(double, double);: El paréntesis altera la precedencia indicando que el operador*se asocia directamente con el identificador. Declara una variable llamadaprocesarcuyo tipo de dato es puntero a función, la cual recibe dos parámetros de tipodoubley retorna un valor de tipodouble.
Solution to Exercise 2
La sintaxis correcta aplicando el sufijo de alias de tipo _t es:
#include <stddef.h>
typedef size_t (*medidor_cadena_fn_t)(const char *);Este alias medidor_cadena_fn_t permite declarar punteros a funciones de forma
sencilla:
medidor_cadena_fn_t mi_funcion = strlen;Solution to Exercise 3
Las dos formas de invocación soportadas son:
Invocación explícita (desreferencia):
int res1 = (*operacion)(10, 20);Invocación implícita o directa:
int res2 = operacion(10, 20);
Ambas son funcionalmente idénticas debido a que el compilador de C promociona automáticamente el identificador de la función a su dirección de instrucción en el segmento de código virtual del proceso. La cátedra prefiere y recomienda la segunda variante (invocación directa) por asemejarse a una llamada de función estándar, mejorando la claridad de lectura del código.
Solution to Exercise 4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15int comparar_reversa(const void *a, const void *b) { // 1. Casteamos los punteros genéricos const void* al tipo real const int* const int *ptr_a = (const int *)a; const int *ptr_b = (const int *)b; // 2. Para orden descendente: // Retorna mayor a 0 si b > a, menor a 0 si a > b, 0 si son iguales. if (*ptr_a < *ptr_b) { return 1; } if (*ptr_a > *ptr_b) { return -1; } return 0; }
Solution to Exercise 5
Al pasar un arreglo de cadenas char *arr[] a qsort, los elementos
individuales que se comparan son de tipo char *. Dado que qsort pasa
punteros a los elementos del arreglo, los punteros genéricos a y b que
recibe el callback deben ser interpretados como punteros a cadenas, es decir,
const char **.
1 2 3 4 5 6 7 8 9 10#include <string.h> int comparar_cadenas(const void *a, const void *b) { // Casteo a puntero de cadena (char**) const char * const *str_a = (const char * const *)a; const char * const *str_b = (const char * const *)b; // Comparación léxica de los contenidos apuntados return strcmp(*str_a, *str_b); }
Solution to Exercise 6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21int comparar_productos(const void *a, const void *b) { const producto_t *p1 = (const producto_t *)a; const producto_t *p2 = (const producto_t *)b; // Primer criterio: comparación de precios if (p1->precio < p2->precio) { return -1; } if (p1->precio > p2->precio) { return 1; } // Segundo criterio (desempate): comparación de códigos if (p1->codigo < p2->codigo) { return -1; } if (p1->codigo > p2->codigo) { return 1; } return 0; }
Solution to Exercise 7
El tipo void * representa una dirección de memoria genérica sin información
sobre el tipo de dato subyacente.
Desreferencia (
*ptr): Para leer o escribir a través de un puntero, el compilador requiere saber cuántos bytes ocupa el dato (por ejemplo, 4 bytes para unint, 8 para undouble) para leer la palabra de memoria correcta. Al servoid(vacío de tipo), el tamaño es indeterminado, resultando en un error de compilación.Aritmética (
ptr++): Para avanzar al siguiente elemento, el compilador escala la suma de bytes por el tamaño del tipo apuntado (sizeof(tipo)). Sin tipo asociado, el tamaño es desconocido. La solución consiste en realizar un casteo (cast) explícito al tipo de puntero real correspondiente antes de operar (por ejemplo,const char *para aritmética byte a byte).
Solution to Exercise 8
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24#include <stdio.h> #include <stddef.h> typedef int (*predicado_fn_t)(const void *); typedef void (*imprimir_fn_t)(const void *); void filtrar_arreglo(const void *base, size_t nmemb, size_t size, predicado_fn_t predicado, imprimir_fn_t imprimir) { if (base == NULL || predicado == NULL || imprimir == NULL) { return; } // Casteo a const char* para aritmética de punteros byte a byte const char *ptr = (const char *)base; for (size_t i = 0; i < nmemb; i++) { // Cálculo de dirección del elemento i-ésimo: base + i * size const void *elem = ptr + (i * size); if (predicado(elem)) { imprimir(elem); } } }
Solution to Exercise 9
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18#include <stddef.h> void intercambiar_bloques(void *a, void *b, size_t size) { if (a == NULL || b == NULL || size == 0) { return; } // Casteo a char* para operar sobre bytes individuales de forma contigua char *ptr_a = (char *)a; char *ptr_b = (char *)b; for (size_t i = 0; i < size; i++) { // Intercambio clásico byte a byte char temp = ptr_a[i]; ptr_a[i] = ptr_b[i]; ptr_b[i] = temp; } }
Glosario¶
- Puntero a Función
- Puntero que almacena la dirección de código ejecutable.
- Callback
- Función pasada como argumento a otra función para ser invocada posteriormente.
- Genericidad
- Técnica de programación que permite escribir algoritmos independientes del tipo de datos.
Síntesis y Resumen¶
En este apunte se han presentado los conceptos fundamentales del tema.
Referencias y Lecturas Complementarias¶
Lecturas Recomendadas¶
King (2008). Capítulo 17: Advanced Uses of Pointers (sección de punteros a funciones).
Kernighan & Ritchie (2014). Capítulo 5: Pointers and Arrays (sección sobre punteros a funciones y qsort).
- King, K. N. (2008). C Programming: A Modern Approach (2nd ed.). W. W. Norton & Company.
- Kernighan, B. W., & Ritchie, D. M. (2014). C Programming Language, 2nd Edition.