C++ Ponteiros Inteligentes

Na aula new e delete, você viu os três erros clássicos de quem cuida do heap à mão: esquecer de devolver, usar depois de devolver e devolver duas vezes. Os ponteiros inteligentes C++11 acabam com os três. São objetos que guardam um ponteiro e fazem o delete sozinhos, na hora certa. Esta é a última aula do grupo Memória e a mais importante para o dia a dia: a regra do C++ moderno é usar ponteiros inteligentes em vez de new e delete.

Você vai aprender
  • std::unique_ptr e std::make_unique: um único dono.
  • A prova, com um destrutor que imprime, de que a memória é liberada sozinha.
  • Por que um unique_ptr não pode ser copiado (o erro real) e como passar a posse com std::move.
  • std::shared_ptr e std::make_shared: vários donos, contados por use_count.
  • A ideia do std::weak_ptr: olhar sem ser dono.
  • Qual usar em cada situação.

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. Tudo desta aula está no cabeçalho <memory>.


A ideia: a chave com dono

Volte ao depósito de boxes da aula anterior. O problema era que a chave (o ponteiro) passava de mão em mão e ninguém sabia quem tinha de devolvê-la. Os ponteiros inteligentes resolvem isso com uma regra de posse:

SituaçãoPonteiro inteligenteQuando o box é devolvido
A chave tem um único dono. Ele pode entregá-la a outra pessoa, mas não pode tirar cópia.std::unique_ptrQuando o dono deixa de existir.
A chave tem várias cópias, e o balcão anota quantas existem.std::shared_ptrQuando a última cópia deixa de existir.
Alguém só tem o número do box anotado, sem chave.std::weak_ptrNão decide nada: só pode perguntar se o box ainda está alugado.

1. std::unique_ptr: um dono só

#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> p = std::make_unique<int>(42);

    std::cout << "*p = " << *p << "\n";
    *p = *p + 1;
    std::cout << "*p = " << *p << "\n";
}   // sem delete: o unique_ptr devolve a memória aqui
#include <memory>
O cabeçalho dos ponteiros inteligentes.
std::unique_ptr<int>
"Ponteiro único para int". Entre < > vai o tipo do que ele aponta, como em std::vector<int>.
std::make_unique<int>(42)
Faz o new int(42) por você e já entrega o resultado dentro de um unique_ptr. Os parênteses levam o valor inicial. C++14
*p
Usa-se como um ponteiro comum: *p para o valor e p->campo para structs.
}
Quando o p morre, ele faz o delete sozinho. Não existe delete no código, então não existe como esquecer nem como repetir.
$ g++ -std=c++23 -Wall -Wextra unique.cpp -o unique
$ ./unique
*p = 42
*p = 43

Na prática, quase sempre se escreve auto p = std::make_unique<int>(42);. O tipo já aparece no lado direito, então o auto evita repetir.

A prova: um destrutor que imprime

Como saber que a memória foi mesmo devolvida? Uma struct pode ter um destrutor: uma função especial, escrita ~Nome(), que roda sozinha no instante em que o objeto é destruído. Aqui ela só imprime uma mensagem. (Destrutores têm aula própria, Destrutores e RAII; o mesmo vale para o construtor Produto(std::string n), que roda quando o objeto nasce. Por enquanto, basta saber que as duas mensagens marcam o nascimento e a morte do objeto.)

#include <iostream>
#include <memory>
#include <string>

struct Produto {
    std::string nome;

    Produto(std::string n) : nome(n) {
        std::cout << "  [" << nome << " criado]\n";
    }
    ~Produto() {
        std::cout << "  [" << nome << " destruído]\n";
    }
};

int main() {
    std::cout << "Início do main\n";
    {
        auto caneta = std::make_unique<Produto>("caneta");
        std::cout << "Usando: " << caneta->nome << "\n";
        std::cout << "Fim do bloco\n";
    }
    std::cout << "Depois do bloco\n";
}
$ g++ -std=c++23 -Wall -Wextra destrutor.cpp -o destrutor
$ ./destrutor
Início do main
  [caneta criado]               « o make_unique criou o Produto no heap
Usando: caneta
Fim do bloco
  [caneta destruído]            « no }, o unique_ptr fez o delete sozinho
Depois do bloco

Agora o mesmo programa com um ponteiro comum e new, esquecendo o delete:

#include <iostream>
#include <string>

struct Produto {
    std::string nome;

