Практический курс по C/C++: https://stepik.org/course/193691
На предыдущем
занятии мы с вами подробно разобрали smart-указатели типа unique_ptr, которые могут
только в единственном числе ссылаться на выделенную область памяти и
автоматически ее освобождать, значительно уменьшая риск утечки памяти. Однако в
практике программирования чаще используются указатели вида shared_ptr, которые также
автоматически освобождают неиспользуемую память, но в отличие от unique_ptr могут ссылаться
на нее во множественном числе:
Указатели типа shared_ptr объявляются в
программе по аналогии с указателями unique_ptr. Вначале также
нужно подключить заголовок:
После чего в
пространстве имен std появится класс shared_ptr:
std::shared_ptr<int> ptr;
std::shared_ptr<int> ptr_2 {};
std::shared_ptr<int> ptr_3 {nullptr};
std::shared_ptr<int> ptr_4 {ptr};
Во всех случаях
объявляются указатели со значением nullptr. Причем,
последний указатель ptr_4 инициализирован адресом указателя ptr. Как мы помним,
с типом unique_ptr такая
конструкция приводила к ошибке на этапе компиляции. Здесь же никаких ошибок
нет, т.к. shared_ptr предполагает
множество разных smart-указателей на одну и ту же область
памяти.
Если нам нужно
сразу в инициализаторе указателя типа shared_ptr выделить
некоторую область памяти, то это следует делать с помощью функции make_shared():
std::shared_ptr<int> ptr {std::make_shared<int>(3)};
В результате ptr будет ссылаться
на область памяти для хранения целочисленного значения 3 типа int.
Пользоваться
указателем ptr можно абсолютно
так же, как и обычным указателем на тип int. Например:
ptr_2 = ptr; // присвоение адреса указателя ptr указателю ptr_2
*ptr = 10; // запись числа 10 по адресу указателя ptr
cout << ptr_2 << *ptr_2 << endl; // чтение и вывод в консоль значения по указателю ptr_2
Обратите
внимание, что операция взятия адреса указателя shared_ptr делается просто
по его имени (без вызова метода get).
А вот выполнять
операции адресной арифметики с такими указателями не получится. При выполнении
следующих команд получим ошибки на этапе компиляции:
ptr_3 += 10;
auto res = ptr_3 - ptr_4;
Это их отличает
от обычных (классических) указателей языка C/C++.
Контроль памяти указателей типа shared_ptr
Давайте теперь
детальнее посмотрим, как указатели типа shared_ptr осуществляют
контроль за памятью. Предположим, указатель ptr инициализируется
на некоторую область памяти:
std::shared_ptr<int> ptr_1 {std::make_shared<int>(3)};
В результате
формируется объект самого указателя ptr_1, область памяти для хранения
целочисленного значения типа int, а также специальный счетчик counter указателей на
выделенную область памяти:
Если объявить еще
один указатель на эту же область:
std::shared_ptr<int> ptr_2 {ptr_1};
то счетчик counter автоматически
увеличивается до двух:
И так далее. С
увеличением числа указателей типа shared_ptr на эту область
памяти, счетчик counter будет все время увеличиваться.
Предположим
теперь, что указатель ptr_2 меняет свой адрес. Например, в
результате выполнения такой команды:
ptr_2 = std::make_shared<int>(10);
Тогда счетчик
для предыдущей области памяти (с int(3)) уменьшится на единицу, а указатель ptr_2 будет
ссылаться на другую область памяти со своим счетчиком, у которого значение
также будет равно единице.
Далее, если
указатель ptr_1 «отвязать» от
текущей области памяти:
то счетчик на
первую область будет равен нулю, а на вторую – два:
И как только для
какой-либо выделенной области счетчик counter устанавливается
в ноль, эта область памяти автоматически освобождается. Так реализован контроль
за памятью для указателей shared_ptr. Кроме того,
смешивать разные типы smart-указателей между собой также запрещено.
Например, следующая конструкция приведет к ошибке на этапе компиляции:
std::unique_ptr<int> ptr_u {std::make_unique<int>(-1)};
std::shared_ptr<int> ptr_1 {ptr_u}; // ошибка
Или конструкция
вида:
также приведет к
ошибке. То есть, мы можем работать либо с указателями unique_ptr, либо с
указателями shared_ptr, не смешивая
их.
Методы указателей shared_ptr
Следующим шагом
рассмотрим методы, которые имеются у smart-указателей типа
shared_ptr. Основные из
них следующие:
- get() – получение
«сырого» указателя на выделенную область памяти;
- reset() – меняет
значение указателя на другую область памяти, либо на значение nullptr, если ничего не
указано;
- swap() – меняет
адреса двух указателей между собой;
- unique() – возвращает
true (1), если на
выделенную область ссылается только один указатель, и false (0) – в
противном случае;
- use_count() –
возвращает текущее значение счетчика conter для текущей
области памяти.
Давайте
посмотрим, как работают эти методы. С первым get() мы уже
знакомы по предыдущему smart-указателю. Здесь все то же самое:
std::shared_ptr<int> ptr_1 {std::make_shared<int>(3)};
std::shared_ptr<int> ptr_2 {ptr_1};
int* p = ptr_1.get();
Следующий метод reset позволяет
заменять одну область памяти на другую. Например:
ptr_2.reset(new int[5] {1, 2, 3});
int* ar = ptr_2.get();
for(int i = 0;i < 5;++i)
cout << ar[i] << " ";
Обратите
внимание, мы выделили новую область памяти с помощью оператора new для массива в 5
элементов. После этого получили стандартный указатель на начало этого массива и
с его помощью прочитали и вывели значения в консоль.
К сожалению,
указатели типа shared_ptr могут работать с
массивами, только начиная с версии языка C++20. До этого
придется использовать оператор new.
Оставшиеся
методы работают очевидным образом. Например:
ptr_2.reset(new int[5] {1, 2, 3});
ptr_2.swap(ptr_1);
cout << *ptr_2 << " " << ptr_1.use_count() << endl;
cout << ptr_1.unique() << endl;
В консоли
увидим:
3
1
1
А если убрать
строчку с reset, то вывод будет
таким:
3
2
0
Работа smart-указателей типа shared_ptr с массивами в стандарте C++20
Начиная со
стандарта C++20 указатели shared_ptr можно
инициализировать и использовать с динамическими массивами. Делается это по
аналогии с указателями unique_ptr следующим
образом:
unsigned total = 10;
std::shared_ptr<int> ar_1 {std::make_shared<int[]>(total)};
После этого
через указатель ar_1 можно работать как с массивом привычным нам
образом:
ar_1[0] = 10;
for(unsigned i = 0;i < total; ++i)
cout << ar_1[i] << " ";
Заключение
Вот основные
возможности «умных» указателей unique_ptr и shared_ptr. Сейчас в
практике программирования на C/C++ они крайне
рекомендуются при написании программ различного уровня сложности, так как существенно
уменьшают проблемы, связанные с утечкой памяти, ее повторным освобождением,
доступом к невыделенной памяти и так далее. Поэтому вместо применения
операторов new/delete по возможности
следует применять тот или иной вид smart-указателей.
Практический курс по C/C++: https://stepik.org/course/193691