Callbacks na prática
Nesta aula, você aprenderá o que são callbacks em C, como passá-los como argumentos para funções, e aplicará esse conceito na prática com qsort e outros exemplos. Veremos também casos de uso comuns e boas práticas para escrever código mais flexível e reutilizável.
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 ponteiroE 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
qsortebsearch, 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
typedefpara 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
- Function pointer - cppreference.com
- qsort - cppreference.com
- Function Pointers - GNU C Library Manual
- Function Pointer in C - GeeksforGeeks
- Callbacks in C - Wikipedia
Exercícios
Escreva uma função
mapque 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; }Crie uma função
filterque 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; }Usando
qsort, ordene um array dechar *(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; }Implemente uma função
de_paraque 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; }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; }