Menu

Ponteiros inteligentes em C++: unique_ptr e shared_ptr explicados

Ponteiros inteligentes são donos da memória do heap e a liberam automaticamente. Aprenda unique_ptr, shared_ptr, make_unique e make_shared, e por que você quase nunca deve escrever new/delete de novo.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

O problema que os ponteiros inteligentes resolvem

Na página anterior você alocou memória com new e a liberou com delete. Isso funciona, mas joga o fardo em cima de você: cada new precisa de um delete correspondente, em cada caminho do código, inclusive naqueles em que uma exceção é lançada no meio do percurso. Esqueça um e você vaza memória; execute delete duas vezes e você corrompe o heap.

Os ponteiros inteligentes resolvem isso amarrando o tempo de vida da memória do heap a um objeto normal na pilha. Quando esse objeto sai de escopo, seu destrutor executa o delete por você, garantido, mesmo que uma exceção desempilhe a pilha. Essa ideia se chama RAII (Resource Acquisition Is Initialization), e os ponteiros inteligentes ficam no cabeçalho <memory>.

Você usa *p e p->member exatamente como faria com um ponteiro bruto. A diferença é que você nunca chama delete: o ponteiro inteligente faz isso.

unique_ptr: um dono, sem compartilhamento

unique_ptr é o ponteiro inteligente que você deve escolher por padrão. Ele representa posse exclusiva: exatamente um unique_ptr é dono do objeto a cada momento, e quando esse ponteiro morre, o objeto morre junto. Ele tem zero sobrecarga em tempo de execução comparado a um ponteiro bruto.

Crie um com make_unique (C++14). Ele recebe os argumentos do construtor e te entrega um ponteiro pronto para uso:

Como só pode haver um dono, um unique_ptr não pode ser copiado. Tentar copiá-lo é um erro de compilação, e esse erro é a linguagem te protegendo de dois donos tentando fazer delete do mesmo objeto:

auto a = make_unique<int>(10);
auto b = a;   // error: call to deleted copy constructor of unique_ptr

Para entregar a posse a outra pessoa, você a move com std::move. Depois do move, o ponteiro original fica vazio (contém nullptr):

Esse é o modelo que você vai querer na maior parte do tempo: sempre há exatamente um dono claro, e o compilador faz cumprir isso.

shared_ptr: posse compartilhada por contagem de referências

Às vezes várias partes do seu programa realmente precisam compartilhar o mesmo objeto, e nenhuma delas sabe qual vai terminar por último. É para isso que serve o shared_ptr. Ele mantém uma contagem de referências: cada cópia aumenta a contagem, cada destruição a diminui, e o objeto só é liberado quando a contagem chega a zero.

Crie-os com make_shared:

Diferente do unique_ptr, copiar um shared_ptr está tudo bem: esse é justamente o propósito. O custo é o preço: a contagem de referências fica no heap e é atualizada de forma atômica (segura entre threads), então o shared_ptr é mais pesado que o unique_ptr. Recorra a ele só quando a posse for de fato compartilhada, e não apenas para evitar pensar em quem é dono do quê.

make_shared também é mais eficiente que shared_ptr<T>(new T(...)): ele aloca o objeto e o bloco de controle em uma única alocação em vez de duas.

weak_ptr e como quebrar ciclos de referência

shared_ptr tem uma armadilha clássica: se dois objetos mantêm shared_ptr um para o outro, suas contagens de referências nunca chegam a zero, então nenhum dos dois é liberado: um vazamento de memória mesmo tendo usado ponteiros inteligentes.

struct Node {
    shared_ptr<Node> next;   // se dois nós apontam um para o outro,
};                           // eles se mantêm vivos para sempre

A solução é o weak_ptr: um observador não-dono de um shared_ptr. Ele não aumenta a contagem de referências, então nunca mantém um objeto vivo. Para usar o objeto, você chama .lock(), que te dá um shared_ptr se o objeto ainda existir, ou um vazio se já tiver sumido.

Use weak_ptr para "ponteiros de volta" e caches: qualquer lugar onde você queira referenciar um objeto sem reivindicar a posse dele.

Erros comuns e armadilhas

Os ponteiros inteligentes removem a maioria dos bugs de memória, mas algumas armadilhas permanecem:

Não misture posse inteligente e bruta da mesma memória. Nunca construa dois ponteiros inteligentes a partir do mesmo ponteiro bruto: cada um vai tentar fazer delete dele:

int* raw = new int(5);
unique_ptr<int> a(raw);
unique_ptr<int> b(raw);   // desastre: ambos vão deletar o mesmo int (double free)

É exatamente por isso que você prefere make_unique/make_shared: não há um ponteiro bruto solto para usar errado.

Um unique_ptr é apenas movível, então passe-o por valor para transferir a posse. Se uma função deve usar mas não possuir o objeto, receba uma referência simples ou um T* bruto: um ponteiro bruto que apenas observa está perfeitamente bem:

void consume(unique_ptr<int> p);   // toma a posse (mover para dentro)
void observe(int* p);              // só olha, não possui nada

Não recorra ao shared_ptr por padrão. É tentador porque ele se copia livremente, mas a contagem atômica de referências custa desempenho real, e a posse compartilhada torna os tempos de vida mais difíceis de raciocinar. Use unique_ptr por padrão; passe para shared_ptr só quando você realmente precisar de vários donos.

unique_ptr para arrays precisa da forma de array. make_unique<int[]>(n) te dá um unique_ptr<int[]> que chama delete[] corretamente. Na prática, prefira std::vector para arrays dinâmicos: ele gerencia a memória por você e ainda te dá controle do tamanho.

Próximo: Strings

Agora você tem o gerenciamento de memória sob controle: os ponteiros inteligentes te dão alocação no heap sem os vazamentos. Uma das coisas mais comuns que você vai alocar e passar adiante é texto, e o C++ te dá uma ferramenta muito mais segura que buffers brutos char*. A próxima página cobre std::string: como ela cresce sozinha, as operações que você vai usar todos os dias e por que ela te libera completamente do trabalho manual com memória.

Perguntas frequentes

O que são ponteiros inteligentes em C++?

Ponteiros inteligentes são objetos do cabeçalho <memory> (unique_ptr, shared_ptr, weak_ptr) que envolvem um ponteiro bruto e fazem delete da memória automaticamente quando saem de escopo. Eles dão alocação no heap sem o delete manual nem os vazamentos que vêm de esquecê-lo.

Qual é a diferença entre unique_ptr e shared_ptr?

unique_ptr é o único dono do seu objeto: não pode ser copiado, apenas movido, e libera a memória no momento em que morre. shared_ptr permite posse compartilhada por meio de uma contagem de referências: vários shared_ptr podem apontar para o mesmo objeto, e ele só é liberado quando o último é destruído. Prefira unique_ptr a menos que você realmente precise de posse compartilhada.

Devo usar make_unique ou new no C++ moderno?

Use make_unique e make_shared. Eles alocam e envolvem o objeto em um único passo, então não existe um new bruto cujo resultado possa vazar antes de chegar a um ponteiro inteligente. Como regra geral, uma base de código moderna em C++ deveria ter quase nenhum new ou delete solto.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR