Ponteiros para Função: Passando Comportamento como Argumento

[387] Ponteiros para Função: Passando Comportamento como Argumento

O nome de uma função, escrito sem os parênteses, é um endereço — e dessa constatação simples nasce a possibilidade de passar comportamento como argumento. Daqui saem a sintaxe intimidante de int (*ptr)(int), o callback que troca o miolo de um algoritmo sem tocar nele, e a tabela que aposenta cadeias de if.
Linguagem C

10 min de leitura

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

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.

Comentários

Mais em Linguagem C

Ponteiros: O que É um Endereço de Memória
Ponteiros: O que É um Endereço de Memória

A memória como uma rua de casas numeradas, e o ponteiro como a variável que…

Explorando a Biblioteca Padrão: stdlib, math, ctype, time
Explorando a Biblioteca Padrão: stdlib, math, ctype, time

Antes de escrever uma função, vale perguntar se ela já existe — e quase sempre…

Lendo e Gravando Arquivos de Texto
Lendo e Gravando Arquivos de Texto

Abrir em modo w apaga o arquivo inteiro antes da primeira escrita — detalhe…