Callbacks são funções passadas como argumentos para outras funções, permitindo que o comportamento de uma função seja personalizado pelo chamador. Em C, isso é feito através de ponteiros para funções, um recurso poderoso que possibilita a implementação de padrões como gerenciamento de eventos, algoritmos genéricos e inversão de controle. Nesta aula, vamos explorar como definir, passar e usar callbacks em C, com foco no uso prático com a função qsort e em outros cenários comuns.

Dominar callbacks é essencial para escrever código modular e reutilizável, pois permite que você separe a lógica de algoritmo da lógica de aplicação. Vamos começar entendendo a sintaxe de ponteiros para funções e, em seguida, aplicá-los em exemplos concretos.

Passando funções como argumento

Em C, funções não podem ser passadas diretamente como argumentos; você deve passar um ponteiro para a função. Um ponteiro para função armazena o endereço de uma função e pode ser chamado através do operador de desreferência * (opcional, pois C permite chamada direta). A sintaxe para declarar um ponteiro para função é:

tipo_de_retorno (*nome_do_ponteiro)(parâmetros);

Por exemplo, para um ponteiro para uma função que recebe dois inteiros e retorna um inteiro:

int (*operacao)(int, int);

Você pode atribuir a esse ponteiro o endereço de uma função compatível:

int soma(int a, int b) { return a + b; }
operacao = soma; // sem o operador &, pois o nome da função já é um ponteiro

E chamar a função através do ponteiro:

int resultado = operacao(2, 3); // chama soma(2, 3)

Agora, para passar uma função como argumento, você escreve uma função que recebe um ponteiro para função como parâmetro. Veja um exemplo completo:

#include <stdio.h>

// Declaração do tipo de ponteiro para função
int (*operacao_t)(int, int);

int soma(int a, int b) { return a + b; }
int subtrai(int a, int b) { return a - b; }

// Função que recebe um ponteiro para função
void aplica(int a, int b, int (*func)(int, int)) {
    printf("Resultado: %d\n", func(a, b));
}

int main() {
    aplica(10, 5, soma);      // imprime 15
    aplica(10, 5, subtrai);   // imprime 5
    return 0;
}

Note que o parâmetro func é declarado como int (*func)(int, int). Você pode simplificar usando um typedef para melhorar a legibilidade:

typedef int (*operacao_t)(int, int);

void aplica(int a, int b, operacao_t func) {
    printf("Resultado: %d\n", func(a, b));
}

Isso torna o código mais limpo e fácil de manter.

qsort com comparador

A função qsort da biblioteca padrão (stdlib.h) é um exemplo clássico de uso de callbacks. Ela ordena um array de qualquer tipo de dado, desde que você forneça uma função comparadora que diga como dois elementos devem ser comparados. O protótipo de qsort é:

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

O último argumento é um ponteiro para uma função de comparação que retorna:

  • um valor negativo se o primeiro elemento deve vir antes do segundo;
  • zero se são equivalentes;
  • um valor positivo se o primeiro deve vir depois.

Para usar qsort, você precisa converter os ponteiros void * para o tipo correto dentro da função comparadora. Exemplo com um array de inteiros:

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

int comparador(const void *a, const void *b) {
    int x = *(int *)a;
    int y = *(int *)b;
    return (x - y); // crescente
}

