C++ Memória Dinâmica (new e delete)

Na aula Como a Memória Funciona, você viu que o heap é um depósito onde o programa pede espaço durante a execução, e que o std::vector faz isso por você. Nesta aula você vai pedir e devolver esse espaço com as próprias mãos, usando new e delete. E vai ver, com os relatórios reais de uma ferramenta de diagnóstico, os três erros clássicos que isso permite. O objetivo é entender o que acontece por baixo; no fim, você vai ver por que o C++ moderno quase não usa new direto.

Você vai aprender
  • Como pedir um valor ao heap com new int e devolver com delete.
  • Como pedir um array de tamanho decidido na execução, new int[n], e devolver com delete[].
  • O que é um vazamento de memória, e como o AddressSanitizer o encontra.
  • O que acontece ao usar memória depois de devolver (heap-use-after-free).
  • O que acontece ao devolver duas vezes (double free).
  • Por que, no C++ moderno, você vai preferir std::vector e ponteiros inteligentes.

Os programas foram compilados na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. Para os erros de memória, a aula usa também -g -fsanitize=address, o AddressSanitizer (ASan), que tem aula própria em Sanitizers. Os relatórios dele são longos; aqui aparecem só os trechos importantes, e os cortes estão marcados com …. Os sanitizers rodam no terminal, com o g++ ou o clang++; o editor do guia não os tem.

No editor do guia, estes erros passam calados

O editor roda o programa em WebAssembly, sem sanitizers. Lá, usar memória depois do delete e fazer delete duas vezes não dão erro nenhum: o programa segue e termina com código 0. Isso não quer dizer que o código está certo. É justamente o perigo do comportamento indefinido: o mesmo programa que "funciona" num lugar cai ou corrompe dados em outro. Por isso as quedas e os relatórios desta aula aparecem só nos terminais da Ana, e as missões treinam o jeito certo de usar new e delete.


A ideia: um depósito de aluguel

Pense numa empresa de boxes de aluguel. A Ana chega no balcão e diz: "preciso de um box". O atendente reserva um e entrega um papel com o número do box. Quando ela não precisar mais, devolve a chave no balcão, e o box fica livre para outra pessoa.

No depósitoEm C++
Alugar um boxnew int: reserva espaço no heap e devolve o endereço (um ponteiro).
O papel com o número do boxO ponteiro que guarda esse endereço: int* p = new int;
Devolver a chavedelete p;
Esquecer de devolver: paga para sempreVazamento de memória (memory leak).
Entrar no box depois de devolver: pode ser de outra pessoaUsar depois de liberar (use after free).
Devolver a mesma chave duas vezes: confusão no balcãoLiberar duas vezes (double free).

Na pilha, o compilador aluga e devolve sozinho, a cada { e }. No heap, com new, quem devolve é você. Toda a dificuldade desta aula vem daí.


1. new e delete: um valor

#include <iostream>

int main() {
    int* p = new int(42);        // pede ao heap espaço para um int, com 42 dentro

    std::cout << "p  = " << p << "\n";
    std::cout << "*p = " << *p << "\n";

    *p = *p + 1;
    std::cout << "*p = " << *p << "\n";

    delete p;                    // devolve o espaço ao heap
}
new int(42)
Pede ao heap espaço para um int, guarda 42 nele e devolve o endereço. Sem os parênteses (new int), o valor inicial seria lixo; com new int() ou new int{}, seria 0.
int* p = …
O endereço vai para um ponteiro comum. O int que mora no heap não tem nome: o único jeito de chegar até ele é pelo p.
*p
Lê e muda o valor, exatamente como na aula Ponteiros.
delete p;
Devolve o espaço. Depois disso, p continua guardando o mesmo endereço, mas ele não vale mais: o box foi devolvido.
$ g++ -std=c++23 -Wall -Wextra um.cpp -o um
$ ./um
p  = 0x57b8de7fa020           « um endereço do heap (não começa com 0x7ff…)
*p = 42
*p = 43
RegiãoNomeO que está guardado
Pilha (some no } do main)p0x57b8de7fa020 → aponta para o heap
Heap (fica até o delete)(sem nome)42, depois 43
O ponteiro p mora na pilha; o int que ele aponta mora no heap. São duas vidas diferentes: o p some sozinho, o int só some com delete.

Esta figura é a chave de tudo. Se o p sumir antes do delete, o int do heap fica lá, alugado, sem ninguém que saiba o endereço dele. Isso é o vazamento, que você vai ver na seção 3.

Missão: peça ao heap um int com o valor 2026, guarde o endereço em ano, mostre Ano: 2026 pelo ponteiro e devolva a memória no fim.

#include <iostream>

int main() {
    // int* ano = ...

    // mostre "Ano: " e o valor apontado

    // devolva a memória
}
#include <iostream>

int main() {
    int* ano = new int(2026);
    std::cout << "Ano: " << *ano << "\n";
    delete ano;
}

2. new[] e delete[]: um array de tamanho variável

Um array comum (int notas[5]) precisa ter o tamanho escrito no código. Com new, o tamanho pode vir de uma variável, decidido durante a execução, por exemplo digitado pelo usuário:

#include <iostream>

int main() {
    int quantidade = 0;
    std::cout << "Quantos alunos? ";
    std::cin >> quantidade;

    int* notas = new int[quantidade]{};   // tamanho decidido na execução; {} zera tudo

    for (int i = 0; i < quantidade; ++i) {
        notas[i] = 10 - i;
    }
    for (int i = 0; i < quantidade; ++i) {
        std::cout << "Aluno " << i + 1 << ": " << notas[i] << "\n";
    }

    delete[] notas;              // array: delete com colchetes
}
$ g++ -std=c++23 -Wall -Wextra varios.cpp -o varios
$ ./varios
Quantos alunos? 3               « a Ana digitou 3
Aluno 1: 10
Aluno 2: 9
Aluno 3: 8
new int[quantidade]
Pede espaço para quantidade inteiros colados, e devolve o endereço do primeiro. Daí em diante, notas[i] funciona como num array (aula Ponteiros e Arrays).
{}
Zera todos os itens. Sem ele, cada item começaria com lixo.
delete[] notas;
Devolve o array inteiro. Os colchetes avisam: "isto foi pedido com new[]".
Par certo: new com delete, new[] com delete[]

Misturar os dois é comportamento indefinido. O g++ às vezes percebe e avisa:

int main() {
    int* notas = new int[5]{};
    delete notas;                // deveria ser delete[]
}
$ g++ -std=c++23 -Wall -Wextra mistura.cpp -o mistura
mistura.cpp: In function ‘int main()’:
mistura.cpp:3:12: warning: ‘void operator delete(void*, long unsigned int)’ called on pointer returned from a mismatched allocation function [-Wmismatched-new-delete]
    3 |     delete notas;                // deveria ser delete[]
      |            ^~~~~
mistura.cpp:2:29: note: returned from ‘void* operator new [](long unsigned int)’
    2 |     int* notas = new int[5]{};
      |                             ^

"operator delete chamado num ponteiro que veio de uma função de alocação que não combina", e a nota mostra que ele veio de operator new []. Com o ASan, a execução para com alloc-dealloc-mismatch (operator new [] vs operator delete).


3. Vazamento de memória: esqueceu o delete

A função abaixo é chamada a cada venda da lojinha. Ela pede 400 bytes (100 ints) e esquece de devolver:

#include <iostream>

void registrar_venda() {
    int* itens = new int[100];   // pede 400 bytes
    itens[0] = 1;
    std::cout << "Venda registrada\n";
    // esqueceu o delete[] itens;
}

int main() {
    registrar_venda();
    registrar_venda();
    registrar_venda();
}
$ g++ -std=c++23 -Wall -Wextra vazamento.cpp -o vazamento
$ ./vazamento
Venda registrada
Venda registrada
Venda registrada

