C++. placement new или как врезать объект в нужный участок памяти

  • placement new не выделяет память, а встраивает что-то уже существующее в заказанный нами участок памяти
  • Одна из разновидностей new ищет блок памяти, имеющий достаточный размер, хватающий под потребность программы.
  • Разновидность new, именуемая placement new, позволяет выбирать лично самому программисту блок памяти, подходящий под потребность программы.
Разновидности new новичок может легко перепутать. Но тем не менее, и разобраться новичок вполне способен относительно просто.
Обычно мы начиная учить С++ достаточно быстро приходим к необходимости выделения памяти под массив через указательные переменные. Да и потом указательные переменные постоянно встают на нашем пути. Когда мы выделяем память под обычную переменную или под массив, то используем разновидность new, запрашивающую свободный блок памяти:
Эта форма выделения должна быть нам уже прилично знакома. Операция new способствует поиску в памяти компьютера участков памяти, в одном случае (для ptr1) хватающего для sizeof(int), в другом (для ptr2) хватающего для sizeof(int * 10). В случае нахождения такого блока памяти new возвращает указатель на начало найденного блока. В случае неудачи возвращается или указатель на нулевой адрес или исключение bad_alloc. Но мы сейчас сосредоточены на выделениях и использованиях, а не на исключениях, поэтому об исключении сейчас умолчится.
  • Обычное new как ищейка, которой сказали найди, и она нашла, если есть.
В отличие от наиболее знакомой формы, в placement new мы сами указываем адрес. При использовании этой разновидности new нужно в круглых скобках указать аргумент, которым будет укажен нужный адрес, и после этого указать тип, ради которого происходит работа с памятью. В целом, синтаксис new остаётся таким же, просто между непосредственно new и конечным типом используются круглые скобки, аргументом в которых выбирается нужный адрес. Для того, чтобы использовать new placement, обязательно включить заголовочный файл new. Конечно, оно в некоторых реализациях компиляторов может и без этого работать, но это не гарантируется.


Массивы умеют неявно приводить название к указателью на первый элемент, поэтому в листинге #1.2 указывать адрес через & необязательно. Также непосредственные указатели часто выступают в роли массивов, тут важно, что массивы не указатели. Если мы можем явно взять адрес массива, что у меня проделано в листинге #1.2, и это будет адрес первого элемента массива, то если брать адрес указателя, то это будет адрес указателя, но не адрес первого элемента массива, на который указатель направлен. Поэтому в листинге #1.2 лучше использовать умение массивов неявно приводить свои имена к указательной переменной и выписывать в аргумент не адрес, а название:

Если бы мы использовали вариант с явным подпихиванием адреса, то из-за того, что вместо адреса начала массива мы бы подпихивали непосредственный адрес указателя, следующий код не сработал бы:


По этой причине вы часто не будете наблюдать явного указания адреса при placement new. Это один из немного слегка путающих факторов, но, надеюсь, теперь понятно, что несмотря на то, что оно иногда не выглядит как явное указание адреса в круглых скобках, в конечном счёте оно всё равно всего лишь указание адреса и есть.
Память компьютеров линейна. Это значит, что память подобна одномерному массиву. Это, в свою очередь, означает, что мы можем эмулировать выделение в памяти путём использования массива как памяти. Такой пример эмуляции с целью объяснения placement new очень распространён:


Когда мы используем память, очень важно следить чтобы то, что мы запихиваем в ячейки, поместилось в доступные ячейки. Поскольку тип double скорее всего больше char, мы используем простую формулу, чтобы эмулировать по-настоящему достаточное количество ячеек под хранение массива double в массиве, который выглядит как массив символов.
Используя placement new мы можем дополнять значениями не только обычные переменные и массивы, любые объекты вообще. Любая из форм new вызывает конструкторы объектов, и это очень важно:

В листинге #4 можно обратить внимание на то, что в певых строчках сработало два конструктора. Последние две строчки можно было бы ожидать срабатывание двух деструкторов, но сработал только один.
  • placement new — это реконструирование объекта. Использованием этой формы new мы напрямую вмешиваемся в уже собранную в памяти структуру.
Для лучшей наглядности изобразим факт несрабатывания одного деструктора из-за использования placement new, сократив показанный код до минимума:


