C Sem Chorar no Terminal — parte 1 de 1
Funções e ponteiros em C
Nesse post vou abordar um conteúdo que sinceramente nunca vi tratado no YouTube ou em cursos, então a ideia aqui é primeiro explicar o que você precisa saber de antemão e depois entrar no assunto.
6 min de leituraatualizado
disponível em:ENPT
Funções e ponteiros#
Nesse post vou abordar um conteúdo que sinceramente nunca vi tratado no YouTube ou em cursos, então a ideia aqui é primeiro explicar o que você precisa saber de antemão e depois entrar no assunto.
O que eu preciso saber para continuar lendo#
- Você precisa compreender o que é um ponteiro e qual a função dele.
- Precisa compreender o básico de funções em C.
- Precisa saber o que é casting e protótipo.
- Básico de C.
Uma breve introdução a ponteiros#
Ponteiros são como setas: eles dizem onde algo está — nesse caso, um endereço na memória.
O que muita gente não observa e acaba confundindo em ponteiros é o seguinte.
Um ponteiro normalmente aparece numa declaração assim:
int y = 10;
int *x = &y;Sacada importante: x continua sendo uma variável normal — o que muda é o tipo dela. x é uma variável do tipo "ponteiro para int", e o valor que ela guarda é um endereço (o endereço de y, obtido com &y). Quando você escreve *x (a dereferência), você não lê o valor de x em si, e sim o valor que está naquele endereço — ou seja, o valor de y.
Resumindo:
x→ o endereço guardado (para onde a seta aponta).*x→ o valor que está nesse endereço (o10dey).
Essa é a sacada que te ajuda a não confundir ponteiros.
Ainda assim é uma noção simples, porque daqui a gente poderia ir além e falar de indireção múltipla (ponteiro para ponteiro) e por aí vai.
Anatomia de uma função em C#
Uma função clássica em C é definida por: a classe de armazenamento (se é static ou não), o tipo de retorno, o nome, os parâmetros e o bloco (o corpo). C é uma linguagem estruturada em blocos, o que permite declarar variáveis locais dentro deles — e, em alguns compiladores (como uma extensão do GCC), até declarar funções aninhadas, embora isso não faça parte do C padrão.
int main(void) {
return 0;
}O exemplo mais comum é a própria função main, o ponto de entrada de todo programa em C.
O nome de uma função e o que acontece na chamada#
Aqui vai um detalhe que confunde muita gente: em C, o nome de uma função quase sempre "vira" um ponteiro para ela. Sempre que você usa o nome de uma função dentro de uma expressão — exceto quando ele é operando de & ou de sizeof — o compilador converte esse nome automaticamente em um ponteiro para a função. Esse comportamento tem nome: function-to-pointer decay (decaimento de função para ponteiro).
É por causa disso que, mais à frente, vamos conseguir atribuir uma função a um ponteiro sem escrever nada de especial. E é por isso também que f, &f, e as chamadas f(), (*f)() e até (**f)() acabam funcionando: no fim das contas todas se referem ao mesmo endereço de entrada da função.
Quando você de fato chama uma função, o programa salta para o endereço de entrada dela e monta um stack frame (registro de ativação) na pilha. Esse frame guarda o que aquela chamada precisa: o endereço de retorno (para onde voltar quando a função terminar), os argumentos e as variáveis locais. O valor de retorno normalmente volta por um registrador, e não pela pilha, mas isso depende da convenção de chamada e da arquitetura.
Chamando uma função com ponteiro#
#include <stdio.h>
int count_girlfriends(void) {
return -1;
}
int main(void) {
// Vamos criar um ponteiro para receber essa função.
int (*cg)(void) = count_girlfriends;
// cg -> o nome da variável (um ponteiro de função); é cg quem aponta para a entrada de count_girlfriends.
// O * em (*cg) é o que marca "isto é um ponteiro"; escrevemos (*cg) para espelhar a forma da chamada (*cg)().
// (void) -> é a "assinatura" (o protótipo) que escolhemos: essa função não recebe parâmetro nenhum.
// int -> lá no começo, é o tipo de retorno.
printf("Return [%d]\n", cg());
return 0;
}Repare que escrevi count_girlfriends(void), e não count_girlfriends(). Isso importa: em C, um () vazio numa declaração significa "não estou dizendo quais nem quantos parâmetros" (o estilo antigo, K&R), e isso desliga a checagem de argumentos do compilador. Já (void) diz explicitamente "zero parâmetros" e faz o compilador te proteger. Como aqui a gente está justamente falando de protótipo, sempre que a função não recebe nada vamos usar (void). (No C23 essa regra mudou e o () vazio passou a significar (void), mas usar (void) continua sendo o hábito mais seguro e portável.)
Uma consequência prática do decaimento que vimos antes: todas estas formas são equivalentes e imprimem o mesmo valor.
int (*cg)(void) = count_girlfriends; // ou &count_girlfriends — dá no mesmo
printf("%d\n", cg()); // forma direta
printf("%d\n", (*cg)()); // dereferenciando "na mão" — mesmo resultadoEsse é o exemplo padrão de como passar a "entrada" de uma função para um ponteiro.
Ponteiro para uma função que retorna uma função#
Em C temos duas formas de escrever uma função que retorna outra função — ou, sendo preciso, um ponteiro para função, já que você não devolve a função em si, e sim o endereço dela.
Uma dessas formas gera dor de cabeça e a outra deixa o código bem mais limpo. A limpa (com typedef) vem já a seguir; a "na unha", mais bagunçada, tem uma seção só dela mais para frente ("Escrevendo funções de uma forma clássica e esquisita").
Vale entender a forma bagunçada porque ela te obriga a enxergar melhor os padrões de como C lida com ponteiros (e a ordem em que a declaração é "lida"). Mas, para código do dia a dia, prefira sempre a opção mais simples. Antes de chegar lá, além da forma limpa, esta seção mostra dois usos vizinhos e igualmente úteis de ponteiros de função: recebê-los como parâmetro (o callback) e guardá-los numa tabela.
1. Uma forma limpa de retornar função em C#
#include <stdio.h>
// Criamos um apelido (typedef) para um tipo "ponteiro de função".
// Vamos usá-lo tanto para retorno quanto para parâmetro.
typedef int (*calc_action)(int a, int b);
// Nossa função alvo.
int sum(int a, int b) {
return a + b;
}
// calc não recebe nada e devolve um calc_action (um ponteiro de função).
calc_action calc(void) {
// Devolvendo nossa função alvo (o nome "sum" decai para ponteiro sozinho).
return sum;
}
int main(void) {
// calc() devolve a função; o segundo par de parênteses já a chama.
printf("Sum: [%d]\n", calc()(1, 1));
// Também dá para guardar o retorno numa variável, porque o typedef
// serve tanto para variáveis quanto para parâmetros.
// Por isso essa é a abordagem limpa que você encontra em bibliotecas modernas.
calc_action c2 = calc();
printf("Sum: [%d]\n", c2(2, 2));
return 0;
}Saída:
Sum: [2]
Sum: [4]2. Recebendo funções como parâmetro em C#
#include <stdio.h>
// O mesmo typedef de antes: um ponteiro para função que recebe (int, int) e devolve int.
typedef int (*calc_action)(int a, int b);
// calc recebe a, b e uma função (ca) que decide qual operação aplicar sobre a e b.
int calc(int a, int b, calc_action ca) {
return ca(a, b);
}
// Nossa função de soma.
int sum(int a, int b) {
return a + b;
}
// Nossa função de subtração.
int sub(int a, int b) {
return a - b;
}
int main(void) {
// Chamando calc e passando funções como argumento.
int rsum = calc(1, 1, sum);
int rsub = calc(1, 1, sub);
printf("rsum: %d, rsub: %d\n", rsum, rsub);
return 0;
}Saída:
rsum: 2, rsub: 0Passar uma função como argumento assim (o famoso callback) é exatamente o que funções da biblioteca padrão como qsort fazem: você entrega a "regra" (como comparar dois elementos) e o algoritmo genérico faz o resto, sem você reescrever nada.
3. Bônus: uma tabela de funções#
Como calc_action é um tipo como qualquer outro, dá até para montar um vetor de ponteiros de função e escolher a operação por índice — um padrão bem comum para substituir sequências grandes de if/switch.
#include <stdio.h>
typedef int (*calc_action)(int a, int b);
int sum(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int main(void) {
// Cada posição do vetor guarda um ponteiro para uma operação.
calc_action ops[] = { sum, sub, mul };
const char *names[] = { "sum", "sub", "mul" };
for (int i = 0; i < 3; i++) {
printf("%s(6, 2) = %d\n", names[i], ops[i](6, 2));
}
return 0;
}Saída:
sum(6, 2) = 8
sub(6, 2) = 4
mul(6, 2) = 12Como você deve ter percebido, dá para usar isso de várias formas em C de um jeito bem claro — e bem menos confuso do que escrever o tipo do ponteiro "na unha" toda vez.
Escrevendo funções de uma forma clássica e esquisita#
Esta é a forma "dor de cabeça" de retornar função que mencionei lá em cima: a escrita "na unha", sem typedef.
É bem provável que, se você olhar bibliotecas antigas em C, encontre a assinatura das funções escrita de jeitos bem diferentes. Então vamos descrever alguns.
#include <stdio.h>
int sum(int a, int b) {
return a + b;
}
// Vamos escrever uma função "p" que retorna uma função.
// A dica para ler a declaração é começar pelo nome e ir de dentro para fora:
//
// int ( *p(void) )(int, int)
// ------- -> p é uma função que recebe (void)...
// ( * ) -> ...e devolve um ponteiro...
// int (int, int) -> ...para uma função que recebe (int, int) e devolve int.
int (*p(void))(int, int) {
return sum;
}
int main(void) {
// p() devolve a função sum; o segundo par de parênteses chama sum(1, 1).
printf("%d\n", p()(1, 1));
return 0;
}Escrevendo uma função que retorna uma função que retorna uma função#
Aqui virou uma grande dor de cabeça, mas a ideia continua a mesma: uma sintaxe meio "recursiva", em camadas.
#include <stdio.h>
int sig(void) {
return -1;
}
// p2 recebe (int, int) e devolve um ponteiro para uma função (void) -> int.
int (*p2(int a, int b))(void) {
(void)a; // a e b não são usados aqui; esse cast para void só silencia o aviso de "parâmetro não utilizado".
(void)b;
return sig;
}
// p recebe (void) e devolve um ponteiro para uma função igual a p2,
// ou seja: uma função (int, int) que devolve um ponteiro para função (void) -> int.
int (*(*p(void))(int, int))(void) {
return p2;
}
int main(void) {
// p() -> devolve p2
// p()(1, 1) -> chama p2, que devolve sig
// p()(1, 1)() -> chama sig, que devolve -1
printf("%d\n", p()(1, 1)());
return 0;
}Escrevendo uma função com parâmetro função#
Fechando com a forma mais direta e comum: um parâmetro que é um ponteiro de função, escrito "na unha" (sem typedef).
#include <stdio.h>
int sum(int a, int b) {
return a + b;
}
// int (*p)(int, int) é o parâmetro: um ponteiro para função (int, int) -> int.
int calc(int a, int b, int (*p)(int, int)) {
return p(a, b);
}
int main(void) {
printf("%d\n", calc(1, 1, sum));
return 0;
}Compare com a Seção 2: é exatamente a mesma ideia, só que lá o typedef calc_action escondia esse int (*p)(int, int) atrás de um nome mais amigável.
Bom, se você praticar e tentar entender como cada peça se encaixa, vai pegar uma boa noção de como usar ponteiros de função — nesses e em vários outros formatos.