Мастерство работы с указателями в C++

Этот курс подробно объясняет концепцию указателей, от базового управления памятью до арифметики и динамического выделения. Вы также изучите современные стандарты C++ и использование умных указателей для безопасного кода.

1. Основы памяти: введение в указатели, адресацию и операторы разыменования

Основы памяти: введение в указатели, адресацию и операторы разыменования

Добро пожаловать на курс «Мастерство работы с указателями в C++». Если вы здесь, значит, вы готовы заглянуть «под капот» программирования и понять, как на самом деле работает ваш компьютер. Указатели часто называют самой сложной темой для новичков, но на самом деле это просто карта, указывающая, где лежит сокровище.

В этой вводной статье мы разберем фундаментальные понятия: что такое оперативная память, как устроен адрес переменной и как мы можем управлять этими адресами с помощью указателей.

Как устроена память компьютера

Чтобы понять указатели, нужно сначала представить, как выглядит оперативная память (RAM) вашего компьютера. Представьте себе бесконечно длинную улицу, на которой стоят дома. У каждого дома есть свой уникальный номер — это адрес.

В мире компьютера: * Дом — это ячейка памяти размером в 1 байт. * Жилец дома — это данные (число, символ или часть большого объекта). * Номер дома — это адрес ячейки (обычно записывается в шестнадцатеричном формате, например, 0x7ffee4).

Когда вы создаете переменную в C++, вы, по сути, бронируете один или несколько таких «домов» подряд, чтобы поселить туда свои данные.

!Схематичное изображение оперативной памяти как последовательности пронумерованных ячеек.

Переменные и их адреса

Рассмотрим простой пример:

