Os ponteiros inteligentes são uma funcionalidade moderna do C++ concebida para proporcionar gestão automática de memória, ajudando a evitar fugas de memória e ponteiros pendentes, problemas habitualmente associados aos ponteiros em bruto. Funcionam como wrappers em torno dos ponteiros em bruto, acrescentando funcionalidades adicionais, como a libertação automática de memória quando o ponteiro já não é necessário.
Os ponteiros inteligentes são normalmente implementados como templates de classe na biblioteca padrão do C++.
Os dois ponteiros inteligentes mais utilizados são std::unique_ptr e std::shared_ptr.
O std::unique_ptr é um ponteiro inteligente que detém o objeto de forma exclusiva.
Garante que, em qualquer momento, apenas um objeto std::unique_ptr detém o recurso.
Quando o std::unique_ptr proprietário é destruído ou reinicializado, destrói automaticamente os objetos e liberta a respetiva 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
std::make_unique()
Ao criar um std::unique_ptr, é preferível usar std::make_unique() em vez de usar diretamente new para alocar memória.
O std::make_unique() oferece várias vantagens:
std::make_unique() garante segurança em relação a exceções.
Se for lançada uma exceção durante a construção do objeto, a memória é libertada automaticamente, o que evita fugas de memória.std::make_unique() torna o código mais claro e conciso.
Elimina a necessidade de especificar explicitamente o tipo a alocar, uma vez que os argumentos de template são deduzidos automaticamente.std::make_unique() de forma mais eficaz do que alocar memória manualmente com new, o que pode resultar num melhor desempenho.std::unique_ptr é construído manualmente.
Isso levaria a comportamento indefinido quando o std::unique_ptr tentasse eliminá-lo no fim do seu âmbito.O std::shared_ptr é um ponteiro inteligente que permite que vários objetos std::shared_ptr partilhem a propriedade do mesmo recurso.
Mantém o registo de quantos ponteiros partilhados referenciam o recurso e só liberta a memória quando o último ponteiro partilhado que detém o recurso sai do âmbito ou é reinicializado.
// 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;
Em C++17 e anteriores, o uso de std::shared_ptr com arrays através de std::make_shared<T[]> não é diretamente suportado.
Embora seja possível alocar arrays com std::make_shared<T[]>, criar ponteiros partilhados diretamente a partir deles pode levar a comportamento indefinido, devido a diferenças na gestão de memória entre objetos individuais e arrays.
Em vez disso, considera usar std::vector ou funções de eliminação personalizadas para gerir arrays com ponteiros partilhados de forma eficaz.
Certifica-te sempre de que há compatibilidade com o teu compilador e a tua implementação da biblioteca padrão quando lidas com alocações de arrays e ponteiros partilhados em C++17.
std::make_shared()
Tal como o std::make_unique(), o std::make_shared() oferece vantagens como maior eficiência de memória, segurança em relação a exceções e legibilidade.
Combina a alocação de memória do bloco de controlo e do objeto gerido numa única operação, o que melhora a eficiência e reduz o risco de fugas 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() promove um código mais limpo, mais seguro e mais eficiente quando trabalhas com objetos std::shared_ptr em C++.
O std::weak_ptr é uma classe complementar do std::shared_ptr que fornece uma referência "fraca" não proprietária a um objeto gerido por um ponteiro partilhado.
Permite aceder ao recurso sem afetar o seu tempo de vida.
Os ponteiros fracos são úteis em cenários em que é necessário quebrar referências cíclicas para evitar fugas 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 a propriedade cíclicaO std::weak_ptr foi concebido para resolver o problema da propriedade cíclica, também conhecida como referências circulares, que pode ocorrer ao usar std::shared_ptr.
Num cenário de propriedade cíclica, dois ou mais objetos std::shared_ptr referenciam-se mutuamente, criando um ciclo em que nenhum dos objetos pode ser eliminado, porque têm referências fortes entre si, o que leva a fugas de memória.
O std::weak_ptr resolve este problema ao permitir referências fracas a objetos partilhados sem contribuir para a sua contagem de referências.
Isto significa que pode observar e aceder ao objeto partilhado, mas não impede que este seja eliminado.
Se todas as referências fortes ao objeto partilhado forem libertadas, o objeto é destruído e os ponteiros fracos que o observam são automaticamente reinicializados para nullptr.
Eis um pequeno exemplo que demonstra a utilização de std::weak_ptr para quebrar a propriedade 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
A mensagem "Node constructed" é impressa duas vezes, seguida de duas linhas de "Node destructed".
Quando next não é um ponteiro fraco mas sim um std::shared_ptr, cria-se uma situação de propriedade cíclica, o que leva a fugas de memória, porque ambos os nós passam a ter referências fortes entre si, impedindo a sua destruição.
struct Node {
Node() { std::cout << "Node constructed\n"; }
~Node() { std::cout << "Node destructed\n"; }
std::shared_ptr<Node> next; // Change to shared_ptr
};
Neste cenário, tanto node1 como node2 mantêm referências fortes entre si através de std::shared_ptr, formando um ciclo.
Quando o programa termina e tenta libertar as referências fortes com reset(), as contagens de referências de ambos os nós continuam diferentes de zero, porque cada nó tem uma referência forte ao outro.
Por conseguinte, os nós não são destruídos, o que leva a fugas de memória.
A mensagem "Node constructed" é impressa duas vezes, mas não é seguida de qualquer linha de "Node destructed".
std::weak_ptr
Os ponteiros pendentes ocorrem quando um ponteiro referencia um objeto que já foi eliminado, o que leva a comportamento indefinido quando o ponteiro é desreferenciado.
O std::weak_ptr ajuda a evitar ponteiros pendentes ao fornecer uma referência fraca, não proprietária, a um objeto gerido por std::shared_ptr.
Eis como evitar ponteiros pendentes com std::weak_ptr:
Cria um std::weak_ptr: em vez de manteres diretamente um std::shared_ptr, cria um std::weak_ptr para o objeto partilhado.
Verifica a validade: antes de usares o std::weak_ptr, verifica a sua validade com a função expired().
Esta função devolve true se o objeto partilhado associado tiver sido eliminado.
Bloqueia o std::weak_ptr: para acederes ao objeto partilhado em segurança, usa a função lock(), que devolve um std::shared_ptr a apontar para o mesmo objeto, se este ainda for válido.
Se o objeto tiver sido eliminado, o lock() devolve um std::shared_ptr vazio.
Eis um exemplo de código que demonstra a utilização 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 aceder ao objeto partilhado, a validade é verificada com a função expired().
Em seguida, usa-se o lock() para aceder ao objeto partilhado em segurança.
Esta abordagem garante que os ponteiros pendentes são evitados quando se trabalha com std::weak_ptr.
Usa ponteiros inteligentes por predefinição: std::unique_ptr para propriedade exclusiva e std::shared_ptr para propriedade partilhada.
Reserva os ponteiros em bruto para referências não proprietárias ou para interagir com código legado.
Na maioria dos casos, o std::unique_ptr é suficiente para a propriedade exclusiva, pois oferece uma gestão de memória leve, sem o custo adicional da contagem de referências.
O std::shared_ptr deve ser usado com moderação, pois introduz custo adicional e complexidade, a menos que seja necessária uma verdadeira propriedade partilhada.
O std::weak_ptr é especializado em quebrar dependências cíclicas ou observar objetos partilhados, mas não é de uso comum.