Punteros a funciones y callbacks

Avanzado
C
C
Actualizado: 18/04/2026

Patrón callback con qsort y funciones de comparación intercambiables

Sintaxis de punteros a funciones

En C, las funciones ocupan una dirección de memoria concreta, igual que cualquier variable. Un puntero a función almacena esa dirección y permite invocar la función de forma indirecta. Esta capacidad es la base de los callbacks y de muchos patrones de diseño que aportan flexibilidad al código.

La declaración de un puntero a función sigue una sintaxis particular que conviene dominar desde el principio:

tipo_retorno (*nombre_puntero)(tipos_parametros);

Los paréntesis alrededor de *nombre_puntero son obligatorios. Sin ellos, el compilador interpretaría la declaración como una función que retorna un puntero, algo completamente diferente.

#include <stdio.h>

int sumar(int a, int b) {
    return a + b;
}

int restar(int a, int b) {
    return a - b;
}

int main() {
    // Declaramos un puntero a función que recibe dos int y retorna int
    int (*operacion)(int, int);

    // Asignamos la dirección de 'sumar'
    operacion = sumar;
    printf("Suma: %d\n", operacion(10, 3));

    // Reasignamos para apuntar a 'restar'
    operacion = restar;
    printf("Resta: %d\n", operacion(10, 3));

    return 0;
}

La salida será:

Suma: 13
Resta: 7

El nombre de una función, usado sin paréntesis de invocación, se convierte automáticamente en un puntero a esa función. Por eso operacion = sumar y operacion = &sumar son equivalentes. De la misma forma, operacion(10, 3) y (*operacion)(10, 3) producen el mismo resultado.

Un puntero a función almacena la dirección de una función y permite invocarla de forma indirecta. Los paréntesis en (*nombre)(...) son imprescindibles para que el compilador distinga un puntero a función de una función que retorna puntero.

Typedef para simplificar la sintaxis

La declaración directa de punteros a funciones resulta verbosa y difícil de leer, especialmente cuando aparece varias veces en el mismo programa. El uso de typedef permite crear un alias de tipo que simplifica enormemente el código.

#include <stdio.h>

// Definimos un alias para un puntero a función (int, int) -> int
typedef int (*OperacionBinaria)(int, int);

int multiplicar(int a, int b) {
    return a * b;
}

int dividir(int a, int b) {
    if (b == 0) {
        printf("Error: division por cero\n");
        return 0;
    }
    return a / b;
}

// Gracias al typedef, el prototipo es limpio y legible
void ejecutar(OperacionBinaria op, int x, int y) {
    printf("Resultado: %d\n", op(x, y));
}

int main() {
    ejecutar(multiplicar, 6, 4);
    ejecutar(dividir, 20, 5);
    return 0;
}
Resultado: 24
Resultado: 4

Sin el typedef, la firma de ejecutar sería void ejecutar(int (*op)(int, int), int x, int y), lo cual dificulta la lectura cuando las firmas se repiten en prototipos, parámetros y variables locales.

typedef aplicado a punteros a funciones elimina la complejidad sintáctica y hace que el código sea más fácil de mantener. Conviene adoptarlo como práctica habitual siempre que un tipo de puntero a función se utilice más de una vez.

El patrón callback

Un callback es una función que se pasa como argumento a otra función para que esta última la invoque en el momento apropiado. Este patrón permite escribir funciones genéricas cuyo comportamiento concreto se decide en tiempo de ejecución.

Comparador personalizado con qsort

La función qsort de la biblioteca estándar (<stdlib.h>) es el ejemplo más conocido de callback en C. Su prototipo es:

void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));

El cuarto parámetro es un puntero a función comparadora que qsort invoca para decidir el orden de los elementos. Proporcionando distintos comparadores se obtienen distintos criterios de ordenación sin modificar qsort.

#include <stdio.h>
#include <stdlib.h>

// Comparador ascendente
int comparar_asc(const void *a, const void *b) {
    int va = *(const int *)a;
    int vb = *(const int *)b;
    return va - vb;
}

// Comparador descendente
int comparar_desc(const void *a, const void *b) {
    int va = *(const int *)a;
    int vb = *(const int *)b;
    return vb - va;
}

void imprimir_array(const int *arr, int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int datos[] = {42, 7, 15, 3, 28, 11};
    int n = sizeof(datos) / sizeof(datos[0]);

    qsort(datos, n, sizeof(int), comparar_asc);
    printf("Ascendente:  ");
    imprimir_array(datos, n);

    qsort(datos, n, sizeof(int), comparar_desc);
    printf("Descendente: ");
    imprimir_array(datos, n);

    return 0;
}
Ascendente:  3 7 11 15 28 42
Descendente: 42 28 15 11 7 3

Procesamiento genérico de arrays

El mismo patrón se aplica para transformar o filtrar colecciones. La siguiente función recorre un array y aplica una operación arbitraria a cada elemento:

#include <stdio.h>

typedef void (*TransformFunc)(int *elemento);

void duplicar(int *elemento) {
    *elemento *= 2;
}

void negar(int *elemento) {
    *elemento = -(*elemento);
}

// Función genérica que aplica cualquier transformación
void transformar_array(int *arr, int n, TransformFunc fn) {
    for (int i = 0; i < n; i++) {
        fn(&arr[i]);
    }
}

