Funções, Escopo e Passagem por Valor

[340] Funções, Escopo e Passagem por Valor

Por que dobrar um número dentro de uma função deixa o original intacto? A resposta é a passagem por valor — o C entrega sempre uma cópia — e é ela que torna os ponteiros inevitáveis em vez de misteriosos. No caminho, os protótipos, o void, o escopo de bloco e o perigo das variáveis globais.
Linguagem C

8 min de leitura

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

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.

Comentários

Mais em Linguagem C

Entrada e Saída Formatada: printf e scanf a Fundo
Entrada e Saída Formatada: printf e scanf a Fundo

Um double se lê com %lf e se imprime com %f — assimetria histórica, sem lógica…

Processos: fork, exec e wait
Processos: fork, exec e wait

A linha do fork é executada uma vez e retorna duas — e é do valor diferente em…

Listas Encadeadas: A Estrutura que Só Existe com Ponteiros
Listas Encadeadas: A Estrutura que Só Existe com Ponteiros

Liberar um nó e só então ler o seu proximo é use-after-free: guarda-se o…