Aula 31 — Ponteiros e Passagem por Referência

O conceito mais crítico de C — e por que usamos LISTA* em vez de LISTA em todas as funções

0. Nomenclatura Formal: Call by Value e Call by Reference

Esta aula aprofunda um conceito já introduzido na Aula 14. Antes de estudar ponteiros com structs e listas, é importante fixar a terminologia padrão da literatura — você a encontrará em qualquer livro de Estruturas de Dados ou Sistemas Operacionais.

Call by Value (Passagem por Valor): A função recebe uma cópia independente do argumento. Modificações feitas dentro da função não afetam a variável original. É o comportamento padrão de C para tipos primitivos e structs quando passados sem ponteiro.
Call by Reference (Passagem por Referência): A função recebe o endereço de memória da variável original. Através do ponteiro, a função pode ler e modificar diretamente o conteúdo na memória — sem criar cópia. Em C, isso é feito explicitamente com & na chamada e * no parâmetro.

Por que isso é crítico para structs — especialmente a LISTA?

Passagem por valor copia o argumento inteiro. Um int ocupa 4 bytes — cópia barata. Uma LISTA com 50 REGISTROs pode ocupar centenas de bytes — cópia cara. Mas o problema real não é desempenho: é que a função não consegue modificar a lista original se receber uma cópia.

/* ❌ CALL BY VALUE — recebe cópia da struct */
void inicializar_errado(LISTA l) {
    l.nroElem = 0;   // zera só a cópia local
}                    // a lista original continua com lixo

/* ✅ CALL BY REFERENCE — recebe endereço da struct */
void inicializar_correto(LISTA *l) {
    l->nroElem = 0;  // zera através do endereço
}                    // a lista original é modificada

int main() {
    LISTA lista;

    inicializar_errado(lista);      // lista.nroElem ainda contém lixo
    inicializar_correto(&lista);    // lista.nroElem agora é 0
    return 0;
}
Call by Value Call by Reference (ponteiro)
O que é passado Cópia do valor/struct Endereço de memória
Pode modificar o original? ❌ Não ✅ Sim
Custo de memória Copia tudo (caro para structs grandes) Copia apenas o endereço (8 bytes)
Sintaxe na chamada f(lista) f(&lista)
Sintaxe no parâmetro void f(LISTA l) void f(LISTA *l)
Acessa campos com l.campo l->campo (equivale a (*l).campo)
Regra prática para listas e structs: Se a função precisa modificar a struct — use ponteiro (LISTA *l). Se a função só precisa ler — use ponteiro com const (const LISTA *l) para eficiência sem risco de modificação. Nunca passe structs grandes por valor.

1. O Problema: funções que não funcionam como esperado

Imagine que você quer escrever uma função inicializar que zera o contador de elementos da lista. Observe a armadilha:

// ❌ VERSÃO ERRADA — compila, mas não inicializa a lista original
void inicializarLista(LISTA l) {
    l.nroElem = 0;   // zera uma CÓPIA local, não a original
}

int main() {
    LISTA lista;
    inicializarLista(lista);
    printf("%d\n", lista.nroElem);  // imprime lixo — não foi inicializada!
    return 0;
}
Por que não funciona? Em C, quando você passa LISTA l para uma função, o C copia a struct inteira para um espaço novo na memória. A função trabalha com a cópia — a original permanece intocada.
Este comportamento se chama 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 — jamais a variável original.

2. O que é um Ponteiro?

Um ponteiro é uma variável que armazena o endereço de memória de outra variável — em vez de armazenar o valor em si.

int x = 42;        // variável comum: armazena o valor 42
int *p = &x;       // ponteiro: armazena o ENDEREÇO de x
                   // &x significa "endereço de x"

printf("%d\n", x);   // imprime 42 (o valor)
printf("%p\n", p);   // imprime algo como 0x7ffd2a1b (o endereço)
printf("%d\n", *p);  // imprime 42 (o valor no endereço — "desreferência")

*p = 100;            // modifica x através do ponteiro
printf("%d\n", x);   // agora imprime 100!
OperadorNomeO que faz
&Endereço-deObtém o endereço de uma variável
* (na declaração)Declaração de ponteiroIndica que a variável é um ponteiro
* (no uso)DesreferênciaAcessa o valor no endereço apontado
->SetaAcessa campo de uma struct via ponteiro (equivale a (*p).campo)

3. Passagem por Referência com Ponteiros

Para que uma função possa modificar a variável original, passamos o endereço da variável — não a variável em si.

#include <stdio.h>

// ✅ VERSÃO CORRETA — recebe o endereço da lista
void inicializarLista(LISTA *l) {
    l->nroElem = 0;   // l-> acessa o campo do original através do ponteiro
}

int main() {
    LISTA lista;
    inicializarLista(&lista);          // passa o ENDEREÇO de lista
    printf("%d\n", lista.nroElem);     // imprime 0 — inicializado corretamente!
    return 0;
}
O operador ->: Quando temos um ponteiro para struct, usar (*l).nroElem é válido mas verboso. O operador -> é um atalho: l->nroElem significa exatamente o mesmo — "acesse o campo nroElem da struct apontada por l".

Diagrama de memória — o que acontece

Endereço Variável Valor
0x1000 lista (em main) nroElem = ? (lixo antes)
0x2000 l (em inicializarLista) 0x1000 ← armazena o endereço de lista
l->nroElem = 0 escreve em 0x1000, alterando lista.nroElem diretamente
0x1000 lista (em main) nroElem = 0 ✅

