Ponteiros para função são um dos recursos mais poderosos e, ao mesmo tempo, mais subutilizados da linguagem C. Eles permitem armazenar o endereço de uma função em uma variável, possibilitando chamar a função indiretamente, passar funções como argumentos para outras funções e até mesmo criar estruturas de dados que armazenam funções. Esse conceito é fundamental para implementar callbacks, tabelas de despacho e polimorfismo em C.

Nesta aula, vamos explorar a sintaxe para declarar ponteiros para função, como atribuir e chamar funções por meio deles, e como aplicar esse conhecimento em cenários práticos como callbacks e tabelas de funções. Ao final, você terá uma compreensão sólida para usar ponteiros para função em seus próprios projetos.

Sintaxe

A declaração de um ponteiro para função pode parecer intimidadora à primeira vista, mas segue uma lógica clara. A forma geral é:

tipo_de_retorno (*nome_do_ponteiro)(tipos_dos_parametros);

Por exemplo, para declarar um ponteiro para uma função que recebe dois inteiros e retorna um inteiro, escrevemos:

int (*operacao)(int, int);

Observe que 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 para inteiro, o que é completamente diferente.

Vamos analisar alguns exemplos de declarações:

// Ponteiro para função que não recebe argumentos e retorna void
void (*funcao_sem_parametros)(void);

// Ponteiro para função que recebe um double e retorna double
double (*funcao_math)(double);

// Ponteiro para função que recebe um char* e um int e retorna char*
char* (*formatar)(const char*, int);

// Ponteiro para função que recebe um ponteiro para struct e retorna int
int (*comparar)(const struct Pessoa*, const struct Pessoa*);

É comum usar typedef para simplificar a declaração de ponteiros para função, especialmente quando o tipo é usado várias vezes. Por exemplo:

typedef int (*OperacaoBinaria)(int, int);

OperacaoBinaria op; // declara um ponteiro para função

Isso torna o código mais legível e facilita a manutenção.

Os ponteiros para função podem ser inicializados na declaração, atribuídos a outras variáveis, passados como argumentos e até mesmo armazenados em arrays ou structs. Veremos isso nas próximas seções.

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 de uma função é convertido implicitamente em um ponteiro para ela, assim como o nome de um array é convertido em um ponteiro para seu primeiro elemento. Por exemplo:

#include <stdio.h>

int somar(int a, int b) {
    return a + b;
}

int subtrair(int a, int b) {
    return a - b;
}

int main() {
    int (*op)(int, int); // declara ponteiro para função

    op = somar;          // atribui a função somar
    printf("Soma: %d\n", op(10, 5)); // chamada indireta

    op = subtrair;       // agora aponta para subtrair
    printf("Subtração: %d\n", op(10, 5));

    return 0;
}

Quando você usa op(10, 5), o compilador desreferencia o ponteiro automaticamente e chama a função apontada. Você também pode usar a sintaxe explícita com o operador de dereferência: (*op)(10, 5), mas isso é opcional e menos comum.

É importante garantir que o ponteiro esteja apontando para uma função válida antes de chamá-lo. Chamar um ponteiro nulo ou não inicializado pode causar comportamento indefinido. Uma prática comum é inicializar ponteiros para função com NULL e verificar antes da chamada.

Vamos ver um exemplo mais completo:

#include <stdio.h>

int multiplicar(int a, int b) {
    return a * b;
}

int dividir(int a, int b) {
    if (b == 0) {
        printf("Erro: divisão por zero\n");
        return 0;
    }
    return a / b;
}

int main() {
    int (*operacao)(int, int) = NULL;
    int opcao;

    printf("Escolha 1 para multiplicar, 2 para dividir: ");
    scanf("%d", &opcao);

    if (opcao == 1)
        operacao = multiplicar;
    else if (opcao == 2)
        operacao = dividir;
    else {
        printf("Opção inválida\n");
        return 1;
    }

    if (operacao != NULL)
        printf("Resultado: %d\n", operacao(20, 4));

    return 0;
}

