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