Placement new c что это
Перейти к содержимому

Placement new c что это

  • автор:

kaktusenok

Ответ на первый вопрос — нисколько. И сразу же переходим ко второму вопросу.
Ответ на второй вопрос — это использование уже выделенной памяти для новых объектов.

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

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

#include #include class A < private: int _x; A() < std::cout public: A(int x) < _x = x; std::cout ~A() < std::cout >; int main() < const int n = 3; // выделить память под A, но не вызывая конструктора char* memory[ n*sizeof(A) ]; A* placementMemory = static_cast(static_cast(memory)); // вызвать конструкторы объектов (без выделения памяти, но с использованием выделенной) for (int i=0; iPlacement new, или как создать объект в выделенной памяти

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

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

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

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

Работа с памятью

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

Кроме того, создание объекта в заранее выделенной памяти занимает значительно меньше времени, чем создание объекты с выделением памяти под него.

Блок памяти можно выделять как статически, так и динамически:

conat int bufferSize = 1024 * 1024; // указываем размер блока в 1 Мб. unsigned char dataBuffer[ bufferSize ]; // заранее выделенный блок памяти 

Когда появляется необходимость создать в этой памяти какой то объект, то его можно создать следующим образом:

1 2 3 4 5
void* ptr = dataBuffer[ offset ]; // указываем позицию в блоке памяти, где мы хотим создать объект. SomeClass* cl = new (ptr) SomeClass(); // создаем объект в указанной памяти Если написать такой код: SomeClass* cl = new SomeClass(); // то будет выделение памяти в куче, а после создание в ней объекта. 
Практическое применение

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

Это можно достичь используя следующий менеджер памяти (это только макет иллюстрирующий саму идею):

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
class MemPool  public: MemPool( int size ); ~MemPool(); void* Allocate( size_t size ); void FreeAll(); private: void* _pool; int _offset; int _size; >; MemPool::MemPool( int size )  _pool = new char[ size ]; _size = size; _offset = 0; > MemPool::~MemPool()  delete( _pool ); > void* MemPool::Allocate( size_t size )  if( _offset + size > _size ) return NULL; void* ptr = _pool[ _offset ]; _offset += size; return ptr; > void MemPool::FreeAll()  _offset = 0; > 

Работа с таким менеджером будет проходить следующим образом:

  1. создание менеджера памяти при старте программы
  2. при загрузке нового уровня все объекты используют заранее заготовленную память. Тут важно то, что если любой системе требуется память под что-либо, то она так же может брать память их этого хранилища.
  3. в конце уровня менеджер очищает память и при старте следующего уровня будет использоваться та же область памяти. Данная операция выполняется крайне быстро, т.к. память никак не модифицируется.
  4. удаление менеджера при выходе
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// создание менеджера памяти при старте программы MemPool* globalPool = new MemPool( size ); // создание объектов в памяти менеджера void* ptr = globalPool->Allocate( sizeof(SomeClass) ); SomeClass* cl = new (ptr) SomeClass(); // получение памяти под массив чисел int arrSize = 10; // размер массива int* intArr = globalPool->Allocate( sizeof(int) * arrSize ); // очистка используемой памяти globalPool->FreeAll(); // удаление менеджера при выходе delete( globalPool ); 

[C++] Всё ли мы знаем об операторах new и delete?

Привет! Ниже речь пойдет об известных всем операторах new и delete, точнее о том, о чем не пишут в книгах (по крайней мере в книгах для начинающих).
На написание данной статьи меня побудило часто встречаемое заблуждение по поводу new и delete, которое я постоянно вижу на форумах и даже(. ) в некоторых книгах.
Все ли мы знаем, что такое на самом деле new и delete? Или только думаем, что знаем?
Эта статья поможет вам разобраться с этим (ну, а те, кто знают, могут покритиковать:))

Note: ниже пойдет речь исключительно об операторе new, для других форм оператора new и для всех форм оператора delete все ниженаписанное также является правдой и применимо по аналогии.

Итак, начнем с того, что обычно пишут в книгах для начинающих, когда описывают new (текст взят «с потолка», но вцелом соответствует правде):

Оператор new выделяет память больше или равную требуемому размеру и, в отличие от функций языка С, вызывает конструктор(ы) для объекта(ов), под которые память выделена… вы можете перегрузить (где-то пишут реализовать) оператор new под свои нужды.

И для примера показывают примитивную перегрузку (реализацию) оператора new, прототип которого выглядит так
void* operator new (std::size_t size) throw (std::bad_alloc);

На что хочется обратить внимание:
1. Нигде не разделяют new key-word языка С++ и оператор new, везде о них говорят как об одной сущности.
2. Везде пишут, что new вызывает конструктор(ы) для объекта(ов).
И первое и второе является распространенным заблуждением.

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

5.3.4
The new-expression attempts to create an object of the type-id (8.1) or new-type-id to which it is applied. /*дальше нам не интересно*/
18.6.1
void* operator new(std::size_t size) throw(std::bad_alloc);
Effects: The allocation function called by a new-expression (5.3.4) to allocate size bytes of
storage suitably aligned to represent any object of that size /*дальше нам не интересно*/

Тут мы уже видим, что в первом случае new именуется как expression, а во втором он объявлен как operator. И это действительно 2 разные сущности!
Попробуем разобраться почему так, для этого нам понадобятся ассемблерные листинги, полученные после компиляции кода, использующего new. Ну, а теперь обо все по порядку.

new-expression — это оператор языка, такой же как if, while и т.д. (хотя if, while и т.д. все же именуются как statement, но отбросим лирику) Т.е. встречая его в листинге компилятор генерирует определенный код, соответствующий этому оператору. Так же new — это одно из key-words языка С++, что еще раз подтверждает его общность с if‘ами, for’ами и т.п. А operator new() в свою очередь — это просто одноименная функция языка С++, поведение которой можно переопределить. ВАЖНОoperator new() НЕ вызывает конструктор(ы) для объекта(ов), под который(ые) выделяется память. Он просто выделяет память нужного размера и все. Его отличие от сишных функций в том, что он может бросить исключение и его можно переопределить, а так же сделать оператором для отдельно взятого класса, тем самым переопределить его только для этого класса (остальное вспомните сами:)).
А вот new-expression как раз и вызывает конструктор(ы) объекта(ов). Хотя правильней сказать, что он тоже ничего не вызывает, просто, встречая его, компилятор генерирует код вызова конструктора(ов).

Для полноты картины рассмотрим следующий пример:

#include class Foo < public: Foo() < std::cout >; int main ()

после исполнения данного кода, как и ожидалось, будет напечатано «Foo()». Разберемся почему, для этого понадобится заглянуть в ассемблер, который я немного прокомментировал для удобства.
(код получен компилятором cl, используемым в MSVS 2012, хотя в основном я использую gcc, но это к делу не относится)

/Foo *bar = new Foo; push 1 ; размер в байтах для объекта Foo call operator new (02013D4h) ; вызываем operator new pop ecx mov dword ptr [ebp-0E0h],eax ; записываем указатель, вернувшийся из new, в bar and dword ptr [ebp-4],0 cmp dword ptr [ebp-0E0h],0 ; проверяем не 0 ли записался в bar je main+69h (0204990h) ; если 0, то уходим отсюда (возможно вообще из main или в какой-то обработчик, в данном случае неважно) mov ecx,dword ptr [ebp-0E0h] ; кладем указатель на выделенную память в ecx (MSVS всегда передает this в ecx(rcx)) call Foo::Foo (02011DBh) ; и вызываем конструктор ; дальше не интересно 

Для тех, кто ничего не понял, вот (почти) аналог того, что получилось на сиподобном псевдокоде (т.е. не надо пробовать это компилировать :))

Foo *bar = operator new (1); // где 1 - требуемый размер bar->Foo(); // вызываем конструктор 

Приведенный код подтверждает все, написанное выше, а именно:
1. оператор (языка) new и operator new() — это НЕ одно и тоже.
2. operator new() НЕ вызывает конструктор(ы)
3. вызов конструктора(ов) генерирует компилятор, встречая в коде key-word «new»

Итог: надеюсь, эта статья помогла вам понять разницу между new-expressionи operator new() или даже узнать, что она (эта разница) вообще существует, если кто-то не знал.

P.S. оператор delete и operator delete() имеют аналогичное различие, поэтому в начале статьи я сказал, что не буду его описывать. Думаю, теперь вы поняли, почему его описание не имеет смысла и сможете самостоятельно проверить справедливость написанного выше для delete.

Update:
Хабражитель с ником khim в личной переписке предложил следующий код, который хорошо демонстрирует суть написанного выше.

#include class Test < public: Test() < std::cout void* operator new (std::size_t size) throw (std::bad_alloc) < std::cout >; int main() < Test *t = new Test(); void *p = Test::operator new(100); // 100 для различия в выводе >

Этот код выведет следующее

Test::operator new(1) Test::Test() Test::operator new(100) 

Placement new c что это

Как известно есть так называемый оператор placement new. Он реально не выделяет память, а только вызывает конструктор объекта. Также существует парный ему placement delete. Вот их определения из STL ():

inline void *__cdecl operator new(size_t, void *_Where) _THROW0() < // construct array with placement at _Where return (_Where); >inline void __cdecl operator delete(void *, void *) _THROW0() < // delete if placement new fails >

Вот пример из последнего MSDN:

// new_op_new.cpp // compile with: /EHsc #include #include using namespace std; class MyClass < public: MyClass( ) < cout ; ~MyClass( ) < imember = 0; cout ; int imember; >; int main( ) < // The first form of new delete MyClass* fPtr = new MyClass( ); delete fPtr; // The second form of new delete char x[sizeof( MyClass )]; MyClass* fPtr2 = new( &x[0] ) MyClass( ); delete(&x[0], fPtr2 ); cout 

Проблема в том, что в VisualC++ placement delete не вызывается. При просмотре ассемблерного кода, видно, что вместо operator delete(void*, void*) вызывается `scalar deleting destructor’, который после вызова деструктора вызывает глобальный operator delete(void*). Естественно при попытке освобождения памяти возникает ошибка.

Эта проблема существовала в VC++ 6.0, осталась она и в VC++ 7.0 (aka .NET).

Borland C++ и GNU C++ с этим примером справляются, хотя и не так изящно. Там не определяется placement delete. Просто компилятор знает, что при использовании такого синтаксиса delete вызывать не нужно и просто генерирует вызов деструктора. Конечно, я тоже могу просто вызвать деструктор, но это как-то некрасиво.

P.S. Кто нибудь знает, где можно взять стандарт по C++ (ISO/IEC 14882-1998) бесплатно. А то буржуи просто так не отдают.

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

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