Aula 23 — Ponteiros e Funções

Passagem por valor vs. por referência — quando copiar e quando apontar

1. Revisão: a Cópia Invisível

Nas aulas de funções você aprendeu que C passa argumentos por valor — a função recebe uma cópia independente de cada argumento. Isso é seguro, mas insuficiente para dois cenários importantes: quando a função precisa modificar a variável original, ou quando passar a cópia de um dado grande (como uma struct) seria caro.

Esta aula aprofunda os dois mecanismos, agora com o conhecimento completo de ponteiros das aulas 20 a 22:

Mecanismo O que é passado Pode modificar o original? Custo Sintaxe na chamada
Por valor Cópia do dado ❌ Não Proporcional ao tamanho do tipo f(x)
Por referência Endereço de memória ✅ Sim Sempre 8 bytes (o ponteiro) f(&x)

2. Passagem por Valor — Anatomia Completa

Quando você chama f(x), o compilador cria um novo frame na pilha para f e copia o valor de x nesse frame. As duas variáveis são independentes a partir daí.

#include <stdio.h>

void tentarZerar(int n) {
    printf("  [dentro] n antes = %d  endereço = %p\n", n, &n);
    n = 0;
    printf("  [dentro] n depois = %d  endereço = %p\n", n, &n);
}

int main() {
    int x = 42;
    printf("[main] x antes = %d  endereço = %p\n", x, &x);
    tentarZerar(x);
    printf("[main] x depois = %d  endereço = %p\n", x, &x);
    return 0;
}
[main] x antes = 42 endereço = 0x7ffd...10
  [dentro] n antes = 42 endereço = 0x7ffd...e0
  [dentro] n depois = 0 endereço = 0x7ffd...e0
[main] x depois = 42 endereço = 0x7ffd...10
Observe os endereços: x e n estão em endereços diferentes — são variáveis distintas. Zerar n não afeta x. Ao retornar de tentarZerar, o frame de n é destruído e x permanece intacto com 42.

Quando passagem por valor é a escolha certa

Situação Justificativa
Tipos primitivos pequenos (int, float, char) Custo de cópia é mínimo (4–8 bytes); evita efeitos colaterais acidentais
A função calcula e retorna o resultado int quadrado(int n) { return n*n; } — limpo e sem efeitos colaterais
Você quer garantir que o dado original não será alterado Isolamento é desejável — a cópia age como proteção

3. Passagem por Referência — Anatomia Completa

Ao chamar f(&x), você passa o endereço de x. A função recebe um ponteiro que aponta para a mesma posição de memória que x ocupa no chamador. Tudo que a função escrever via *p modifica diretamente x.

#include <stdio.h>

void zerar(int *p) {
    printf("  [dentro] *p antes = %d  endereço de p = %p  *p em = %p\n",
           *p, &p, p);
    *p = 0;
    printf("  [dentro] *p depois = %d\n", *p);
}

int main() {
    int x = 42;
    printf("[main] x antes = %d  endereço de x = %p\n", x, &x);
    zerar(&x);
    printf("[main] x depois = %d\n", x);
    return 0;
}
[main] x antes = 42 endereço de x = 0x7ffd...10
  [dentro] *p antes = 42 endereço de p = 0x7ffd...e0 *p em = 0x7ffd...10
  [dentro] *p depois = 0
[main] x depois = 0
Repare nos endereços: O ponteiro p dentro de zerar está em um endereço diferente (na stack de zerar), mas o valor que ele guarda (*p em) é o mesmo endereço de x. Eles compartilham a mesma posição de memória de dado — como dois controles remotos apontando para a mesma TV.

Quando passagem por referência é necessária

Situação Exemplo
A função precisa modificar a variável do chamador void incrementar(int *n) { (*n)++; }
A função precisa retornar mais de um resultado void minMax(int[], int, int*, int*)
Passar struct grande para evitar cópia cara void exibir(const Aluno *a)
Alocar memória e devolver o ponteiro ao chamador void criar(int **pp, int n)

4. Os Quatro Padrões de Parâmetro com Ponteiros

Em C profissional, a combinação de * e const em parâmetros comunica com precisão a intenção da função — o que ela faz e o que não faz com os dados:

