Лекция 8
А как работает вызывать конструктор объекта в выделенной ОС памяти мне?
Placement new
Оператор new как и многие другие может быть переопределен.
Placement new
В С++ определен, так называемый, placement new , который НЕ выделяет память, а только создает объект в области памяти, которая передана в качестве аргумента.
Таким образом, можно конструировать объекты в известной области памяти. Эта память, в свою очередь, может быть выделена любым способом.
Новые new
Ок, а какие еще есть переопределения new ?
nothrow new
Как известно, если опертатор new не смог выделить память, то он генерирует исключение std::bad_alloc .
Функция malloc не генерирует исключения, а возвращает NULL.
nothrow new
Иногда требуется, чтобы оператор new не генерировал исключение в случае ошибки, а возвращал невалидный адрес ( nullptr ). Для таких целей можно использовать переопределенный оператор new с параметром std::nothrow .
nothrow new
Пользовательские new
Можно определять свои операторы new
Операторы для классов
С++ позволяет переопределять методы new и delete для классов.
Резюме
Переопределенные операторы new позволяют управлять памятью в вашем приложении на любом уровне и позволяют писать приложения под любые требования.
Но, к счастью (или сожалению), управлять памятью на таком уровне требуется не часто, только в очень специфичных условиях.
Модели управления памятью
Простейшая модель управления памятью

Простейшая модель управления памятью
Модель основана на массиве блоков памяти.
Выделение памяти сравнимо с задачей нахождения первого свободного блока.
Освобождение памяти: задача “удаление” элемента из массива.
Простейшая модель управления памятью
В начале в памяти располагается только одна структура, размер которой равен всей свободной памяти.
По мере выделения памяти появляются новые блоки. Размер блоков определяется размером объектов распологаемых в блоках.
Простейшая модель управления памятью
При новом запросе на выделение памяти последовательно проверяются все доступные блоки необходимого размера.
Освобождение блока памяти приводит к очередному поиску предыдущего свободного блока и изменению его размера.
Простейшая модель управления памятью
Главный минус такой модели – скорость выделения и освобождения памяти: O(N), где N — количество блоков.
Vector vs list
Но можно получить постоянное время освобождение памяти путем изменения служебной структуры.
Простая модель управления памятью
Модель основана на списке из блоков памяти одинакового размера.

Простая модель управления памятью
Выделение памяти: возвращаем первый элемент списка (а ля pop ).

Простая модель управления памятью
Выделение памяти: возвращаем первый элемент списка (а ля pop ).

Простая модель управления памятью
Освобождение памяти: добавляем в список участок памяти (а ля push ).

Простая модель управления памятью
Освобождение памяти: добавляем в список участок памяти (а ля push ).

