Trilhas
/
C++
C++
/
Programa
/
Ponteiros Inteligentes
Po

Ponteiros Inteligentes em C++

1 exercício

Sobre Ponteiros Inteligentes

Ponteiros inteligentes são um recurso moderno do C++ criado para oferecer gerenciamento automático de memória, ajudando a evitar vazamentos de memória e ponteiros pendentes, problemas comuns com ponteiros brutos. Eles envolvem os ponteiros brutos e acrescentam funcionalidades extras, como a liberação automática da memória quando o ponteiro não é mais necessário.

Sintaxe geral

Ponteiros inteligentes são normalmente implementados como templates de classe na biblioteca padrão do C++. Os dois ponteiros inteligentes mais usados são std::unique_ptr e std::shared_ptr.

Ponteiros únicos

std::unique_ptr é um ponteiro inteligente que possui o objeto com exclusividade. Ele garante que, a qualquer momento, apenas um objeto std::unique_ptr seja dono do recurso. Quando o std::unique_ptr dono é destruído ou resetado, ele destrói automaticamente os objetos e libera a memória.

#include <memory>
// Declaring and defining a unique pointer
auto rightful_king_of_england = std::make_unique<std::string>("Excalibur");

// Unique pointers cannot be copied or assigned
auto mordred = rightful_king_of_england; // Error: Cannot copy a unique_ptr

Vantagens de std::make_unique()

Ao criar um std::unique_ptr, é preferível usar std::make_unique() em vez de usar new diretamente para alocar memória. std::make_unique() oferece várias vantagens:

  1. Segurança quanto a exceções: std::make_unique() garante segurança quanto a exceções. Se uma exceção for lançada durante a construção do objeto, a memória será liberada automaticamente, evitando vazamentos de memória.
  2. Clareza: Usar std::make_unique() deixa o código mais claro e conciso. Elimina a necessidade de especificar explicitamente o tipo que está sendo alocado, já que os argumentos de template são deduzidos automaticamente.
  3. Oportunidades de otimização: Os compiladores têm a oportunidade de otimizar std::make_unique() de forma mais eficaz do que alocar memória manualmente com new, o que pode resultar em melhor desempenho.
  4. Evitar uso indevido: É possível deletar o recurso subjacente quando o std::unique_ptr é construído manualmente. Isso levaria a um comportamento indefinido quando o std::unique_ptr tenta deletá-lo no fim do seu escopo.

Ponteiros compartilhados

std::shared_ptr é um ponteiro inteligente que permite que vários objetos std::shared_ptr compartilhem a posse do mesmo recurso. Ele mantém o controle de quantos ponteiros compartilhados estão referenciando o recurso e só libera a memória quando o último ponteiro compartilhado dono do recurso sai do escopo ou é resetado.

// Declaring and defining a shared pointer to a dynamically allocated string
auto martian_congressional_republic = std::make_shared<std::string>("protomolecule");

// Creating more shared pointer that shares ownership
auto outer_planets_alliance = martian_congressional_republic;
auto united_nations = martian_congressional_republic;
Caution

Em C++17 e versões anteriores, não há suporte direto ao uso de std::shared_ptr com arrays por meio de std::make_shared<T[]>. Embora seja possível alocar arrays com std::make_shared<T[]>, criar ponteiros compartilhados diretamente a partir deles pode levar a um comportamento indefinido, devido a diferenças no gerenciamento de memória entre objetos únicos e arrays. Em vez disso, considere usar std::vector ou funções de deleção personalizadas para gerenciar arrays com ponteiros compartilhados de forma eficaz. Ao lidar com alocação de arrays e ponteiros compartilhados em C++17, verifique sempre a compatibilidade com o seu compilador e com a implementação da biblioteca padrão.

Vantagens de std::make_shared()

Assim como std::make_unique(), std::make_shared() oferece benefícios como melhor eficiência de memória, segurança quanto a exceções e legibilidade. Ele combina a alocação de memória do bloco de controle e do objeto gerenciado em uma única operação, o que melhora a eficiência e reduz o risco de vazamentos de memória. Além disso, a dedução automática dos argumentos de template simplifica o código e melhora a legibilidade. Usar std::make_shared() resulta em um código mais limpo, mais seguro e mais eficiente ao trabalhar com objetos std::shared_ptr em C++.

Ponteiros fracos

std::weak_ptr é uma classe complementar ao std::shared_ptr que oferece uma referência "fraca" e sem posse a um objeto gerenciado por um ponteiro compartilhado. Ele permite acessar o recurso sem afetar seu tempo de vida. Ponteiros fracos são úteis em cenários nos quais referências cíclicas precisam ser quebradas para evitar vazamentos de memória.

// Creating a shared pointer
auto your_account = std::make_shared<std::string>("secret_subscription_password");
// Creating a shared pointer that shares ownership
auto your_flatmates_account = your_account;

// Creating a weak pointer from the shared pointer
auto your_flatmates_boyfriends_account = your_flatmates_account;
// if your_account and your_flatmates_account are deleted, there is no more reference to the shared pointer.
// your_flatmates_boyfriends_account will be a null pointer and cannot use the associated object any longer.

std::weak_ptr e posse cíclica

std::weak_ptr foi criado para resolver o problema da posse cíclica, também conhecida como referências circulares, que pode ocorrer ao usar std::shared_ptr.

Em um cenário de posse cíclica, dois ou mais objetos std::shared_ptr referenciam um ao outro, criando um ciclo em que nenhum dos objetos pode ser deletado, porque eles mantêm referências fortes entre si, o que leva a vazamentos de memória.

std::weak_ptr resolve esse problema ao permitir referências fracas a objetos compartilhados sem contribuir para a contagem de referências deles. Isso significa que ele pode observar e acessar o objeto compartilhado, mas não impede que ele seja deletado. Se todas as referências fortes ao objeto compartilhado forem liberadas, o objeto é destruído, e os ponteiros fracos que o observam são resetados automaticamente para nullptr.

Veja um exemplo curto que demonstra o uso de std::weak_ptr para quebrar a posse cíclica:

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::weak_ptr<Node> next;
};

// Creating two nodes
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Creating a cycle by making each node point to the other
node1->next = node2;
node2->next = node1;

// Release strong references to nodes
node1.reset();
node2.reset();

// Nodes are automatically destructed due to weak references

"Node constructed" é impresso duas vezes, seguido por duas linhas de "Node destructed".

Quando next não é um ponteiro fraco, mas um std::shared_ptr, cria-se uma situação de posse cíclica, o que leva a vazamentos de memória, porque os dois nós mantêm referências fortes um ao outro, impedindo a destruição deles.

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::shared_ptr<Node> next; // Change to shared_ptr
};

Nesse cenário, tanto node1 quanto node2 mantêm referências fortes um ao outro por meio de std::shared_ptr, formando um ciclo. Quando o programa termina e tenta liberar as referências fortes com reset(), as contagens de referência dos dois nós continuam diferentes de zero, porque cada nó mantém uma referência forte ao outro. Portanto, os nós não são destruídos, o que leva a vazamentos de memória. "Node constructed" é impresso duas vezes, mas não é seguido por nenhuma linha de "Node destructed".

Ponteiros pendentes e std::weak_ptr

Ponteiros pendentes ocorrem quando um ponteiro referencia um objeto que já foi deletado, o que leva a um comportamento indefinido quando o ponteiro é desreferenciado. std::weak_ptr ajuda a evitar ponteiros pendentes ao oferecer uma referência fraca e sem posse a um objeto gerenciado por um std::shared_ptr. Veja como evitar ponteiros pendentes com std::weak_ptr:

  1. Crie um std::weak_ptr: Em vez de manter diretamente um std::shared_ptr, crie um std::weak_ptr para o objeto compartilhado.

  2. Verifique a validade: Antes de usar o std::weak_ptr, verifique sua validade com a função expired(). Essa função retorna true se o objeto compartilhado associado tiver sido deletado.

  3. Faça o lock do std::weak_ptr: Para acessar o objeto compartilhado com segurança, use a função lock(), que retorna um std::shared_ptr apontando para o mesmo objeto, caso ele ainda seja válido. Se o objeto tiver sido deletado, lock() retorna um std::shared_ptr vazio.

Veja um exemplo de código que demonstra o uso de std::weak_ptr para evitar ponteiros pendentes:

auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Create weak pointers
node1->next = node2;
node2->next = node1;

// Check validity and lock
if (auto lockedNode2 = node1->next.lock()) {
    // Use lockedNode2 safely
} else {
    // Handle case where node2 has been deleted
}

Neste exemplo, node1->next e node2->next são objetos std::weak_ptr. Antes de acessar o objeto compartilhado, a validade é verificada com a função expired(). Em seguida, lock() é usado para acessar o objeto compartilhado com segurança. Essa abordagem garante que ponteiros pendentes sejam evitados ao trabalhar com std::weak_ptr.

Recomendações de uso

Use ponteiros inteligentes por padrão: std::unique_ptr para posse exclusiva e std::shared_ptr para posse compartilhada. Reserve os ponteiros brutos para referências sem posse ou para fazer interface com código legado. Na maioria dos casos, std::unique_ptr é suficiente para posse exclusiva, pois oferece um gerenciamento de memória leve, sem a sobrecarga da contagem de referências. std::shared_ptr deve ser usado com moderação, pois introduz sobrecarga e complexidade, a menos que a posse compartilhada de fato seja necessária. std::weak_ptr é especializado em quebrar dependências cíclicas ou observar objetos compartilhados, mas não é usado com frequência.

Editar via GitHub O link abre em uma nova janela ou aba

Aprenda Ponteiros Inteligentes