Callbacks

Nossa jornada pelos ponteiros ainda não acabou. Nesta unidade vamos estudar o uso de ponteiros com funções. Isso mesmo, uma função também possui um endereço de memória. Poderemos chamar funções indiretamente e passar funções como paramêtros para outras funções.

Ponteiros de Funções

Um ponteiro também pode armazenar o endereço de uma função. O raciocínio é o mesmo de antes, mas a sintaxe para a declaração é mais específica, pois ela precisa descrever a assinatura completa da função: seu tipo de retorno e seus parâmetros.

A sintaxe de declaração pode parecer confusa no início, mas pode ser lida assim:

tipo_de_retorno (*nome_do_ponteiro)(tipos_de_parametros);

Vamos tomar a função media como exemplo. Ela recebe dois valores e retorna a média aritmética deles.

double media (double n1, double n2){
  return (n1 + n2)/2;
}

// Declaração de um ponteiro de função:
double (*media_ptr)(double, double);
media_ptr = media;

A sintaxe de declaração do ponteiro media_prt deve ser lida da seguinte forma:

  • double: é o tipo de retorno;
  • (*media_ptr): indica que media_ptr é um ponteiro para função. Os parênteses são obrigatórios para diferenciá-lo de uma função que retorna um ponteiro;
  • (double, double): são os tipos dos parâmetros da função apontada.
Nota

Diferentemente das variáveis, ao atribuir o endereço de uma função a um ponteiro de função, não é necessário usar o operador & (o nome da função sozinho já representa seu endereço).

Note que a função media obedece a assinatura (ou contrato) estabelecido pelo ponteiro de função media_ptr, ou seja, possui o mesmo tipo de retorno e os mesmos tipos de parâmetros de entrada (double, double). Portanto, devemos manter a compatibilidade de tipos, como fizemos com as variáveis na seção anterior.

NotaExercício de Fixação

Crie ponteiros para as seguintes funções

float media (float a, float b, float c){
  return (a+b+c)/3;
}

void aniversario (int *idade){
  (*idade)++;
}

void aponta_para(int **p, int *q) {
  *p = q;
}
float (*media_ptr) (float, float, float) = media;
void (*aniversario_ptr) (int *) = aniversario; 
void (*aponta_para_ptr)(int **, int *) = aponta_para;

Introdução aos Callbacks

O verdadeiro poder dos ponteiros de função aparece quando uma função é passada como argumento para outra. Nesse caso, a função que recebe outra função é chamada de função de ordem superior, enquanto a função fornecida como argumento é chamada de callback.

Essa técnica permite que um trecho de código delegue parte de seu comportamento a outro, tornando o programa mais flexível, modular e paramétrico.

Abaixo, um exemplo de uma calculadora de inteiros, onde calcular é uma função de ordem superior, pois recebe outra função como parâmetro. As funções soma, subtracao, produto e divisao são callbacks, pois são passadas para calcular.

calculadora_param.c
#include <stdio.h>

// Funções concretas (implementam comportamentos específicos)
int soma (int a, int b)     { return a + b; }
int subtracao (int a, int b){ return a - b; }
int produto (int a, int b)  { return a * b; }
int divisao (int a, int b)  { return a / b; }

// Função coordenadora (de ordem superior)
// Recebe duas variáveis e um ponteiro de função como parâmetros
int calcular(int a, int b, int (*operacao)(int, int)) {
  return operacao(a, b);
}

int main(){

  int n1 = 10;
  int n2 = 2;
  
  printf("Soma:      %d\n", calcular(n1, n2, soma));
  printf("Subtração: %d\n", calcular(n1, n2, subtracao));
  printf("Produto:   %d\n", calcular(n1, n2, produto));
  printf("Divisão:   %d\n", calcular(n1, n2, divisao));  
  
  return 0;
}

Se liga! A função calcular não sabe qual operação será executada, ela apenas invoca o comportamento recebido como parâmetro. Isso é o que chamamos de abstração de comportamento.

A saída desse programa será

  Soma:      12
  Subtração: 8
  Produto:   20
  Divisão:   5 

Ao passar a função como parâmetro, tornamos o código de C extremamente poderoso e reutilizável. A função calcular não se importa como o cálculo é feito, apenas que a função fornecida (o callback) respeite o contrato de tipos (int para retorno, e int, int para parâmetros).

Podemos ir além nessa reutilização? Sim, mas atingimos o limite do simples em C. Imagine que você queira adicionar uma operação com números de ponto flutuante:

double somad (double a, double b) { return a + b; }

O problema é que não podemos passar somad para a função calcular e nem a usar para operar com double, pois o callback int (*operacao)(int, int) e os parâmetros fixos em calcular(int a, int b, ...) tornam essa função totalmente incompatível com o tipo double.

Para resolver esse problema e criar uma única função verdadeiramente genérica que aceite qualquer tipo de dado, precisamos abandonar os tipos concretos (int, double) e generalizar usando o ponteiro sem tipo (void*).

Antes de darmos o salto para o código verdadeiramente genérico usando void*, vamos usar o typedef para melhorar a legibilidade de nosso código.

A sintaxe de um ponteiro de função (por exemplo, int (*operacao)(int, int)) pode dificultar a leitura do código, principalmente como argumentos de função. O typedef permite que você crie um apelido para essa assinatura complexa, tratando-a como um novo tipo de dado simples.

Ao definir o tipo do callback, tornamos a função calcular muito mais limpa:

// Definimos 'Operacao' como o tipo para qualquer função
// que retorna int e aceita dois int como parâmetros.
typedef int (*Operacao)(int, int);

// A função 'calcular' agora usa o tipo 'Operacao', 
// simplificando sua assinatura.
int calcular(int a, int b, Operacao operacao); 

O código final ficaria assim:

calculadora_typedef.c
#include <stdio.h>

// Definição do TIPO de ponteiro de função
typedef int (*Operacao)(int, int);

// Funções concretas (omitidas para brevidade)
int soma (int a, int b) { return a + b; }
// ...

// A Função Coordenadora agora é mais legível
int calcular(int a, int b, Operacao operacao) {
  return operacao(a, b);
}

int main(){

  int n1 = 10;
  int n2 = 2;
  
  printf("Soma: %d\n", calcular(n1, n2, soma));
  
  return 0;
}

Se liga! Com typedef, ganhamos mais clareza no código. A função calcular se torna muito mais limpa e legível.

Com essa abrodardagem, ganhamos:

  • Reutilização: A função calcular é reutilizada para todas as operações.
  • Fácil extensão: Para adicionar uma nova operação, basta criar a função e passá-la para calcular. Não precisamos de blocos if/else ou switch dentro de calcular.
  • Baixo acoplamento: calcular não precisa saber como a operação é feita (se é soma ou subtração), apenas que a função passada segue o contrato definido pelo tipo Operacao.

Dessa maneira, calcular se comporta de várias formas (polimorfismo).

Uma das grandes vantagens de usar ponteiros para funções é a possibilidade de criar um menu interativo, permitindo ao usuário escolher qual operação deseja executar.

Operacao escolha(){
  printf("Esolha a Operação Desejada\n");
  printf("1 - Soma\n");
  printf("2 - Subtração\n");
  printf("3 - Multiplicação\n");
  printf("4 - Divisão\n");
    
  int s = 0;
  scanf("%d", &s);

  if (s == 1) return soma;
  if (s == 2) return subtracao;
  if (s == 3) return produto;
  if (s == 4) return divisao;
  
  return NULL;  // Retorna NULL se a escolha for inválida
}

Na versão acima, a possibilidade de repetir o menu, em caso de entrada inválida, não está implementado. Todavia, podemos modificar para um problema específico. O que notamos de mais problemático é que a função escolha possui duas responsabilidades e devemos evitar isso no futuro para deixar o código mais modular e facilitar a manutenção.

Ponteiro genérico