Простая модель управления памятью
Главный плюс такой модели – константная скорость выделения и освобождения памяти.
Минус – фрагментация памяти. Участки памяти перемешиваются, расходуется памяти больше, чем необходимо под объект.
Основные проблемы управления памятью
- фрагментация памяти
- параллелизм и конкурентность
Борьба с фрагментацией памяти
Чтобы уменьшить проблему с фрагментацией в описаной выше модели управления памятью, можно завести несколько подобных структур, с блоками разного размера.
Когда требуется выделить N байт, модель ищет экземпляр аллокатора c размером блока, больше или равным N.
Работа в условиях многопоточности
Самый простой подход добавить поддержку многопоточности в аллокатор – это добавить mutex или любой другой механизм синхронизации потоков. Недостатки такого решения очевидны.
Чтобы как-то нивелировать недостатки, связанные с постоянной синхронизацией, создают локальный для каждого потока кэш.
TCMalloc* от Google
Основная идея: для каждого потока есть свой стек свободных блоков. Если надо выделить блок, который находится в локальном стеке, то не требуется времени на синхронизацию между потоками.
Если локальный кэш переполняется, то идет обращение к глобальному стеку свободных блоков памяти.
Резюме
Существуют различные модели управления памятью, каждая из которых может хорошо работать в одних услосиях и плохо в других.
Выбор модели необходимо осуществлять в зависимости от задачи, требований и других условий.
STL Allocator
В некоторых частях стандартной библиотеки языка С++ используются специальные объекты для выделения и освобождения памяти, которые называются аллокаторами.
STL Allocator
STL аллокаторы используются как абстракция, преобразующая запросы на выделение памяти в физическую операцию её выделения.
Где и как используются
Все стандартные контейнеры используют распределители памяти для выделения динамической памяти. Это позволяет гибко изменять политики выделения/освобождения памяти в стандартных контейнерах.
Где и как используются
Как работают аллокаторы
- allocate(size_t N) – выделяет память для N элементов ( n * sizeof(T) )
- construct(void* p, Args&&. args) – инициализирует элемент по адресу p , используя аргументы args
- destroy(void *p) – уничтожает элемент по адресу p
- deallocate(void* p, size_t N) – освобождает память по адресу p в которой располагается N элементов.
Пользовательские аллокаторы
Начиная с С++11 для создания собственного аллокатора требуется определить только функции allocate и deallocate .
По умолчанию, функция construct использует placement new , а destroy явно вызовает деструктор.
Резюме
STL аллокаторы являются еще абстракцией в библиотеке STL, которая помогает использовать различные модели управления памятью в ваших приложениях без переписывания большой части кода.
Так же аллокаторы могут быть полезны при профилировании вашего приложения.
Как работает delete c
Delete is an operator that is used to destroy array and non-array(pointer) objects which are created by new expression.
- Delete can be used by either using Delete operator or Delete [ ] operator
- New operator is used for dynamic memory allocation which puts variables on heap memory.
- Which means Delete operator deallocates memory from heap.
- Pointer to object is not destroyed, value or memory block pointed by pointer is destroyed.
- The delete operator has void return type does not return a value.
Here, Below are examples where we can apply delete operator:
1. Deleting Array Objects: We delete an array using [] brackets.
delete vs delete[] operators in C++
What is the difference between delete and delete[] operators in C++?
7 Answers 7
The delete operator deallocates memory and calls the destructor for a single object created with new .
The delete [] operator deallocates memory and calls destructors for an array of objects created with new [] .
Using delete on a pointer returned by new [] or delete [] on a pointer returned by new results in undefined behavior.
The delete[] operator is used to delete arrays. The delete operator is used to delete non-array objects. It calls operator delete[] and operator delete function respectively to delete the memory that the array or non-array object occupied after (eventually) calling the destructors for the array’s elements or the non-array object.
The following shows the relations:
For the new that creates an array (so, either the new type[] or new applied to an array type construct), the Standard looks for an operator new[] in the array’s element type class or in the global scope, and passes the amount of memory requested. It may request more than N * sizeof(ElementType) if it wants (for instance to store the number of elements, so it later when deleting knows how many destructor calls to done). If the class declares an operator new[] that additional to the amount of memory accepts another size_t , that second parameter will receive the number of elements allocated — it may use this for any purpose it wants (debugging, etc. ).
For the new that creates a non-array object, it will look for an operator new in the element’s class or in the global scope. It passes the amount of memory requested (exactly sizeof(T) always).
For the delete[] , it looks into the arrays’ element class type and calls their destructors. The operator delete[] function used is the one in the element type’s class, or if there is none then in the global scope.
For the delete , if the pointer passed is a base class of the actual object’s type, the base class must have a virtual destructor (otherwise, behavior is undefined). If it is not a base class, then the destructor of that class is called, and an operator delete in that class or the global operator delete is used. If a base class was passed, then the actual object type’s destructor is called, and the operator delete found in that class is used, or if there is none, a global operator delete is called. If the operator delete in the class has a second parameter of type size_t , it will receive the number of elements to deallocate.
Почему в С++ массивы нужно удалять через delete[]
Заметка рассчитана на начинающих C++ программистов, которым стало интересно, почему везде твердят, что нужно использовать delete[] для массивов, но вместо внятного объяснения – просто прикрываются магическим «undefined behavior». Немного кода, несколько картинок и взгляд под капот компиляторов – всех заинтересованных прошу под кат.

Введение
Может быть, вы не замечали, или даже просто не обращали внимания, но, когда вы пишете код для освобождения памяти, занятой массивами, то вам не приходится писать количество элементов, которые нужно удалить. При этом всё замечательно работает.
Это что, магия? Отчасти – да. Причём разработчики различных компиляторов видят и реализуют её по-разному.

Существует два основных подхода к тому, как компиляторы запоминают количество элементов в массиве:
- Запись количества элементов перед самим массивом («Over-Allocation»)
- Хранение количества элементов в обособленном ассоциативном контейнере («Associative Array»)
Over-Allocation
Первый способ, как понятно из названия, реализуется простой записью количества элементов перед массивом. Обратите внимание, что в таком случае указатель, который вы получите после выполнения оператора new, будет указывать на первый элемент массива, а не на его фактическое начало.

Такой указатель ни в коем случае нельзя передавать обычному оператору delete. Скорее всего, он просто удалит первый элемент массива, а остальные оставит нетронутыми. Заметьте, я не просто так написал «скорее всего» – ведь никто не может гарантировать, что произойдёт на самом деле и как дальше будет вести себя ваша программа. Всё зависит от того, какие объекты находились в массиве и делали ли они что-то важное в своих деструкторах. То есть получаем классическое неопределённое поведение. Согласитесь, это не то, чего вы ожидаете при попытке удалить массив.
Чем же отличается оператор delete[]? А он как раз считывает количество элементов в массиве, вызывает деструктор для каждого объекта и уже после этого очищает память (вместе со скрытой переменной).
Если кому будет интересно, то примерно в такой псевдокод превращается конструкция delete[] p; при использовании этой стратегии:
Этим способом пользуются компиляторы MSVC, GCC и Clang. В этом можно убедиться, взглянув на код работы с памятью в соответствующих репозиториях (GCC и Clang) или воспользовавшись сервисом Compiler Explorer.

Как видно на изображении выше (верхняя часть – код, нижняя – ассемблерный вывод компилятора), я набросал простенький код, в котором объявлена структура и функция для создания массива этих самых структур.
Что же происходит с точки зрения ассемблера простым языком:
- cтрока N3: запись требуемого количества памяти (20 байт на 5 объектов + 8 байт на размер массива) в регистр;
- cтрока N4: вызов оператора new для выделения памяти;
- cтрока N5: запись количества элементов в начало выделенной памяти;
- cтрока N6: смещение указателя на начало массива на sizeof(size_t), полученный результат является возвращаемым значением.
К достоинствам этого способа можно отнести его лёгкость в реализации и скорость работы, ну а к недостаткам – то, что он не прощает ошибок с некорректным выбором оператора delete. В лучшем случае – сразу получите падение программы с ошибкой «Heap Corrupt», а в худшем – будете долго и мучительно искать причины странного поведения программы.
Associative Array
Второй способ подразумевает существование скрытого глобального контейнера, в котором хранятся указатели на массивы и сколько элементов они содержат. В таком случае перед массивами нет никаких скрытых данных, а вызов delete[] p; реализуется примерно вот так:
Что ж, выглядит не так «магически», как прошлый вариант. Есть ли ещё какие различия? Да.
Кроме уже упомянутого отсутствия скрытых данных перед массивом, мы получаем небольшое замедление работы из-за необходимости поиска данных в глобальном хранилище. Но компенсируем это тем, что программа может более снисходительно относиться к неверному выбору оператора delete.
Данный подход использовался в компиляторе Cfront. Останавливаться на его реализации мы не будем, но если кому интересно покопаться во внутренностях одного из первых C++ компиляторов, то сделать это можно на GitHub.
Мини-послесловие
Всё вышеописанное является внутренней кухней компиляторов, и полагаться на то или иное поведение не стоит. Особенно это касается случаев, когда планируется портирование программы на разные платформы. Благо что есть несколько вариантов как можно избежать данного класса ошибок:
- Использовать семейства функций std::make_*. Например: std::make_unique, std::make_shared.
- Использовать средства статического анализа для раннего выявления ошибок, например PVS-Studio.
Если же вас заинтересовала тема неопределённого поведения и особенностей работы компиляторов, то могу посоветовать ещё парочку дополнительных материалов: