Por que você pediria memória em tempo de execução
Até agora, todas as variáveis que você criou viveram na pilha (stack): seu tamanho é conhecido em tempo de compilação e elas são destruídas automaticamente quando seu escopo termina. Isso é rápido e seguro, mas não dá conta do caso em que você não sabe quanta memória vai precisar até o programa estar em execução: um buffer dimensionado pela entrada do usuário, uma estrutura que precisa sobreviver à função que a criou, ou um grafo cujo formato não é conhecido de antemão.
Para esses casos, C++ permite que você aloque da heap (também chamada de free store) com new, e a devolva com delete. A expressão new reserva um bloco, executa o construtor e retorna um ponteiro para ele, construindo diretamente sobre os ponteiros que você viu na página anterior.
A variável p em si vive na pilha: é apenas um ponteiro. O int para o qual ela aponta vive na heap e permanece vivo até você liberá-lo com delete, não importa quantos escopos venham e vão.
Pilha vs heap
Essa distinção é a própria razão de new existir, então vale a pena torná-la concreta.
void demo() {
int a = 10; // na pilha - some quando demo() retorna
int* b = new int(10); // 'b' na pilha, o int para o qual aponta na heap
} // 'a' destruído; o int da heap VAZA - nunca é liberado
Diferenças principais:
- Pilha - tempo de vida automático, muito rápida, tamanho limitado (geralmente alguns MB), liberada por você quando o escopo termina.
- Heap - tempo de vida manual, um pouco mais lenta, grande, e liberada somente quando você chama
delete.
A troca é flexibilidade por responsabilidade: a memória da heap vive exatamente o tempo que você quiser, mas você se torna o responsável por lembrar de liberá-la.
Alocando arrays com new[]
Quando você precisa de um bloco cujo tamanho é decidido em tempo de execução, use a forma de array new T[n]. Ela retorna um ponteiro para o primeiro elemento, e você o libera com o delete[] correspondente.
A regra é rígida e fácil de errar: a memória de new é liberada com delete, e a memória de new[] é liberada com delete[]. Misturá-las (delete arr em algo alocado com new[]) é comportamento indefinido, mesmo que pareça funcionar na sua máquina.
Os três bugs clássicos
O gerenciamento manual de memória tem um pequeno conjunto de erros que respondem pela maioria dos bugs de heap. Aprenda a reconhecer os três.
1. Vazamento de memória - você nunca chama delete. O bloco fica reservado para sempre. Inofensivo uma vez, fatal dentro de um laço.
void leaky() {
int* p = new int(5);
// ... sem delete ...
} // p some; o int da heap agora é inalcançável E não liberado
2. Ponteiro pendurado - você usa a memória depois de liberá-la. O ponteiro ainda guarda o endereço antigo, mas aquela memória não é mais sua.
3. Double-free - você libera o mesmo bloco duas vezes. Isso corrompe a contabilidade interna da heap e geralmente causa um travamento.
int* p = new int(1);
delete p;
delete p; // double-free - comportamento indefinido, frequentemente um travamento
Definir um ponteiro como nullptr depois de liberá-lo neutraliza tanto o uso pendurado quanto o double-free: desreferenciar nullptr falha imediatamente (fácil de depurar), e delete nullptr é explicitamente uma operação segura que não faz nada.
Um ciclo realista de alocar-usar-liberar
Juntando tudo, esta é a forma de um gerenciamento manual correto: alocar, usar, liberar exatamente uma vez e não tocar no ponteiro depois.
Note que delete u faz duas coisas para um tipo de classe: primeiro executa o destrutor do objeto e depois libera a memória bruta. Essa ordem importa assim que seus objetos passam a possuir recursos próprios.
Uma pegadinha sutil: se uma exceção for lançada entre new e delete, o delete nunca é executado e você causa um vazamento. Envolver cada alocação em try/catch para lidar com isso é tedioso e propenso a erros, que é exatamente o problema que a próxima página resolve.
Próximo: Ponteiros inteligentes
Você já viu o custo completo de gerenciar a memória manualmente: cada new é uma promessa de delete depois, e uma única liberação esquecida, duplicada ou prematura é comportamento indefinido. O C++ moderno quase nunca faz essa promessa manualmente. A próxima página apresenta os ponteiros inteligentes - std::unique_ptr e std::shared_ptr - objetos que possuem uma alocação da heap e chamam delete por você automaticamente quando saem de escopo, transformando os três bugs clássicos em coisas que o compilador e o RAII resolvem por você.
Perguntas frequentes
Qual é a diferença entre new e delete em C++?
new aloca memória na heap em tempo de execução e retorna um ponteiro para ela; delete libera a memória que foi alocada com new. Cada new deve ser pareado com exatamente um delete, ou você causa um vazamento de memória. Para arrays, use new[] com delete[].
O que acontece se você esquecer de chamar delete em C++?
Você tem um vazamento de memória: o bloco da heap permanece reservado durante toda a vida do programa, mesmo que nada mais aponte para ele. Um único vazamento costuma ser inofensivo, mas vazamentos dentro de um laço ou em um serviço de longa duração crescem até o programa ficar sem memória e travar.
Devo usar new e delete diretamente no C++ moderno?
Raramente. Prefira contêineres como std::vector ou ponteiros inteligentes (std::unique_ptr, std::shared_ptr) que liberam a memória automaticamente. Vale a pena entender new/delete puros porque os ponteiros inteligentes os encapsulam, mas no código do dia a dia eles são uma fonte de vazamentos e ponteiros pendurados.