Callbacks são uma técnica fundamental em C que permite passar funções como argumentos para outras funções. Isso possibilita escrever código mais genérico e reutilizável, delegando comportamentos específicos para o chamador. Nesta aula, vamos entender como declarar e usar ponteiros para funções, aplicar callbacks com a função de ordenação qsort e explorar casos de uso comuns.

Dominar callbacks é essencial para programação em C, especialmente em bibliotecas e sistemas embarcados, onde a flexibilidade de comportamento é crucial. Vamos começar pelo básico: como passar uma função como argumento.

Passando funções como argumento

Em C, funções não são valores de primeira classe, mas podemos usar ponteiros para funções. Um ponteiro para função armazena o endereço de uma função, permitindo que ela seja chamada indiretamente. A sintaxe para declarar um ponteiro para função é:

tipo_retorno (*nome_ponteiro)(parametros);

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

int (*operacao)(int, int);

Podemos atribuir a esse ponteiro o endereço de uma função compatível, como soma ou subtrai. Para chamar a função através do ponteiro, usamos a sintaxe operacao(a, b) ou (*operacao)(a, b).

Um exemplo completo de passar funções como argumento:

#include <stdio.h>

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

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

int main() {
    int x = 10, y = 5;
    executa(x, y, soma);
    executa(x, y, subtrai);
    return 0;
}

No código acima, a função executa recebe um ponteiro para função func e a chama com os argumentos passados. Isso é um callback simples: a função executa chama de volta a função fornecida.

qsort com comparador

A função qsort da biblioteca padrão é um exemplo clássico de uso de callbacks. Ela ordena um array de qualquer tipo usando um comparador fornecido pelo usuário. A assinatura de qsort é:

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

O quarto parâmetro é um ponteiro para função que compara dois elementos. A função comparadora deve retornar um inteiro negativo se o primeiro elemento for menor que o segundo, zero se forem iguais, e positivo se o primeiro for maior.

Vamos ordenar um array de inteiros:

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

int compar_int(const void *a, const void *b) {
    int x = *(const int *)a;
    int y = *(const int *)b;
    return (x > y) - (x < y);
}

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

O comparador recebe ponteiros const void * e precisa convertê-los para o tipo correto antes de desreferenciar. Isso permite que qsort funcione com qualquer tipo de dado, desde que o comparador saiba como interpretá-los.

Casos de uso

Callbacks são amplamente usados em C para:

  • Ordenação e busca: como qsort e bsearch, que exigem comparadores personalizados.
  • Tratamento de eventos: em GUIs ou sistemas embarcados, callbacks são registrados para responder a eventos como cliques ou interrupções.
  • Estratégias algorítmicas: funções como map, filter e reduce podem ser implementadas com callbacks para operar em coleções.
  • Personalização de comportamento: bibliotecas podem permitir que o usuário forneça funções para customizar a saída ou o processamento.

Um exemplo prático é usar callbacks para implementar uma função map que aplica uma transformação a cada elemento de um array:

#include <stdio.h>

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

int dobro(int x) { return x * 2; }

int main() {
    int arr[] = {1, 2, 3, 4};
    size_t n = sizeof(arr) / sizeof(arr[0]);
    map(arr, n, dobro);
    for (size_t i = 0; i < n; i++) printf("%d ", arr[i]);
    return 0;
}

Exemplos

Vamos consolidar com um exemplo mais completo: um sistema de registro de eventos que usa callbacks para notificar diferentes partes do programa.

#include <stdio.h>

// Tipo de callback para evento
typedef void (*event_handler)(int event_id);

void registrar_evento(event_handler handler) {
    // Simula um evento
    handler(42);
}

void meu_handler(int id) {
    printf("Evento recebido: %d\n", id);
}

int main() {
    registrar_evento(meu_handler);
    return 0;
}

Outro exemplo: usar qsort para ordenar strings em ordem alfabética.

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

int compar_str(const void *a, const void *b) {
    return strcmp(*(const char **)a, *(const char **)b);
}

int main() {
    const char *nomes[] = {"João", "Maria", "Ana", "Pedro"};
    size_t n = sizeof(nomes) / sizeof(nomes[0]);
    qsort(nomes, n, sizeof(char *), compar_str);
    for (size_t i = 0; i < n; i++) printf("%s\n", nomes[i]);
    return 0;
}

Perceba que o comparador de strings recebe ponteiros para char * (ponteiro para ponteiro), pois o array é de ponteiros.

Boas práticas e observações finais

  • Sempre verifique se o ponteiro de função não é NULL antes de chamá-lo, para evitar crashes.
  • Documente claramente o contrato do callback: parâmetros, retorno e significado.
  • Use typedef para simplificar declarações de ponteiros de função e melhorar a legibilidade.
  • Em callbacks que recebem const void *, faça a conversão de tipo com cuidado para evitar violações de aliasing.
  • Prefira funções estáticas para callbacks internos, reduzindo o escopo e evitando conflitos de nomes.

Referências

Exercícios

  1. Escreva uma função apply_twice que recebe um inteiro e um ponteiro para função que modifica um inteiro, e retorna o resultado de aplicar a função duas vezes ao valor inicial.

    ✓ Resposta:
    int apply_twice(int x, int (*func)(int)) { return func(func(x)); }
  2. Crie um comparador para qsort que ordene números de ponto flutuante em ordem decrescente.

    ✓ Resposta:
    int compar_double_desc(const void *a, const void *b) {
        double da = *(const double *)a;
        double db = *(const double *)b;
        if (da < db) return 1;
        if (da > db) return -1;
        return 0;
    }
  3. Implemente uma função filter que recebe um array de inteiros, seu tamanho, um predicado (função que retorna 1 para manter, 0 para descartar) e um ponteiro para um array de saída. A função deve preencher o array de saída com os elementos que satisfazem o predicado e retornar o novo tamanho.

    ✓ Resposta:
    size_t filter(const int *arr, size_t n, int (*pred)(int), int *out) {
        size_t j = 0;
        for (size_t i = 0; i < n; i++) {
            if (pred(arr[i])) out[j++] = arr[i];
        }
        return j;
    }
  4. Explique por que o comparador de qsort recebe ponteiros para const void e como isso permite ordenar qualquer tipo de dado.

    ✓ Resposta: A função qsort é genérica e não sabe o tipo dos elementos. Usar void * permite que ela trate qualquer tipo de dado, e const garante que os elementos não sejam modificados. O comparador converte os ponteiros para o tipo correto, permitindo que a comparação seja feita de forma específica.
  5. Escreva um programa que usa qsort para ordenar um array de structs Pessoa (com campos nome e idade) por idade.

    ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    typedef struct { char nome[50]; int idade; } Pessoa;
    
    int compar_pessoa(const void *a, const void *b) {
        const Pessoa *pa = a;
        const Pessoa *pb = b;
        return pa->idade - pb->idade;
    }
    
    int main() {
        Pessoa p[] = {{"Ana", 25}, {"João", 30}, {"Maria", 20}};
        size_t n = sizeof(p) / sizeof(p[0]);
        qsort(p, n, sizeof(Pessoa), compar_pessoa);
        for (size_t i = 0; i < n; i++) printf("%s: %d\n", p[i].nome, p[i].idade);
        return 0;
    }