    Produto(std::string n) : nome(n) {
        std::cout << "  [" << nome << " criado]\n";
    }
    ~Produto() {
        std::cout << "  [" << nome << " destruído]\n";
    }
};

int main() {
    std::cout << "Início do main\n";
    {
        Produto* caneta = new Produto("caneta");
        std::cout << "Usando: " << caneta->nome << "\n";
        std::cout << "Fim do bloco\n";
    }   // esqueceu o delete caneta;
    std::cout << "Depois do bloco\n";
}
$ g++ -std=c++23 -Wall -Wextra cru.cpp -o cru
$ ./cru
Início do main
  [caneta criado]
Usando: caneta
Fim do bloco
Depois do bloco                 « o "[caneta destruído]" nunca apareceu: vazou

O ponteiro cru morreu no }, mas o Produto do heap ficou lá, sem dono, até o fim do programa. Com o unique_ptr, a morte do ponteiro leva junto o objeto. É essa a "inteligência".

Missão: reescreva o programa sem new e sem delete: troque o ponteiro cru por auto pontos = std::make_unique<int>(100); (não esqueça o #include <memory>) e apague a linha do delete. A saída continua Pontos: 150.

#include <iostream>

int main() {
    int* pontos = new int(100);
    *pontos = *pontos + 50;
    std::cout << "Pontos: " << *pontos << "\n";
    delete pontos;
}
#include <iostream>
#include <memory>

int main() {
    auto pontos = std::make_unique<int>(100);
    *pontos = *pontos + 50;
    std::cout << "Pontos: " << *pontos << "\n";
}

2. O unique_ptr não copia

Se fosse possível copiar um unique_ptr, haveria dois donos, e os dois fariam delete no fim: o "devolver duas vezes" da aula anterior. Por isso a cópia é proibida, e o compilador barra:

#include <memory>

int main() {
    auto a = std::make_unique<int>(10);
    auto b = a;      // tentar copiar
}
$ g++ -std=c++23 -Wall -Wextra copia.cpp -o copia
copia.cpp: In function ‘int main()’:
copia.cpp:5:14: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = int; _Dp = std::default_delete<int>]’
    5 |     auto b = a;      // tentar copiar
      |              ^
In file included from /usr/include/c++/15/memory:80,
                 from copia.cpp:1:
/usr/include/c++/15/bits/unique_ptr.h:524:7: note: declared here
  524 |       unique_ptr(const unique_ptr&) = delete;
      |       ^~~~~~~~~~
copia.cpp:5:14: note: use ‘-fdiagnostics-all-candidates’ to display considered candidates
    5 |     auto b = a;      // tentar copiar
      |              ^

A mensagem é comprida, mas tem só uma ideia. Leia por partes:

  • use of deleted function = "uso de uma função apagada". Em C++, uma classe pode marcar uma operação como proibida com = delete.
  • ‘std::unique_ptr<…>::unique_ptr(const std::unique_ptr<…>&)’ = o construtor de cópia: a função que criaria b como cópia de a. Os _Tp e _Dp são nomes internos; o [with _Tp = int …] diz que aqui eles valem int.
  • note: declared here … unique_ptr(const unique_ptr&) = delete; = a linha da biblioteca padrão onde a cópia foi proibida de propósito.
  • A última nota só sugere uma opção para ver mais detalhes; pode ignorar.

No editor do guia (Clang), a mensagem começa com call to deleted constructor ou call to implicitly-deleted copy constructor: a mesma ideia.

Passar a posse: std::move

Copiar não pode, mas entregar pode. O std::move (do cabeçalho <utility>) diz: "pode levar, eu não preciso mais". A chave muda de mão, e quem entregou fica de mãos vazias (nullptr):

#include <iostream>
#include <memory>
#include <utility>

int main() {
    auto a = std::make_unique<int>(10);
    std::cout << "a aponta para " << *a << "\n";

    auto b = std::move(a);       // passa a posse de a para b

    if (a == nullptr) {
        std::cout << "a agora está vazio\n";
    }
    std::cout << "b aponta para " << *b << "\n";
}
$ g++ -std=c++23 -Wall -Wextra mover.cpp -o mover
$ ./mover
a aponta para 10
a agora está vazio
b aponta para 10
MomentoabO int 10 no heap
Depois do make_uniquedono(não existe)1 dono: a
Depois do std::move(a)nullptrdono1 dono: b
No } do mainos dois morrem; só o b tinha algo a devolverdevolvido uma única vez
O objeto no heap nunca muda de lugar e nunca é copiado: só a posse passa de a para b.