В листинге #5 объект уже был сконструирован, но потом использованием placement new мы вмешались в структуру уже сконструриованного программой объекта, всё, что было, переписали. Своеобразое очень жёсткое хирургическое вмешательство получилось. В итоге в объекте не осталось информации, как разрушать объект, однажды уже построенный, но появилась другая информация, как разрушать реконструированный. Да даже сам факт несрабатывания одного деструктора говорит о том, что если бы в объекте, сконструированным поначалу, происходило бы динамическое выделение памяти, то стёртый деструктор обозначал бы утечку памяти. Какой можно сделать вывод? Что в уже сконструированные объекты вмешательство с помощью placement new опасная затея. Но что можно сделать, чтобы этого избежать? Ведь если placement new существует, то, значит, оно кому-то нужно, а если оно кому-то нужно, люди как-то его используют.
  • Используйте placement new для реконструирования сырой памяти, которая была просто отведена
Если говорить образно, то сырая память как глина из которой можно собрать разные фигурки (они же — объекты). Но если фигурка уже собрана, то реконструкция фигурки дело неблагородное. Если мы собрали лошадку, а потом из лошадки делаем кувшин, то что-то мы не то явно делаем. Видите, даже если провести подобные параллели, то получается, что в однажды сконструированный объект реконструкций делать не стоит. Но чтобы лепить объект из сырой памяти нам всего-то и нужно не давать конструкторам сработать. Это возможно разными способами: использовать функции С, либо использовать функцию С++, называемую operator new.
В С++ не у всех типов есть конструкторы. Так, например, конструкторов у встроенных типов и у массивов нет, поэтому примеры, когда с помощью placement new массивы как бы реконструировались — это нормально. Но если бы это был, например, не Си-массив, а std::vector, то поскольку таковой является объектом класса, и у него имеются конструкторы, то при реконструкции однажды уже сконструированного вектора можно сразу расчитывать на утечку. Надеюсь, что понятно донёс идею, что если конструктор есть, и он уже отработал, то реконструировать не стоит.
Функция operator new — это не столько разновидность new, сколько самая обычная функция. Разновидности new, обычное new и placement new объединяют названием new-expression, а функцию выделяют как operator new. Так вот, в отличие от разновидностей new функция operator new не провоцирует запуск конструкторов, а просто даёт сырой блок памяти, с которым мы можем делать, что захотим:



В листинге #6 мы использовали функцию, которая помогла нам просто взять память, при этом дополнительных телодвижений никаких не провоцирующую. В этой выбранной памяти мы сконструировали объект типа MyClass. Поскольку объект сконструирован единожды, то реконструкции непосредственно объекта не произошло (можно сказать о реконструкции памяти, но не о реконструкции объекта), то информация о разрушении, описанная в деструкторе, осталась непереписанной. Это очень важно, потому что теперь мы можем правильно объект разобрать, когда он прекратит быть нужным программе. Но есть одно но: деструктор нужно вызывать явно. new expression провоцирует работу конструкторов, но не деструкторов. Кроме того, что мы должны вручную запускать деструктор где-то сконструированного нашего объекта, мы должны высвобождать память, которую мы выбирали при помощи функции operator new для программы. Как вы можете догадаться, поскольку требуется ручное управление очисткой, следить за этими делами нужно очень внимаетельно.
  • операция placement new просто использует адрес, переданный в качестве аргумента; она не анализирует, свободна ли указанная область памяти, а также не ищет блок неиспользуемой памяти. В результате часть забот об управлении памятью возлагается на программиста.
Если вы используете Си-массив как площадку для конструирования объекта, то в зависимости от того, какой это массив (как мы помним, это может быть самый обычный массив, либо же указатель может указывать на массив, который был создан с помощью обычной формы new), операцию delete [] может быть или нужно использовать или нельзя. На самом деле к этому моменту это можно уже было успеть понять, но если вдруг кто-то не догадается, только ради него это упоминание:


  • Программист может использовать placement new для построения собственных процедур управления памятью, для взаимодействия с оборудованием, доступ к которому осуществляется по определенному адресу, или для создания объектов в конкретной ячейке памяти.
  • Объекты, созданные операцией placement new, должны удаляться в порядке, обратном порядку их создания. Причина в том, что более поздний объект может зависеть от более ранних. А буфер, используемый для хранения объектов, можно освободить только после уничтожения всех содержащихся в нем объектов.

