Trilha 06 · Memória e execução

Ponteiros e referências

Em C um ponteiro é um número — o endereço de um byte na memória. Em Python, Java e JavaScript "referências" são a mesma ideia com um invólucro mais seguro. Entender a diferença entre copiar um valor e copiar uma referência evita uma categoria inteira de bugs.

① Intuição

Valor vs. endereço

Quando você escreve int b = a; em C, copia o valor de a para b. São duas gavetas diferentes na stack, cada uma com sua própria cópia do número. Alterar uma não afeta a outra.

Quando você passa um objeto em Python (b = a), copia a referência — b e a são etiquetas diferentes para o mesmo objeto no heap. Modificar o objeto via b também é visível por a, porque apontam para o mesmo endereço.

Ponteiros em C são explícitos sobre isso: a sintaxe *p e &x deixa claro que você está lidando com endereços. Em linguagens de alto nível a referência é implícita, o que é mais confortável mas pode surpreender quem não sabe o que acontece por baixo.

Passagem por valor vs. por referência: em C, tudo é passado por valor — inclusive ponteiros. Passar um ponteiro é passagem por referência indireta: você passa o endereço (por valor), e a função pode modificar o que está naquele endereço. Java passa referências de objeto por valor (não por referência), o que confunde muita gente.
② Visualização interativa

Valor vs. referência na prática

Alterne entre os dois modos e experimente modificar variáveis para ver o que muda e o que não muda.

int a = 42;
int b = a; // b recebe uma cópia independente
stack
a
42
stack
b
42
Clique em +1 em qualquer variável. A outra não muda — cada uma ocupa seu próprio espaço na stack.
③ Explicação técnica

Ponteiros em C

// Ponteiros em C: variáveis que guardam endereços
int x = 42;
int* p = &x;        // & = "endereço de" → p guarda o endereço de x

printf("%p\n", p);    // imprime ex: 0x7ffd3a20  (endereço)
printf("%d\n", *p);   // * = "valor em" → imprime 42
*p = 100;            // escreve via ponteiro → x agora é 100

// Aritmética de ponteiro
int arr[4] = {10, 20, 30, 40};
int* q = arr;        // arr decai para &arr[0]
printf("%d\n", *(q + 2));  // arr[2] = 30 (avança 2 × sizeof(int))

// Ponteiro para ponteiro
int** pp = &p;       // pp aponta para p, que aponta para x
**pp = 200;         // modifica x via dois níveis de indireção

Semântica de referência em Python

// Python: semântica de referência para objetos mutáveis
a = [1, 2, 3]
b = a            // b aponta para o MESMO objeto
b.append(4)
print(a)         // [1, 2, 3, 4] — a viu a mudança!

// Para copiar:
c = a.copy()           // cópia rasa (shallow) — objetos aninhados ainda são refs
import copy
d = copy.deepcopy(a)   // cópia profunda — tudo novo

// Tipos imutáveis (int, str, tuple) têm semântica de valor na prática
x = 42
y = x
y = 99
print(x)         // 42 — y recebeu uma referência ao inteiro 42,
                 // mas y = 99 criou um novo objeto inteiro, não modificou o 42
nullptr e referências em C++: C++ tem referências (int& r = x) que são ponteiros que não podem ser nulos e não precisam de * para dereferenciar. C++11 introduziu nullptr (tipo-seguro) no lugar de NULL (que é apenas 0, e pode ser confundido com inteiro). Rust vai além: o compilador garante que referências nunca apontam para memória inválida — sem null, sem dangling pointer, sem corrida de dados, em tempo de compilação.
④ Projeto para programar

Ponteiros na prática

Mini projeto: em C, implemente swap(int* a, int* b) que troca o conteúdo de dois inteiros. Teste chamando swap(&x, &y) e confirme que x e y foram realmente trocados. Compare com uma versão errada que recebe int por valor.

Projeto principal: em Python, implemente uma função deep_equal(a, b) que compara dois objetos arbitrariamente aninhados (dicionários com listas de dicionários) e retorna True se os valores são iguais — mesmo que sejam objetos diferentes na memória. Use is para checar identidade e == para checar igualdade. Demonstre a diferença em exemplos concretos.

Desafio extra: em C, implemente uma lista encadeada simples (linked list) usando ponteiros. Adicione funções para inserir no início, remover um elemento por valor e imprimir a lista. Confirme com Valgrind que todos os nós são liberados corretamente ao destruir a lista. Por que arrays contíguos têm melhor cache locality que listas encadeadas?

⑤ Exercícios rápidos

Teste sua intuição

int x = 42; int b = x; b = 99; — qual é o valor de x?
Em Python: a = [1,2,3]; b = a; b.append(4) — a lista apontada por a também mudou?
Quando dereferenciar um ponteiro em C causa undefined behavior?
⑥ Aplicações no mundo real

Onde você encontra isso

🔁

Passagem eficiente de dados

Passar um objeto de 100 MB por valor copiaria 100 MB a cada chamada de função — inviável. Passar um ponteiro/referência copia apenas 8 bytes (o endereço). É por isso que C aceita arrays como ponteiros, Java passa objetos por referência e C++ usa const& para evitar cópias desnecessárias.

🔀

Estruturas de dados dinâmicas

Listas encadeadas, árvores e grafos dependem de ponteiros: cada nó aponta para o próximo. O Redis armazena estruturas internas (listas, dicionários, conjuntos) como C structs com ponteiros explícitos, otimizados para cache locality. Saber onde os ponteiros apontam é essencial para entender o custo de memória real dessas estruturas.

🛡️

Ownership no Rust

Rust torna o gerenciamento de ponteiros um problema de tempo de compilação: cada valor tem exatamente um dono, e empréstimos (borrows) são verificados pelo borrow checker. Ponteiros mutáveis e imutáveis nunca coexistem — garantia que Java e Python não dão. Isso elimina data races e use-after-free sem GC.

← Anterior: Heap e alocação dinâmica Próxima: Vazamentos e erros →