Agora que compreendemos o funcionamento dos ponteiros, passagem por referência e callbacks, podemos dar um passo além. Vamos estudar um método para representar qualquer tipo de dado.

Em C, o tipo void * é conhecido como ponteiro genérico (generic pointer). Ele é um tipo especial de ponteiro que pode armazenar o endereço de qualquer tipo de dado, por exemplo int, double, char, struct etc.

int idade = 20;
double meta = 8.5;
char letra = 'A';

void *ptr;

ptr = &idade;
printf("idade: %d\n", *(int *)ptr);

ptr = &meta;
printf("meta: %.2f\n", *(double *)ptr);

ptr = &letra;
printf("letra: %c\n", *(char *)ptr);

Se liga! Não podemos desreferenciar diretamente um ponteiro void * , pois o compilador não sabe quantos bytes ele deve ler. Devemos sempre fazer um cast (conversão) do void * para o tipo específico de ponteiro que desejamos, antes de poder usá-lo.

Repare que, embora ptr sempre seja do tipo void *, precisamos fazer um cast (conversão explícita) para o tipo correto antes de acessar o valor. Isso ocorre porque o compilador não sabe qual é o tipo real do dado armazenado e, portanto, não pode fazer aritmética de ponteiros nem desreferenciação direta.

Essa característica é o que permite criar estruturas de dados genéricas em C, como listas, pilhas ou filas capazes de armazenar qualquer tipo. Essas estruturas irão guardar apenas endereços genéricos (void *) e não se importar com o tipo concreto do dado. A responsabilidade de interpretar corretamente o tipo será de quem usar a estrutura.

O papel de size_t

Ao manipular dados de tipos diferentes, precisamos também saber quanto de memória reservar ou copiar. É aí que entra o tipo size_t, definido no cabeçalho <stddef.h> (ou implicitamente incluído via <stdlib.h> ou <stdio.h>).

Como um tipo numérico sem sinal (unsigned), size_t é usado para representar tamanhos e quantidades de bytes. Ele é o tipo de retorno de funções como sizeof, malloc, calloc e strlen.

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

int main() {
    int x = 10;
    double y = 3.14;

    printf("sizeof(int)   : %zu bytes\n", sizeof(int));
    printf("sizeof(double): %zu bytes\n", sizeof(double));

    void *ptr = malloc(sizeof(double));
    printf("Alocado %zu bytes em %p\n", sizeof(double), ptr);

    free(ptr);
    return 0;
}

Se liga!

  • z é um modificador de comprimento que especifica que o argumento é do tipo size_t
  • u é o código de conversão para inteiro decimal sem sinal (unsigned).

Saída típica:

sizeof(int)   : 4 bytes
sizeof(double): 8 bytes
Alocado 8 bytes em 0x7ffee1

A combinação de void * e size_t é fundamental para criar estruturas genéricas, pois usaremos void * para representar o dado, independentemente de tipo e size_t para indicar o tamanho desse dado, permitindo cópia, alocação e manipulação seguras.

Nota🎯 Desafio de Código

Crie uma função aplicar que recebe um vetor de inteiros e uma função callback que é aplicada a cada elemento do vetor. Para as funções de callback, implemente:

  • dobra: dobra o valor de cada elemento do vetor
  • zerar: Define cada elemento como zero
  • incrementar: soma um a cada elemento do vetor

Segue a declação da funções de callback e ordem superior, além de um exemplo de uso.

#include <stdio.h>

void dobrar(int *x);
void zerar(int *x);
void incrementar(int *x);

void aplicar(int *vetor, int tamanho, void (*funcao)(int *));

int main() {
    int numeros[] = {1, 2, 3, 4, 5};
    aplicar(numeros, 5, dobrar);

    for (int i = 0; i < 5; i++) {
        printf("%d ", numeros[i]);
    }

    return 0;
}
void dobrar(int *x){ *x = *x * 2; }

void zerar(int *x){ *x = 0; }

void incrementar(int *x){ *x = *x + 1; }