Что происходит, когда выполняется эта строка?

  • Система находит в памяти свободное место. Так как тип int обычно занимает 4 байта, система резервирует 4 ячейки подряд.
  • Система запоминает адрес первой ячейки (начало нашего блока).
  • В эти ячейки записывается число 100 (в двоичном виде).
  • В вашем коде имя score становится псевдонимом для этого адреса.
  • Оператор взятия адреса (&)

    Мы знаем, что переменная score хранит значение 100. Но как узнать, где именно в памяти она находится? Для этого в C++ существует оператор взятия адреса, который обозначается символом амперсанда — &.

    Если вы поставите & перед именем переменной, программа вернет не значение переменной, а её адрес в памяти.

    Адрес 0x61ff0c — это шестнадцатеричное число, которое говорит процессору, в какой именно ячейке начинается наша переменная.

    Что такое указатель?

    Теперь мы подошли к главному. Указатель — это переменная, значением которой является адрес другой переменной.

    Прочитайте это определение еще раз. Обычная переменная хранит данные (число, символ). Указатель хранит координаты этих данных.

    Если обычная переменная — это дом, то указатель — это записка, на которой написан адрес этого дома.

    Объявление указателя

    Чтобы объявить указатель, мы используем символ звездочки * между типом данных и именем переменной.

    Здесь ptr — это имя нашей переменной-указателя. Тип int* означает «указатель на целое число». Это важно: указатель всегда должен знать, на какой тип данных он указывает (мы обсудим это ниже).

    Инициализация указателя

    Указатель бесполезен (и даже опасен), если он никуда не указывает. Давайте запишем в него адрес нашей переменной score.

    Теперь у нас есть связь: * score == 100 * &score == 0x61ff0c (адрес) * ptr == 0x61ff0c (тот же самый адрес)

    !Визуализация связи указателя и переменной: указатель хранит адрес, который ссылается на переменную.

    Оператор разыменования (*)

    Иметь адрес — это хорошо, но как получить доступ к данным, которые находятся по этому адресу? Для этого используется оператор разыменования, который тоже обозначается звездочкой *.

    Внимание! Символ * используется в двух разных контекстах, и это часто путает новичков:

  • При объявлении (int* p): означает «создать переменную типа указатель».
  • При использовании (*p): означает «перейти по адресу и взять значение».
  • Пример использования:

    Когда мы пишем *ptr, мы говорим компьютеру: «Возьми адрес, который записан в ptr, иди в ту ячейку памяти и работай с тем, что там лежит».

    Зачем указателям типы?

    Почему мы не можем просто создать универсальный тип pointer для всего? Почему мы обязаны писать int, double, char*?

    Дело в том, что память — это сплошной поток байтов. Адрес указывает только на начальный байт. Компьютеру нужно знать, сколько байтов нужно прочитать, начиная с этого адреса, и как их интерпретировать.

    Рассмотрим простую математическую зависимость размера памяти от типа данных:

    Где — общий размер занимаемой памяти в байтах, — количество переменных (в случае массива или одной переменной, где ), а — размер одного элемента данного типа в байтах.

    Если у нас есть указатель int* p, компилятор знает, что:

  • Нужно прочитать 4 байта (на большинстве современных архитектур).
  • Эти байты нужно интерпретировать как целое число.
  • Если бы это был char* p, компилятор прочитал бы только 1 байт и интерпретировал его как символ.

    > «Указатель без типа подобен письму без индекса: почтальон найдет дом, но не будет знать, в какую квартиру звонить и посылку какого размера вручать.»

    Нулевой указатель (nullptr)

    Работа с памятью требует дисциплины. Если вы объявили указатель, но не присвоили ему адрес, он будет указывать на случайное место в памяти (мусор). Попытка записать что-то по этому случайному адресу может привести к краху программы.

    Чтобы избежать этого, в современном C++ используется ключевое слово nullptr.

    Это гарантирует, что указатель «пуст» и никуда не указывает. Это стандарт безопасности. Перед использованием указателя хорошим тоном считается проверка:

    Итоги

    Сегодня мы заложили фундамент для всего курса. Давайте закрепим основные тезисы:

  • Память — это огромный массив пронумерованных ячеек.
  • Любая переменная имеет адрес, который можно получить через оператор &.
  • Указатель — это переменная, хранящая адрес другой переменной.
  • Чтобы получить или изменить значение по адресу, используется оператор разыменования *.
  • Тип указателя важен, так как он сообщает компилятору, сколько памяти считывать.
  • В следующей статье мы разберем арифметику указателей и узнаем, как они связаны с массивами. Готовьтесь, будет интересно!

    2. Арифметика указателей и их взаимосвязь с массивами и строками

    Арифметика указателей и их взаимосвязь с массивами и строками

    Добро пожаловать обратно на курс «Мастерство работы с указателями в C++». В предыдущей статье мы научились находить «дома» в памяти компьютера и заглядывать внутрь них с помощью операторов & и *. Мы выяснили, что указатель — это переменная, хранящая адрес.

    Но что, если нам нужно не просто стоять на месте, а перемещаться по памяти? Что, если наши данные — это не одинокий дом, а целый жилой квартал (массив)? Здесь на сцену выходит арифметика указателей.

    Это одна из самых мощных и одновременно пугающих возможностей C++. Она позволяет вам «гулять» по памяти, переходя от одной ячейки к другой. Давайте разберемся, как это работает, и почему 1 + 1 в мире указателей не всегда равно 2.

    Магия инкремента: почему +1 не добавляет 1 байт?

    В обычной арифметике, если у вас есть число 1000 и вы прибавляете к нему 1, вы получаете 1001. С указателями всё иначе.

    Вспомните формулу из прошлой лекции: тип данных определяет размер ячейки. Если у нас есть указатель на int (который обычно занимает 4 байта), то следующий int может начаться только через 4 байта, а не через 1.

    Основное правило арифметики указателей

    Когда вы прибавляете целое число к указателю, адрес увеличивается не на , а на , умноженное на размер типа данных, на который ссылается указатель.

    Математически это можно выразить следующей формулой:

    Где: * — новый адрес памяти, который мы получим. * — исходный адрес, хранящийся в указателе. * — целое число, которое мы прибавляем (смещение). * — размер типа данных в байтах (результат оператора sizeof).

    !Визуализация того, как тип данных влияет на шаг перемещения указателя по памяти.

    Рассмотрим пример в коде:

    Если первый адрес был 0x...00, то после ptr++ он станет 0x...04 (при условии, что int занимает 4 байта). Компьютер автоматически «перешагивает» через всё тело текущей переменной, чтобы попасть точно в начало следующей.

    Операции, доступные для указателей

    Не все математические действия разрешены с адресами. Вот что можно делать:

  • Сложение с целым числом (ptr + n): Сдвиг вперед на n элементов.
  • Вычитание целого числа (ptr - n): Сдвиг назад на n элементов.
  • Инкремент и декремент (ptr++, ptr--): Сдвиг на один элемент вперед или назад.
  • Вычитание двух указателей (ptr1 - ptr2): Это интересная операция. Результатом будет не адрес, а количество элементов между двумя указателями.
  • > «Вычитание указателей отвечает на вопрос

    3. Указатели и функции: передача по ссылке и возвращаемые значения

    Указатели и функции: передача по ссылке и возвращаемые значения

    Добро пожаловать на третью лекцию курса «Мастерство работы с указателями в C++». В предыдущих статьях мы научились создавать указатели, разыменовывать их и даже путешествовать по массивам с помощью адресной арифметики. Теперь пришло время применить эти знания на практике в самом сердце процедурного программирования — в функциях.

    Вы когда-нибудь задумывались, почему некоторые функции могут изменять ваши переменные, а другие — нет? Или как вернуть из функции не просто одно число, а целый набор данных без копирования? Ответ кроется в понимании того, как указатели взаимодействуют с функциями.

    Проблема передачи по значению

    Прежде чем говорить об указателях, давайте вспомним, как работает обычная передача аргументов в функцию. В C++ по умолчанию используется механизм передачи по значению (pass-by-value).

    Представьте, что вы написали эссе и хотите, чтобы друг его проверил. При передаче по значению вы делаете ксерокопию своего эссе и отдаете её другу. Друг берет красную ручку, зачеркивает ошибки, исправляет текст на копии и возвращает её вам (или просто выбрасывает). Что произошло с вашим оригиналом? Ничего. Он остался нетронутым.

    Рассмотрим код:

    Когда вызывается tryToChange(score), создается копия переменной score. Внутри функции эта копия называется x. Функция меняет x, но как только функция завершается, x уничтожается, а score остается прежним.

    Математика копирования

    Копирование безопасно, но может быть дорогостоящим для производительности, если мы передаем большие объекты (например, изображения или огромные структуры данных).

    Затраты памяти при передаче по значению можно выразить формулой:

    Где: * — общий объем памяти, затраченный на копирование. * — размер передаваемого объекта в байтах. * — количество вызовов функции (или глубина рекурсии).

    Если велик, программа начинает работать медленно и потреблять лишнюю память.

    Передача через указатель (Pass-by-Pointer)

    Теперь изменим подход. Вместо того чтобы давать другу ксерокопию эссе, вы говорите ему: «Мое эссе лежит на столе в комнате 302. Иди и исправь его там».

    Вы передали не данные, а адрес данных. Друг приходит по адресу и правит оригинал.

    В C++ это реализуется через передачу указателя в функцию. Это позволяет функции получать доступ к памяти вызывающего кода и изменять её.

    !Визуализация того, как функция получает доступ к переменной из другой области видимости через указатель.

    Синтаксис передачи

    Чтобы реализовать это, нужно сделать два изменения:

  • В объявлении функции указать, что она принимает указатель (например, int*).
  • При вызове функции передать адрес переменной (используя &).
  • Разберем, что произошло:

  • ptr в функции reallyChange получил значение адреса score (например, 0x001).
  • Оператор *ptr = 500 сказал: «Запиши 500 в ячейку по адресу 0x001».
  • Переменная score в main изменилась, так как она живет именно по этому адресу.
  • Классический пример: функция swap

    Самый известный пример использования указателей — функция swap, которая меняет местами значения двух переменных. Без указателей написать такую функцию невозможно (в рамках чистого Си-стиля).

    Указатели и массивы в функциях

    Вспомните прошлую лекцию: имя массива — это, по сути, указатель на его первый элемент. Это означает, что когда вы передаете массив в функцию, вы всегда передаете его по ссылке (через указатель), даже если не используете символ &.

    Копирование массива не происходит. Функция получает адрес первого элемента.

    Важно: Поскольку массив передается как указатель, функция не знает его размера. Именно поэтому мы почти всегда передаем второй аргумент size.

    Защита данных: const и указатели

    Иногда мы хотим передать указатель, чтобы избежать копирования большого объекта (для скорости), но не хотим, чтобы функция могла изменить этот объект. Для этого используется ключевое слово const.

    Существует три варианта использования const с указателями, и их часто путают. Давайте разберем их раз и навсегда.

    1. Указатель на константу (const int* ptr)

    Это самый частый сценарий для функций. Он означает: «Я могу менять, куда указывает стрелка, но я не могу менять данные по этому адресу».

    Используйте это, когда функция должна только читать данные.

    2. Константный указатель (int* const ptr)

    Это обратная ситуация: «Я могу менять данные, но не могу перенаправить стрелку на другой адрес».

    3. Константный указатель на константу (const int* const ptr)

    Полная блокировка. Нельзя менять ни адрес, ни данные.

    Возврат указателей из функций

    Функция может не только принимать указатели, но и возвращать их. Тип возвращаемого значения в таком случае помечается звездочкой, например int*.

    Это полезно, когда нужно вернуть ссылку на элемент внутри структуры данных.

    Смертельная ловушка: возврат адреса локальной переменной

    Здесь кроется одна из самых коварных ошибок в C++. Никогда не возвращайте адрес локальной переменной!

    Почему?

    Локальные переменные живут в области памяти, называемой стек. Когда функция завершается, её часть стека «сворачивается», и память, которую занимали локальные переменные, помечается как свободная. Она может быть тут же перезаписана другой функцией.

    Рассмотрим ошибочный код:

    !Иллюстрация проблемы висячего указателя при возврате адреса локальной переменной.

    Если вам нужно создать объект внутри функции и вернуть его, чтобы он жил и после завершения функции, вам потребуется динамическая память (куча). Но это тема нашей следующей большой статьи.

    Указатели vs Ссылки (References)

    В C++ есть альтернатива указателям — ссылки (обозначаются & при объявлении, например void func(int& x)). Ссылки безопаснее и удобнее синтаксически (не нужно писать * и & везде), но под капотом они часто реализованы как те же самые указатели.

    Почему мы учим указатели, если есть ссылки?

  • Указатели могут быть nullptr (ссылка всегда должна на что-то указывать).
  • Указатели поддерживают арифметику (перемещение по массиву).
  • Понимание указателей необходимо для работы с динамической памятью и системным API.
  • Итоги

    Сегодня мы сделали огромный шаг вперед, научившись связывать функции и данные через адреса.

    Ключевые моменты:

  • Передача по значению копирует данные, что безопасно, но может быть медленно.
  • Передача по указателю передает адрес, позволяя функции менять оригинал и избегать копирования.
  • Используйте const, чтобы защитить данные от случайного изменения внутри функции.
  • Массивы всегда передаются как указатели.
  • Никогда не возвращайте указатель на локальную переменную функции.
  • В следующей статье мы разберем тему, которая открывает истинную мощь C++: динамическое выделение памяти (new и delete). Вы научитесь создавать массивы любого размера прямо во время работы программы.

    А пока — выполните домашнее задание, чтобы закрепить материал!

    4. Динамическая память: работа с кучей, операторы new и delete

    Динамическая память: работа с кучей, операторы new и delete

    Добро пожаловать на четвертую лекцию курса «Мастерство работы с указателями в C++». В предыдущей статье мы столкнулись с серьезным ограничением: мы не могли вернуть указатель на локальную переменную из функции, потому что локальные данные уничтожаются при выходе из функции. Кроме того, мы всегда должны были знать размер массива заранее, на этапе написания кода.

    Сегодня мы снимем эти оковы. Мы входим в мир динамической памяти.

    Стек и Куча: два мира памяти

    Чтобы понять, как работает динамическая память, нужно разобраться, как программа организует свое рабочее пространство. Память, доступная вашей программе, делится на две основные области: Стек (Stack) и Куча (Heap).

    Стек (Stack)

    Это область памяти для автоматических переменных. Именно здесь живут все переменные, которые вы создавали ранее (например, int x = 10;).

    * Управление: Автоматическое. Вы входите в функцию — память выделяется. Выходите — память освобождается. * Скорость: Очень быстрая. * Размер: Ограничен (обычно несколько мегабайт). * Ограничение: Переменные живут только пока выполняется блок кода, в котором они объявлены.

    Куча (Heap)

    Это огромное пространство свободной памяти, которым вы управляете вручную. Это «дикий запад» программирования.

    * Управление: Ручное. Вы сами просите память и сами ее возвращаете. * Скорость: Чуть медленнее стека (из-за сложности поиска свободного места). * Размер: Ограничен только физической памятью компьютера (гигабайты). * Преимущество: Данные живут столько, сколько вы захотите — хоть до конца работы программы.

    !Визуализация различий между упорядоченным Стеком и свободной Кучей.

    Оператор new: выделение памяти

    В C++ для работы с кучей используется оператор new. Его задача — найти в куче свободный участок памяти нужного размера и вернуть его адрес.

    Синтаксис прост:

    Что здесь происходит:

  • Оператор new обращается к операционной системе: «Мне нужно место под один int (4 байта)».
  • Система находит свободный адрес в куче (например, 0x5000).
  • new возвращает этот адрес.
  • Мы сохраняем этот адрес в указателе ptr.
  • Обратите внимание: сам указатель ptr находится на стеке (как обычная переменная), но он хранит адрес данных, которые лежат в куче.

    Инициализация

    Вы можете сразу записать значение в выделенную память:

    Оператор delete: освобождение памяти

    В отличие от стека, память в куче не очищается автоматически. Если вы взяли память, вы обязаны ее вернуть. Для этого используется оператор delete.

    Этот оператор говорит системе: «Я закончил работать с адресом, который хранится в ptr. Можешь забрать эту память обратно».

    Утечки памяти (Memory Leaks)

    Что произойдет, если вы не вызовете delete? Произойдет утечка памяти. Ваша программа будет занимать все больше и больше оперативной памяти, пока система не закроет ее принудительно или компьютер не зависнет.

    Рассмотрим математическую модель утечки памяти в цикле:

    Где — общий объем потерянной памяти, — количество итераций цикла (сколько раз мы выделили память без удаления), а — размер одного объекта в байтах. Даже если мал, при большом утечка может стать фатальной.

    Пример утечки:

    > «Утечка памяти похожа на то, как если бы вы брали книги в библиотеке и никогда их не возвращали. Рано или поздно полки опустеют, и библиотекарь выгонит вас.»

    Динамические массивы

    Самая частая причина использования кучи — необходимость создать массив, размер которого мы узнаем только во время работы программы (например, вводит пользователь).

    Создание динамического массива

    Удаление динамического массива

    Здесь кроется важный нюанс. Чтобы удалить массив, нужно использовать специальную форму оператора delete с квадратными скобками [].

    !Иллюстрация важности использования квадратных скобок при удалении массивов.

    Опасность висячих указателей (Dangling Pointers)

    После того как вы вызвали delete ptr, переменная ptr никуда не исчезает. Она все еще хранит тот же самый адрес (например, 0x5000). Но память по этому адресу уже не ваша. Она может быть пустой, а может быть уже занята другими данными.

    Попытка обратиться к *ptr после удаления называется использованием висячего указателя. Это приводит к непредсказуемому поведению или краху программы.

    Правило безопасности: Сразу после удаления присваивайте указателю значение nullptr.

    Это гарантирует, что если вы случайно попытаетесь использовать ptr снова, программа упадет сразу и явно (что хорошо для отладки), а не будет тихо портить данные.

    Возврат динамической памяти из функций

    Теперь мы можем решить проблему из прошлой лекции. Мы можем создать объект в функции и вернуть его, так как куча не очищается при выходе из функции.

    Итоги

    Работа с динамической памятью дает огромную власть, но налагает большую ответственность.

  • Стек — для временных переменных, Куча — для долгоживущих объектов и массивов переменного размера.
  • new выделяет память, delete освобождает ее.
  • Для массивов всегда используйте пару new[] и delete[].
  • Всегда удаляйте то, что выделили, чтобы избежать утечек памяти.
  • После удаления зануляйте указатель (ptr = nullptr), чтобы избежать ошибок.
  • В следующей статье мы рассмотрим, как современные стандарты C++ помогают автоматизировать этот процесс с помощью «умных указателей», чтобы вам не приходилось вызывать delete вручную. Но чтобы понять их магию, вы должны были пройти этот путь ручного управления.

    5. Современный C++: умные указатели unique_ptr, shared_ptr и управление ресурсами

    Современный C++: умные указатели unique_ptr, shared_ptr и управление ресурсами

    Добро пожаловать на пятую, и, пожалуй, самую важную статью курса «Мастерство работы с указателями в C++». В прошлой лекции мы погрузились в опасные воды динамической памяти. Мы научились использовать new для создания миров и delete для их разрушения. Но мы также узнали горькую правду: один забытый delete может привести к утечке памяти, а один лишний delete — к краху программы.

    Программисты на C++ жили с этой болью десятилетиями, пока не был выработан современный подход. Сегодня мы отправим «сырые» указатели (raw pointers) на пенсию (или, по крайней мере, переведем их на легкий труд) и познакомимся с умными указателями.

    Философия RAII: Ресурс — это ответственность

    Прежде чем мы перейдем к коду, нужно понять философию, на которой строится современный C++. Она называется RAII (Resource Acquisition Is Initialization — Получение ресурса есть инициализация).

    Звучит сложно, но суть проста: жизнь ресурса должна быть привязана к жизни переменной на стеке.

    Вспомните, как работает стек: когда функция завершается, все локальные переменные уничтожаются автоматически. Умный указатель — это обертка (класс), которая хранит «сырой» адрес внутри себя.

  • В конструкторе он получает память (делает new).
  • В деструкторе он освобождает память (делает delete).
  • Поскольку деструктор локальной переменной вызывается автоматически при выходе из области видимости, нам больше никогда не придется писать delete вручную.

    std::unique_ptr: Единоличный владелец

    Первый и самый популярный умный указатель — std::unique_ptr. Его имя говорит само за себя: он гарантирует, что у объекта в памяти есть только один владелец.

    Представьте, что это сейф, ключ от которого может быть только у одного человека. Вы не можете сделать дубликат ключа (скопировать указатель), вы можете только передать его другому (переместить).

    Как создать unique_ptr

    Для работы с умными указателями нужно подключить библиотеку <memory>. Начиная с C++14, правильный способ создания такого указателя — функция std::make_unique.

    Запрет копирования

    Попытка скопировать unique_ptr приведет к ошибке компиляции. Это защита от дурака: если бы у нас было две копии уникального указателя, оба попытались бы удалить одну и ту же память в конце программы, что привело бы к ошибке.

    Передача владения (Move semantics)

    Если вы хотите передать ресурс другому указателю или в функцию, вы должны явно сказать: «Я отдаю это». Для этого используется функция std::move.

    !Визуализация перемещения владения ресурсом: данные не копируются, меняется только владелец.

    Когда использовать: В 90% случаев. Если вам просто нужно выделить память в куче и вы не планируете делить этот объект между несколькими частями программы одновременно.

    std::shared_ptr: Совместное владение

    Иногда данные действительно нужны в нескольких местах одновременно. Например, у вас есть текстура в игре, которую используют десять разных монстров. Удалять текстуру можно только тогда, когда погибнет последний монстр.

    Здесь на сцену выходит std::shared_ptr. Он использует технологию подсчета ссылок (Reference Counting).

    Математика подсчета ссылок

    Внутри shared_ptr есть не только указатель на данные, но и указатель на специальный «контрольный блок», где хранится счетчик владельцев.

    Формулу изменения счетчика можно представить так:

    Где — новое значение счетчика ссылок, — текущее значение, а — изменение ( при создании копии указателя, при уничтожении указателя). Память освобождается только тогда, когда становится равным .

    Использование shared_ptr

    Создается он через std::make_shared.

    !Принцип работы shared_ptr: объект живет, пока его держит хотя бы одна «веревка» (указатель).

    Цена удобства: shared_ptr работает медленнее и занимает больше памяти, чем unique_ptr, из-за необходимости поддерживать контрольный блок и обеспечивать потокобезопасность счетчика.

    Проблема циклических ссылок и std::weak_ptr

    shared_ptr кажется идеальным, но у него есть Ахиллесова пята. Это циклические ссылки.

    Представьте ситуацию: * Объект А хранит shared_ptr на объект Б. * Объект Б хранит shared_ptr на объект А.

    Даже если вы удалите все внешние указатели на них, они будут держать друг друга. Их счетчики ссылок никогда не станут нулем. Это классическая утечка памяти, но уже с умными указателями.

    Решение: Слабый указатель

    Для разрыва таких порочных кругов существует std::weak_ptr. Это «наблюдатель». Он может смотреть на объект, управляемый shared_ptr, но не владеет им и не увеличивает счетчик ссылок.

    Если объект будет удален, weak_ptr просто «протухнет», но не помешает удалению.

    Чтобы использовать данные из weak_ptr, его нужно временно превратить в shared_ptr с помощью метода .lock(), предварительно проверив, жив ли объект.

    Итоги и правило большого пальца

    Мы прошли долгий путь от ручного управления памятью до автоматизированных систем современного C++. Давайте подведем итог, как выбирать инструмент:

  • По умолчанию всегда используйте std::unique_ptr. Это быстро, безопасно и покрывает большинство задач.
  • Используйте std::shared_ptr только если вам действительно нужно несколько владельцев для одного объекта.
  • Используйте std::weak_ptr, чтобы разорвать циклы ссылок или для кэширования, когда объект может исчезнуть в любой момент.
  • Используйте «сырые» указатели (int*) только для наблюдения и передачи в функции, которые не владеют памятью (как мы делали в статье про функции), или для взаимодействия со старыми библиотеками C.
  • Теперь вы владеете полным арсеналом работы с памятью. Вы знаете, как она устроена на низком уровне, и умеете управлять ею безопасно на высоком уровне. Это и есть мастерство.

    В следующем (и заключительном) блоке заданий вы проверите, насколько хорошо усвоили разницу между этими типами указателей. Удачи!