Esse exemplo mostra como a atribuição condicional pode tornar o código mais flexível. Em vez de usar um switch para cada operação, podemos simplesmente selecionar a função desejada.

Callbacks

Um callback é uma função que é passada como argumento para outra função, permitindo que esta última chame o callback em momentos específicos. Isso é extremamente útil para personalizar o comportamento de funções genéricas, como ordenação, busca ou processamento de eventos.

Em C, callbacks são implementados usando ponteiros para função. Um exemplo clássico é a função qsort da biblioteca padrão, que recebe um ponteiro para uma função de comparação:

#include <stdio.h>
#include <stdlib.h>

int comparar_inteiros(const void *a, const void *b) {
    int ia = *(const int *)a;
    int ib = *(const int *)b;
    return (ia > ib) - (ia < ib);
}

int main() {
    int valores[] = {5, 2, 9, 1, 7};
    size_t n = sizeof(valores) / sizeof(valores[0]);

    qsort(valores, n, sizeof(int), comparar_inteiros);

    for (size_t i = 0; i < n; i++) {
        printf("%d ", valores[i]);
    }
    printf("\n");

    return 0;
}

Aqui, comparar_inteiros é um callback que define a ordem de comparação. A função qsort não sabe nada sobre o tipo dos elementos; ela depende do callback para comparar dois elementos.

Outro exemplo comum é em bibliotecas gráficas ou de interface, onde você registra funções para serem chamadas quando um evento ocorre (clique de mouse, pressionamento de tecla). Vamos criar um exemplo simples de um sistema de eventos:

#include <stdio.h>

// Tipo do callback
typedef void (*EventoCallback)(int);

void evento_clique(int x) {
    printf("Clique em %d\n", x);
}

void evento_tecla(int tecla) {
    printf("Tecla pressionada: %c\n", tecla);
}

void simular_evento(EventoCallback cb, int valor) {
    if (cb != NULL) {
        cb(valor);
    }
}

int main() {
    simular_evento(evento_clique, 100);
    simular_evento(evento_tecla, 'A');
    return 0;
}

Callbacks são amplamente usados em bibliotecas como GLib, GTK, e em muitas APIs de sistema operacional. Dominar esse conceito é essencial para programação de sistemas e desenvolvimento de bibliotecas.

Tabelas de funções

Tabelas de funções são arrays de ponteiros para função. Elas permitem associar um índice (ou chave) a uma função específica, facilitando o despacho de operações sem longas cadeias de if-else ou switch. Isso é muito útil em interpretadores, máquinas de estado, e em qualquer situação onde você precisa selecionar uma ação com base em um valor.

Vamos criar uma calculadora simples usando uma tabela de funções:

#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 dividir(int a, int b) { return b != 0 ? a / b : 0; }

int main() {
    // Tabela de funções: cada posição corresponde a uma operação
    int (*operacoes[4])(int, int) = {somar, subtrair, multiplicar, dividir};

    int a = 10, b = 5;
    for (int i = 0; i < 4; i++) {
        printf("Operação %d: %d\n", i, operacoes[i](a, b));
    }

    return 0;
}

Podemos também usar typedef para simplificar a declaração:

typedef int (*Operacao)(int, int);

Operacao operacoes[] = {somar, subtrair, multiplicar, dividir};
int n_ops = sizeof(operacoes) / sizeof(operacoes[0]);

Um exemplo mais avançado é uma máquina de estados simples:

#include <stdio.h>

void estado_inicial() { printf("Estado inicial\n"); }
void estado_processando() { printf("Processando\n"); }
void estado_final() { printf("Final\n"); }

int main() {
    void (*estados[3])() = {estado_inicial, estado_processando, estado_final};

    int transicao = 1; // exemplo
    estados[transicao](); // chama a função correspondente

    return 0;
}

Tabelas de funções são eficientes e organizadas, tornando o código mais declarativo e fácil de estender. Se você precisar adicionar uma nova operação, basta adicionar a função e incluí-la na tabela.

Boas práticas e observações

  • Sempre inicialize ponteiros para função com NULL e verifique antes de chamar.
  • Use typedef para simplificar declarações complexas e melhorar a legibilidade.
  • Prefira callbacks a funções com muitos parâmetros de controle, pois tornam o código mais modular.
  • Ao passar funções como argumentos, documente claramente o contrato esperado (parâmetros e retorno).
  • Lembre-se de que ponteiros para função são apenas endereços; você pode armazená-los em estruturas de dados, retorná-los de funções, etc.
  • Evite usar ponteiros para função em excesso se não houver necessidade, pois podem dificultar a depuração.

Exercícios

  1. Escreva uma função aplicar que recebe um ponteiro para função que opera sobre um inteiro e retorna um inteiro, e um inteiro x. A função deve chamar o callback com x e retornar o resultado. Teste com funções que incrementam e dobram um número.
  2. ✓ Resposta:
    #include <stdio.h>
    
    int incrementar(int x) { return x + 1; }
    int dobrar(int x) { return x * 2; }
    
    int aplicar(int (*func)(int), int x) {
        return func(x);
    }
    
    int main() {
        printf("Incrementar: %d\n", aplicar(incrementar, 5));
        printf("Dobrar: %d\n", aplicar(dobrar, 5));
        return 0;
    }
  3. Crie uma função calcular que recebe dois inteiros e um ponteiro para uma função de comparação (que recebe dois inteiros e retorna um inteiro), e retorna o maior ou menor valor conforme a função de comparação. Teste com funções que retornam o maior e o menor.
  4. ✓ Resposta:
    #include <stdio.h>
    
    int maior(int a, int b) { return a > b ? a : b; }
    int menor(int a, int b) { return a < b ? a : b; }
    
    int calcular(int a, int b, int (*comparar)(int, int)) {
        return comparar(a, b);
    }
    
    int main() {
        int x = 10, y = 20;
        printf("Maior: %d\n", calcular(x, y, maior));
        printf("Menor: %d\n", calcular(x, y, menor));
        return 0;
    }
  5. Implemente uma tabela de funções para as quatro operações aritméticas (soma, subtração, multiplicação, divisão) e use um menu para selecionar a operação. O programa deve ler dois números e a operação desejada, e exibir o resultado.
  6. ✓ 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 dividir(int a, int b) { return b != 0 ? a / b : 0; }
    
    int main() {
        int (*ops[4])(int, int) = {somar, subtrair, multiplicar, dividir};
        int a, b, escolha;
    
        printf("Digite dois números: ");
        scanf("%d %d", &a, &b);
        printf("Escolha a operação (0=soma, 1=sub, 2=mult, 3=div): ");
        scanf("%d", &escolha);
    
        if (escolha >= 0 && escolha < 4) {
            printf("Resultado: %d\n", ops[escolha](a, b));
        } else {
            printf("Operação inválida\n");
        }
        return 0;
    }
  7. Escreva uma função registrar_callback que armazena um ponteiro para função em uma variável global e uma função executar_callback que chama essa função se não for nula. Use isso para simular um sistema de eventos.
  8. ✓ Resposta:
    #include <stdio.h>
    
    void (*callback)(void) = NULL;
    
    void registrar_callback(void (*cb)(void)) {
        callback = cb;
    }
    
    void executar_callback() {
        if (callback != NULL) {
            callback();
        } else {
            printf("Nenhum callback registrado\n");
        }
    }
    
    void meu_callback() {
        printf("Callback executado!\n");
    }
    
    int main() {
        registrar_callback(meu_callback);
        executar_callback();
        return 0;
    }
  9. Explique a diferença entre int (*func)(int) e int *func(int). Dê um exemplo de declaração válida para cada.
  10. ✓ Resposta:

    int (*func)(int) é um ponteiro para uma função que recebe um inteiro e retorna um inteiro. int *func(int) é uma função que recebe um inteiro e retorna um ponteiro para inteiro. A presença dos parênteses muda completamente o significado. Exemplo:

    int (*func_ptr)(int); // ponteiro para função
    int *func_ret_ptr(int); // função que retorna ponteiro

Referências