Aula 20 — Ponteiros e Alocação de Memória

Operadores & e * — endereços, desreferência e a memória por dentro

1. Por que Ponteiros Existem?

Em Java, você nunca precisa saber em qual endereço de memória um objeto está — a JVM gerencia isso. Em C, a memória é sua responsabilidade. E às vezes você precisa exatamente saber onde um dado está, não apenas o que ele vale.

Três situações comuns que só se resolvem com ponteiros:

Situação Por que ponteiro é necessário
Modificar uma variável dentro de uma função Parâmetros são cópias — a função precisa do endereço para acessar o original
Retornar múltiplos valores de uma função return só devolve um valor; ponteiros permitem escrever em vários endereços
Alocar memória em tempo de execução Arrays dinâmicos, listas encadeadas — tudo requer ponteiros para o bloco alocado
Um ponteiro é uma variável que armazena o endereço de memória de outra variável — em vez de armazenar um valor diretamente. O tipo do ponteiro indica qual tipo de dado está no endereço apontado, o que permite ao compilador calcular tamanhos e fazer verificações de tipo.

2. Como a Memória Funciona

Imagine a memória RAM como uma sequência enorme de caixas numeradas. Cada caixa tem um endereço (um número) e guarda um valor. Quando você declara int x = 42;, o compilador reserva 4 caixas consecutivas (pois int ocupa 4 bytes) e coloca o valor 42 lá.

Endereço Conteúdo Variável
0x100042 (byte 0)int x = 42
0x10010 (byte 1)
0x10020 (byte 2)
0x10030 (byte 3)
0x10040x1000int *p = &x

O ponteiro p guarda o valor 0x1000 — o endereço onde x começa. Ele aponta para x.

Tamanho de um ponteiro: Todo ponteiro ocupa o mesmo tamanho, independentemente do tipo que aponta — 8 bytes em sistemas 64-bit (o suficiente para endereçar qualquer posição na memória). Um int * e um double * têm o mesmo tamanho de 8 bytes; o tipo informa apenas como interpretar o que está no endereço.

3. O Operador & — Endereço-de

O operador & (lê-se "endereço de") aplicado a uma variável retorna o endereço de memória onde ela está armazenada. O resultado é um ponteiro para aquela variável.
#include <stdio.h>

int main() {
    int    x = 42;
    float  f = 3.14f;
    char   c = 'A';

    /* %p imprime o endereço em hexadecimal */
    printf("Valor de x:     %d\n",  x);
    printf("Endereço de x:  %p\n",  &x);   // ex: 0x7ffd2a1b

    printf("Valor de f:     %.2f\n", f);
    printf("Endereço de f:  %p\n",  &f);

    printf("Valor de c:     %c\n",  c);
    printf("Endereço de c:  %p\n",  &c);

    /* Endereços são consecutivos na stack — observe a diferença */
    printf("\nDiferença x→f:  %ld bytes\n",
           (char*)&x - (char*)&f);

    return 0;
}
Valor de x: 42
Endereço de x: 0x7ffd2a1b1c
Valor de f: 3.14
Endereço de f: 0x7ffd2a1b18
Valor de c: A
Endereço de c: 0x7ffd2a1b17

Diferença x→f: 4 bytes
Você já usou & antes! Toda vez que escreveu scanf("%d", &variavel), estava passando o endereço da variável para que scanf pudesse escrever o valor digitado diretamente naquele endereço de memória. Agora você sabe exatamente por quê.

4. Declarando e Inicializando Ponteiros

Um ponteiro é declarado com um asterisco * entre o tipo e o nome:

int    *p;       // ponteiro para int
float  *pf;      // ponteiro para float
char   *pc;      // ponteiro para char
double *pd;      // ponteiro para double
⚠️ Ponteiro não inicializado é extremamente perigoso. Um ponteiro declarado sem inicialização contém um endereço aleatório (lixo de memória). Usá-lo pode corromper dados ou causar segmentation fault. Sempre inicialize ponteiros — seja com &variavel ou com NULL.
int *p;             // ❌ ponteiro "selvagem" — aponta para lugar desconhecido
*p = 10;            // ❌ comportamento indefinido — pode destruir dados!

int *q = NULL;      // ✅ ponteiro nulo — seguro, indica "não aponta para nada"
int x = 5;
int *r = &x;        // ✅ ponteiro válido — aponta para x

Anatomia completa: declaração, atribuição e uso

#include <stdio.h>

int main() {
    int x = 10;        // variável comum: armazena o valor 10
    int *p = &x;       // p armazena o ENDEREÇO de x

    printf("x     = %d\n",   x);    // valor de x: 10
    printf("&x    = %p\n",  &x);    // endereço de x: ex. 0x7ffd...
    printf("p     = %p\n",   p);    // valor de p (= endereço de x)
    printf("&p    = %p\n",  &p);    // endereço do próprio ponteiro p
    printf("*p    = %d\n",  *p);    // valor no endereço apontado: 10

    return 0;
}
x = 10
&x = 0x7ffd1a2b3c
p = 0x7ffd1a2b3c
&p = 0x7ffd1a2b44
*p = 10

Repare: &x e p têm o mesmo valor (o endereço de x), mas &p é diferente — é o endereço do próprio ponteiro, que também precisa de espaço na memória.

5. O Operador * — Desreferência

O operador * aplicado a um ponteiro (fora da declaração) é chamado de desreferência (ou indireção). Ele acessa o valor armazenado no endereço apontado — como se você "seguisse a seta" do ponteiro até o dado real.

O * tem três significados distintos em C — é crucial não confundi-los:

Contexto Significado Exemplo
Na declaração "Esta variável é um ponteiro" int *p;
Em expressão (leitura) "Leia o valor no endereço apontado" printf("%d", *p);
Em expressão (escrita) "Escreva no endereço apontado" *p = 100;
#include <stdio.h>

int main() {
    int x = 5;
    int *p = &x;

    printf("Antes: x = %d\n", x);    // 5

    *p = 100;                         // escreve 100 no endereço de x

    printf("Depois: x = %d\n", x);   // 100 — x foi alterado via ponteiro!
    printf("Depois: *p = %d\n", *p); // 100 — *p e x são a mesma posição

    (*p)++;                           // incrementa x através do ponteiro
    printf("Após ++: x = %d\n", x);  // 101

    return 0;
}
Antes: x = 5
Depois: x = 100
Depois: *p = 100
Após ++: x = 101
⚠️ Cuidado com a precedência: *p++ e (*p)++ são completamente diferentes. O operador ++ tem precedência maior que *, então *p++ incrementa o ponteiro p (faz ele apontar para o próximo endereço), e só depois desreferencia. (*p)++ incrementa o valor apontado. Use parênteses para deixar a intenção clara.

6. Ponteiros e Funções — Modificando o Original

O uso mais imediato de ponteiros é permitir que funções modifiquem variáveis do chamador — o que parâmetros por valor nunca conseguem:

#include <stdio.h>

/* Troca os valores de duas variáveis — impossível sem ponteiros */
void trocar(int *a, int *b) {
    int temp = *a;   // guarda o valor em *a
    *a = *b;         // copia *b para *a
    *b = temp;       // copia temp para *b
}

/* Divide e retorna quociente + resto via ponteiros */
void dividir(int dividendo, int divisor,
             int *quociente, int *resto) {
    *quociente = dividendo / divisor;
    *resto     = dividendo % divisor;
}

int main() {
    int x = 10, y = 20;
    printf("Antes:  x=%d  y=%d\n", x, y);
    trocar(&x, &y);
    printf("Depois: x=%d  y=%d\n", x, y);

    int q, r;
    dividir(17, 5, &q, &r);
    printf("\n17 ÷ 5 = %d  resto %d\n", q, r);

    return 0;
}
Antes: x=10 y=20
Depois: x=20 y=10

17 ÷ 5 = 3 resto 2

7. Ponteiro Nulo — NULL

NULL é uma constante especial (geralmente definida como 0 ou (void*)0) que representa um ponteiro que não aponta para nada. É o valor seguro para ponteiros não inicializados ou para indicar ausência de dado válido.
#include <stdio.h>