Declaração do parâmetro Pode mudar *p? Pode mudar p? Uso típico
int *p ✅ Sim ✅ Sim Parâmetro de saída — função escreve resultado via ponteiro
const int *p ❌ Não ✅ Sim Parâmetro de entrada — função só lê, não modifica o dado
int * const p ✅ Sim ❌ Não Raro — o ponteiro em si não pode ser redirecionado
const int * const p ❌ Não ❌ Não Ponteiro completamente imutável — leitura pura
#include <stdio.h>

/* Só lê — const int* protege o dado */
float calcularMedia(const float *v, int n) {
    float soma = 0;
    for (int i = 0; i < n; i++)
        soma += v[i];           // OK: leitura
    // v[0] = 0;               // ❌ ERRO: const proíbe escrita
    return soma / n;
}

/* Escreve resultado via ponteiro de saída */
void minMax(const int *v, int n, int *minimo, int *maximo) {
    *minimo = *maximo = v[0];
    for (int i = 1; i < n; i++) {
        if (v[i] < *minimo) *minimo = v[i];
        if (v[i] > *maximo) *maximo = v[i];
    }
}

int main() {
    float notas[]  = {7.5f, 8.0f, 6.0f, 9.5f, 5.5f};
    int   valores[] = {3, 7, 1, 9, 4};

    printf("Média: %.1f\n", calcularMedia(notas, 5));

    int mn, mx;
    minMax(valores, 5, &mn, &mx);
    printf("Min: %d  Max: %d\n", mn, mx);

    return 0;
}
Média: 7.3
Min: 1 Max: 9
Regra de ouro: Se a função só lê os dados via ponteiro, use const tipo *p. Isso é documentação executável — o compilador garante que a função não modifica o dado acidentalmente, e o chamador sabe que seus dados estão protegidos.

5. Retorno Múltiplo via Ponteiros

Uma função só pode devolver um único valor com return. Quando você precisa de dois ou mais resultados, use ponteiros de saída — é o padrão idiomático em C:

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

/* Converte coordenadas cartesianas para polares */
void cartesianParaPolar(double x, double y,
                        double *raio, double *angulo) {
    *raio   = sqrt(x*x + y*y);
    *angulo = atan2(y, x) * 180.0 / 3.14159265;  // em graus
}

/* Divide e retorna quociente + resto */
int dividir(int dividendo, int divisor,
            int *quociente, int *resto) {
    if (divisor == 0) return 0;           // erro: retorna 0 (falso)
    *quociente = dividendo / divisor;
    *resto     = dividendo % divisor;
    return 1;                             // sucesso: retorna 1 (verdadeiro)
}

/* Estatísticas completas de um vetor */
void estatisticas(const float *v, int n,
                  float *media, float *maior, float *menor, float *desvio) {
    float soma = 0;
    *maior = *menor = v[0];
    for (int i = 0; i < n; i++) {
        soma += v[i];
        if (v[i] > *maior) *maior = v[i];
        if (v[i] < *menor) *menor = v[i];
    }
    *media = soma / n;
    float varSoma = 0;
    for (int i = 0; i < n; i++)
        varSoma += (v[i] - *media) * (v[i] - *media);
    *desvio = sqrt(varSoma / n);
}

int main() {
    double r, ang;
    cartesianParaPolar(3.0, 4.0, &r, &ang);
    printf("(3,4) → raio=%.2f  ângulo=%.1f°\n", r, ang);

    int q, res;
    if (dividir(17, 5, &q, &res))
        printf("17 ÷ 5 = %d  resto %d\n", q, res);
    if (!dividir(10, 0, &q, &res))
        printf("Divisão por zero!\n");

    float notas[] = {6.0f, 8.5f, 7.0f, 9.0f, 5.5f};
    float med, mx, mn, desv;
    estatisticas(notas, 5, &med, &mx, &mn, &desv);
    printf("\nMédia: %.2f  Maior: %.1f  Menor: %.1f  Desvio: %.2f\n",
           med, mx, mn, desv);

    return 0;
}
(3,4) → raio=5.00 ângulo=53.1°
17 ÷ 5 = 3 resto 2
Divisão por zero!

Média: 7.20 Maior: 9.0 Menor: 5.5 Desvio: 1.25
Padrão código de erro: A função dividir usa o return para indicar sucesso/falha (0 = erro, 1 = ok) e ponteiros para os resultados. Esse padrão é onipresente na biblioteca padrão de C — scanf, fgets, malloc e dezenas de outras funções usam exatamente essa convenção.

6. Ponteiros para Structs — Eficiência e Modificação

Passar uma struct por valor copia todos os seus bytes. Para uma struct de 200 bytes chamada 100 mil vezes, isso é 20 MB de cópia desnecessária. Ponteiros resolvem os dois problemas:

#include <stdio.h>
#include <string.h>

typedef struct {
    char  nome[50];
    float salario;
    int   nivel;
} Funcionario;

/* Só lê — const evita modificação acidental, custo = 8 bytes */
void exibir(const Funcionario *f) {
    printf("%-20s | R$%8.2f | Nível %d\n",
           f->nome, f->salario, f->nivel);
}

/* Modifica — aplica reajuste direto na struct original */
void aplicarReajuste(Funcionario *f, float percentual) {
    f->salario *= (1.0f + percentual / 100.0f);
    if (f->salario > 10000.0f && f->nivel < 3)
        f->nivel++;    // promoção automática acima de R$10k
}

/* Encontra o de maior salário — retorna ponteiro para o elemento */
Funcionario *melhorPago(Funcionario *equipe, int n) {
    Funcionario *melhor = &equipe[0];
    for (int i = 1; i < n; i++)
        if (equipe[i].salario > melhor->salario)
            melhor = &equipe[i];
    return melhor;
}

int main() {
    Funcionario equipe[] = {
        {"Ana Oliveira",  8500.0f, 2},
        {"Bruno Santos",  6200.0f, 1},
        {"Carla Mendes", 11000.0f, 3},
        {"Diego Nunes",   7800.0f, 2}
    };
    int n = 4;

    printf("=== Antes do reajuste ===\n");
    for (int i = 0; i < n; i++) exibir(&equipe[i]);

    for (int i = 0; i < n; i++) aplicarReajuste(&equipe[i], 10.0f);

    printf("\n=== Após reajuste de 10%% ===\n");
    for (int i = 0; i < n; i++) exibir(&equipe[i]);

    Funcionario *star = melhorPago(equipe, n);
    printf("\nMelhor pago: %s (R$%.2f)\n", star->nome, star->salario);

    return 0;
}
=== Antes do reajuste ===
Ana Oliveira | R$ 8500.00 | Nível 2
Bruno Santos | R$ 6200.00 | Nível 1
Carla Mendes | R$ 11000.00 | Nível 3
Diego Nunes | R$ 7800.00 | Nível 2

=== Após reajuste de 10% ===
Ana Oliveira | R$ 9350.00 | Nível 2
Bruno Santos | R$ 6820.00 | Nível 1
Carla Mendes | R$ 12100.00 | Nível 3
Diego Nunes | R$ 8580.00 | Nível 2

Melhor pago: Carla Mendes (R$12100.00)

7. Ponteiro para Ponteiro — Alocar e Devolver ao Chamador

Quando uma função precisa alocar memória e devolver o ponteiro ao chamador, ela recebe um ** (ponteiro para ponteiro). Isso é necessário porque o ponteiro em si precisa ser modificado:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Cria e inicializa um vetor de zeros — modifica o ponteiro do chamador */
int criarVetor(int **pp, int n) {
    *pp = (int *) calloc(n, sizeof(int));   // escreve no ponteiro original
    if (*pp == NULL) return 0;              // falhou
    return 1;                               // sucesso
}

/* Duplica uma string em memória dinâmica */
char *duplicarString(const char *src) {
    int len = strlen(src);
    char *copia = (char *) malloc((len + 1) * sizeof(char));
    if (copia == NULL) return NULL;
    strcpy(copia, src);
    return copia;    // chamador é responsável por fazer free
}

int main() {
    int *v = NULL;

    if (criarVetor(&v, 5)) {   // passa &v — função recebe int **
        for (int i = 0; i < 5; i++)
            v[i] = (i + 1) * 10;
        printf("Vetor: ");
        for (int i = 0; i < 5; i++) printf("%d ", v[i]);
        printf("\n");
        free(v);
    }

    char *s = duplicarString("Ponteiros em C");
    if (s) {
        printf("Cópia: %s\n", s);
        free(s);   // chamador libera o que a função alocou
    }

    return 0;
}
Vetor: 10 20 30 40 50
Cópia: Ponteiros em C
Contrato de propriedade: Quando uma função aloca memória e retorna o ponteiro (como duplicarString), o chamador fica responsável por chamar free. Isso deve ser documentado no cabeçalho ou num comentário — é um "contrato" entre quem cria e quem usa a função.

