Розумні вказівники - це сучасна можливість C++, покликана забезпечити автоматичне керування памʼяттю та допомогти запобігти витокам памʼяті й висячим вказівникам, які зазвичай повʼязують із сирими вказівниками. Вони обгортають сирі вказівники й додають додаткову функціональність, як-от автоматичне звільнення памʼяті, коли вказівник більше не потрібен.
Зазвичай розумні вказівники реалізовано як шаблони класів у стандартній бібліотеці C++.
Два найуживаніші розумні вказівники - це std::unique_ptr і std::shared_ptr.
std::unique_ptr - це розумний вказівник, який одноосібно володіє обʼєктом.
Він гарантує, що в будь-який момент лише один обʼєкт std::unique_ptr володіє ресурсом.
Коли std::unique_ptr-власник знищується або скидається, він автоматично знищує обʼєкти й звільняє їхню памʼять.
#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()
Коли ми створюємо std::unique_ptr, краще використовувати std::make_unique(), а не виділяти памʼять безпосередньо через new.
std::make_unique() дає кілька переваг:
std::make_unique() гарантує безпечність щодо винятків.
Якщо під час конструювання обʼєкта буде викинуто виняток, памʼять буде звільнено автоматично, що запобігає витокам памʼяті.std::make_unique() робить код яснішим і стислішим.
Воно позбавляє потреби явно вказувати тип, який виділяється, бо аргументи шаблону виводяться автоматично.std::make_unique() ефективніше, ніж ручне виділення памʼяті через new, що потенційно підвищує продуктивність.std::unique_ptr сконструйовано вручну, базовий ресурс можна видалити.
Це призвело б до невизначеної поведінки, коли std::unique_ptr спробує видалити його в кінці своєї області видимості.std::shared_ptr - це розумний вказівник, який дозволяє кільком обʼєктам std::shared_ptr спільно володіти одним ресурсом.
Він відстежує, скільки спільних вказівників посилаються на ресурс, і звільняє памʼять лише тоді, коли останній спільний вказівник, що володіє ресурсом, виходить з області видимості або скидається.
// 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;
У C++17 і раніших версіях використання std::shared_ptr із масивами через std::make_shared<T[]> не підтримується напряму.
Хоча виділити масиви за допомогою std::make_shared<T[]> і можливо, створення спільних вказівників безпосередньо з них може призвести до невизначеної поведінки через відмінності в керуванні памʼяттю між окремими обʼєктами та масивами.
Натомість варто розглянути std::vector або власні функції видалення, щоб ефективно керувати масивами зі спільними вказівниками.
Завжди дбаймо про сумісність із нашим компілятором і реалізацією стандартної бібліотеки, коли працюємо з виділенням масивів і спільними вказівниками в C++17.
std::make_shared()
Подібно до std::make_unique(), std::make_shared() дає такі переваги, як краща ефективність використання памʼяті, безпечність щодо винятків і читабельність.
Він поєднує виділення памʼяті для блоку керування й керованого обʼєкта в одну операцію, підвищуючи ефективність і знижуючи ризик витоків памʼяті.
До того ж автоматичний вивід аргументів шаблону спрощує код і покращує читабельність.
Використання std::make_shared() сприяє чистішому, безпечнішому й ефективнішому коду, коли ми працюємо з обʼєктами std::shared_ptr у C++.
std::weak_ptr - це супутній клас до std::shared_ptr, який дає «слабке» посилання без володіння на обʼєкт, яким керує спільний вказівник.
Він дозволяє отримати доступ до ресурсу, не впливаючи на час його життя.
Слабкі вказівники корисні в ситуаціях, коли циклічні посилання потрібно розірвати, щоб запобігти витокам памʼяті.
// 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 і циклічне володінняstd::weak_ptr створили, щоб розвʼязати проблему циклічного володіння, також відомого як кругові посилання, яка може виникнути під час використання std::shared_ptr.
У сценарії циклічного володіння два або більше обʼєктів std::shared_ptr посилаються один на одного, утворюючи цикл, у якому жоден з обʼєктів не можна видалити, бо вони мають сильні посилання один на одного, що призводить до витоків памʼяті.
std::weak_ptr дає розвʼязок цієї проблеми, дозволяючи слабкі посилання на спільні обʼєкти без збільшення їхнього лічильника посилань.
Це означає, що він може спостерігати за спільним обʼєктом і отримувати до нього доступ, але не заважає його видаленню.
Якщо всі сильні посилання на спільний обʼєкт звільнено, обʼєкт знищується, а слабкі вказівники, що за ним спостерігають, автоматично скидаються до nullptr.
Ось короткий приклад, який демонструє використання std::weak_ptr для розривання циклічного володіння:
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» друкується двічі, а далі йдуть два рядки «Node destructed».
Коли next - не слабкий вказівник, а std::shared_ptr, це створює ситуацію циклічного володіння й призводить до витоків памʼяті, бо обидва вузли тримають сильні посилання один на одного, що заважає їхньому знищенню.
struct Node {
Node() { std::cout << "Node constructed\n"; }
~Node() { std::cout << "Node destructed\n"; }
std::shared_ptr<Node> next; // Change to shared_ptr
};
У цьому сценарії і node1, і node2 тримають сильні посилання один на одного через std::shared_ptr, утворюючи цикл.
Коли програма завершується й намагається звільнити сильні посилання за допомогою reset(), лічильники посилань обох вузлів залишаються ненульовими, бо кожен вузол тримає сильне посилання на інший.
Тому вузли не знищуються, що призводить до витоків памʼяті.
«Node constructed» друкується двічі, але за ним не йде жодного рядка «Node destructed».
std::weak_ptr
Висячі вказівники виникають, коли вказівник посилається на обʼєкт, який було видалено, що призводить до невизначеної поведінки під час розіменування вказівника.
std::weak_ptr допомагає уникнути висячих вказівників, даючи слабке посилання без володіння на обʼєкт, яким керує std::shared_ptr.
Ось як уникнути висячих вказівників зі std::weak_ptr:
Створімо std::weak_ptr: Замість того щоб тримати std::shared_ptr безпосередньо, створімо std::weak_ptr на спільний обʼєкт.
Перевірмо дійсність: Перш ніж використовувати std::weak_ptr, перевірмо його дійсність за допомогою функції expired().
Ця функція повертає true, якщо повʼязаний спільний обʼєкт було видалено.
Заблокуймо std::weak_ptr: Щоб безпечно отримати доступ до спільного обʼєкта, скористаймося функцією lock(), яка повертає std::shared_ptr, що вказує на той самий обʼєкт, якщо він досі дійсний.
Якщо обʼєкт було видалено, lock() повертає порожній std::shared_ptr.
Ось приклад коду, який демонструє використання std::weak_ptr для уникнення висячих вказівників:
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
}
У цьому прикладі node1->next і node2->next - обʼєкти std::weak_ptr.
Перш ніж отримати доступ до спільного обʼєкта, перевірмо його дійсність за допомогою функції expired().
Потім скористаймося lock(), щоб безпечно отримати доступ до спільного обʼєкта.
Такий підхід гарантує, що ми уникаємо висячих вказівників, працюючи зі std::weak_ptr.
Типово використовуймо розумні вказівники: std::unique_ptr для одноосібного володіння, а std::shared_ptr для спільного володіння.
Сирі вказівники залишаймо для посилань без володіння або для взаємодії із застарілим кодом.
У більшості випадків std::unique_ptr достатньо для одноосібного володіння, бо він пропонує легке керування памʼяттю без накладних витрат на підрахунок посилань.
std::shared_ptr варто використовувати помірковано, бо він додає накладні витрати й складність, якщо справжнє спільне володіння не потрібне.
std::weak_ptr призначений для розривання циклічних залежностей або спостереження за спільними обʼєктами, але його використовують нечасто.