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 quemedia_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.
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.
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:
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 blocosif/elseouswitchdentro decalcular. - Baixo acoplamento:
calcularnão precisa saber como a operação é feita (se é soma ou subtração), apenas que a função passada segue o contrato definido pelo tipoOperacao.
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 tiposize_tué 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.
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.