Você já usou uma função sem perceber: main é uma. E já chamou outra: printf. Hoje vamos aprender a criar as nossas. Funções são a ferramenta mais básica de organização em C — mas por trás delas há dois conceitos que definem o caráter da linguagem: a exigência de declarar antes de usar e a passagem por valor. Entender esse segundo ponto hoje vai fazer os ponteiros, mais adiante, parecerem inevitáveis em vez de misteriosos.
Anatomia de uma função
Uma função em C tem: um tipo de retorno, um nome, uma lista de parâmetros (cada um com seu tipo) e um corpo:
#include <stdio.h>
int quadrado(int n) {
return n * n;
}
int main(void) {
int resultado = quadrado(7);
printf("7² = %d\n", resultado); // 49
return 0;
}
int quadrado(int n) diz: "esta função recebe um int chamado n e devolve um int". Ao contrário do Rust, o return em C é sempre explícito — não existe retorno implícito da última expressão. Se a função promete devolver um int, ela precisa de um return.
Funções que não retornam nada: void
Quando a função só executa uma ação e não devolve valor, seu tipo de retorno é void:
#include <stdio.h>
void imprimir_linha(const char *texto) {
printf("%s\n", texto);
}
int main(void) {
imprimir_linha("Primeira linha");
imprimir_linha("Segunda linha");
return 0;
}
O const char *texto é um parâmetro do tipo string — que estudaremos a fundo depois. Por ora, basta saber que %s imprime texto.
A ordem importa: declare antes de usar
Aqui o C difere do Rust de forma marcante. O compilador lê o arquivo de cima para baixo e precisa conhecer uma função antes que ela seja chamada. Se você definir quadrado depois de main, o compilador reclama ao encontrar a chamada.
A solução idiomática é o protótipo: você anuncia a assinatura da função no topo do arquivo e escreve o corpo onde quiser:
#include <stdio.h>
// Protótipos — anunciam as funções antes do uso
int quadrado(int n);
void imprimir_resultado(int valor);
int main(void) {
imprimir_resultado(quadrado(9));
return 0;
}
// Definições — a ordem aqui já não importa
int quadrado(int n) {
return n * n;
}
void imprimir_resultado(int valor) {
printf("Resultado: %d\n", valor);
}
O protótipo termina com ponto e vírgula e não tem corpo. Ele é uma promessa ao compilador: "esta função existe e tem esta assinatura; confie em mim, a definição vem depois". É exatamente esse mecanismo que, mais adiante, vai permitir espalhar funções por vários arquivos usando cabeçalhos (.h).
Passagem por valor: o conceito central de hoje
Preste muita atenção aqui, porque isso explica metade dos "comportamentos estranhos" que iniciantes veem em C. Quando você passa uma variável para uma função, o C entrega uma cópia dela. A função trabalha sobre a cópia; o original permanece intocado.
#include <stdio.h>
void tentar_dobrar(int x) {
x = x * 2;
printf("Dentro da função: %d\n", x);
}
int main(void) {
int numero = 10;
tentar_dobrar(numero);
printf("Depois da função: %d\n", numero);
return 0;
}
Saída:
Dentro da função: 20
Depois da função: 10
O numero continua 10. A função dobrou a cópia que recebeu, não o original. Isso não é um bug — é uma garantia: uma função não pode alterar suas variáveis por acidente.
"Mas e se eu quiser que a função altere o original?" Excelente pergunta — e é exatamente para isso que existem os ponteiros, que veremos em detalhe em breve. Por ora, guarde a pergunta; ela é a ponte para um dos temas mais importantes do C.
Escopo: onde uma variável vive
Uma variável declarada dentro de um bloco ({ ... }) só existe ali. Fora dele, é como se nunca tivesse existido:
#include <stdio.h>
int main(void) {
int fora = 1;
{
int dentro = 2;
printf("%d %d\n", fora, dentro); // ok: 1 2
}
// printf("%d\n", dentro); // ERRO: 'dentro' não existe aqui
printf("%d\n", fora); // ok
return 0;
}
O mesmo vale para parâmetros e variáveis locais de uma função: eles nascem quando a função é chamada e morrem quando ela retorna. Cada chamada tem seu próprio conjunto de variáveis locais, independentes das outras.
Existem também variáveis globais, declaradas fora de qualquer função, visíveis por todo o arquivo. Elas são tentadoras, mas perigosas: qualquer função pode alterá-las, o que torna o código difícil de rastrear. Use-as com muita parcimônia — a regra é preferir sempre o escopo mais restrito possível.
Um exemplo que junta tudo
#include <stdio.h>
double celsius_para_fahrenheit(double c);
void descrever(double celsius);
int main(void) {
double temperaturas[] = {0.0, 20.0, 37.0, 100.0};
int total = 4;
for (int i = 0; i < total; i++) {
descrever(temperaturas[i]);
}
return 0;
}
double celsius_para_fahrenheit(double c) {
return c * 1.8 + 32.0;
}
void descrever(double celsius) {
double f = celsius_para_fahrenheit(celsius);
printf("%.1f°C = %.1f°F\n", celsius, f);
}
Saída:
0.0°C = 32.0°F
20.0°C = 68.0°F
37.0°C = 98.6°F
100.0°C = 212.0°F
Repare em como o programa fica legível: main cuida do laço, descrever cuida da apresentação, e celsius_para_fahrenheit cuida da conta. Cada função faz uma coisa. Esse é o começo do que, mais adiante, chamaremos de código limpo.
O que vem a seguir
Hoje aprendemos a criar funções, a anunciá-las com protótipos e — o mais importante — entendemos que o C passa argumentos por valor, sempre em cópia. Essa única ideia é a semente dos ponteiros. No próximo artigo, vamos ao controle de fluxo: if, switch, e os laços while, do-while e for. Com funções e decisões nas mãos, você já será capaz de escrever programas completos e úteis.
Fontes e leituras recomendadas
- The C Programming Language (K&R), Kernighan & Ritchie — Cap. 4, Functions and Program Structure
- cppreference — Functions — https://en.cppreference.com/w/c/language/functions
- cppreference — Scope — https://en.cppreference.com/w/c/language/scope
- Modern C, Jens Gustedt — capítulo sobre funções e o modelo de execução — https://gustedt.gitlabpages.inria.fr/modern-c/
- GCC Warning Options — use
-Wall -Wextrapara o compilador te avisar sobre protótipos faltando — https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
Exercícios
Exercício 1
Escreva uma função int cubo(int n) que devolva n ao cubo, e chame-a a partir de main para os números de 1 a 5.
Ver resposta
✓ Resposta:
#include <stdio.h>
int cubo(int n) {
return n * n * n;
}
int main(void) {
for (int i = 1; i <= 5; i++) {
printf("%d³ = %d\n", i, cubo(i));
}
return 0;
}
Exercício 2
Escreva uma função void saudar(const char *nome) que imprima "Olá, <nome>!". Chame-a com três nomes diferentes.
Ver resposta
✓ Resposta:
#include <stdio.h>
void saudar(const char *nome) {
printf("Olá, %s!\n", nome);
}
int main(void) {
saudar("Ana");
saudar("Bruno");
saudar("Marcelo");
return 0;
}
Exercício 3
No exemplo de passagem por valor, adicione um printf dentro de tentar_dobrar mostrando o valor antes de multiplicar. Explique, olhando a saída, por que o numero original em main não muda.
Ver resposta
✓ Resposta: Com o printf extra, a saída de tentar_dobrar mostra o valor entrando como 10 e saindo como 20, mas o main segue imprimindo 10. O motivo: a função recebeu uma cópia de numero. Todas as operações (x = x * 2) modificaram essa cópia local, que é destruída quando a função retorna. O original em main nunca foi tocado — a passagem por valor garante isso.
Exercício 4
Reescreva o programa do cubo movendo a definição de cubo para depois de main, sem usar protótipo. Compile com gcc -Wall. Que aviso ou erro aparece? Depois, adicione o protótipo e compile de novo.
Ver resposta
✓ Resposta: Sem protótipo e com a definição depois de main, o gcc -Wall emite um aviso como "implicit declaration of function 'cubo'" (e, em padrões recentes como C23, isso já é erro). O compilador encontrou a chamada antes de conhecer a função. Ao adicionar o protótipo int cubo(int n); no topo, o aviso desaparece: você anunciou a assinatura antes do uso.
Exercício 5
Declare uma variável dentro de um bloco { } interno em main e tente usá-la fora do bloco. Que erro o compilador dá? O que isso ensina sobre escopo?
Ver resposta
✓ Resposta: O compilador dá um erro do tipo "'x' undeclared (first use in this function)" ao tentar usar a variável fora do bloco onde ela foi declarada. Isso mostra que variáveis têm escopo de bloco: elas existem apenas dentro do par de chaves em que foram criadas, e deixam de existir ao fim dele. É o mecanismo que evita que nomes locais de uma parte do código colidam com os de outra.