4. Por que LISTA* em Todas as Funções da Lista?

Agora que você entende passagem por referência, fica claro por que todas as funções da Lista Linear Sequencial recebem LISTA *l:

Função Por que precisa de LISTA*
inicializarLista(LISTA *l) Modifica l->nroElem
inserirElemLista(LISTA *l, ...) Modifica l->A[] e incrementa l->nroElem
excluirElemLista(LISTA *l, ...) Modifica l->A[] e decrementa l->nroElem
exibirLista(LISTA *l) Só lê — mas ponteiro evita copiar 200+ bytes desnecessariamente
tamanho(LISTA *l) Só lê — mesma razão: eficiência
buscaSequencial(LISTA *l, ...) Usa const LISTA* — lê sem modificar, mas sem copiar
Para funções que apenas leem a lista, o ideal é usar const LISTA *l: assim o compilador impede modificações acidentais, mas a eficiência de não copiar é mantida. Quando a aula de estruturas de dados apresentar o código completo, você verá esse padrão em ação.

5. Ponto (.) vs. Seta (->)

Um erro muito comum ao começar com structs e ponteiros é confundir os dois operadores de acesso:

Situação Operador Exemplo
Variável é a própria struct . (ponto) lista.nroElem
Variável é um ponteiro para struct -> (seta) l->nroElem
LISTA lista;          // variável struct
LISTA *p = &lista;   // ponteiro para struct

// As quatro formas abaixo são equivalentes:
lista.nroElem = 0;    // ✅ ponto — acesso direto
(*p).nroElem = 0;     // ✅ desreferência + ponto (verboso)
p->nroElem = 0;      // ✅ seta — atalho para (*p).nroElem

// ❌ Erros comuns:
// p.nroElem = 0;    — p é ponteiro, não struct — use ->
// lista->nroElem;   — lista é struct, não ponteiro — use .
Regra mnemônica: "Ponto para variável, seta para ponteiro." Ou mais formalmente: se o tipo antes do operador tem um * na declaração, use ->. Se não tem *, use ..

6. Exemplo Prático Completo

Um exemplo autossuficiente que demonstra tudo que vimos: struct, ponteiro para struct, passagem por referência, operador seta e eficiência de memória.

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

typedef struct {
    int   matricula;
    char  nome[50];
    float nota;
} Aluno;

/* Inicializa via ponteiro — modifica o original */
void inicializarAluno(Aluno *a, int mat, const char *nome, float nota) {
    a->matricula = mat;
    strncpy(a->nome, nome, 49);
    a->nota = nota;
}

/* Só lê — const protege de modificação acidental */
void exibirAluno(const Aluno *a) {
    printf("Mat: %d | Nome: %-20s | Nota: %.1f | %s\n",
           a->matricula, a->nome, a->nota,
           a->nota >= 5.0f ? "Aprovado" : "Reprovado");
}

/* Aplica bônus — modifica via ponteiro */
void aplicarBonus(Aluno *a, float bonus) {
    a->nota += bonus;
    if (a->nota > 10.0f) a->nota = 10.0f;
}

int main() {
    Aluno a1, a2;

    inicializarAluno(&a1, 1001, "Ana Souza",   7.5f);
    inicializarAluno(&a2, 1002, "Bruno Lima",  4.0f);

    printf("=== Antes do bônus ===\n");
    exibirAluno(&a1);
    exibirAluno(&a2);

    aplicarBonus(&a2, 1.5f);

    printf("\n=== Após bônus de 1.5 para Bruno ===\n");
    exibirAluno(&a1);
    exibirAluno(&a2);

    return 0;
}
=== Antes do bônus ===
Mat: 1001 | Nome: Ana Souza | Nota: 7.5 | Aprovado
Mat: 1002 | Nome: Bruno Lima | Nota: 4.0 | Reprovado

=== Após bônus de 1.5 para Bruno ===
Mat: 1001 | Nome: Ana Souza | Nota: 7.5 | Aprovado
Mat: 1002 | Nome: Bruno Lima | Nota: 5.5 | Aprovado

7. C vs. Java

Aspecto C Java
Passagem de struct/objeto Por valor (cópia) ou por ponteiro (explícito) Objetos sempre por referência automática
Modificar struct em função Obrigatório usar LISTA *l e l->campo Automático — qualquer método pode modificar o objeto
Acesso a campos variavel.campo ou ponteiro->campo Sempre objeto.campo
Proteção somente leitura const LISTA *l Sem equivalente direto para campos individuais
Custo de cópia de struct grande ⚠️ Você controla — use ponteiro para evitar Automático — JVM gerencia cópias via GC

Quiz — Aula 31

1. Qual é a saída do código abaixo?

typedef struct { int x; } S;
void f(S s) { s.x = 99; }
int main() {
    S s; s.x = 1;
    f(s);
    printf("%d", s.x);
}

2. Para que a função abaixo consiga modificar o campo nroElem da lista original, o que deve substituir ????

void zerar(??? l) { l->nroElem = 0; }

3. Dado LISTA *l, qual expressão acessa corretamente o campo nroElem?

4. Por que passar uma LISTA grande por ponteiro é mais eficiente do que por valor?

5. p->campo é equivalente a qual expressão?

6. Quando uma função recebe const LISTA *l, o que isso garante?

7. Por que usar LISTA *l em exibirLista, sendo que ela apenas lê a lista sem modificar?

8. Em Java, modificar campos de um objeto dentro de um método é automático. Em C, para modificar campos de uma struct passada a uma função, é necessário: