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.
std::unique_ptrestd::make_unique: um único dono.- A prova, com um destrutor que imprime, de que a memória é liberada sozinha.
- Por que um
unique_ptrnão pode ser copiado (o erro real) e como passar a posse comstd::move. std::shared_ptrestd::make_shared: vários donos, contados poruse_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ção | Ponteiro inteligente | Quando 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_ptr | Quando o dono deixa de existir. |
| A chave tem várias cópias, e o balcão anota quantas existem. | std::shared_ptr | Quando a última cópia deixa de existir. |
| Alguém só tem o número do box anotado, sem chave. | std::weak_ptr | Nã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 emstd::vector<int>. - std::make_unique<int>(42)
- Faz o
new int(42)por você e já entrega o resultado dentro de umunique_ptr. Os parênteses levam o valor inicial. C++14 - *p
- Usa-se como um ponteiro comum:
*ppara o valor ep->campopara structs. - }
- Quando o
pmorre, ele faz odeletesozinho. Não existedeleteno 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
bcomo cópia dea. Os_Tpe_Dpsão nomes internos; o[with _Tp = int …]diz que aqui eles valemint. - 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
| Momento | a | b | O int 10 no heap |
|---|---|---|---|
Depois do make_unique | dono | (não existe) | 1 dono: a |
Depois do std::move(a) | nullptr | dono | 1 dono: b |
No } do main | os dois morrem; só o b tinha algo a devolver | devolvido uma única vez | |
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&(ouProduto¶ 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 comstd::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
Playlistno heap, junto com o "caderninho" onde a contagem fica anotada, e devolve umstd::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_ptrapontam para o objeto agora. Serve para estudar e depurar; programas raramente precisam dele. - .reset()
- Este
shared_ptrlarga 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.
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 umshared_ptrtemporá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ção | Use |
|---|---|
| Uma lista de coisas | std::vector (nem precisa de ponteiro) |
| Um objeto que pode ficar na pilha | Uma variável comum: Produto p{…}; |
| Um objeto no heap com um dono | auto p = std::make_unique<T>(…); |
| Um objeto no heap com vários donos, sem ordem clara | auto p = std::make_shared<T>(…); |
Observar um objeto de shared_ptr sem mantê-lo vivo | std::weak_ptr<T> |
| Uma função que só usa o objeto | const T& / T& (ou T* se puder ser "nada") |
new e delete escritos à mão | Quase 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
$ 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.
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) { … }.
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ódigo | O 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:
2. Entregar a posse
Complete para passar a posse de a para b:
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
deletesozinhos: sem vazamento, sem liberar duas vezes. std::unique_ptr(std::make_unique): um dono. Não copia (use of deleted function); passa a posse comstd::move, e o antigo ficanullptr.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 ounique_ptrpor valor. - Regra: no C++ moderno, use
std::vector, variáveis comuns e ponteiros inteligentes em vez denew/delete. - Próximo grupo: Structs, para criar seus próprios tipos.