Aula 14b — Parâmetros e Retorno de Funções

Call by Value, Call by Reference, ponteiros como parâmetros e retorno múltiplo

1. Os Quatro Tipos de Função em C

Na Aula 13 você aprendeu a declarar, definir e chamar funções. Agora vamos entender como os dados entram e saem delas — que é onde C se diferencia radicalmente do Java.

Tipo Retorno Parâmetros Quando usar
void f(void) Nenhum Nenhum Ação pura (ex: imprimir cabeçalho)
void f(int x) Nenhum Por valor Ação que usa dados mas não precisa devolver resultado
int f(int x) Um valor Por valor Cálculo simples com um resultado
void f(int *x) Nenhum (via ponteiro) Por referência Modificar variáveis externas ou retornar múltiplos resultados
A limitação fundamental do return: Uma função em C só pode retornar um único valor com return. Para "retornar" múltiplos resultados, a solução é passar ponteiros como parâmetros — a função escreve os resultados diretamente na memória dos argumentos.

1b. Nomenclatura Formal: Call by Value e Call by Reference

Antes de mergulhar nos exemplos, vale fixar os dois nomes que você verá em qualquer livro de C, livro de algoritmos ou entrevista técnica:

Call by Value (Passagem por Valor): A função recebe uma cópia do argumento. Qualquer modificação feita dentro da função afeta apenas a cópia local — a variável original permanece intocada. É o comportamento padrão de C para tipos primitivos (int, float, char…).
Call by Reference (Passagem por Referência): A função recebe o endereço de memória da variável original e pode modificá-la diretamente. Em C, isso é feito explicitamente passando um ponteiro (&variavel) e usando o operador de desreferência (*) dentro da função.
Mecanismo O que é passado Pode modificar o original? Sintaxe na chamada Sintaxe no parâmetro
Call by Value Cópia do valor ❌ Não f(x) void f(int x)
Call by Reference Endereço de memória ✅ Sim f(&x) void f(int *x)
Comparação com Java: Java usa call by value para tipos primitivos (int, double…) — igual ao C. Para objetos, Java passa a referência por valor (cópia do endereço), permitindo modificar o conteúdo do objeto mas não a referência em si. C é mais explícito: você decide manualmente quando usar ponteiros.

O exemplo mais direto: a mesma tarefa pelos dois mecanismos

#include <stdio.h>

/* Call by Value: recebe cópia, retorna novo valor */
int somarDez_valor(int numero) {
    return numero + 10;             // não altera o original
}

/* Call by Reference: recebe endereço, modifica diretamente */
void somarDez_referencia(int *numero) {
    *numero = *numero + 10;         // modifica no endereço apontado
}

int main() {
    int a = 5;

    printf("a = %d\n", a);                   // 5

    a = somarDez_valor(a);                   // precisa reatribuir o retorno
    printf("após valor:      a = %d\n", a);  // 15

    somarDez_referencia(&a);                // &a = endereço de a
    printf("após referência: a = %d\n", a);  // 25

    return 0;
}
a = 5
após valor: a = 15
após referência: a = 25

2. Passagem por Valor — a Cópia Invisível

Quando você escreve f(x), C cria uma cópia de x na stack da função. A variável original fica completamente isolada.

#include <stdio.h>

void tentaDobrar(int n) {
    n = n * 2;                          // modifica apenas a cópia local
    printf("dentro: n = %d\n", n);      // imprime 10
}

int main() {
    int n = 5;
    tentaDobrar(n);
    printf("fora:   n = %d\n", n);      // ainda 5 — original intocado
    return 0;
}
dentro: n = 10
fora: n = 5

O que acontece na memória

Momento n em main (end. 0x100) n em tentaDobrar (end. 0x200)
Antes da chamada5—
Ao entrar na função55 (cópia criada)
Após n = n * 2510
Após retorno5— (destruída)

3. Passagem por Ponteiro (Call by Reference)

Para que uma função modifique a variável original, passamos seu endereço. A função recebe um ponteiro e usa * para acessar o valor naquele endereço.

Operador Nome Onde aparece Significado
& Endereço-de Na chamada: f(&x) Obtém o endereço de memória de x
*tipo Declaração de ponteiro No parâmetro: int *p Declara que p é um ponteiro para int
*ponteiro Desreferência No corpo: *p = 10 Acessa/modifica o valor no endereço apontado por p

Exemplo 1 — dobrar um valor

#include <stdio.h>

void dobrar(int *p) {
    *p = *p * 2;    // lê o valor em 0x100, multiplica por 2, salva de volta
}

int main() {
    int x = 5;
    dobrar(&x);     // passa o endereço 0x100
    printf("x = %d\n", x);   // 10
    return 0;
}
x = 10

Exemplo 2 — trocar dois valores (o swap clássico)

O swap é impossível com passagem por valor. Com ponteiros, funciona perfeitamente:

#include <stdio.h>