int main() {
    int arr[] = {5, 2, 9, 1, 7};
    size_t n = sizeof(arr) / sizeof(arr[0]);
    qsort(arr, n, sizeof(int), comparador);
    for (size_t i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    return 0;
}

Ao usar qsort, você pode ordenar qualquer tipo de dado: estruturas, ponteiros, strings, etc. Basta adaptar o comparador. Por exemplo, para ordenar um array de structs por um campo:

typedef struct {
    int id;
    char nome[50];
} Pessoa;

int comparar_pessoas(const void *a, const void *b) {
    const Pessoa *p1 = (const Pessoa *)a;
    const Pessoa *p2 = (const Pessoa *)b;
    return p1->id - p2->id; // ordena por id
}

Isso demonstra a flexibilidade dos callbacks: a mesma função qsort pode ordenar qualquer coisa, desde que você forneça a lógica de comparação.

Casos de uso

Callbacks são usados em diversas situações na programação em C, especialmente em bibliotecas e frameworks. Alguns casos comuns incluem:

  • Gerenciamento de eventos: como em interfaces gráficas ou sistemas de entrada, onde você registra funções para serem chamadas quando ocorrem eventos (cliques, teclas, etc.).
  • Algoritmos genéricos: como qsort e bsearch, que funcionam para qualquer tipo de dado com um comparador.
  • Iteração sobre coleções: funções que percorrem listas, árvores ou outros contêineres e executam uma operação fornecida pelo usuário em cada elemento.
  • Inversão de controle: em bibliotecas de rede, timers ou operações assíncronas, onde você fornece um callback para ser executado quando a operação terminar.
  • Plugins e extensões: permitem que o código principal chame funções definidas pelo usuário, como em sistemas de plugins.

Um exemplo prático de callback para iterar sobre um array:

#include <stdio.h>

typedef void (*visitante_t)(int);

void para_cada(int *arr, size_t n, visitante_t visitante) {
    for (size_t i = 0; i < n; i++) {
        visitante(arr[i]);
    }
}

void imprime_dobro(int x) {
    printf("%d ", x * 2);
}

int main() {
    int numeros[] = {1, 2, 3, 4};
    para_cada(numeros, 4, imprime_dobro);
    return 0;
}

Esse padrão permite que a função para_cada seja reutilizada com diferentes ações.

Exemplos

Vamos consolidar com dois exemplos mais completos. O primeiro mostra um sistema de eventos simples, e o segundo demonstra o uso de callbacks com structs.

Exemplo 1: Sistema de eventos

#include <stdio.h>

typedef struct {
    void (*on_clique)(int x, int y);
    void (*on_tecla)(char tecla);
} Eventos;

void clique(int x, int y) {
    printf("Clique em (%d, %d)\n", x, y);
}

void tecla(char c) {
    printf("Tecla pressionada: %c\n", c);
}

void simular_loop(Eventos *ev) {
    // Simula eventos
    ev->on_clique(10, 20);
    ev->on_tecla('A');
}

int main() {
    Eventos eventos = {clique, tecla};
    simular_loop(&eventos);
    return 0;
}

Exemplo 2: Ordenação de structs com qsort

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

typedef struct {
    char nome[50];
    int idade;
} Pessoa;

int comparar_por_nome(const void *a, const void *b) {
    const Pessoa *p1 = (const Pessoa *)a;
    const Pessoa *p2 = (const Pessoa *)b;
    return strcmp(p1->nome, p2->nome);
}

int main() {
    Pessoa pessoas[] = {
        {"Ana", 30},
        {"Carlos", 25},
        {"Beatriz", 35}
    };
    size_t n = sizeof(pessoas) / sizeof(pessoas[0]);
    qsort(pessoas, n, sizeof(Pessoa), comparar_por_nome);
    for (size_t i = 0; i < n; i++) {
        printf("%s (%d)\n", pessoas[i].nome, pessoas[i].idade);
    }
    return 0;
}

Nesses exemplos, os callbacks permitem que o código seja flexível e reutilizável, separando a lógica de controle da lógica de negócio.

Boas práticas e observações finais

Ao trabalhar com callbacks, siga estas boas práticas para evitar erros comuns:

  • Use typedef para simplificar a declaração de ponteiros para função.
  • Garanta que a assinatura do callback corresponda exatamente ao que a função espera; erros de tipo são difíceis de detectar.
  • Não chame um callback se ele for NULL; verifique primeiro para evitar crashes.
  • Em callbacks que recebem void *, faça casts explícitos e seguros para o tipo correto.
  • Documente claramente o que o callback deve fazer e o que os parâmetros significam.

Callbacks são uma ferramenta poderosa para criar código modular e extensível. Dominar seu uso é um passo importante para se tornar um programador C proficiente.

Referências

Exercícios

  1. Escreva uma função map que recebe um array de inteiros, seu tamanho e um callback que transforma um inteiro em outro. A função deve aplicar o callback a cada elemento e armazenar o resultado em um novo array. Teste com uma função que dobra o valor.

    ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    typedef int (*transform_t)(int);
    
    int dobra(int x) { return x * 2; }
    
    int *map(int *arr, size_t n, transform_t f) {
        int *novo = malloc(n * sizeof(int));
        if (!novo) return NULL;
        for (size_t i = 0; i < n; i++) {
            novo[i] = f(arr[i]);
        }
        return novo;
    }
    
    int main() {
        int numeros[] = {1, 2, 3, 4};
        size_t n = sizeof(numeros) / sizeof(numeros[0]);
        int *resultado = map(numeros, n, dobra);
        if (resultado) {
            for (size_t i = 0; i < n; i++) {
                printf("%d ", resultado[i]);
            }
            printf("\n");
            free(resultado);
        }
        return 0;
    }
  2. Crie uma função filter que recebe um array de inteiros, seu tamanho e um predicado (função que retorna 1 para verdadeiro e 0 para falso). A função deve retornar um novo array contendo apenas os elementos que satisfazem o predicado, e também preencher um parâmetro de saída com o tamanho do novo array. Teste com um predicado que verifica se o número é par.

    ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    typedef int (*predicado_t)(int);
    
    int eh_par(int x) { return x % 2 == 0; }
    
    int *filter(int *arr, size_t n, predicado_t pred, size_t *new_size) {
        int *temp = malloc(n * sizeof(int));
        if (!temp) return NULL;
        size_t count = 0;
        for (size_t i = 0; i < n; i++) {
            if (pred(arr[i])) {
                temp[count++] = arr[i];
            }
        }
        int *novo = realloc(temp, count * sizeof(int));
        if (!novo) { free(temp); return NULL; }
        *new_size = count;
        return novo;
    }
    
    int main() {
        int numeros[] = {1, 2, 3, 4, 5, 6};
        size_t n = sizeof(numeros) / sizeof(numeros[0]);
        size_t novo_n;
        int *resultado = filter(numeros, n, eh_par, &novo_n);
        if (resultado) {
            for (size_t i = 0; i < novo_n; i++) {
                printf("%d ", resultado[i]);
            }
            printf("\n");
            free(resultado);
        }
        return 0;
    }
  3. Usando qsort, ordene um array de char * (strings) em ordem alfabética. Escreva o comparador adequado.

    ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int comparar_strings(const void *a, const void *b) {
        const char **str_a = (const char **)a;
        const char **str_b = (const char **)b;
        return strcmp(*str_a, *str_b);
    }
    
    int main() {
        const char *nomes[] = {"Carlos", "Ana", "Beatriz", "Daniel"};
        size_t n = sizeof(nomes) / sizeof(nomes[0]);
        qsort(nomes, n, sizeof(char *), comparar_strings);
        for (size_t i = 0; i < n; i++) {
            printf("%s\n", nomes[i]);
        }
        return 0;
    }
  4. Implemente uma função de_para que recebe dois arrays de mesmo tamanho (um de chaves e um de valores) e um callback que recebe a chave e o valor e imprime algo. Use um callback que imprime "chave: valor".

    ✓ Resposta:
    #include <stdio.h>
    
    typedef void (*imprimir_t)(int, int);
    
    void imprime_par(int chave, int valor) {
        printf("%d: %d\n", chave, valor);
    }
    
    void de_para(int *chaves, int *valores, size_t n, imprimir_t func) {
        for (size_t i = 0; i < n; i++) {
            func(chaves[i], valores[i]);
        }
    }
    
    int main() {
        int chaves[] = {1, 2, 3};
        int valores[] = {10, 20, 30};
        de_para(chaves, valores, 3, imprime_par);
        return 0;
    }
  5. Escreva um programa que registra dois callbacks em uma struct (um para soma e outro para multiplicação) e os chama com dois números. Use typedef para o tipo de ponteiro.

    ✓ Resposta:
    #include <stdio.h>
    
    typedef int (*operacao_t)(int, int);
    
    typedef struct {
        operacao_t soma;
        operacao_t mult;
    } Operacoes;
    
    int soma(int a, int b) { return a + b; }
    int mult(int a, int b) { return a * b; }
    
    int main() {
        Operacoes ops = {soma, mult};
        int x = 4, y = 5;
        printf("Soma: %d\n", ops.soma(x, y));
        printf("Multiplicação: %d\n", ops.mult(x, y));
        return 0;
    }