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.
- Como pedir um valor ao heap com
new inte devolver comdelete. - Como pedir um array de tamanho decidido na execução,
new int[n], e devolver comdelete[]. - 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::vectore 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.
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ósito | Em C++ |
|---|---|
| Alugar um box | new int: reserva espaço no heap e devolve o endereço (um ponteiro). |
| O papel com o número do box | O ponteiro que guarda esse endereço: int* p = new int; |
| Devolver a chave | delete p; |
| Esquecer de devolver: paga para sempre | Vazamento de memória (memory leak). |
| Entrar no box depois de devolver: pode ser de outra pessoa | Usar depois de liberar (use after free). |
| Devolver a mesma chave duas vezes: confusão no balcão | Liberar 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; comnew int()ounew int{}, seria 0. - int* p = …
- O endereço vai para um ponteiro comum. O
intque mora no heap não tem nome: o único jeito de chegar até ele é pelop. - *p
- Lê e muda o valor, exatamente como na aula Ponteiros.
- delete p;
- Devolve o espaço. Depois disso,
pcontinua 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ão | Nome | O que está guardado |
|---|---|---|
Pilha (some no } do main) | p | 0x57b8de7fa020 → aponta para o heap |
Heap (fica até o delete) | (sem nome) | 42, depois 43 |
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
quantidadeinteiros 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[]".
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". Ofreeé a função de baixo nível que odeleteusa; 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. OFimnunca 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 de | Use |
|---|---|---|
| Uma lista de tamanho variável | new int[n] / delete[] | std::vector<int> |
| Um texto | new char[n] | std::string |
| Um objeto no heap, com um dono | new Produto / delete | std::unique_ptr com std::make_unique |
| Um objeto no heap, com vários donos | new 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 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.
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.
$ ./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.
$ 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ódigo | O 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 nullptr | Não faz nada (é seguro). |
std::bad_alloc | Exceção lançada quando o new não consegue memória. |
-g -fsanitize=address | Pega 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:
2. O hábito
Complete a linha que vem logo depois de delete p; para evitar usar o endereço velho:
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
newpede memória ao heap e devolve um ponteiro;deletedevolve. Para arrays:new T[n]edelete[].- 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 acusaDirect 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çap = nullptr;. Melhor ainda: não escrevadelete. - No C++ moderno, use
std::vector,std::stringe ponteiros inteligentes. Próxima aula: Ponteiros Inteligentes.