/* ❌ Versão errada — por valor */
void trocar_errado(int a, int b) {
    int temp = a; a = b; b = temp;   // troca as cópias, não os originais
}

/* ✅ Versão correta — por ponteiro */
void trocar(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 10, y = 20;
    printf("antes:  x=%d  y=%d\n", x, y);

    trocar_errado(x, y);
    printf("errado: x=%d  y=%d\n", x, y);   // ainda 10 e 20

    trocar(&x, &y);
    printf("certo:  x=%d  y=%d\n", x, y);   // 20 e 10
    return 0;
}
antes: x=10 y=20
errado: x=10 y=20
certo: x=20 y=10

Exemplo 3 — retornar dois resultados simultaneamente (minMax)

#include <stdio.h>

void minMax(int v[], int tam, int *minimo, int *maximo) {
    *minimo = *maximo = v[0];
    for (int i = 1; i < tam; i++) {
        if (v[i] < *minimo) *minimo = v[i];
        if (v[i] > *maximo) *maximo = v[i];
    }
}

int main() {
    int notas[] = {7, 3, 9, 5, 8};
    int min, max;

    minMax(notas, 5, &min, &max);
    printf("Mínimo: %d  Máximo: %d\n", min, max);
    return 0;
}
Mínimo: 3 Máximo: 9

4. Arrays Sempre por Ponteiro

Arrays em C têm um comportamento especial: ao serem passados para uma função, decaem automaticamente para um ponteiro para o primeiro elemento. Você nunca precisa (nem consegue) passar um array por valor.

#include <stdio.h>

void dobrarTodos(int *v, int tam) {
    for (int i = 0; i < tam; i++)
        v[i] *= 2;   // modifica o array original — sem & necessário na chamada
}

int main() {
    int nums[] = {1, 2, 3, 4, 5};
    dobrarTodos(nums, 5);                       // nums já é o endereço do [0]
    for (int i = 0; i < 5; i++)
        printf("%d ", nums[i]);
    printf("\n");
    return 0;
}
2 4 6 8 10
Pointer decay: dobrarTodos(nums, 5) é equivalente a dobrarTodos(&nums[0], 5). O nome do array decai para um ponteiro para o primeiro elemento. Por isso a função já recebe o endereço original e pode modificar todos os elementos.
Use const int *v quando a função só precisa ler o array — o compilador avisa se você tentar modificá-lo por acidente.

Exemplo completo: imprimir e modificar um array via funções

Ao passar um array para uma função, o ponteiro permite tanto lê-lo quanto modificá-lo diretamente. Use const quando quiser garantir que a função não alterará os dados:

#include <stdio.h>

/* Apenas lê o array — const protege contra modificação acidental */
void printArray(const int *v, int tam) {
    int i;
    for (i = 0; i < tam; i++)
        printf("%d ", v[i]);
    printf("\n");
}

/* Modifica o array original — incrementa cada elemento */
void incrementarTodos(int *v, int tam) {
    int i;
    for (i = 0; i < tam; i++)
        v[i] = v[i] + 1;    // equivalente a *(v+i) = *(v+i) + 1
}

int main() {
    int notas[5] = {6, 7, 8, 5, 9};

    printf("Antes:  ");
    printArray(notas, 5);

    incrementarTodos(notas, 5);   // sem & — arrays já são ponteiros

    printf("Depois: ");
    printArray(notas, 5);

    return 0;
}
Antes: 6 7 8 5 9
Depois: 7 8 9 6 10
Por que sem &? Arrays em C decaem automaticamente para o ponteiro do primeiro elemento quando passados para funções. Escrever printArray(notas, 5) já é equivalente a printArray(&notas[0], 5). Use const int *v no parâmetro quando a função não precisar modificar o array — isso documenta a intenção e o compilador avisa se você tentar modificar por acidente.

5. Tipos de Retorno

Tipo de retorno Exemplo Uso típico
void void imprimirSeparador(void) Ação sem resultado
Tipo primitivo int ehPrimo(int n) Cálculo simples
Código de erro (int) int salvarArquivo(...) 0 = sucesso, -1 = erro
Múltiplos (via ponteiro) void minMax(int[], int, int*, int*) Quando precisar de mais de um resultado

Múltiplos return e o padrão código-de-erro

#include <stdio.h>

int ehPrimo(int n) {
    if (n < 2) return 0;                  // saída antecipada
    for (int i = 2; i * i <= n; i++)
        if (n % i == 0) return 0;
    return 1;
}

/* Padrão código-de-erro: 0 = OK, -1 = falha */
int dividir(int a, int b, float *resultado) {
    if (b == 0) return -1;                // indica erro ao chamador
    *resultado = (float)a / b;
    return 0;                             // indica sucesso
}