O std::move em si não move nada; ele só dá permissão para mover. O mecanismo completo é a Semântica de Movimento, no Nível 3.

unique_ptr e funções

Uma regra prática para funções:

#include <iostream>
#include <memory>
#include <string>

struct Produto {
    std::string nome;
    double preco;
};

void mostrar(const Produto& p) {              // só usa: recebe referência
    std::cout << p.nome << ": R$ " << p.preco << "\n";
}

void guardar(std::unique_ptr<Produto> p) {     // fica com a posse
    std::cout << "Guardei " << p->nome << "\n";
}   // p morre aqui e o Produto é destruído

int main() {
    auto caderno = std::make_unique<Produto>(Produto{"caderno", 15.90});
    mostrar(*caderno);
    guardar(std::move(caderno));
    std::cout << "caderno está vazio? " << (caderno == nullptr ? "sim" : "não") << "\n";
}
$ g++ -std=c++23 -Wall -Wextra dono-funcao.cpp -o dono-funcao
$ ./dono-funcao
caderno: R$ 15.9
Guardei caderno
caderno está vazio? sim
  • A função só usa o objeto? Receba const Produto& (ou Produto& para mudar) e chame com *caderno. Ela nem fica sabendo que existe um ponteiro inteligente.
  • A função vai ficar com o objeto (guardar numa lista, por exemplo)? Receba std::unique_ptr<Produto> por valor e chame com std::move. Quem chama vê, no próprio código, que entregou a posse.

3. std::shared_ptr: vários donos

Às vezes um objeto precisa mesmo de vários donos, e não dá para saber qual deles vai terminar por último. Pense numa playlist compartilhada: a Ana criou, o Bruno e a Carla também seguem. A playlist só deve ser apagada quando ninguém mais a seguir. O std::shared_ptr faz isso contando os donos: cada cópia soma 1, cada cópia que morre subtrai 1, e quando a conta chega a zero, ele faz o delete.

#include <iostream>
#include <memory>
#include <string>

struct Playlist {
    std::string nome;
    Playlist(std::string n) : nome(n) { std::cout << "  [playlist " << nome << " criada]\n"; }
    ~Playlist() { std::cout << "  [playlist " << nome << " destruída]\n"; }
};

int main() {
    auto da_ana = std::make_shared<Playlist>("Viagem");
    std::cout << "donos: " << da_ana.use_count() << "\n";

    auto do_bruno = da_ana;                 // copiar PODE: mais um dono
    std::cout << "donos: " << da_ana.use_count() << "\n";

    {
        auto da_carla = da_ana;             // mais um, só dentro do bloco
        std::cout << "donos: " << da_ana.use_count() << "\n";
    }
    std::cout << "donos depois do bloco: " << da_ana.use_count() << "\n";

    da_ana.reset();                         // a Ana larga a playlist
    std::cout << "donos: " << do_bruno.use_count() << " (só o Bruno)\n";

    std::cout << "Fim do main\n";
}
$ g++ -std=c++23 -Wall -Wextra shared.cpp -o shared
$ ./shared
  [playlist Viagem criada]
donos: 1
donos: 2                        « do_bruno é uma cópia: mais um dono
donos: 3                        « da_carla, dentro do bloco
donos depois do bloco: 2        « a da_carla morreu no }
donos: 1 (só o Bruno)           « reset(): a Ana largou
Fim do main
  [playlist Viagem destruída]   « o último dono (do_bruno) morreu: delete
std::make_shared<Playlist>("Viagem")
Cria a Playlist no heap, junto com o "caderninho" onde a contagem fica anotada, e devolve um std::shared_ptr<Playlist>.
auto do_bruno = da_ana;
Copiar um shared_ptr é permitido: a cópia aponta para o mesmo objeto e soma 1 na contagem. A playlist não é copiada.
.use_count()
"Contagem de uso": quantos shared_ptr apontam para o objeto agora. Serve para estudar e depurar; programas raramente precisam dele.
.reset()
Este shared_ptr larga o objeto (fica vazio) e a contagem cai 1.

Repare que a playlist foi destruída depois do "Fim do main", quando o último dono, do_bruno, morreu no } final. Ninguém escreveu delete, e ela foi destruída exatamente uma vez.

Prefira unique_ptr