Выбирая нужный участок памяти мы можем заполнять этот участок нужными значениями. Но очень важно следить, чтобы выбранный участок действительно был занят программой. Иначе, если выбранный участок, программой не используется, то поведение будет непредсказуемо. Это сейчас к тому, что если используем, например, указатели, но площадка под использование памяти предварительно не подготовлена, то нельзя использовать память, никак не связанную программой. Если же память используется программой, то заполнять значениями эту память можно или используя непосредственные конструкторы объектов или функциональную нотацию для, например, встроенных типов (int,double…), у которых нет конструкторов. Функциональная нотация помогает маскировать отсутствие конструкторов.
Но для начала инициализируем обычную переменную с помощью функциональной нотации, чтобы было понятно, что это такое:


Так же можно и присваивать. Это немного напоминает приведения типов в стиле языка С и в то же время напоминает инициацию конструктора с одним параметром, но больше походит на вызов функции с подсовыванием ей одного аргумента. Это называется функциональной нотацией, и если у тип не может иметь конструкторов, то будет вызвана именно функция. Раньше нельзя было такого проделывать для массивов. Поэтому использование placement new для заполнения немассивов и массивов немного отличается. Заполнять выбранный участок для немассивов можно на лету, как бы инициализируя выбранный участок на ходу сконструированым объектом:

В отличие от встроенных типов, у которых нет конструкторов, у объектов классов в большинстве случаев конструкторы есть, в частности, есть конструктор копирования, поэтому недостаток вида неумения копирования у объектов классов компенсируется конструктором копировани. Хоть мы и использовали сейчас функциональную нотацию, близко напоминающую конструктор с одним параметром, всё-таки конструкторы объектов классов дают больше возможностей. Для объектов класса в конечном счёте всё происходит по тому же синтаксису, но для них это уже не функциональная нотация, а использование конструкторов.


Разумеется, для объектов класса можно использовать любой доступный объекту конструктор. Поскольку в листинге #10 мы не выделяли память с помощью разновидностей new, т. е. не выбирали памятьиз кучи, то и delete нам не нужен. Но поскольку мы выбирали сырой участок памяти путём использования функции operator new, нам нужно высвободить заимствованную память путём использования функции operator delete. Надеюсь, с этим понятно. Важно очень внимательно следить за правильным высвобождением.
Вернёмся к наиболее простым примерам. new placement часто называют new с размещением. Сначала мы определяем участок памяти, которого должно хватить для размещения в нём встраиваемого объекта, а потом подставить в эту память или уже существующий или свежеиспечённый объект:


Листинг #11 показывает механизм работы placement new. В данном случае p сырой указатель. Непосредственно ему память мы не выделяем. Но мы выделяем память переменной a и сразу в переменную записываем значение. После этого мы копируем структуру созданной переменной в участок p. Поскольку мы просто скопировали структуру, а не вклинивались в a, изменение значения, с которым связан указатель p, это изменение копии. А поскольку копия это всего-лишь отдельная сущность, то оригинальная переменная не изменила своего значения. Сам этот процесс напоминает отложенную инициализацию.
Также можно проделывать и с массивами, но надо учитывать, что непосредственно массив в массив скопировать нельзя. Поэтому чтобы использовать placement new к массиву, необходимо указателю подготовить сырой памяти:


Если использовать placement new для размещения нескольких объектов в одну подготовленную зону памяти, то важно следить и за тем, чтобы размещаемые элементы уместились в доступные ячейки взятой памяти, и чтобы не происходило перекрытия. Перекрытие — это когда происходит или полный или частичный наезд одного объекта на другой, из-за чего "объект заднего плана" теряет часть своей информации и становится непригодным к использованию. Чтобы не происходило перекрытия, нужно смещать размещаемые объекты относительно занятых расположений. Это можно делать так:



В строчке:

Мы используем адресную арифметику, чтобы сместить начальную точку размещения в незанятое пространство. Т. е сейчас мы берём адрес начала массива и делаем прыжок через занятое объектом *object пространство. Поскольку в массиве, выступающем в роли памяти места достаточно и объекты не наезжают на территории друг друга, то всё хорошо. А по той причине, что посредством new placement деструкторы не вызываются, несмотря на то, что сейчас деструкторы в каждом классе генерируются неявно, явно их зазываем.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Поиск

 
     

Случайная книга в электронном формате

https://www.litres.ru/maks-shlee/qt-5-3-professionalnoe-programmirovanie-na-c-19236076/?lfrom=15589587
Яндекс.Метрика