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 double

Note 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 explicitamente

Na 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

Exercícios

  1. Escreva uma função aplicar_duplo que recebe um ponteiro para uma função que recebe um inteiro e retorna um inteiro, e um inteiro x, e retorna o resultado de chamar a função duas vezes sobre x (ou seja, f(f(x))).
  2. ✓ Resposta:
    int aplicar_duplo(int (*f)(int), int x) {
        return f(f(x));
    }
  3. Crie um vetor de ponteiros para função que aponte para as funções soma, subtrai, multiplica e divide (todas recebendo dois int e retornando int). Use esse vetor para calcular e imprimir o resultado de 20 + 5, 20 - 5, 20 * 5 e 20 / 5.
  4. ✓ Resposta:
    #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;
    }
  5. Implemente uma função registrar_callback que recebe um ponteiro para função void (*cb)(int) e um inteiro evento, e armazena o callback em um vetor global de callbacks indexado pelo evento. Assuma que há espaço para até 10 eventos.
  6. ✓ Resposta:
    #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;
        }
    }
  7. Usando typedef, defina um tipo OperacaoBinaria para um ponteiro para função que recebe dois double e retorna double. Escreva uma função que recebe um vetor de OperacaoBinaria e um tamanho, e imprime o resultado de aplicar cada operação a dois números fixos (ex.: 3.5 e 2.0).
  8. ✓ Resposta:
    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));
        }
    }
  9. Explique por que a declaração int *func(int) é diferente de int (*func)(int) e dê um exemplo de uso para cada um.
  10. ✓ Resposta: 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.