void aplicar(int *vetor, int tamanho, void (*funcao)(int *)){
  for (int i = 0; i < tamanho; i++)
        funcao(&vetor[i]);
}    

Nesta unidade, você aprendeu

✅ a declarar um ponteiro de função

✅ a criar funções de ordem superior e de callbacks

✅ a usar ponteiros sem tipo (void *)

✅ a usar site_t para designar tamanho ou quantidade de bytes

Falta pouco para criarmos nossas estruturas genéricas com comportamentos também genéricos. Porém, ainda precisamos pontuar algumas questões sobre o gerenciamento de memória.

# O que é um ponteiro de função? > Ele armazena o endereço de uma função e permite chamá-la indiretamente. 1. [ ] Um tipo especial de ponteiro usado apenas para variáveis globais 1. [ ] Um ponteiro que aponta para dados armazenados na pilha 1. [x] Um ponteiro que armazena o endereço de uma função 1. [ ] Um operador utilizado para alocar memória dinâmica # Qual é a sintaxe correta para declarar um ponteiro para função que retorna `int` e recebe dois `int`? > Observe a posição dos parênteses e do asterisco. 1. [x] `int (*ptr)(int, int);` 1. [ ] `int *ptr(int, int);` 1. [ ] `(*int ptr)(int, int);` 1. [ ] `int (ptr*)(int, int);` # Sobre a atribuição de uma função a um ponteiro de função, é correto afirmar que... > Lembre-se de como nomes de funções se comportam em C. 1. [ ] É necessário usar o operador `&` sempre. 1. [x] O nome da função já representa seu endereço. 1. [ ] O operador `*` deve ser usado antes do nome da função. 1. [ ] Só é possível atribuir funções `void` a ponteiros. # O que caracteriza uma função *callback*? > Ela é passada como argumento para outra função. 1. [ ] Uma função que chama a si mesma recursivamente. 1. [ ] Uma função que é executada no início do programa. 1. [x] Uma função passada como parâmetro para outra. 1. [ ] Uma função que aloca memória dinamicamente. # No exemplo da função `calcular`, qual das seguintes opções é verdadeira? > Considere o papel de `calcular` e das funções `soma`, `subtracao` etc. - [x] `calcular` é uma função de ordem superior. - [x] `soma` e `subtracao` são funções *callback*. - [ ] `calcular` precisa conhecer a lógica de cada operação. - [ ] `soma` e `subtracao` devem ter tipos de retorno diferentes. # Quais das opções abaixo são vantagens do uso de `typedef` para ponteiros de função? > Pense em legibilidade e clareza do código. - [x] Aumenta a legibilidade e clareza do código. - [x] Simplifica a assinatura de funções que usam ponteiros de função. - [ ] Impede erros de compilação. - [ ] Aumenta o desempenho em tempo de execução. # Coloque em ordem as etapas de funcionamento da função `calcular` com um *callback*. > Pense na sequência de chamadas entre `main`, `calcular` e `operacao`. 1. A função `main` chama `calcular`, passando uma função de operação. 2. `calcular` recebe os parâmetros e o ponteiro de função. 3. `calcular` invoca a função apontada. 4. A função apontada executa o cálculo e retorna o resultado. 5. O resultado é exibido em `main`. # Sobre o uso de `void *`, marque as afirmativas corretas. > Ele é o ponteiro genérico em C. - [x] Pode armazenar o endereço de qualquer tipo de dado. - [x] Precisa ser convertido (cast) antes de ser desreferenciado. - [ ] Pode ser desreferenciado diretamente. - [ ] Armazena sempre um número inteiro, não um endereço. # O que o tipo `size_t` representa em C? > Ele é usado para representar quantidades de bytes. 1. [ ] Um tipo de dado usado para caracteres Unicode. 1. [x] Um tipo sem sinal que representa tamanhos e quantidades de bytes. 1. [ ] Um ponteiro genérico que aponta para qualquer tipo. 1. [ ] Um tipo que indica o número de elementos em um vetor.
De volta ao topo