Nenhum aviso, nenhum erro, tudo "funcionou". É isso que torna o vazamento traiçoeiro. Mas veja o que aconteceu: a cada chamada, o ponteiro itens (que mora na pilha) morreu no } da função, e os 400 bytes do heap ficaram alugados, sem ninguém que saiba o endereço. Ninguém mais consegue devolvê-los. Com três vendas, são 1200 bytes perdidos; num programa que roda o dia inteiro (um servidor, um jogo), a memória usada só cresce, até a máquina ficar lenta ou o sistema matar o programa.

O AddressSanitizer inclui um detetive de vazamentos, o LeakSanitizer, que confere tudo quando o programa termina:

$ g++ -std=c++23 -Wall -Wextra -g -fsanitize=address vazamento.cpp -o vazamento
$ ./vazamento
Venda registrada
Venda registrada
Venda registrada

=================================================================
==552745==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 400 byte(s) in 1 object(s) allocated from:
    #0 0x7c855252c8cf in operator new[](unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:111
    #1 0x55a5b194521e in registrar_venda() /home/ana/cpp/vazamento.cpp:4
    #2 0x55a5b1945291 in main /home/ana/cpp/vazamento.cpp:12
…                                « mais duas vezes o mesmo, para as linhas 13 e 11

SUMMARY: AddressSanitizer: 1200 byte(s) leaked in 3 allocation(s).
$ echo $?
1
LeakSanitizer: detected memory leaks
"Vazamentos de memória detectados".
Direct leak of 400 byte(s) in 1 object(s)
"Vazamento direto de 400 bytes em 1 objeto": um pedido de 400 bytes que nunca foi devolvido.
allocated from:
"pedido a partir de": a lista de funções que levou até o new, da mais recente (#0) para a mais antiga.
#0 … operator new[]
O próprio new[], dentro da biblioteca. Pule essa linha.
#1 … registrar_venda() /home/ana/cpp/vazamento.cpp:4
A linha que interessa: o new int[100] da linha 4.
#2 … main … vazamento.cpp:12
Quem chamou: o main, na linha 12.
SUMMARY: … 1200 byte(s) leaked in 3 allocation(s)
O resumo: 1200 bytes perdidos em 3 pedidos.
$? = 1
O sanitizer muda o código de saída para 1, para que scripts e testes percebam o problema.

Conserto: delete[] itens; antes do fim da função. Ou, melhor, nenhum new: com std::vector<int> itens(100);, a devolução é automática (seção 7).


4. Usar depois de liberar (use after free)

Depois do delete, o ponteiro continua guardando o endereço antigo. Nada impede o programa de seguir esse endereço, e é aí que mora o perigo:

#include <iostream>

int main() {
    int* saldo = new int(500);
    delete saldo;

    std::cout << "Saldo: " << *saldo << "\n";   // usa depois de devolver!
}
$ g++ -std=c++23 -Wall -Wextra depois.cpp -o depois
$ ./depois
Saldo: 66794023                « nem 500, nem erro: lixo
$ echo $?
0

Compilou sem aviso, rodou sem cair e mostrou um saldo de 66 milhões. O heap já tinha reaproveitado aquelas casas para as suas anotações internas. Num programa de verdade, o box devolvido pode já ter sido alugado para outra parte do programa, e escrever nele estraga dados de outra pessoa. Esse tipo de defeito é uma das falhas de segurança mais exploradas em programas escritos em C e C++.

Com o ASan, o erro é pego na hora, com três endereços de linha: onde foi o uso, onde foi devolvido e onde foi pedido:

$ g++ -std=c++23 -Wall -Wextra -g -fsanitize=address depois.cpp -o depois
$ ./depois
=================================================================
==552906==ERROR: AddressSanitizer: heap-use-after-free on address 0x6d0cf6be0010 at pc 0x65373d1da340 bp 0x7ffed72aabb0 sp 0x7ffed72aaba0
READ of size 4 at 0x6d0cf6be0010 thread T0
    #0 0x65373d1da33f in main /home/ana/cpp/depois.cpp:7
…
0x6d0cf6be0010 is located 0 bytes inside of 4-byte region [0x6d0cf6be0010,0x6d0cf6be0014)
freed by thread T0 here:
    #0 0x70ecf812d89f in operator delete(void*, unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:190
    #1 0x65373d1da2eb in main /home/ana/cpp/depois.cpp:5
…
previously allocated by thread T0 here:
    #0 0x70ecf812c73f in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:109
    #1 0x65373d1da27e in main /home/ana/cpp/depois.cpp:4
…
SUMMARY: AddressSanitizer: heap-use-after-free /home/ana/cpp/depois.cpp:7 in main
…                                « um mapa da memória ("Shadow bytes"), que pode ignorar
==552906==ABORTING
  • heap-use-after-free: "uso de memória do heap depois de liberada".
  • READ of size 4 … depois.cpp:7: uma leitura de 4 bytes (um int) na linha 7.
  • freed by thread T0 here: … depois.cpp:5: a memória foi devolvida na linha 5 (o delete).
  • previously allocated by thread T0 here: … depois.cpp:4: e tinha sido pedida na linha 4 (o new).

Conserto: use o valor antes do delete. E crie o hábito da próxima seção: depois de delete p;, escreva p = nullptr;.


5. delete duas vezes (double free)

#include <iostream>

int main() {
    int* p = new int(7);
    std::cout << "*p = " << *p << "\n";
    delete p;
    delete p;                    // devolve de novo a mesma memória
    std::cout << "Fim\n";
}
$ g++ -std=c++23 -Wall -Wextra duas.cpp -o duas
$ ./duas
*p = 7
free(): double free detected in tcache 2
Abortado                   (imagem do núcleo gravada) ./duas
$ echo $?
134
free(): double free detected in tcache 2
Quem percebeu foi a própria biblioteca do sistema (a glibc), que administra o heap: "free(): liberação dupla detectada". O free é a função de baixo nível que o delete usa; o tcache é uma das listas internas onde ela anota os boxes livres.
Abortado
A biblioteca matou o programa de propósito (o sinal SIGABRT, "abortar"), porque o heap ficou num estado em que não dá para confiar. Em inglês: Aborted (core dumped).
134
128 + 6, o número do SIGABRT. O Fim nunca apareceu.

Desta vez a glibc pegou o erro, mas ela não pega todos os casos (por exemplo, quando outro new acontece entre os dois delete). O ASan pega sempre, e diz as linhas:

$ g++ -std=c++23 -Wall -Wextra -g -fsanitize=address duas.cpp -o duas
$ ./duas
*p = 7
=================================================================
==553324==ERROR: AddressSanitizer: attempting double-free on 0x7132b9fe0010 in thread T0:
    #0 0x7512bb52d89f in operator delete(void*, unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:190
    #1 0x5c1c0438c37a in main /home/ana/cpp/duas.cpp:7
…
freed by thread T0 here:
    #0 0x7512bb52d89f in operator delete(void*, unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:190
    #1 0x5c1c0438c364 in main /home/ana/cpp/duas.cpp:6
…
SUMMARY: AddressSanitizer: double-free /home/ana/cpp/duas.cpp:7 in main
==553324==ABORTING

"Tentativa de liberação dupla" na linha 7; a primeira liberação foi na linha 6.

O hábito: zerar o ponteiro depois do delete

Um delete num nullptr não faz nada, e um ponteiro nulo pode ser testado (aula nullptr). Então, logo depois de devolver, apague o endereço do papel:

#include <iostream>

int main() {
    int* p = new int(7);
    delete p;
    p = nullptr;                 // o papel agora diz "nenhum endereço"

    if (p == nullptr) {
        std::cout << "p não aponta mais para nada\n";
    }
    delete p;                    // delete em nullptr não faz nada: é seguro
    std::cout << "Fim\n";
}
$ g++ -std=c++23 -Wall -Wextra zerar.cpp -o zerar
$ ./zerar
p não aponta mais para nada
Fim

Isso ajuda, mas não resolve tudo: se existir uma cópia do ponteiro (int* q = p;), ela continua com o endereço velho. É por isso que a solução de verdade é não ter delete escrito à mão.


6. E se o heap não tiver espaço?

Se o pedido for grande demais, o new não devolve um ponteiro nulo: ele lança uma exceção chamada std::bad_alloc ("alocação ruim"). Para mostrar sem travar a máquina, a Ana limitou a memória do terminal a cerca de 500 MB com ulimit -v e pediu 4 GB:

#include <iostream>

int main() {
    std::cout << "Pedindo 4 GB...\n";
    int* p = new int[1'000'000'000];
    std::cout << "Consegui!\n";
    delete[] p;
}
$ g++ -std=c++23 -Wall -Wextra gigante.cpp -o gigante
$ ulimit -v 500000              « limita a memória deste terminal (em KB)
$ ./gigante
Pedindo 4 GB...
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
Abortado                   (imagem do núcleo gravada) ./gigante
$ echo $?
134

"Encerrado depois de lançar uma std::bad_alloc": ninguém tratou a exceção, então o programa abortou. Num programa comum, você raramente vai tratar esse caso; o importante é saber que new nunca devolve nullptr por falta de memória (salvo com a forma especial new (std::nothrow)). O ulimit -v vale até você fechar o terminal.


7. Por que o C++ moderno quase não usa new

Você viu três erros graves, e nenhum deles foi pego pelo compilador. Agora imagine um programa com centenas de new, funções com vários return no meio, exceções que pulam o delete... Garantir que cada new tenha exatamente um delete, no momento certo, é muito difícil. Por isso a regra do C++ moderno é: quem pede a memória é um objeto que sabe devolvê-la sozinho.

O varios.cpp da seção 2, sem nenhum new:

#include <iostream>
#include <vector>

int main() {
    int quantidade = 0;
    std::cout << "Quantos alunos? ";
    std::cin >> quantidade;

    std::vector<int> notas(quantidade);   // o vector pede e devolve sozinho

    for (int i = 0; i < quantidade; ++i) {
        notas[i] = 10 - i;
    }
    for (int i = 0; i < quantidade; ++i) {
        std::cout << "Aluno " << i + 1 << ": " << notas[i] << "\n";
    }
}                                         // sem delete: nada para esquecer
$ g++ -std=c++23 -Wall -Wextra vetor.cpp -o vetor
$ ./vetor
Quantos alunos? 4
Aluno 1: 10
Aluno 2: 9
Aluno 3: 8
Aluno 4: 7

O std::vector faz o new[] por dentro e o delete[] no } em que ele morre, sempre, mesmo que a função saia por um return no meio ou por uma exceção. Essa técnica de "o objeto devolve no fim da vida dele" se chama RAII e tem aula própria: Destrutores e RAII.

Você precisa de…Em vez deUse
Uma lista de tamanho variávelnew int[n] / delete[]std::vector<int>
Um textonew char[n]std::string
Um objeto no heap, com um dononew Produto / deletestd::unique_ptr com std::make_unique
Um objeto no heap, com vários donosnew e contar "na mão"std::shared_ptr com std::make_shared

Então por que aprender new e delete? Porque eles estão por baixo de tudo isso, porque você vai encontrá-los em código antigo e porque entender os três erros desta aula é o que faz os ponteiros inteligentes (próxima aula) fazerem sentido.


Quando dá errado

O programa usa cada vez mais memória (vazamento)

O que aconteceu: algum new não tem o delete correspondente, muitas vezes porque a função saiu antes (um return no meio, uma exceção) ou porque o ponteiro foi sobrescrito.

Como resolver: compile com -g -fsanitize=address, rode até o fim e procure as linhas Direct leak of … allocated from:; a primeira linha com um arquivo seu (/home/ana/…) é o new culpado. Depois troque esse new por std::vector ou std::make_unique.

Valores absurdos depois de um delete (heap-use-after-free)
$ ./depois
Saldo: 66794023

O que aconteceu: o programa leu memória que já tinha sido devolvida. Sem sanitizer, o sintoma é lixo, ou uma queda bem longe do erro de verdade.

Como resolver: o ASan mostra três linhas: o uso (READ/WRITE), o freed by e o previously allocated by. Mova o uso para antes do delete e zere o ponteiro (p = nullptr;). Melhor: use um ponteiro inteligente.

"free(): double free detected" e "Abortado" (134)
$ ./duas
*p = 7
free(): double free detected in tcache 2
Abortado                   (imagem do núcleo gravada) ./duas

O que aconteceu: o mesmo endereço foi devolvido duas vezes, às vezes por duas cópias do mesmo ponteiro.

Como resolver: cada new tem um dono, responsável pelo único delete. É exatamente o que o std::unique_ptr garante.

"called on pointer returned from a mismatched allocation function"
$ g++ -std=c++23 -Wall -Wextra mistura.cpp -o mistura
mistura.cpp: In function ‘int main()’:
mistura.cpp:3:12: warning: ‘void operator delete(void*, long unsigned int)’ called on pointer returned from a mismatched allocation function [-Wmismatched-new-delete]
    3 |     delete notas;                // deveria ser delete[]
      |            ^~~~~
mistura.cpp:2:29: note: returned from ‘void* operator new [](long unsigned int)’
    2 |     int* notas = new int[5]{};
      |                             ^

O que aconteceu: memória pedida com new[] foi devolvida com delete (sem colchetes), ou o contrário.

Como resolver: new ↔ delete, new[] ↔ delete[]. Ou std::vector, que não tem como errar.


Referência rápida

CódigoO que faz
int* p = new int;Pede um int ao heap (valor inicial: lixo).
int* p = new int(42); / new int{}Pede um int com 42 / com 0.
delete p;Devolve o que foi pedido com new.
int* v = new int[n]{};Pede n inteiros zerados (n pode ser uma variável).
delete[] v;Devolve o que foi pedido com new[].
p = nullptr;Depois do delete: evita usar ou devolver de novo por engano.
delete nullptrNão faz nada (é seguro).
std::bad_allocExceção lançada quando o new não consegue memória.
-g -fsanitize=addressPega vazamentos, heap-use-after-free, double-free e alloc-dealloc-mismatch.

Exercícios

1. O par certo

A memória foi pedida com double* precos = new double[10];. Complete a devolução:

precos;

2. O hábito

Complete a linha que vem logo depois de delete p; para evitar usar o endereço velho:

p = ;

3. Uma função faz int* v = new int[100]; e termina sem delete[]. O que acontece?

4. O ASan mostrou heap-use-after-free. O que o programa fez?

5. No C++ moderno, o que usar no lugar de new int[n]?

Missão final: o carrinho no heap

Missão: usando new double[quantidade], crie um array precos no heap, guarde 4.5, 12.0 e 3.0, some os três em total e, no fim, devolva o array com o delete certo. A saída deve ser Total: 19.5. (Depois, como exercício extra, reescreva com std::vector<double> e repare que o delete[] desaparece.)

#include <iostream>

int main() {
    int quantidade = 3;
    // peça ao heap um array de 3 double chamado precos

    // guarde 4.5, 12.0 e 3.0

    double total = 0;
    // some os preços em total

    std::cout << "Total: " << total << "\n";

    // devolva o array
}
#include <iostream>

int main() {
    int quantidade = 3;
    double* precos = new double[quantidade];

    precos[0] = 4.5;
    precos[1] = 12.0;
    precos[2] = 3.0;

    double total = 0;
    for (int i = 0; i < quantidade; ++i) {
        total = total + precos[i];
    }
    std::cout << "Total: " << total << "\n";

    delete[] precos;
}

Resumo

  • new pede memória ao heap e devolve um ponteiro; delete devolve. Para arrays: new T[n] e delete[].
  • O ponteiro mora na pilha e some sozinho; a memória do heap só some com delete.
  • Vazamento: esqueceu o delete. Nada aparece; o LeakSanitizer acusa Direct leak of … bytes.
  • Usar depois de liberar: mostra lixo ou corrompe dados; o ASan acusa heap-use-after-free.
  • Liberar duas vezes: free(): double free detected, Abortado, código 134.
  • Depois de delete p;, faça p = nullptr;. Melhor ainda: não escreva delete.
  • No C++ moderno, use std::vector, std::string e ponteiros inteligentes. Próxima aula: Ponteiros Inteligentes.