[БЕЗ_ЗВУКА] В предыдущем видео мы с вами познакомились с соглашениями по владению динамическими объектами, а также узнали, что этим соглашениям должен следовать программист, но формально он может этого не делать. И если он этого не делает, то могут возникнуть определенные проблемы. Давайте с вами посмотрим примеры, какие именно. Вот первый пример такой программы. Вопрос: как вы думаете, как отработает данная программа? Это очень простая программа, здесь мы создаем динамический объект и потом его не удаляем. Очевидно, здесь будет утечка. Причем в данной программе мы нарушили наше соглашение — мы получили владеющий сырой указатель, потому что этот ptr нам нужно было на нем вызвать delete, что мы забыли сделать. Посмотрим следующий пример. Вот такая программа, чуть-чуть посложнее. Как вы думаете, как она отработает? В этой программе мы уже вызываем delete, после того как мы вызвали new, но дело в том, что до вызова delete у нас присутствует некоторое условие, причем в результате выполнения этого условия мы можем выйти из функции, и delete не будет вызван. Здесь функция вызывается как раз с таким аргументом, что у нас условие будет выполнено и до delete мы не дойдем. То есть в данном случае у нас опять же будет утечка. Этот пример может выглядеть немного надуманным, но на практике вот именно такая проблема и встречается чаще всего. Как это происходит? Функция обычно начинается с того, что она достаточно небольшая. Где-то в начале этой функции мы объект создаем, в конце мы его удаляем, сама функция пять строчек, очевидно, что все будет хорошо. Никакой утечки не будет. Потом ваш проект эволюционирует, эта функция увеличивается. В ней в какой-то момент становится уже сотни строчек, на ней начинают работать люди, которые даже не знают тех, кто изначально ее написал, и дальше в какой-то момент кто-то вставляет посередине вот такое условие — с досрочным выходом из функции. И забывает посмотреть, что у нас где-то там находится delete, его тоже нужно вызвать. И у нас получается вот такая ситуация — возникает утечка. Здесь мы нарушили то же самое соглашение, что и в предыдущем примере: мы создали владеющий сырой указатель, потому что здесь мы как бы сами его удаляем, то есть мы оставляем на нем ответственность за то, чтобы объект был удален. Посмотрим следующий пример. Здесь у нас уже появляются умные указатели. Как вы думаете, как отработает данная программа? Несмотря на то, что мы здесь используем умные указатели, все равно программа отработает неправильно. Недостаточно просто использовать умные указатели, нужно использовать их правильно. Что у нас здесь происходит? Мы создаем новый динамический объект, сохраняем его в сыром указателе. А потом передаем этот сырой указатель в конструктор двух unique_ptr'ов. И у нас получается, что два unique_ptr'а указывают на один и тот же объект. Но unique_ptr — это очень простой класс, он в своем деструкторе смотрит: объект есть какой-то, на который я указываю? Есть. Значит, я его удалю. И оба unique_ptr'а в данном случае попытаются удалить объект. Это приведет к некорректной работе программы. Здесь мы нарушили соглашение об эксклюзивном владении unique_ptr'ами. Мы знаем, что одним объектом должен владеть только один unique_ptr. Здесь мы его нарушили и получили проблему. Посмотрим следующий пример. Здесь все то же самое, но вместо unique_ptr'ов используются shared_ptr'ы. Как вы думаете, как отработает данная программа? Несмотря на то, что, казалось бы, shared_ptr как раз и нужен для того, чтобы организовать разделяемое владение и несколько shared_ptr'ов действительно могут ссылаться на один и тот же объект, здесь ситуация гораздо более интересная. Мы создали новый объект, поместили его в сырой указатель. И опять же, мы передаем сырой указатель в конструктор двух shared_ptr'ов. Когда мы создаем первый shared_ptr, он видит: сырой указатель, отлично, создам для него контрольный блок, все хорошо. Когда мы создаем второй shared_ptr, у него нет никакой возможности узнать, что есть уже какой-то другой shared_ptr, который указывает на тот же самый объект, поэтому он спокойно создаст еще один контрольный блок и будет указывать на тот же самый объект. И у нас получится два никак не связанных shared_ptr'а, каждый со своим контрольным блоком, которые указывают на один и тот же объект. И каждый из них, когда будет удаляться, будет считать, что на этот объект больше никто не указывает, и попытается его удалить. У нас, опять же, здесь произойдет, как и в предыдущем примере, двойное удаление. В этой программе мы более тонко нарушили наше соглашение, на самом деле вот этот ptr в данном случае является у нас владеющим. Мы об этом чуть подробнее поговорим попозже. Давайте рассмотрим следующий пример. Как вы думаете, как отработает данная программа? Эта программа похожа на предыдущую, но она уже отработает корректно, потому что здесь у нас создаются два умных указателя, оба создаются из сырых указателей, но каждый раз у нас создается новый динамический объект. То есть здесь у нас все будет хорошо: unique_ptr будет указывать на свой объект, shared_ptr будет указывать на свой объект. Каждый из них его удалит. То есть вроде бы проблем нет. Но давайте подумаем, что будет, если кто-то вызовет функцию MakeA и забудет передать этот указатель в конструктор умного указателя. Вот в этом случае у нас может произойти утечка, может быть точно такая же ситуация, которую мы рассмотрели в первых нескольких примерах. Поэтому несмотря на то, что эта программа работает корректно, она нарушает соглашение. Она создает владеющий сырой указатель, который возвращает функцию MakeA. Ее следует переписать с выполнением наших соглашений, например, вот таким образом: здесь функция MakeA возвращает unique_ptr. То есть она в явном виде артикулирует, что владение этим объектом передается на вызывающую сторону. И дальше мы либо этот unique_ptr сохраняем в unique_ptr, либо мы конвертируем его в shared_ptr, то есть мы можем взять и создать shared_ptr из unique_ptr. Это абсолютно корректная ситуация. И вот такая программа работает точно так же, корректно, но уже следует нашим соглашениям. Именно так и следует писать. И давайте посмотрим еще один, самый интересный пример. Как вы думаете, как отработает такая программа? В этой программе, несмотря на то, что мы даже не писали в явном виде new и delete, мы с вами все равно умудрились нарушить соглашение и создать такую ситуацию, что произойдет двойное удаление. Смотрите, что получается. Мы создаем новый динамический объект с помощью функции make_unique. Все замечательно. Получаем unique_ptr up1. После этого мы достаем из него сырой указатель и подаем его в конструктор второго unique_ptr. И у нас опять получается ситуация, когда два unique_ptr'а указывают на один и тот же объект. В итоге это, конечно, приведет к двойному удалению. Идея здесь в том, что указатель, который передается в конструктор умного указателя, трактуется как владеющий. То есть в данном случае сырой указатель, который возвращает get, был трактован как владеющий. Давайте подведем небольшой итог. Мы рассмотрели соглашение по владению динамическими объектами. И после этого привели множество примеров, как эти соглашения можно нарушить. Причем некоторые из них были не совсем очевидными, как, например, последний пример. Давайте теперь сформулируем несколько практических положений, то есть что на практике означают эти соглашения, что это значит на уровне написания кода. На самом деле это означает, во-первых, не нужно использовать new и delete, как мы это уже знаем. Поскольку нельзя использовать new для создания объектов, нужно использовать функции make_unique и make_shared. И последнее: нельзя использовать конструкторы умных указателей, которые принимают сырые указатели. То есть нельзя создавать умные указатели напрямую из сырых. А это ровно то, что мы сделали в последнем примере, потому что вот такой конструктор, он трактует сырой указатель, который ему передают, как владеющий, ведь он же сейчас создаст умный указатель, который будет владеть этим объектом. А кто им владел до того? Значит, это сырой указатель. Давайте даже зафиксируем это на вот такой интересной картинке. Значит, у нас есть new, delete, и у нас есть конструкторы умных указателей, принимающих сырые указатели. new и конструкторы скрываются от нас в недрах функций make_unique и make_shared. То есть new и конструкторы — это некоторый низкоуровневый инструмент управления, которым пользоваться не нужно. Вместо этого нужно пользоваться высокоуровневым — функциями make_unique и make_shared. Это ровно то, что они делают: создают объект и конструируют умные указатели из указателя на динамический объект. А вот вызов delete скрывается от нас в деструкторах умных указателей. То есть, опять же, delete мы напрямую не вызываем. Мы полагаемся на то, что этим займутся за нас умные указатели. Хорошо. Подведем итог. Мы с вами выяснили, что есть два вида объектов, с точки зрения времени их жизни: автоматические и динамические. Мы знаем, что владение автоматических объектов берет на себя компилятор и существуют строгие правила, которым он следует. А вот владение динамическими объектами описывается соглашениями, которым следует программист. Но эти соглашения могут быть нарушены. И их нарушение может привести к проблемам, как мы видели. Если же им следовать, то тогда мы гарантируем, что в нашей программе будут отсутствовать утечки памяти и отсутствовать двойные удаления. В следующем видео мы с вами вернемся немного назад к практике и посмотрим на присваивание умных указателей.