Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Punteros a Funciones

Universidad Nacional de Río Negro

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, ...);
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
int
Asignació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:

  1. void *base: Dirección del primer elemento.

  2. size_t nmemb: Cantidad de elementos del arreglo.

  3. size_t size: Tamaño en bytes de cada elemento (ej: sizeof(int)).

  4. comparar: Un callback de comparación que acepta dos punteros constantes genéricos const 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 *
  1. No se puede desreferenciar directamente un void *: Como el compilador no sabe cuántos bytes ocupa el tipo apuntado, la expresión *p_void genera 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.

  2. No se permite aritmética de punteros sobre void *: Sumar o restar a un void * 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
  1. double *procesar(double, double);: Declara un prototipo de función ordinario llamado procesar que recibe dos parámetros de tipo double y retorna un puntero a double (double *).

  2. double (*procesar)(double, double);: El paréntesis altera la precedencia indicando que el operador * se asocia directamente con el identificador. Declara una variable llamada procesar cuyo tipo de dato es puntero a función, la cual recibe dos parámetros de tipo double y retorna un valor de tipo double.

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:

  1. Invocación explícita (desreferencia):

    int res1 = (*operacion)(10, 20);
  2. 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
15
int 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
21
int 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 un int, 8 para un double) para leer la palabra de memoria correcta. Al ser void (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

References
  1. King, K. N. (2008). C Programming: A Modern Approach (2nd ed.). W. W. Norton & Company.
  2. Kernighan, B. W., & Ritchie, D. M. (2014). C Programming Language, 2nd Edition.