int main() {
    printf("7 é primo? %s\n", ehPrimo(7) ? "Sim" : "Não");
    printf("4 é primo? %s\n", ehPrimo(4) ? "Sim" : "Não");

    float res;
    if (dividir(10, 3, &res) == 0)
        printf("10 / 3 = %.2f\n", res);
    else
        printf("Erro: divisão por zero!\n");

    dividir(5, 0, &res);   // retorna -1, res não é modificado
    return 0;
}
7 é primo? Sim
4 é primo? Não
10 / 3 = 3.33
⚠️ Nunca retorne ponteiro para variável local! Variáveis locais são destruídas ao final da função. Um ponteiro para elas fica "pendurado" (dangling pointer).
/* ❌ ERRADO — retorna endereço de variável local destruída */
int* funcaoErrada() {
    int x = 42;
    return &x;   // x deixa de existir após o return!
}

6. Exemplo Completo — Calculadora Modular

Reunindo os quatro tipos de função num único programa:

#include <stdio.h>
#include <math.h>

/* void sem parâmetros */
void imprimirSeparador(void) {
    printf("──────────────────────\n");
}

/* retorno simples */
double calcularHipotenusa(double a, double b) {
    return sqrt(a * a + b * b);
}

/* retorno como código: 1=acutangulo 2=retangulo 3=obtusangulo 0=invalido */
int classificarTriangulo(double a, double b, double c) {
    if (a <= 0 || b <= 0 || c <= 0) return 0;
    if (a + b <= c || a + c <= b || b + c <= a) return 0;
    double maior2 = (a>b && a>c) ? a*a : (b>c ? b*b : c*c);
    double somaQuad = a*a + b*b + c*c - maior2;
    if (maior2 < somaQuad) return 1;
    if (maior2 == somaQuad) return 2;
    return 3;
}

/* múltiplos resultados via ponteiro */
void estatisticasVetor(const float v[], int tam,
                       float *media, float *maximo, float *minimo) {
    *media = *maximo = *minimo = v[0];
    for (int i = 1; i < tam; i++) {
        *media += v[i];
        if (v[i] > *maximo) *maximo = v[i];
        if (v[i] < *minimo) *minimo = v[i];
    }
    *media /= tam;
}

int main() {
    imprimirSeparador();
    printf("Hipotenusa (3,4): %.2f\n", calcularHipotenusa(3, 4));

    imprimirSeparador();
    const char *tipos[] = {"Inválido","Acutângulo","Retângulo","Obtusângulo"};
    printf("Triângulo 3-4-5: %s\n", tipos[classificarTriangulo(3,4,5)]);
    printf("Triângulo 5-5-5: %s\n", tipos[classificarTriangulo(5,5,5)]);

    imprimirSeparador();
    float notas[] = {7.5f, 8.0f, 6.0f, 9.5f, 5.5f};
    float med, mx, mn;
    estatisticasVetor(notas, 5, &med, &mx, &mn);
    printf("Média: %.1f | Máx: %.1f | Mín: %.1f\n", med, mx, mn);

    return 0;
}
──────────────────────
Hipotenusa (3,4): 5.00
──────────────────────
Triângulo 3-4-5: Retângulo
Triângulo 5-5-5: Acutângulo
──────────────────────
Média: 7.3 | Máx: 9.5 | Mín: 5.5

7. C vs. Java

Aspecto C Java
Passagem de primitivos Sempre por valor (cópia) Sempre por valor (cópia)
Modificar variável externa Ponteiro explícito (&x, *p) Impossível para primitivos; objetos podem ser modificados via referência
Retornar múltiplos valores Ponteiros como parâmetros de saída Retornar objeto/array contendo os valores
Swap de dois inteiros ✅ Possível com ponteiros ❌ Impossível para primitivos — Java não tem ponteiros explícitos
Arrays passados para funções Sempre por ponteiro (decay automático) Por referência automática
Proteger array de modificação const int *v Não há equivalente direto para arrays primitivos

O swap — impossível em Java, trivial em C com ponteiros

C — funciona ✅
void trocar(int *a, int *b) {
    int t = *a;
    *a = *b;
    *b = t;
}
// uso:
trocar(&x, &y);
Java — não modifica os originais ❌
void trocar(int a, int b) {
    int t = a;
    a = b;
    b = t;
    // x e y fora do método
    // continuam inalterados
}

Quiz — Aula 14

1. Qual é a saída do programa abaixo?

void f(int x) { x = x + 1; }
int main() {
    int a = 5;
    f(a);
    printf("%d", a);
}

2. Para que a função void zerar(int x) consiga zerar a variável original, qual deve ser a assinatura correta?

3. Qual é a saída?

void dobrar(int *p) { *p = *p * 2; }
int main() {
    int x = 5;
    dobrar(&x);
    printf("%d", x);
}

4. Em call by reference, o que é passado para a função?

5. Por que o swap de dois inteiros é impossível em Java para primitivos, mas trivial em C?

6. Por que arrays são sempre passados por ponteiro em C, mesmo sem usar &?

7. O que o parâmetro const int *v garante dentro de uma função?

8. Qual é o padrão de retorno mais comum em C para indicar sucesso ou falha de uma operação?