Smart pointers или "интеллектуальные указатели" — это объекты, которые
имитируют стандартные указатели: они также содержат адрес (как правило, адрес выделенной динамической памяти), и их можно также использовать для обращения к объектам по этому адресу.
Но главное их отличие от стандартных указателей состоит в том, что нам не надо беспокоиться об освобождении памяти
с помощью операторов delete
или delete[]
. Вся выделенная память, используемая интеллектуальными указателями, будет освобождаться автоматически,
когда она станет не нужна. Соответственно это означает, что мы не столкнемся с утечками памяти, не соответствием между выделениями и освобождениями памяти и болтающимися указателями. Таким образом, smart-указатели позволяют упростить и
обезопасить управление памятью. Типы интеллектуальных указателей определены в модуле memory стандартной библиотеки языка С++ и доступны в пространстве имен std.
Указатель unique_ptr<T> представляет указатель на тип T, который является "уникальным" в том смысле, что
что может быть только один объект unique_ptr
, который содержит один и тот же адрес. То есть не может одновременно быть двух или более объектов
unique_ptr<T>
, которые указывают один и тот же адрес памяти. Если же мы попробуем определить два одновременно существующих указателя, которые указывают на
один и тот же адрес, компилятор не скопирует код.
И когда unique_ptr уничтожается, уничтожается и значение, на которое он указывает. Соответственно данный тип указателей полезен, когда нужен указатель на объект, на который НЕ будет других указателей и который будет удален после удаления указателя.
По умолчанию unique_ptr<T>
инициализируется значением nullptr
std::unique_ptr<int> ptr; // ptr = nullptr // аналогично std::unique_ptr<int> ptr{}; std::unique_ptr<int> ptr{nullptr};
Чтобы выделить память и создать в ней объект, на который будет указывать указатель, применяется функция std::make_unique<T>. В качестве параметра в нее передается объект, на который будет указывать указатель:
std::unique_ptr<int> ptr { std::make_unique<int>(125) };
В данном случае выделяется динамическая память для хранения числа 125, и указатель ptr указывает на эту память.
Стоит отметить, что до принятия стандарта C++14 применялась другая форма создания указателя:
std::unique_ptr<int> ptr { new int(125) };
Для получения стандартного указателя из std::unique_ptr
применяется функция get():
std::unique_ptr<int> ptr { std::make_unique<int>(125) }; int* pointer = ptr.get();
После определения интеллектуального указателя мы можем получать и изменять значение, на которое он указывает, так же, как и при работе с обычными указателями:
#include <iostream> #include <memory> int main() { // указатель ptr указывает на объект 125 std::unique_ptr<int> ptr { std::make_unique<int>(125) }; std::cout << "Address: " << ptr.get() << std::endl; // получим адрес объекта std::cout << "Initial value: " << *ptr << std::endl; // получим значение объекта // изменяем значение *ptr = 254; std::cout << "New value: " << *ptr << std::endl; // получим значение объекта }
Консольный вывод:
Address: 0x2775dfa8030 Initial value: 125 New value: 254
Стоит отметить, что начиная со стандарта C++ 20 получить адрес из smart-указателя мы можем напрямую без функции get:
std::cout << "Address: " << ptr << std::endl; // получим адрес объекта
unique_ptr
также может работать с массивами. Например, определим указатель, который ссылается на массив:
unsigned n{5}; // размер массива auto pnumbers { std::make_unique<int[]>(n) }; // массив из n элементов, равных 0
В данном случае указать pnumbers указаывает на массив из 5 элементов. При этом все эти элементы уже инициализированы значениями по умолчанию (для примитивных типов - числом 0).
С помощью квадратных скобок можно обращаться к определенным элементам массива:
#include <iostream> #include <memory> int main() { unsigned n{5}; // размер массива auto pnumbers { std::make_unique<int[]>(n) }; // массив {0, 0, 0, 0, 0} std::cout << "pnumbers[1] initial value: " << pnumbers[1] << std::endl; // pnumbers[1] initial value: 0 pnumbers[1] = 121; // изменяем значение std::cout << "pnumbers[1] new value: " << pnumbers[1] << std::endl; // pnumbers[1] new value: 121 }
Можно пройтись по массиву в цикле
#include <iostream> #include <memory> int main() { unsigned n{5}; // размер массива auto pnumbers { std::make_unique<int[]>(n) }; // массив из n элементов // изменим и выведем все элементы на консоль for (unsigned i {}; i < n; i++) { pnumbers[i] = i+1; std::cout << "pnumbers[" << i <<"] = " << pnumbers[i] << std::endl; } }
Консольный вывод:
pnumbers[0] = 1 pnumbers[1] = 2 pnumbers[2] = 3 pnumbers[3] = 4 pnumbers[4] = 5
Если потребуется освободить память, на которую указывает указатель, то можно применить функцию reset():
#include <iostream> #include <memory> int main() { auto ptr { std::make_unique<int>(123) }; // освобождаем память и удаляем объект 123 ptr.reset(); if(!ptr) // если ptr { std::cout << "Memory is free" << std::endl; } else { std::cout << *ptr << std::endl; } }
После выполнения функции reset()
указатель получает значение nullptr
Также можно передать в функцию reset()
новый объект, для которого будет выделяться память и на который будет указывать указатель.
#include <iostream> #include <memory> int main() { auto ptr { std::make_unique<int>(123) }; std::cout << "Old address: " << ptr.get() << std::endl; std::cout << "Old value: " << *ptr << std::endl; // освобождаем память и удаляем массив ptr.reset(new int{254}); std::cout << "New address: " << ptr.get() << std::endl; std::cout << "New value: " << *ptr << std::endl; }
В данном случае после вызова функции reset()
изначальная память освобождается и выделяется новый участок памяти для числа 254. Соотвественно меняется адрес и само значение, на которое указывает
указатель. Консольный вывод:
Old address: 0x2b13dd73270 Old value: 123 New address: 0x2b13dd68010 New value: 254