void imprimir(const int *arr, int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int valores[] = {1, 2, 3, 4, 5};
    int n = 5;

    transformar_array(valores, n, duplicar);
    printf("Duplicados: ");
    imprimir(valores, n);

    transformar_array(valores, n, negar);
    printf("Negados:    ");
    imprimir(valores, n);

    return 0;
}
Duplicados: 2 4 6 8 10
Negados:    -2 -4 -6 -8 -10

La función transformar_array no sabe qué operación va a realizar. Simplemente delega en el callback que recibe como tercer parámetro. Esto la convierte en una pieza reutilizable para cualquier transformación de enteros.

Arrays de punteros a funciones

Cuando un programa necesita seleccionar entre varias funciones en función de un índice o un código numérico, resulta práctico almacenar los punteros a funciones en un array. Esto evita largas cadenas de if/else o switch y facilita la extensión del programa.

#include <stdio.h>

double sumar_d(double a, double b) { return a + b; }
double restar_d(double a, double b) { return a - b; }
double multiplicar_d(double a, double b) { return a * b; }
double dividir_d(double a, double b) { return b != 0 ? a / b : 0; }

int main() {
    typedef double (*OpMatematica)(double, double);

    // Array de punteros a funciones
    OpMatematica operaciones[] = {sumar_d, restar_d, multiplicar_d, dividir_d};
    const char *nombres[] = {"Suma", "Resta", "Multiplicacion", "Division"};

    double x = 20.0, y = 4.0;

    for (int i = 0; i < 4; i++) {
        printf("%s: %.2f\n", nombres[i], operaciones[i](x, y));
    }

    return 0;
}
Suma: 24.00
Resta: 16.00
Multiplicacion: 80.00
Division: 5.00

La estructura operaciones[i](x, y) invoca directamente la función almacenada en la posición i del array. Añadir una nueva operación solo requiere definir la función e incluirla en el array, sin tocar la lógica del bucle.

Ejemplo práctico: manejador de eventos

Un caso de uso habitual en programación de sistemas es un despachador de eventos donde cada tipo de evento tiene asociada una función manejadora. El siguiente ejemplo modela un sistema sencillo que responde a eventos de teclado, ratón y temporizador.

#include <stdio.h>

// Tipos de evento
enum TipoEvento {
    EVENTO_TECLADO,
    EVENTO_RATON,
    EVENTO_TEMPORIZADOR,
    NUM_EVENTOS
};

// Estructura de evento
typedef struct {
    enum TipoEvento tipo;
    int codigo;
} Evento;

// Tipo para los manejadores
typedef void (*Manejador)(const Evento *);

// Funciones manejadoras concretas
void manejar_teclado(const Evento *ev) {
    printf("Teclado: tecla %d pulsada\n", ev->codigo);
}

void manejar_raton(const Evento *ev) {
    printf("Raton: boton %d pulsado\n", ev->codigo);
}

void manejar_temporizador(const Evento *ev) {
    printf("Temporizador: tick %d\n", ev->codigo);
}

// Despachador basado en array de punteros a funciones
typedef struct {
    Manejador tabla[NUM_EVENTOS];
} Despachador;

void registrar(Despachador *d, enum TipoEvento tipo, Manejador fn) {
    d->tabla[tipo] = fn;
}

void despachar(Despachador *d, const Evento *ev) {
    if (ev->tipo >= 0 && ev->tipo < NUM_EVENTOS && d->tabla[ev->tipo] != NULL) {
        d->tabla[ev->tipo](ev);
    } else {
        printf("Evento no registrado\n");
    }
}

int main() {
    Despachador desp = { .tabla = {NULL} };

    // Registramos los manejadores
    registrar(&desp, EVENTO_TECLADO, manejar_teclado);
    registrar(&desp, EVENTO_RATON, manejar_raton);
    registrar(&desp, EVENTO_TEMPORIZADOR, manejar_temporizador);

    // Simulamos eventos
    Evento eventos[] = {
        {EVENTO_TECLADO, 65},
        {EVENTO_RATON, 1},
        {EVENTO_TEMPORIZADOR, 100},
        {EVENTO_TECLADO, 27}
    };

    int n = sizeof(eventos) / sizeof(eventos[0]);
    for (int i = 0; i < n; i++) {
        despachar(&desp, &eventos[i]);
    }

    return 0;
}
Teclado: tecla 65 pulsada
Raton: boton 1 pulsado
Temporizador: tick 100
Teclado: tecla 27 pulsada

Este diseño sigue el patrón estrategia: el despachador no conoce la implementación de cada manejador. Solo sabe que la firma coincide con el tipo Manejador. Sustituir o ampliar manejadores se reduce a llamar a registrar con una función diferente, sin modificar la lógica de despacho.

La combinación de typedef, arrays de punteros a funciones y estructuras permite construir sistemas extensibles en C puro. Las bibliotecas de interfaz gráfica, los controladores de dispositivos y los servidores de red recurren a estos mismos mecanismos para desacoplar la lógica genérica de las implementaciones concretas que varían según el contexto.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, C es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de C

Explora más contenido relacionado con C y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Declarar y asignar punteros a funciones. Usar typedef para simplificar la sintaxis de punteros a funciones. Implementar callbacks para personalizar el comportamiento de funciones genéricas. Aplicar punteros a funciones en ordenación personalizada y procesamiento de datos.