O shared_ptr é mais pesado: precisa manter a contagem (e de um jeito seguro para threads) e torna mais difícil saber quando o objeto morre. Comece sempre com unique_ptr; só troque por shared_ptr quando houver de verdade vários donos sem uma ordem clara. Um unique_ptr pode virar shared_ptr depois (std::shared_ptr<T> s = std::move(u);), o contrário não.


4. std::weak_ptr: olhar sem ser dono (só a ideia)

O std::weak_ptr ("ponteiro fraco") aponta para um objeto de um shared_ptr sem entrar na contagem. Ele não mantém o objeto vivo; só pode perguntar "ainda existe?" e, se existir, pegar emprestado por um instante:

#include <iostream>
#include <memory>

int main() {
    auto dono = std::make_shared<int>(7);
    std::weak_ptr<int> observador = dono;     // olha, mas não é dono

    std::cout << "donos: " << dono.use_count() << "\n";

    if (auto p = observador.lock()) {         // ainda existe? pega emprestado
        std::cout << "ainda existe: " << *p << "\n";
    }

    dono.reset();                             // o único dono larga

    if (observador.expired()) {
        std::cout << "o valor já foi destruído\n";
    }
}
$ g++ -std=c++23 -Wall -Wextra weak.cpp -o weak
$ ./weak
donos: 1                        « o weak_ptr não contou
ainda existe: 7
o valor já foi destruído
  • .lock() devolve um shared_ptr temporário: vazio se o objeto já morreu, ou um dono a mais enquanto você o usa.
  • .expired() ("expirou?") responde se o objeto já foi destruído.

Para que serve? O caso clássico são dois objetos que apontam um para o outro com shared_ptr (um ciclo): cada um mantém o outro vivo, a contagem nunca chega a zero, e os dois vazam. Trocando um dos lados por weak_ptr, o ciclo se quebra. Você vai precisar disso raramente; por enquanto, basta saber que ele existe.


Qual usar

SituaçãoUse
Uma lista de coisasstd::vector (nem precisa de ponteiro)
Um objeto que pode ficar na pilhaUma variável comum: Produto p{…};
Um objeto no heap com um donoauto p = std::make_unique<T>(…);
Um objeto no heap com vários donos, sem ordem claraauto p = std::make_shared<T>(…);
Observar um objeto de shared_ptr sem mantê-lo vivostd::weak_ptr<T>
Uma função que só usa o objetoconst T& / T& (ou T* se puder ser "nada")
new e delete escritos à mãoQuase nunca. Só dentro de classes que você mesmo cria para gerenciar memória (aula Destrutores e RAII).

Um ponteiro cru (T*) continua tendo lugar no C++ moderno, mas com um papel limitado: apontar sem ser dono. Quem tem um T* nunca faz delete nele. O .get() de um ponteiro inteligente devolve esse ponteiro cru, quando uma função antiga pedir um.


Quando dá errado

"use of deleted function ‘std::unique_ptr …’"
$ g++ -std=c++23 -Wall -Wextra copia.cpp -o copia
copia.cpp: In function ‘int main()’:
copia.cpp:5:14: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = int; _Dp = std::default_delete<int>]’
    5 |     auto b = a;      // tentar copiar
      |              ^
…

O que aconteceu: você tentou copiar um unique_ptr: numa variável, num push_back para um vetor, ou passando por valor para uma função.

Como resolver: decida o que você quer. Entregar a posse: std::move(a). Só usar o objeto: passe *a para um parâmetro const T&. Vários donos de verdade: troque por std::shared_ptr.

Usar o unique_ptr depois do std::move
#include <iostream>
#include <memory>
#include <utility>

int main() {
    auto a = std::make_unique<int>(10);
    auto b = std::move(a);
    std::cout << "Antes\n";
    std::cout << *a << "\n";      // a está vazio!
}
$ g++ -std=c++23 -Wall -Wextra depois-move.cpp -o depois-move
$ ./depois-move
Antes
/usr/include/c++/15/bits/unique_ptr.h:455: constexpr typename std::add_lvalue_reference<_Tp>::type std::unique_ptr<_Tp, _Dp>::operator*() const [with _Tp = int; _Dp = std::default_delete<int>; typename std::add_lvalue_reference<_Tp>::type = int&]: Assertion 'get() != pointer()' failed.
Abortado                   (imagem do núcleo gravada) ./depois-move
$ echo $?
134