8. Erros Clássicos com Ponteiros em Funções

Erro 1 — Retornar ponteiro para variável local

/* ❌ ERRADO — variável local é destruída ao retornar */
int *funcaoPerigosa() {
    int resultado = 42;
    return &resultado;   // endereço de variável que não existe mais!
}

/* ✅ CORRETO — aloca no heap: sobrevive ao retorno da função */
int *funcaoCorreta() {
    int *p = (int *) malloc(sizeof(int));
    if (p) *p = 42;
    return p;   // chamador deve fazer free(p)
}

/* ✅ TAMBÉM CORRETO — ponteiro para static: persiste entre chamadas */
int *funcaoComStatic() {
    static int resultado = 42;
    return &resultado;   // static vive durante todo o programa
}

Erro 2 — Não verificar NULL antes de desreferenciar

/* ❌ ERRADO — se p for NULL, *p causa segfault */
void processar_errado(int *p) {
    *p = *p * 2;    // crash se p == NULL
}

/* ✅ CORRETO — verificar sempre */
int processar_correto(int *p) {
    if (p == NULL) return 0;   // guard clause
    *p = *p * 2;
    return 1;
}

Erro 3 — Esquecer & na chamada

void dobrar(int *n) { *n *= 2; }

int main() {
    int x = 5;

    dobrar(x);    // ❌ passa o VALOR 5 como se fosse endereço — segfault!
    dobrar(&x);   // ✅ passa o endereço de x

    return 0;
}

Erro 4 — Modificar ponteiro de entrada quando intenção é só ler

/* ❌ Função "leitora" que acidentalmente modifica dados */
float somar_errado(float *v, int n) {
    float soma = 0;
    while (n--)
        soma += *v++;   // modifica v — pode ser bug se ponteiro for reutilizado
    return soma;
}

/* ✅ Correto: const garante que o compilador avisa qualquer modificação */
float somar_correto(const float *v, int n) {
    float soma = 0;
    for (int i = 0; i < n; i++)
        soma += v[i];
    return soma;
}
Erro Sintoma Solução
Retornar ponteiro para local Comportamento indefinido — dados corrompidos Usar malloc ou static
Desreferenciar sem verificar NULL Segmentation fault Sempre verificar if (p == NULL)
Esquecer & na chamada Segmentation fault ou valor errado Usar f(&x) para passar por referência
Modificar dados de entrada inadvertidamente Bug sutil e difícil de encontrar Usar const tipo * para parâmetros somente leitura

9. Exemplo Integrador — Sistema de Notas Completo

Um programa que usa todos os padrões desta aula: leitura por ponteiro, cálculo com const, retorno múltiplo e alocação dinâmica:

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

typedef struct {
    char  nome[40];
    float *notas;    // array dinâmico — tamanho definido em execução
    int   qtdNotas;
} Aluno;

/* Cria aluno com array dinâmico de notas */
int inicializarAluno(Aluno *a, const char *nome, int qtd) {
    if (a == NULL || qtd <= 0) return 0;
    strncpy(a->nome, nome, 39);
    a->nome[39] = '\0';
    a->notas = (float *) malloc(qtd * sizeof(float));
    if (a->notas == NULL) return 0;
    a->qtdNotas = qtd;
    return 1;
}

/* Calcula estatísticas — const: só lê */
void calcularEstatisticas(const Aluno *a,
                           float *media, float *maior, float *menor) {
    *media = *maior = *menor = a->notas[0];
    for (int i = 1; i < a->qtdNotas; i++) {
        *media += a->notas[i];
        if (a->notas[i] > *maior) *maior = a->notas[i];
        if (a->notas[i] < *menor) *menor = a->notas[i];
    }
    *media /= a->qtdNotas;
}

/* Exibe relatório — const: só lê */
void exibirAluno(const Aluno *a) {
    float med, mx, mn;
    calcularEstatisticas(a, &med, &mx, &mn);
    printf("%-15s | Notas: ", a->nome);
    for (int i = 0; i < a->qtdNotas; i++)
        printf("%.1f ", a->notas[i]);
    printf("| Média: %.1f | %s\n",
           med, med >= 7.0f ? "Aprovado" : "Reprovado");
}

/* Libera memória dinâmica do aluno */
void liberarAluno(Aluno *a) {
    if (a && a->notas) {
        free(a->notas);
        a->notas = NULL;
        a->qtdNotas = 0;
    }
}

int main() {
    int qtd = 3;
    Aluno turma[3];

    const char *nomes[] = {"Ana Silva", "Bruno Lima", "Carla Matos"};
    float notasDados[][3] = {
        {8.0f, 9.0f, 7.5f},
        {5.0f, 6.0f, 4.5f},
        {9.5f, 10.f, 9.0f}
    };

    /* Inicializa cada aluno com array dinâmico */
    for (int i = 0; i < qtd; i++) {
        if (!inicializarAluno(&turma[i], nomes[i], 3)) {
            fprintf(stderr, "Erro ao alocar aluno %d\n", i);
            for (int j = 0; j < i; j++) liberarAluno(&turma[j]);
            return 1;
        }
        for (int j = 0; j < 3; j++)
            turma[i].notas[j] = notasDados[i][j];
    }

    printf("=== Boletim da Turma ===\n");
    for (int i = 0; i < qtd; i++)
        exibirAluno(&turma[i]);

    /* Libera toda a memória dinâmica */
    for (int i = 0; i < qtd; i++)
        liberarAluno(&turma[i]);

    return 0;
}
=== Boletim da Turma ===
Ana Silva | Notas: 8.0 9.0 7.5 | Média: 8.2 | Aprovado
Bruno Lima | Notas: 5.0 6.0 4.5 | Média: 5.2 | Reprovado
Carla Matos | Notas: 9.5 10.0 9.0 | Média: 9.5 | Aprovado

10. C vs. Java — Passagem de Parâmetros

Aspecto C Java
Primitivos (int, float…) Por valor — cópia Por valor — cópia (idêntico)
Modificar primitivo no chamador Ponteiro explícito: void f(int *p) Impossível diretamente — primitivos sempre por valor
Structs / objetos Por valor (cópia de todos os bytes) — use ponteiro para evitar Referência por valor — objetos sempre por referência implícita
Múltiplos retornos Ponteiros de saída: void f(int *a, int *b) Retornar array, classe ou usar wrapper
Proteção de leitura const tipo *p Sem equivalente direto para primitivos; final para referências
Swap de dois inteiros ✅ trocar(&a, &b) ❌ Impossível para primitivos

Side-by-side: o swap clássico

C — possível com ponteiros
void trocar(int *a, int *b) {
    int t = *a;
    *a = *b;
    *b = t;
}

int x = 10, y = 20;
trocar(&x, &y);
// x = 20, y = 10 ✅
Java — impossível para primitivos
static void trocar(int a, int b) {
    int t = a;
    a = b;
    b = t;
    // modifica cópias locais
}

int x = 10, y = 20;
trocar(x, y);
// x = 10, y = 20 ❌ (inalterados)

Quiz — Aula 23

1. Qual é a saída?

#include <stdio.h>
void f(int x) { x = 99; }
int main() {
    int a = 5;
    f(a);
    printf("%d", a);
}

2. Qual é a saída?

#include <stdio.h>
void f(int *p) { *p = 99; }
int main() {
    int a = 5;
    f(&a);
    printf("%d", a);
}

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

4. Por que a função abaixo está errada?

int *calcular() {
    int resultado = 42;
    return &resultado;
}

5. Qual é a saída?

#include <stdio.h>
void trocar(int *a, int *b) {
    int t = *a; *a = *b; *b = t;
}
int main() {
    int x = 10, y = 20;
    trocar(&x, &y);
    printf("%d %d", x, y);
}

6. Quando é vantajoso passar uma struct grande para uma função usando const Struct *p em vez de Struct s por valor?

7. Uma função recebe int **pp como parâmetro e executa *pp = malloc(10 * sizeof(int)). O que isso faz?

8. Em Java, trocar os valores de dois int dentro de um método não altera os originais no chamador. A razão é: