placement new не выделяет память, а встраивает что-то уже существующее в заказанный нами участок памяти
Одна из разновидностей new ищет блок памяти, имеющий достаточный размер, хватающий под потребность программы.
Разновидность new, именуемая placement new, позволяет выбирать лично самому программисту блок памяти, подходящий под потребность программы.
Разновидности new новичок может легко перепутать. Но тем не менее, и разобраться новичок вполне способен относительно просто.
Обычно мы начиная учить С++ достаточно быстро приходим к необходимости выделения памяти под массив через указательные переменные. Да и потом указательные переменные постоянно встают на нашем пути. Когда мы выделяем память под обычную переменную или под массив, то используем разновидность new, запрашивающую свободный блок памяти:
C++
1
2
3
4
5
6
//new
int*ptr1=newint;//используем обычную форму new
int*ptr2=newint[10];//используем обычную форму new
delete[]ptr2;
delete ptr;
Эта форма выделения должна быть нам уже прилично знакома. Операция new способствует поиску в памяти компьютера участков памяти, в одном случае (для ptr1) хватающего для sizeof(int), в другом (для ptr2) хватающего для sizeof(int * 10). В случае нахождения такого блока памяти new возвращает указатель на начало найденного блока. В случае неудачи возвращается или указатель на нулевой адрес или исключение bad_alloc. Но мы сейчас сосредоточены на выделениях и использованиях, а не на исключениях, поэтому об исключении сейчас умолчится.
Обычное new как ищейка, которой сказали найди, и она нашла, если есть.
В отличие от наиболее знакомой формы, в placement new мы сами указываем адрес. При использовании этой разновидности new нужно в круглых скобках указать аргумент, которым будет укажен нужный адрес, и после этого указать тип, ради которого происходит работа с памятью. В целом, синтаксис new остаётся таким же, просто между непосредственно new и конечным типом используются круглые скобки, аргументом в которых выбирается нужный адрес. Для того, чтобы использовать new placement, обязательно включить заголовочный файл new. Конечно, оно в некоторых реализациях компиляторов может и без этого работать, но это не гарантируется.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Листинг #1.1 new placement для int
#include <iostream>
#include <new>
usingstd::cout;
intmain(){
inta;
int*ptr=new(&a) int;//использован placement new
(*ptr)++;
cout<<a<<'\n';
}
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Листинг #1.2 new placement для int[10]
#include <iostream>
#include <new>
usingstd::cout;
intmain(){
inta[10];
int*ptr=new(&a) int[10];//использован placement new
for(inti=0;i<10;i++)ptr[i]=i;//посредством указателя заполнили массив а
for(inti=0;i<10;i++)cout<<a[i]<<'\t';//вывели массив а на экран
}
Массивы умеют неявно приводить название к указателью на первый элемент, поэтому в листинге #1.2 указывать адрес через & необязательно. Также непосредственные указатели часто выступают в роли массивов, тут важно, что массивы не указатели. Если мы можем явно взять адрес массива, что у меня проделано в листинге #1.2, и это будет адрес первого элемента массива, то если брать адрес указателя, то это будет адрес указателя, но не адрес первого элемента массива, на который указатель направлен. Поэтому в листинге #1.2 лучше использовать умение массивов неявно приводить свои имена к указательной переменной и выписывать в аргумент не адрес, а название:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Листинг #1.3 new placement для int[10]
#include <iostream>
#include <new>
usingstd::cout;
intmain(){
inta[10];
int*ptr=new(a)int[10];//использован placement new Имя массива неявно привелось у указателю на первый элемент массива
for(inti=0;i<10;i++)ptr[i]=i;//посредством указателя заполнили массив а
for(inti=0;i<10;i++)cout<<a[i]<<'\t';//вывели массив а на экран
}
Если бы мы использовали вариант с явным подпихиванием адреса, то из-за того, что вместо адреса начала массива мы бы подпихивали непосредственный адрес указателя, следующий код не сработал бы:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Листинг #2.1 new placement
#include <iostream>
#include <new>
usingstd::cout;
intmain(){
int*a=newint[10];//нашли блок памяти в куче и направили туда указатель а
int*ptr=new(a)int[10];//использован placement new (если в круглых скобках указать &a, то происходит сбой)
for(inti=0;i<10;i++)ptr[i]=i;//посредством указателя заполнили массив а
for(inti=0;i<10;i++)cout<<a[i]<<'\t';//вывели массив а на экран
delete[]a;//то, что было выделено new[] чистим delete
}
По этой причине вы часто не будете наблюдать явного указания адреса при placement new. Это один из немного слегка путающих факторов, но, надеюсь, теперь понятно, что несмотря на то, что оно иногда не выглядит как явное указание адреса в круглых скобках, в конечном счёте оно всё равно всего лишь указание адреса и есть.
Память компьютеров линейна. Это значит, что память подобна одномерному массиву. Это, в свою очередь, означает, что мы можем эмулировать выделение в памяти путём использования массива как памяти. Такой пример эмуляции с целью объяснения placement new очень распространён:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//Листинг #3 Эмулируем память массивом placement new
#include <iostream>
#include <new>
usingstd::cout;
intmain(){
constintN=10;
constintBUFF_SIZE=N*sizeof(double);
charbuffer[BUFF_SIZE];//эмулируем 10 свободных ячеек памяти, т. е. свободный блок
/////////////////////////////////
double*ptr=nullptr;//указатель, пока что он устремлён вникуда. (в прошлых примерах использовал инициализация, для разнообразия обойдёмся без инициализации)
ptr=new(buffer)double[N];//используем placement new и размещаем числа double в сэмулированном блоке памяти
doublevalue=0.3;
for(inti=0;i<N;i++){
ptr[i]=value;//заполняем массив, на который направлен ptr, значениями
value=value+0.3;//чтобы видно было что double действительно double
}
for(inti=0;i<N;i++){//Выводим на экран
cout<<ptr[i]<<'\n';
}
}
Когда мы используем память, очень важно следить чтобы то, что мы запихиваем в ячейки, поместилось в доступные ячейки. Поскольку тип double скорее всего больше char, мы используем простую формулу, чтобы эмулировать по-настоящему достаточное количество ячеек под хранение массива double в массиве, который выглядит как массив символов.
Используя placement new мы можем дополнять значениями не только обычные переменные и массивы, любые объекты вообще. Любая из форм new вызывает конструкторы объектов, и это очень важно:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//Листинг #4 placement new new — это срабатывание конструкторов
#include <iostream>
#include <new>
usingstd::cout;
structMyClass{
MyClass(){//конструктор по умолчанию
cout<<"ctor\n";
}
~MyClass(){
cout<<"destructor\n";
}
};
intmain(){
/////////////////////////////////
{
MyClassx;//1-й конструктор по умолчанию
new(&x) MyClass;//2-й конструктор по умолчанию
cout<<'\n';
MyClass*ptr1=newMyClass;//3-й конструктор по умолчанию
delete ptr1;
cout<<'\n';
MyClass*ptr2=newMyClass[10];//комплекс кнструкторов и деструкторов, для каждой ячейки массива
delete[]ptr2;
cout<<'\n';
//где-то здесь будет вызван деструктор для x
}
}
В листинге #4 можно обратить внимание на то, что в певых строчках сработало два конструктора. Последние две строчки можно было бы ожидать срабатывание двух деструкторов, но сработал только один.
placement new — это реконструирование объекта. Использованием этой формы new мы напрямую вмешиваемся в уже собранную в памяти структуру.
Для лучшей наглядности изобразим факт несрабатывания одного деструктора из-за использования placement new, сократив показанный код до минимума:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//Листинг #5 placement new
#include <iostream>
#include <new>
usingstd::cout;
structMyClass{
MyClass(){//конструктор по умолчанию
cout<<"ctor:\n";
}
~MyClass(){
cout<<"dtor: \n";
}
};
intmain(){
{
MyClassx;
new(&x) MyClass;//Используем placement new, реконструируем x
}
//Два срабатывания конструктора
//Одно срабатывание деструктора
}
В листинге #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 не провоцирует запуск конструкторов, а просто даёт сырой блок памяти, с которым мы можем делать, что захотим:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//Листинг #6 placement new
#include <iostream>
#include <new>
usingstd::cout;
structMyClass{
MyClass(){//конструктор по умолчанию
cout<<"ctor:\n";
}
~MyClass(){
cout<<"dtor: \n";
}
};
intmain(){
{
MyClass*p=(MyClass*)operatornew(sizeof(MyClass));//C помощью функции operator new выделили себе сырую память, хватающую для одного объекта MyClass
MyClass*ptr=new(p)MyClass;//Реконструировали сырую память, собрав объект типа MyClass (срабатывает конструктор)
p->~MyClass();//обязательно явно вызываем деструктор, сам он вызван не будет
operatordelete(p);//высвобождаем ранее взятую память
}
}
В листинге #6 мы использовали функцию, которая помогла нам просто взять память, при этом дополнительных телодвижений никаких не провоцирующую. В этой выбранной памяти мы сконструировали объект типа MyClass. Поскольку объект сконструирован единожды, то реконструкции непосредственно объекта не произошло (можно сказать о реконструкции памяти, но не о реконструкции объекта), то информация о разрушении, описанная в деструкторе, осталась непереписанной. Это очень важно, потому что теперь мы можем правильно объект разобрать, когда он прекратит быть нужным программе. Но есть одно но: деструктор нужно вызывать явно. new expression провоцирует работу конструкторов, но не деструкторов. Кроме того, что мы должны вручную запускать деструктор где-то сконструированного нашего объекта, мы должны высвобождать память, которую мы выбирали при помощи функции operator new для программы. Как вы можете догадаться, поскольку требуется ручное управление очисткой, следить за этими делами нужно очень внимаетельно.
операция placement new просто использует адрес, переданный в качестве аргумента; она не анализирует, свободна ли указанная область памяти, а также не ищет блок неиспользуемой памяти. В результате часть забот об управлении памятью возлагается на программиста.
Если вы используете Си-массив как площадку для конструирования объекта, то в зависимости от того, какой это массив (как мы помним, это может быть самый обычный массив, либо же указатель может указывать на массив, который был создан с помощью обычной формы new), операцию delete [] может быть или нужно использовать или нельзя. На самом деле к этому моменту это можно уже было успеть понять, но если вдруг кто-то не догадается, только ради него это упоминание:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Листинг #7 new expression
#include <iostream>
#include <new>
usingstd::cout;
intmain(){
intarr_usual[5];//обычный, автоматический массив
int*ptr=newint[5];//массив, выделенный в куче, динамически-создаваемый
int*p1=new(arr_usual)int[5];//используем arr_usual как площадку для массива под указатель p1
int*p2=new(ptr)int[5];//используем ptr как площадку для массива под указатель p2
delete[]p2;//нормально, потому что сейчас p2 то же, что и arr_usual
// delete []p1; //ненормально, потому что к обычному массиву нельзя delete [] Поведение программы непредсказуемо
}
Программист может использовать placement new для построения собственных процедур управления памятью, для взаимодействия с оборудованием, доступ к которому осуществляется по определенному адресу, или для создания объектов в конкретной ячейке памяти.
Объекты, созданные операцией placement new, должны удаляться в порядке, обратном порядку их создания. Причина в том, что более поздний объект может зависеть от более ранних. А буфер, используемый для хранения объектов, можно освободить только после уничтожения всех содержащихся в нем объектов.
Выбирая нужный участок памяти мы можем заполнять этот участок нужными значениями. Но очень важно следить, чтобы выбранный участок действительно был занят программой. Иначе, если выбранный участок, программой не используется, то поведение будет непредсказуемо. Это сейчас к тому, что если используем, например, указатели, но площадка под использование памяти предварительно не подготовлена, то нельзя использовать память, никак не связанную программой. Если же память используется программой, то заполнять значениями эту память можно или используя непосредственные конструкторы объектов или функциональную нотацию для, например, встроенных типов (int,double…), у которых нет конструкторов. Функциональная нотация помогает маскировать отсутствие конструкторов.
Но для начала инициализируем обычную переменную с помощью функциональной нотации, чтобы было понятно, что это такое:
C++
1
2
3
4
5
6
7
8
9
10
//Листинг #8 Инициализация с помощью функциональной нотации
#include <iostream>
usingstd::cout;
intmain(){
inta=int(10);//Используем функциональную нотацию, маскируется под конструктор с параметром
cout<<a;
}
Так же можно и присваивать. Это немного напоминает приведения типов в стиле языка С и в то же время напоминает инициацию конструктора с одним параметром, но больше походит на вызов функции с подсовыванием ей одного аргумента. Это называется функциональной нотацией, и если у тип не может иметь конструкторов, то будет вызвана именно функция. Раньше нельзя было такого проделывать для массивов. Поэтому использование placement new для заполнения немассивов и массивов немного отличается. Заполнять выбранный участок для немассивов можно на лету, как бы инициализируя выбранный участок на ходу сконструированым объектом:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//Листинг #9.1 Пример встраивания в сырую память встроенных типов с использование placement new
#include <iostream>
#include <new>
usingstd::cout;
intmain(){
inta;
new(&a) int(10);//Используем функциональную нотацию, маскируется под конструктор с параметром
cout<<a;
cout<<'\n';
int*ptr=newint;
ptr=(int*)operatornew(sizeof(int));//Добываем сырую память
ptr=new(ptr)int(33);//используем placement new с записыванием в добытую память значением
cout<<*ptr<<'\n';//33
operatordelete(ptr);//высвобождаем добытую память
delete ptr;//высвобождаем выделенную память
}
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Листинг #9.2 Для массива Только С++11 и старше (используется списковая инициализация)
#include <iostream>
#include <new>
usingstd::cout;
intmain(){
inta[10];
new(a)int[10]{1,2,3,4,5,6,7,8,9,0};
for(inti=0;i<10;i++)cout<<a[i]<<'\n';
}
В отличие от встроенных типов, у которых нет конструкторов, у объектов классов в большинстве случаев конструкторы есть, в частности, есть конструктор копирования, поэтому недостаток вида неумения копирования у объектов классов компенсируется конструктором копировани. Хоть мы и использовали сейчас функциональную нотацию, близко напоминающую конструктор с одним параметром, всё-таки конструкторы объектов классов дают больше возможностей. Для объектов класса в конечном счёте всё происходит по тому же синтаксису, но для них это уже не функциональная нотация, а использование конструкторов.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//Листинг #10 Для объектов класса с использованием конструктора
#include <iostream>
#include <new>
usingstd::cout;
classMyClass{
intx;
public:
MyClass(){x=1;}//конструктор по умолчанию
MyClass(MyClass&){ x = 2;}//контруктор копирования
MyClass(constintvalue):x(value){}//конструктор с параметром
MyClass& operator=(const MyClass&) = delete;//операция присваивания нам пока не нужна
voidshow()const{cout<<x;}
};
intmain(){
MyClass*ptr=(MyClass*)operatornew(sizeof(MyClass));//Добыли сырой памяти
MyClass*object=new(ptr)MyClass(7);//Сконструриовали объект путем использования
//конструктора с параметром
//и этот объект сконструирован в добытой памяти
object->show();
operatordelete(ptr);//высвободили добытую память
}
Разумеется, для объектов класса можно использовать любой доступный объекту конструктор. Поскольку в листинге #10 мы не выделяли память с помощью разновидностей new, т. е. не выбирали памятьиз кучи, то и delete нам не нужен. Но поскольку мы выбирали сырой участок памяти путём использования функции operator new, нам нужно высвободить заимствованную память путём использования функции operator delete. Надеюсь, с этим понятно. Важно очень внимательно следить за правильным высвобождением.
Вернёмся к наиболее простым примерам. new placement часто называют new с размещением. Сначала мы определяем участок памяти, которого должно хватить для размещения в нём встраиваемого объекта, а потом подставить в эту память или уже существующий или свежеиспечённый объект:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
//Листинг #11 new с размещением
#include <iostream>
usingstd::cout;
intmain(){
inta=234;
int*p=new(p)int(a);//разместили в участке памяти, на который направлен p, значение a
(*p)++;//увеличили размещённое значение на один
cout<<*p<<'\n';//235
cout<<a<<'\n';//234
}
Листинг #11 показывает механизм работы placement new. В данном случае p сырой указатель. Непосредственно ему память мы не выделяем. Но мы выделяем память переменной a и сразу в переменную записываем значение. После этого мы копируем структуру созданной переменной в участок p. Поскольку мы просто скопировали структуру, а не вклинивались в a, изменение значения, с которым связан указатель p, это изменение копии. А поскольку копия это всего-лишь отдельная сущность, то оригинальная переменная не изменила своего значения. Сам этот процесс напоминает отложенную инициализацию.
Также можно проделывать и с массивами, но надо учитывать, что непосредственно массив в массив скопировать нельзя. Поэтому чтобы использовать placement new к массиву, необходимо указателю подготовить сырой памяти:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Листинг #12
//Листинг #11
#include <iostream>
usingstd::cout;
intmain(){
constintN=5;
inta[N]={1,2,3,4,5};//оригинальный массив
int*p=(int*)operatornew(5*sizeof(int));//выделяем сырую память, хватающую для вмещения a[5]
for(inti=0;i<5;i++)new(p+i)int(a[i]);//копируем каждую ячейку а в выделенную сырую память, чтобы трансформировать сырое в готовое
for(inti=0;i<5;i++)cout<<a[i]<<'\t';//выводим на экран
operatordelete(p);
}
Если использовать placement new для размещения нескольких объектов в одну подготовленную зону памяти, то важно следить и за тем, чтобы размещаемые элементы уместились в доступные ячейки взятой памяти, и чтобы не происходило перекрытия. Перекрытие — это когда происходит или полный или частичный наезд одного объекта на другой, из-за чего "объект заднего плана" теряет часть своей информации и становится непригодным к использованию. Чтобы не происходило перекрытия, нужно смещать размещаемые объекты относительно занятых расположений. Это можно делать так:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//Листинг #12 Размещение в одном блоке нескольких объектов placement new
#include <iostream>
#include <new>
usingstd::cout;
constintSIZE_BUFFER=1024;//количество ячеек, хватающих для расположения объектов в массиве
classMyClass{
intx;
doubley;
public:
MyClass():x(20),y(20.5){}//конструктор по умолчанию
explicitMyClass(constinti,constdoubled):x(i),y(d){}//конструктор с одним параметром
MyClass(constMyClass&) = delete;//конструктор копирования пока не нужен
MyClass& operator=(const MyClass&) = delete;//операция присваивания пока не нужна
voidshow()const{
cout<<"x == "<<x<<'\n'
<<"y == "<<y<<'\n';
cout<<'\n';
}
};
intmain(){
charbuffer[SIZE_BUFFER];//для разнообразия не будем использовать operator new
MyClass*object1=new(buffer)MyClass;//Встроили в буффер объект *object
Мы используем адресную арифметику, чтобы сместить начальную точку размещения в незанятое пространство. Т. е сейчас мы берём адрес начала массива и делаем прыжок через занятое объектом *object пространство. Поскольку в массиве, выступающем в роли памяти места достаточно и объекты не наезжают на территории друг друга, то всё хорошо. А по той причине, что посредством new placement деструкторы не вызываются, несмотря на то, что сейчас деструкторы в каждом классе генерируются неявно, явно их зазываем.
Добавить комментарий