O que aconteceu: depois do std::move, o a ficou com nullptr, e o *a tentou seguir um ponteiro nulo. A biblioteca padrão do g++ 15, quando o programa é compilado sem otimização (sem -O2), liga sozinha algumas conferências extras; uma delas (Assertion 'get() != pointer()' failed, "a afirmação o ponteiro guardado não é nulo falhou") parou o programa com Abortado, código 134. Com -O2, ou com outro compilador, essas conferências somem e o sintoma pode ser uma Falha de segmentação (veja nullptr).

Como resolver: depois de entregar com std::move, não use mais a variável antiga. Se não tiver certeza, confira antes: if (a) { … }.

Dois shared_ptr que se apontam e nunca são destruídos

O que aconteceu: um ciclo de shared_ptr: A guarda B e B guarda A. A contagem de cada um nunca chega a zero, o destrutor nunca roda e a memória vaza, mesmo sem nenhum new no código.

Como resolver: troque um dos lados (normalmente o que aponta "para trás", do filho para o pai) por std::weak_ptr. Para descobrir o vazamento, o -fsanitize=address da aula new e delete também funciona aqui.


Referência rápida

CódigoO que faz
#include <memory>Cabeçalho dos ponteiros inteligentes.
auto p = std::make_unique<T>(args);Cria um T no heap com um único dono. C++14
auto b = std::move(a);Passa a posse de a para b; a fica nullptr.
auto s = std::make_shared<T>(args);Cria um T no heap com contagem de donos.
s.use_count()Quantos shared_ptr apontam para o objeto.
p.reset()Larga o objeto (no unique_ptr, destrói na hora).
std::weak_ptr<T> w = s;Observa sem contar; w.lock(), w.expired().
*p, p->campo, if (p)Usa-se como um ponteiro comum.
p.get()O ponteiro cru, para quem só vai olhar (nunca faça delete nele).

Exercícios

1. Criar

Complete para criar um double no heap, com o valor 9.5, com um único dono:

auto nota = std::<double>(9.5);

2. Entregar a posse

Complete para passar a posse de a para b:

auto b = std::(a);

3. Por que auto b = a; não compila quando a é um std::unique_ptr?

4. Três shared_ptr apontam para a mesma playlist. Dois deles morrem. O que acontece?

5. Qual é a escolha padrão para um objeto no heap com um dono só?

Missão final: o bolo vai para a vitrine

Missão: o programa não compila, porque tenta copiar um unique_ptr. Rode uma vez para ver o erro. Depois, conserte a linha da vitrine para passar a posse do bolo. A saída deve ser:

meu está vazio
vitrine: bolo
[bolo destruído]

Repare na última linha: ninguém escreveu delete.

#include <iostream>
#include <memory>
#include <string>
#include <utility>

struct Produto {
    std::string nome;
    Produto(std::string n) : nome(n) {}
    ~Produto() { std::cout << "[" << nome << " destruído]\n"; }
};

int main() {
    auto meu = std::make_unique<Produto>("bolo");

    auto vitrine = meu;     // não compila: unique_ptr não copia

    if (meu == nullptr) {
        std::cout << "meu está vazio\n";
    }
    std::cout << "vitrine: " << vitrine->nome << "\n";
}
#include <iostream>
#include <memory>
#include <string>
#include <utility>

struct Produto {
    std::string nome;
    Produto(std::string n) : nome(n) {}
    ~Produto() { std::cout << "[" << nome << " destruído]\n"; }
};

int main() {
    auto meu = std::make_unique<Produto>("bolo");

    auto vitrine = std::move(meu);

    if (meu == nullptr) {
        std::cout << "meu está vazio\n";
    }
    std::cout << "vitrine: " << vitrine->nome << "\n";
}

Resumo

  • Ponteiros inteligentes C++11 fazem o delete sozinhos: sem vazamento, sem liberar duas vezes.
  • std::unique_ptr (std::make_unique): um dono. Não copia (use of deleted function); passa a posse com std::move, e o antigo fica nullptr.
  • std::shared_ptr (std::make_shared): vários donos; use_count() conta; o último a morrer destrói.
  • std::weak_ptr: observa sem contar; serve para quebrar ciclos.
  • Funções que só usam o objeto recebem const T&; as que ficam com ele recebem o unique_ptr por valor.
  • Regra: no C++ moderno, use std::vector, variáveis comuns e ponteiros inteligentes em vez de new/delete.
  • Próximo grupo: Structs, para criar seus próprios tipos.