Ponteiros para função
Esta aula aprofunda o uso de ponteiros para função em C, cobrindo sintaxe, atribuição e chamada, callbacks e tabelas de funções, com exemplos práticos e exercícios para fixação.
Ponteiros para função são um recurso poderoso e frequentemente subutilizado da linguagem C. Eles permitem armazenar o endereço de uma função em uma variável, possibilitando que funções sejam passadas como argumentos, retornadas de outras funções e armazenadas em estruturas de dados. Esse mecanismo é a base para implementar callbacks, tabelas de funções (como vetores de funções) e polimorfismo em C. Compreender ponteiros para função é essencial para programadores que desejam escrever código mais flexível, modular e reutilizável.
Nesta aula, vamos explorar a sintaxe correta, como atribuir e chamar funções através de ponteiros, e aplicar esses conceitos em dois cenários práticos: callbacks e tabelas de funções. Ao final, você terá uma base sólida para usar esse recurso em seus projetos.
Sintaxe
A declaração de um ponteiro para função segue uma sintaxe que pode parecer confusa à primeira vista. A forma geral é:
tipo_de_retorno (*nome_do_ponteiro)(parâmetros);Os parênteses ao redor de *nome_do_ponteiro são obrigatórios. Sem eles, a declaração seria interpretada como uma função que retorna um ponteiro, o que é completamente diferente. Por exemplo, int *func(int) declara uma função que recebe um inteiro e retorna um ponteiro para inteiro, enquanto int (*func)(int) declara um ponteiro para uma função que recebe um inteiro e retorna um inteiro.
Vejamos alguns exemplos de declaração:
int (*ptr)(int, int); // ponteiro para função que recebe dois ints e retorna int
void (*log)(const char *); // ponteiro para função que recebe uma string e retorna void
double (*calc)(double); // ponteiro para função que recebe double e retorna doubleNote que os nomes dos parâmetros podem ser omitidos na declaração, assim como em protótipos de função. Apenas os tipos são necessários.
Atribuição e chamada
Para atribuir uma função a um ponteiro, basta usar o nome da função sem os parênteses. O nome da função é convertido implicitamente para um ponteiro. Por exemplo:
int soma(int a, int b) {
return a + b;
}
int main() {
int (*ptr)(int, int);
ptr = soma; // atribui o endereço da função soma ao ponteiro ptr
// também poderia ser: ptr = &soma;
return 0;
}A chamada de uma função através do ponteiro pode ser feita de duas formas: usando o ponteiro diretamente como uma função ou desreferenciando-o explicitamente. Ambas são equivalentes:
int resultado = ptr(3, 4); // forma direta
int resultado2 = (*ptr)(3, 4); // desreferenciando explicitamenteNa prática, a forma direta é mais comum. O operador de desreferência é opcional, pois o compilador entende que o ponteiro deve ser chamado.
É importante garantir que o ponteiro seja inicializado antes de ser usado, caso contrário, o comportamento será indefinido. Uma boa prática é inicializar com NULL e verificar antes de chamar.
int (*ptr)(int, int) = NULL;
if (ptr != NULL) {
int r = ptr(1, 2);
}Callbacks
Callbacks são funções que são passadas como argumentos para outra função, permitindo que a função receptora invoque a função fornecida em momentos específicos. Isso é amplamente utilizado em bibliotecas para personalizar comportamento, como em funções de ordenação, tratamento de eventos, etc.
Um exemplo clássico é a função qsort da biblioteca padrão, que recebe um ponteiro para uma função de comparação. Essa função é chamada internamente para comparar os elementos do vetor.
#include <stdio.h>
#include <stdlib.h>
int comparar(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
return (ia > ib) - (ia < ib);
}
int main() {
int arr[] = {5, 2, 8, 1, 9};
size_t n = sizeof(arr) / sizeof(arr[0]);
qsort(arr, n, sizeof(int), comparar);
for (size_t i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}Neste exemplo, a função comparar é passada como callback para qsort. A assinatura da função de comparação deve ser exatamente int (*)(const void *, const void *).
Podemos também criar nossas próprias funções que aceitam callbacks. Por exemplo, uma função que percorre um vetor e aplica uma operação a cada elemento:
void aplicar(int *v, size_t n, int (*op)(int)) {
for (size_t i = 0; i < n; i++) {
v[i] = op(v[i]);
}
}
int dobrar(int x) { return x * 2; }
int quadrado(int x) { return x * x; }
int main() {
int v[] = {1, 2, 3, 4};
size_t n = sizeof(v) / sizeof(v[0]);
aplicar(v, n, dobrar);
// v agora é {2, 4, 6, 8}
aplicar(v, n, quadrado);
// v agora é {4, 16, 36, 64}
return 0;
}Tabelas de funções
Tabelas de funções são vetores de ponteiros para função. Elas permitem selecionar dinamicamente qual função executar com base em um índice ou valor, eliminando longas cadeias de if-else ou switch. Isso é particularmente útil em implementações de máquinas de estado, interpretadores, calculadoras, etc.
Um exemplo simples é uma calculadora que suporta as quatro operações básicas:
#include <stdio.h>
double soma(double a, double b) { return a + b; }
double subtrai(double a, double b) { return a - b; }
double multiplica(double a, double b) { return a * b; }
double divide(double a, double b) { return a / b; }
int main() {
double (*ops[])(double, double) = {soma, subtrai, multiplica, divide};
char simbolos[] = {'+', '-', '*', '/'};
double a = 10, b = 5;
for (int i = 0; i < 4; i++) {
printf("%.2f %c %.2f = %.2f\n", a, simbolos[i], b, ops[i](a, b));
}
return 0;
}Aqui, ops é um vetor de ponteiros para função que recebem dois doubles e retornam double. O índice i seleciona a operação desejada.
Outro uso comum é em menus ou comandos, onde cada opção corresponde a uma função. Por exemplo:
void opcao1() { printf("Opção 1 selecionada\n"); }
void opcao2() { printf("Opção 2 selecionada\n"); }
void opcao3() { printf("Opção 3 selecionada\n"); }
int main() {
void (*menu[])() = {opcao1, opcao2, opcao3};
int escolha;
do {
printf("Escolha 1-3 (0 para sair): ");
scanf("%d", &escolha);
if (escolha >= 1 && escolha <= 3) {
menu[escolha - 1]();
}
} while (escolha != 0);
return 0;
}Note que, em C, um ponteiro para função pode ser usado com uma lista de argumentos vazia () para indicar que a função não recebe argumentos (embora em C moderno seja melhor usar (void)).
Boas práticas e observações finais
Ao trabalhar com ponteiros para função, é importante manter a legibilidade e a segurança. Use typedef para simplificar declarações complexas. Por exemplo:
typedef int (*Operacao)(int, int);
Operacao op = soma;Isso torna o código mais claro e fácil de manter. Sempre verifique se o ponteiro não é NULL antes de chamar a função, especialmente em APIs públicas. Além disso, documente bem a assinatura esperada das funções para evitar erros de tipo.
Referências
- Ponteiros (cppreference.com)
- qsort (cppreference.com)
- Function Pointers (GNU C Library Manual)
- Function Pointers (learn-c.org)
- Function pointer (Wikipedia)
- qsort (POSIX)
Exercícios
- Escreva uma função
aplicar_duploque recebe um ponteiro para uma função que recebe um inteiro e retorna um inteiro, e um inteirox, e retorna o resultado de chamar a função duas vezes sobrex(ou seja,f(f(x))). - Crie um vetor de ponteiros para função que aponte para as funções
soma,subtrai,multiplicaedivide(todas recebendo doisinte retornandoint). Use esse vetor para calcular e imprimir o resultado de 20 + 5, 20 - 5, 20 * 5 e 20 / 5. - Implemente uma função
registrar_callbackque recebe um ponteiro para funçãovoid (*cb)(int)e um inteiroevento, e armazena o callback em um vetor global de callbacks indexado pelo evento. Assuma que há espaço para até 10 eventos. - Usando
typedef, defina um tipoOperacaoBinariapara um ponteiro para função que recebe doisdoublee retornadouble. Escreva uma função que recebe um vetor deOperacaoBinariae um tamanho, e imprime o resultado de aplicar cada operação a dois números fixos (ex.: 3.5 e 2.0). - Explique por que a declaração
int *func(int)é diferente deint (*func)(int)e dê um exemplo de uso para cada um.
int aplicar_duplo(int (*f)(int), int x) {
return f(f(x));
}#include <stdio.h>
int soma(int a, int b) { return a + b; }
int subtrai(int a, int b) { return a - b; }
int multiplica(int a, int b) { return a * b; }
int divide(int a, int b) { return a / b; }
int main() {
int (*ops[])(int, int) = {soma, subtrai, multiplica, divide};
int a = 20, b = 5;
for (int i = 0; i < 4; i++) {
printf("%d\n", ops[i](a, b));
}
return 0;
}#define MAX_EVENTOS 10
void (*callbacks[MAX_EVENTOS])(int);
void registrar_callback(void (*cb)(int), int evento) {
if (evento >= 0 && evento < MAX_EVENTOS) {
callbacks[evento] = cb;
}
}typedef double (*OperacaoBinaria)(double, double);
void imprimir_operacoes(OperacaoBinaria *ops, size_t n) {
double a = 3.5, b = 2.0;
for (size_t i = 0; i < n; i++) {
printf("%f\n", ops[i](a, b));
}
}int *func(int) declara uma função chamada func que recebe um int e retorna um ponteiro para int. Exemplo: int *alocar(int n) { return malloc(n * sizeof(int)); }. Já int (*func)(int) declara um ponteiro para uma função que recebe um int e retorna int. Exemplo: int dobrar(int x) { return x * 2; } int (*func)(int) = dobrar;. A diferença está no operador * e os parênteses: sem parênteses, o * se aplica ao tipo de retorno; com parênteses, ele se aplica ao nome do ponteiro.