Ponteiros e Memória Dinâmica

Programação Estruturada

Victor S. Portella

UFABC

2026-10-16

Memória e endereços

O que veremos hoje

Nesta aula vamos conectar três ideias:

  • endereços de memória;
  • ponteiros;
  • alocação dinâmica.

Memória como uma sequência de bytes

Podemos imaginar a memória como um grande vetor de bytes.

Cada byte possui um endereço.

Endereço      Conteúdo
0x1000        ...
0x1001        ...
0x1002        ...
0x1003        ...
...

Uma variável ocupa um ou mais bytes consecutivos.

O endereço de uma variável

O operador & retorna o endereço de uma variável.

#include <stdio.h>

int main() {
    int n = 507;

    printf("n = %d\n", n);
    printf("endereco = %p\n", &n);

    return 0;
}

O endereço não é endereço físico da memória RAM, mas um endereço dentro do espaço de memória do processo.

Medidas de segurança fazem com que endereços mudem entre diferentes execuções.

O que é um ponteiro?

Um ponteiro é uma variável cujo valor é um endereço de memória.

int x = 507;
int *ptr = &n;
char ch = 'a';
int *cptr = &ch;

  • ptr é um ponteiro para uma variável do tipo int
  • cptr é um ponteiro para uma variável do tipo char
    • O tipo de ptr é int * e o de cptr é char *
  • O valor de ptr é o endereço de x
  • O valor de cptr é o endereço de ch

Um ponteiro também é uma variável

int n = 12;
int *ptr = &n;

Temos duas variáveis:

  • n, cujo valor é 12;
  • ptr, cujo valor é o endereço de n.

O ponteiro também tem um endereço próprio: &ptr.

Acessando o valor apontado

O operador * também é usado para desreferenciar um ponteiro.

int n = 12;
int *ptr = &n;

printf("%d\n", *ptr);


Sintaxe: *ptr significa:

acesse o valor armazenado no endereço contido em ptr.

Saída:

12

Modificando uma variável através do ponteiro

ptr e n permitem acessar a mesma região de memória.

int n = 12;
int *ptr = &n;

*ptr = 25;

printf("%d\n", n);
25

Dois caminhos para o mesmo valor

int n = 10;
int *ptr = &n;

n = n + 1;
*ptr = *ptr + 1;

Qual o valor final de n?

12

Ponteiro não inicializado

Este código está errado:

int *ptr;
*ptr = 10;

ptr ainda não contém um endereço válido.

Desreferenciar um ponteiro inválido causa comportamento indefinido.

O ponteiro nulo

Quando um ponteiro não aponta para um objeto válido, podemos usar NULL.

int *ptr = NULL;
char *cptr = NULL;

Mas ainda não podemos fazer:

*ptr = 10; // ERRADO

Para desreferenciar, precisamos ter uma região válida de memória.

Ponteiros e funções

Relembrando: parâmetros são passados por valor

void muda_valor(int x) {
    x = 10;
}

int main() {
    int n = 5;
    muda_valor(n);
    printf("%d\n", n);
    return 0;
}
5

A função recebe uma cópia do valor de n.

Passando um endereço para a função

Também podemos passar um endereço por valor.

void muda_valor(int *ptr) {
    *ptr = 10;
}

int main() {
    int n = 5;
    muda_valor(&n);
    printf("%d\n", n);
    return 0;
}
10

O que aconteceu na chamada?

muda_valor(&n);

A função recebe uma cópia do endereço de n.

void muda_valor(int *ptr) {
    *ptr = 10;
}

*ptr acessa a variável original.

Em C, continuamos passando tudo por valor.

Exemplo clássico: trocando dois valores