int *buscar(int v[], int tam, int alvo) {
    for (int i = 0; i < tam; i++)
        if (v[i] == alvo)
            return &v[i];   // retorna endereço do elemento encontrado
    return NULL;             // não encontrado — retorna NULL
}

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

    int *p = buscar(notas, 5, 9);

    if (p != NULL)           // SEMPRE verifique antes de desreferenciar
        printf("Encontrado: %d (endereço %p)\n", *p, p);
    else
        printf("Não encontrado.\n");

    int *q = buscar(notas, 5, 42);
    if (q != NULL)
        printf("Encontrado: %d\n", *q);
    else
        printf("42 não encontrado.\n");

    return 0;
}
Encontrado: 9 (endereço 0x7ffd...)
42 não encontrado.
⚠️ Nunca desreferencie um ponteiro NULL. *NULL causa segmentation fault imediato — o programa trava. Sempre verifique if (p != NULL) antes de usar *p, especialmente quando o ponteiro veio de uma função que pode retornar NULL.

8. O Tipo do Ponteiro Importa

O tipo antes do * não afeta o tamanho do ponteiro (sempre 8 bytes em 64-bit), mas determina como o compilador interpreta os bytes no endereço e quanto avança em aritmética de ponteiros.

#include <stdio.h>

int main() {
    int    vi = 42;
    float  vf = 3.14f;
    double vd = 2.718;
    char   vc = 'Z';

    int    *pi = &vi;
    float  *pf = &vf;
    double *pd = &vd;
    char   *pc = &vc;

    printf("sizeof(int*)    = %zu bytes\n", sizeof(pi));
    printf("sizeof(float*)  = %zu bytes\n", sizeof(pf));
    printf("sizeof(double*) = %zu bytes\n", sizeof(pd));
    printf("sizeof(char*)   = %zu bytes\n", sizeof(pc));

    printf("\nValores via desreferência:\n");
    printf("*pi = %d\n",  *pi);
    printf("*pf = %.2f\n", *pf);
    printf("*pd = %.3f\n", *pd);
    printf("*pc = %c\n",  *pc);

    return 0;
}
sizeof(int*) = 8 bytes
sizeof(float*) = 8 bytes
sizeof(double*) = 8 bytes
sizeof(char*) = 8 bytes

Valores via desreferência:
*pi = 42
*pf = 3.14
*pd = 2.718
*pc = Z
Ponteiro para void (void *): É um ponteiro genérico — pode armazenar qualquer endereço, mas não pode ser desreferenciado diretamente (o compilador não sabe quantos bytes ler). É usado em funções genéricas como malloc(), que retorna void * e precisa ser convertido para o tipo adequado.

9. Introdução à Alocação Dinâmica de Memória

Até agora, toda a memória foi alocada em tempo de compilação — o compilador reserva espaço fixo para cada variável. Com ponteiros, é possível pedir memória em tempo de execução, na quantidade exata que o programa precisar.

A alocação dinâmica permite reservar blocos de memória durante a execução do programa, cujo tamanho pode depender de entradas do usuário ou de cálculos em tempo real. Em C, isso é feito com as funções malloc(), calloc() e realloc() da biblioteca <stdlib.h>, e a memória deve ser liberada com free() quando não for mais necessária.

As regiões de memória de um programa C

Região O que armazena Gerenciamento
Stack (Pilha) Variáveis locais, parâmetros, frames de função Automático — criadas e destruídas com cada chamada
Heap Memória alocada dinamicamente com malloc Manual — você aloca e você libera com free
Segmento de dados Variáveis globais e static Automático — existem durante toda a execução
Segmento de código Código compilado das funções Fixo — carregado uma vez pelo SO

malloc() — alocando um bloco de memória

#include <stdio.h>
#include <stdlib.h>    // necessário para malloc e free

int main() {
    int n;
    printf("Quantos valores? ");
    scanf("%d", &n);

    /* Aloca n inteiros no heap — malloc retorna void* */
    int *vetor = (int *) malloc(n * sizeof(int));

    /* SEMPRE verifique se a alocação teve sucesso */
    if (vetor == NULL) {
        printf("Erro: memória insuficiente!\n");
        return 1;
    }

    /* Usa o vetor normalmente */
    for (int i = 0; i < n; i++) {
        printf("Valor %d: ", i + 1);
        scanf("%d", &vetor[i]);
    }

    printf("\nVocê digitou: ");
    for (int i = 0; i < n; i++)
        printf("%d ", vetor[i]);
    printf("\n");

    /* Libera a memória — obrigatório para evitar vazamento */
    free(vetor);
    vetor = NULL;    // boa prática: evita uso acidental após free

    return 0;
}
Quantos valores? 4
Valor 1: 10
Valor 2: 30
Valor 3: 20
Valor 4: 40

Você digitou: 10 30 20 40

Comparando as três funções de alocação

Função Sintaxe Inicializa com zero? Uso típico
malloc(n) malloc(n * sizeof(tipo)) ❌ Contém lixo Alocação simples — quando o programa vai preencher imediatamente
calloc(qtd, tam) calloc(n, sizeof(tipo)) ✅ Zeros Quando os valores iniciais precisam ser zero (contadores, flags)
realloc(p, novo_n) realloc(p, novo_n * sizeof(tipo)) — Redimensionar um bloco já alocado (crescimento dinâmico)

Exemplo com calloc

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

int main() {
    int n = 5;

    /* calloc: aloca 5 ints e inicializa todos com 0 */
    int *contadores = (int *) calloc(n, sizeof(int));

    if (contadores == NULL) { printf("Sem memória!\n"); return 1; }

    printf("Valores iniciais (calloc): ");
    for (int i = 0; i < n; i++)
        printf("%d ", contadores[i]);   // todos serão 0
    printf("\n");

    contadores[2] = 10;
    contadores[4] = 25;

    printf("Após modificação:         ");
    for (int i = 0; i < n; i++)
        printf("%d ", contadores[i]);
    printf("\n");

    free(contadores);
    return 0;
}
Valores iniciais (calloc): 0 0 0 0 0
Após modificação: 0 0 10 0 25
⚠️ Memory leak (vazamento de memória): Se você alocar memória com malloc/calloc e não liberar com free, essa memória permanece ocupada até o programa terminar — mesmo que nenhuma variável mais a referencie. Em programas longos isso consome memória progressivamente. Regra: para cada malloc, deve haver um free correspondente.

10. Resumo dos Operadores de Ponteiro

Operador Nome Onde aparece O que faz Exemplo
& Endereço-de Expressão Retorna o endereço de memória de uma variável p = &x
*tipo Declaração de ponteiro Declaração Declara que a variável é um ponteiro int *p;
*p Desreferência (leitura) Expressão Lê o valor no endereço apontado x = *p;
*p = val Desreferência (escrita) Expressão Escreve no endereço apontado *p = 10;
-> Seta Expressão Acessa campo de struct via ponteiro (= (*p).campo) p->x
NULL Ponteiro nulo Declaração / teste Ponteiro que não aponta para nada — valor seguro p = NULL;

11. C vs. Java — Ponteiros e Memória

Aspecto C Java
Ponteiros explícitos ✅ Sim — você manipula endereços diretamente ❌ Não — Java esconde ponteiros; usa referências implícitas
Gerenciamento de memória Manual — malloc e free são sua responsabilidade Automático — Garbage Collector libera memória não usada
Ponteiro nulo NULL (definido em <stddef.h>) null (palavra-chave da linguagem)
Desreferenciar nulo Segmentation fault (falha silenciosa) NullPointerException (exceção capturável)
Memory leak ⚠️ Possível e silencioso — responsabilidade do programador ✅ Improvável — GC coleta memória inacessível
Acesso à memória fora dos limites ⚠️ Comportamento indefinido — corrupção silenciosa ✅ Lança ArrayIndexOutOfBoundsException

Side-by-side: passar e modificar um inteiro

C — requer ponteiro explícito
void dobrar(int *n) {
    *n = *n * 2;
}

int main() {
    int x = 5;
    dobrar(&x);
    printf("%d\n", x);  // 10
}
Java — primitivos são imutáveis por referência
static void dobrar(int n) {
    n = n * 2;  // apenas a cópia
}
// Java não permite modificar
// primitivos pelo chamador.
// Solução: retornar o valor ou
// usar Integer[] de um elemento.

Quiz — Aula 20

1. O que o operador & retorna quando aplicado a uma variável?

2. Qual é a saída?

#include <stdio.h>
int main() {
    int x = 7;
    int *p = &x;
    *p = 20;
    printf("%d", x);
}

3. Em sistemas 64-bit, qual o tamanho em bytes de um ponteiro double *?

4. O que acontece ao tentar usar um ponteiro que não foi inicializado?

5. Qual é a diferença entre *p++ e (*p)++?

6. O que malloc(n * sizeof(int)) retorna se a alocação falhar?

7. Qual é a diferença entre malloc e calloc?

8. Por que é boa prática escrever vetor = NULL logo após free(vetor)?