Até agora, ponteiros sempre apontaram para dados — um inteiro, uma struct, uma string na memória. Hoje descobrimos algo surpreendente: funções também vivem na memória, têm endereços, e um ponteiro pode apontar para elas. Isso destrava uma ideia poderosa — passar comportamento como argumento, não apenas dados. É o que permite escrever código genérico, que recebe "o que fazer" de fora, e é a base de recursos como as funções de ordenação da biblioteca padrão, que veremos na próxima aula. É uma das ideias mais elegantes do C.
Funções também têm endereço
Lembre-se de que, na aula sobre a memória, vimos que o código do programa — as instruções das funções — vive numa região própria. Isso significa que cada função tem um endereço, assim como cada variável. E, se tem endereço, podemos guardá-lo num ponteiro. O nome de uma função, usado sem os parênteses, é justamente o seu endereço:
#include <stdio.h>
int dobrar(int x) {
return x * 2;
}
int main(void) {
// ptr é um ponteiro para função que recebe int e retorna int
int (*ptr)(int) = dobrar; // 'dobrar' sem () é o endereço da função
printf("%d\n", ptr(5)); // 10 — chamamos a função ATRAVÉS do ponteiro
printf("%d\n", (*ptr)(5)); // 10 — forma explícita, equivalente
return 0;
}
A sintaxe de declaração é a parte mais intimidante, então vamos decompô-la. int (*ptr)(int) lê-se assim: ptr é um ponteiro (*ptr) para uma função que recebe um int e retorna um int. Os parênteses em torno de *ptr são obrigatórios — sem eles, int *ptr(int) significaria outra coisa (uma função que retorna int *). Depois de declarado, você chama a função pelo ponteiro simplesmente com ptr(5), como se fosse a função original. É estranho no começo, mas você se acostuma rápido.
Por que isso é útil: comportamento como parâmetro
A verdadeira força aparece quando passamos uma função para outra função. Isso permite escrever uma função genérica cujo comportamento específico é fornecido por quem a chama:
#include <stdio.h>
int somar(int a, int b) { return a + b; }
int subtrair(int a, int b) { return a - b; }
int multiplicar(int a, int b) { return a * b; }
// recebe uma OPERAÇÃO como argumento (um ponteiro para função)
int calcular(int x, int y, int (*operacao)(int, int)) {
return operacao(x, y); // chama a função que foi passada
}
int main(void) {
printf("%d\n", calcular(6, 3, somar)); // 9
printf("%d\n", calcular(6, 3, subtrair)); // 3
printf("%d\n", calcular(6, 3, multiplicar)); // 18
return 0;
}
Observe o que aconteceu: a função calcular não sabe, de antemão, que operação fará — ela recebe essa decisão como argumento. Passamos somar, subtrair ou multiplicar (sem parênteses, pois queremos o endereço, não o resultado), e calcular executa a operação escolhida. Uma única função, comportamentos ilimitados. Esse é o coração do código genérico: separar o "esqueleto" do algoritmo da "carne" da operação específica.
Callbacks: funções chamadas de volta
Uma função passada para ser chamada por outra é chamada de callback ("chamar de volta"). É um padrão onipresente. Imagine uma função que aplica uma transformação a cada elemento de um vetor, sem saber qual transformação:
#include <stdio.h>
int quadrado(int x) { return x * x; }
int negativo(int x) { return -x; }
// aplica a função 'transformar' a cada elemento do vetor
void mapear(int *v, int n, int (*transformar)(int)) {
for (int i = 0; i < n; i++) {
v[i] = transformar(v[i]);
}
}
int main(void) {
int numeros[5] = {1, 2, 3, 4, 5};
mapear(numeros, 5, quadrado); // aplica quadrado a cada um
for (int i = 0; i < 5; i++) printf("%d ", numeros[i]); // 1 4 9 16 25
printf("\n");
mapear(numeros, 5, negativo); // agora inverte o sinal de cada um
for (int i = 0; i < 5; i++) printf("%d ", numeros[i]); // -1 -4 -9 -16 -25
printf("\n");
return 0;
}
A função mapear implementa o "percorrer o vetor e transformar cada elemento" uma única vez; a transformação concreta vem de fora, como callback. Trocando o callback, mudamos completamente o resultado sem tocar em mapear. Se você já viu esse conceito em linguagens modernas (o map, filter, forEach), saiba que ele nasce exatamente daqui — de ponteiros para função.
Deixando a sintaxe legível com typedef
A declaração de ponteiros para função é notoriamente feia, especialmente em assinaturas de função. Aqui o typedef da aula typedef e o Design de Tipos Legíveis mostra seu valor — e este é um dos casos em que esconder um ponteiro num typedef é recomendado, ao contrário do que vimos para ponteiros de dados:
#include <stdio.h>
// define 'Operacao' como "ponteiro para função (int, int) -> int"
typedef int (*Operacao)(int, int);
int somar(int a, int b) { return a + b; }
int maior(int a, int b) { return (a > b) ? a : b; }
// agora a assinatura fica limpa e legível
int aplicar(int x, int y, Operacao op) {
return op(x, y);
}
int main(void) {
printf("%d\n", aplicar(4, 9, somar)); // 13
printf("%d\n", aplicar(4, 9, maior)); // 9
return 0;
}
Com typedef int (*Operacao)(int, int);, o tipo "ponteiro para função que recebe dois int e retorna int" ganha o nome limpo Operacao. A assinatura int aplicar(int x, int y, Operacao op) fica muito mais legível que a versão com a sintaxe crua de ponteiro para função. Sempre que você usar ponteiros para função em assinaturas, considere um typedef — a clareza compensa muito.
Tabelas de funções: escolhendo comportamento por índice
Um uso avançado e elegante: guardar ponteiros para função num vetor, criando uma "tabela de operações" que você seleciona por índice. Isso substitui longas cadeias de if/else ou switch por uma indexação direta:
#include <stdio.h>
int somar(int a, int b) { return a + b; }
int subtrair(int a, int b) { return a - b; }
int multiplicar(int a, int b) { return a * b; }
int main(void) {
// um vetor de ponteiros para função
int (*operacoes[3])(int, int) = {somar, subtrair, multiplicar};
const char *nomes[3] = {"soma", "subtração", "multiplicação"};
int x = 10, y = 4;
for (int i = 0; i < 3; i++) {
printf("%s: %d\n", nomes[i], operacoes[i](x, y));
}
return 0;
}
O vetor operacoes guarda três funções, e operacoes[i](x, y) chama a i-ésima delas. Esse padrão — a tabela de despacho — é usado em interpretadores, máquinas de estado e sistemas de menu, onde a ação a executar é escolhida dinamicamente. É código enxuto e extensível: adicionar uma operação é acrescentar uma entrada à tabela.
O que vem a seguir
Hoje demos um salto conceitual: descobrimos que funções têm endereços e que ponteiros para função nos permitem tratar comportamento como um valor — guardá-lo em variáveis, passá-lo como argumento (callbacks), e organizá-lo em tabelas. Vimos também que o typedef é o aliado que domestica sua sintaxe difícil. Esse recurso é a fundação do código genérico em C. Na próxima aula, colhemos o fruto imediato disso: as funções qsort e bsearch da biblioteca padrão, que ordenam e buscam em qualquer tipo de dado justamente porque recebem, via ponteiro para função, o critério de comparação. Você verá a teoria de hoje virar uma ferramenta prática e poderosa.
Fontes e leituras recomendadas
- The C Programming Language (K&R), Kernighan & Ritchie — Cap. 5.11, Pointers to Functions
- cppreference — Pointers to functions — https://en.cppreference.com/w/c/language/pointer
- Modern C, Jens Gustedt — seção sobre ponteiros para função e código genérico — https://gustedt.gitlabpages.inria.fr/modern-c/
- Expert C Programming, Peter van der Linden — capítulo célebre sobre decifrar declarações de ponteiro para função
- 21st Century C, Ben Klemens (O'Reilly) — uso idiomático de callbacks
Exercícios
Exercício 1
Declare um ponteiro para função chamado op que aponte para uma função int quadrado(int). Chame a função através do ponteiro e imprima o resultado de op(6) (esperado: 36).
Ver resposta
✓ Resposta:
#include <stdio.h>
int quadrado(int x) { return x * x; }
int main(void) {
int (*op)(int) = quadrado;
printf("%d\n", op(6)); // 36
return 0;
}
Exercício 2
Escreva uma função int reduzir(int *v, int n, int (*combinar)(int, int), int inicial) que combine todos os elementos do vetor usando a função passada, começando de inicial. Teste-a com uma função de soma (resultado: a soma do vetor) e uma de máximo (resultado: o maior elemento).
Ver resposta
✓ Resposta:
#include <stdio.h>
int soma(int a, int b) { return a + b; }
int maximo(int a, int b) { return (a > b) ? a : b; }
int reduzir(int *v, int n, int (*combinar)(int, int), int inicial) {
int acumulado = inicial;
for (int i = 0; i < n; i++) {
acumulado = combinar(acumulado, v[i]);
}
return acumulado;
}
int main(void) {
int v[5] = {3, 7, 1, 9, 4};
printf("Soma: %d\n", reduzir(v, 5, soma, 0)); // 24
printf("Máximo: %d\n", reduzir(v, 5, maximo, v[0])); // 9
return 0;
}
A função reduzir implementa o padrão de acumulação genérico; a operação concreta vem via callback.
Exercício 3
Crie um typedef chamado Filtro para "ponteiro para função que recebe int e retorna int" (0 ou 1). Escreva uma função contar_se(int *v, int n, Filtro passa) que conte quantos elementos satisfazem o filtro. Teste com um filtro "é par".
Ver resposta
✓ Resposta:
#include <stdio.h>
typedef int (*Filtro)(int);
int eh_par(int x) { return x % 2 == 0; }
int contar_se(int *v, int n, Filtro passa) {
int qtd = 0;
for (int i = 0; i < n; i++) {
if (passa(v[i])) qtd++;
}
return qtd;
}
int main(void) {
int v[6] = {1, 2, 3, 4, 5, 6};
printf("Pares: %d\n", contar_se(v, 6, eh_par)); // 3
return 0;
}
O typedef Filtro torna a assinatura de contar_se limpa; o callback eh_par define o critério.
Exercício 4
Crie um vetor de 3 ponteiros para função (soma, subtração, multiplicação) e um menu simples: leia um índice de 0 a 2 e dois números, e aplique a operação correspondente da tabela. Explique a vantagem sobre um switch.
Ver resposta
✓ Resposta:
#include <stdio.h>
int somar(int a, int b) { return a + b; }
int subtrair(int a, int b) { return a - b; }
int multiplicar(int a, int b) { return a * b; }
int main(void) {
int (*ops[3])(int, int) = {somar, subtrair, multiplicar};
int indice = 2, x = 6, y = 3; // simulando entrada: multiplicação
if (indice >= 0 && indice < 3) {
printf("Resultado: %d\n", ops[indice](x, y)); // 18
}
return 0;
}
A vantagem sobre um switch: a tabela é mais enxuta e extensível. Adicionar uma nova operação é acrescentar uma função à tabela (uma linha), sem mexer na lógica de despacho; um switch exigiria adicionar um novo case. a seleção por índice é direta (acesso O(1) ao vetor), enquanto um switch grande precisa avaliar casos. Para conjuntos de operações que crescem, a tabela é mais elegante.
Exercício 5
Explique, com suas palavras, a diferença entre passar funcao e funcao() como argumento. Por que, ao passar uma função como callback, usamos o nome sem os parênteses?
Ver resposta
✓ Resposta: Passar funcao (sem parênteses) passa o endereço da função — o ponteiro para o código dela, que a função receptora poderá chamar depois. Passar funcao() (com parênteses) executa a função imediatamente e passa o valor que ela retorna. São coisas completamente diferentes: no primeiro caso, entregamos "a receita" (o que fazer); no segundo, entregamos "o prato pronto" (o resultado). Ao registrar um callback, queremos que a função receptora decida quando e com quais argumentos chamar nossa função — por isso passamos o nome sem parênteses, entregando a função em si, não o resultado de uma chamada. Usar parênteses ali seria um erro: passaríamos um valor, não um comportamento.