void troca(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

Chamada:

int x = 10, y = 20;
troca(&x, &y);

Depois da função: x == 20 e y == 10.

Revisitando scanf

Para escrever em uma variável, o scanf precisa receber seu endereço.

int n;
scanf("%d", &n);

Se já temos um ponteiro válido:

int n;
int *ptr = &n;
scanf("%d", ptr);

Não precisamos de &! Aqui, ptr já é um endereço.

Funções podem produzir vários resultados

Um return devolve apenas um valor.

Ponteiros permitem modificar várias variáveis do chamador:

void divisao(int a, int b, int *q, int *r) {
    *q = a / b;
    *r = a % b;
}
int quociente, resto;
divisao(17, 5, &quociente, &resto);

Alocação dinâmica

Por que alocar memória dinamicamente?

Às vezes o tamanho necessário só é conhecido durante a execução.

int n;
scanf("%d", &n);

Queremos reservar espaço para n inteiros.

Como já discutido, vamos evitar o uso de vetores locais de tamanho variável (VLAs).

Comportamento estranho dependendo do compilador, e técnica não muito geral.

Para criar um vetor com tamanho escolhido em execução, usaremos alocação dinâmica.

Pergunta: podemos retornar um endereço?

int *cria_numero() {
    int n = 507;
    return &n;
}

int main() {
    int *ptr = cria_numero();

    printf("%d\n", *ptr);

    return 0;
}

Qual a saída?

[1]    59710 segmentation fault (core dumped)  ./exemplo

O problema: n deixa de existir

int *cria_numero() {
    int n = 507;   // n é uma variável local
    return &n;
}                  // n deixa de existir aqui
  • n é uma variável local;
  • seu tempo de vida termina quando a função retorna;
    • Tempo de vida: período durante o qual um objeto existe e pode ser acessado validamente
  • o endereço retornado não aponta mais para um objeto válido.

Então como retornar algo criado pela função?

Queremos uma região de memória que:

  • seja criada durante cria_numero;
  • continue válida depois que a função terminar;
  • possa ser liberada quando não for mais necessária.

Para isso usamos alocação dinâmica.

malloc: reservando memória dinâmicamente

int *cria_numero() {
    int *ptr = malloc(sizeof(int));

    if (ptr != NULL)
        *p = 507;

    return p;
}

malloc reserva (se possível) um bloco de memória no heap e retorna o endereço do início do bloco.

Se não tiver memória disponível, malloc retorna NULL

Memória do programa: Stack e heap

Stack (pilha)

  • variáveis locais;
  • parâmetros de funções;
  • gerenciamento automático.

Heap

  • memória solicitada durante a execução;
  • tamanho decidido em tempo de execução;
  • precisamos liberar essa memória.

Stack e heap na memória

Stack e heap trabalhando juntos

int *ptr = malloc(sizeof(int));

if (ptr != NULL)
    *ptr = 6;

free: liberando a memória

Memória alocada com malloc permanece reservada até ser liberada.

int *ptr = malloc(sizeof(int));

if (ptr != NULL) {
    *ptr = 6;
    printf("%d\n", *ptr);
    free(ptr);
}

free(ptr) devolve o bloco ao sistema de gerenciamento de memória.

Depois de free, o bloco não é mais nosso

int *ptr = malloc(sizeof(int));
*ptr = 10;

free(ptr);

Depois disso:

printf("%d\n", *ptr); // ERRADO

O ponteiro ainda contém um endereço, mas aquela memória não pertence mais ao programa.

Erros comuns com memória dinâmica

  • memory leak: esquecer de chamar free;
  • use-after-free: usar a memória depois de free;
  • double free: liberar duas vezes o mesmo bloco;
  • desreferenciar um ponteiro não inicializado ou NULL.

Esses erros podem compilar normalmente e falhar apenas durante a execução.

Quem é responsável por chamar free?

Uma regra útil:

Toda alocação precisa ter um free correspondente.

Precisamos saber quem é responsável pelo bloco alocado.

Essa responsabilidade fica especialmente importante quando começarmos a retornar memória de funções.

Alocando um vetor dinamicamente

int n;
scanf("%d", &n);

int *vetor = malloc(n * sizeof(int));

Se a alocação funcionar, temos espaço para n inteiros consecutivos.

if (vetor == NULL) {
    return 1;
}

Usando o vetor alocado

Depois de alocar, podemos usar a notação usual de vetores:

int *vetor = malloc(n * sizeof(int));

if (vetor == NULL)
    return 1;

for (int i = 0; i < n; i++)
    vetor[i] = i * i;

free(vetor);

Os colchetes continuam funcionando!

Vetor local e vetor dinâmico

Vetor local

int v[100];
  • tamanho fixado aqui;
  • vida ligada ao bloco/função;
  • gerenciamento automático.

Vetor dinâmico

int *v = malloc(n * sizeof(int));
  • tamanho decidido em execução;
  • vive até free;
  • gerenciamento manual.

Ferramentas ajudam a encontrar erros

Erros de memória podem ser difíceis de enxergar no código.

Algumas ferramentas úteis:

  • AddressSanitizer: detecta vários acessos inválidos em execução;
  • Valgrind: ajuda a detectar acessos inválidos e vazamentos.

Exemplo com gcc:

gcc -Wall -Wextra -fsanitize=address -g programa.c

Fechando a aula

O modelo mental que queremos levar

  • uma variável ocupa uma região da memória;
  • um ponteiro armazena um endereço;
  • &x obtém o endereço de x;
  • *ptr acessa o objeto apontado por ptr;
  • ponteiros permitem alterar dados do chamador;
  • malloc cria objetos no heap;
  • free encerra nosso uso daquele bloco.

Exercícios rápidos

  1. O que será impresso?
int x = 4;
int *ptr = &x;
*ptr = *ptr + 3;
printf("%d\n", x);
  1. Por que este código está errado?
int *ptr;
scanf("%d", ptr);
  1. Escreva uma função zera que receba o endereço de um int e altere seu valor para zero.