Как работает динамическая память и какими операторами пользоваться для работы с ней в C++?
Ответ
В С++ объекты создаются динамически при помощи new-выражений и разрушаются при помощи delete-выражений.
T* obj = new T;
delete obj;
T* arr = new T[100];
delete[] arr;
New-выражения и delete-выражения используют функции выделения динамической памяти operator new, operator new[] и функции освобождения operator delete, operator delete[] соответственно.
Реализация С++ предоставляет несколько глобальных функций выделения и освобождения. При этом они могут быть переопределены пользователем без нарушения ODR (но не более одного раза в программе).
void* operator new(std::size_t);
void operator delete(void*) noexcept;
void operator delete(void*, std::size_t) noexcept;
void* operator new[](std::size_t);
...
Пользователь также может добавлять свои функции выделения и освобождения, в виде статических членов класса или свободных функций в глобальном пространстве имен. Такие пользовательские функции должны соблюдать семантику встроенных (не выделять уже выделенные адреса и т.п.).
Выражения new и delete для одного объекта
Функции operator new и operator delete только выделяют и освобождают память. Конструирование и разрушение объекта происходит в самом выражении new и delete. Для вызова конструктора используется new который не выделяет память - размещающий new
// Эквивалент кода
// T* obj = new T(a1, a2);
T* obj;
{
void* mem = operator new(sizeof(T));
try {
obj = new(mem) T(a1, a2); // Размещающий new, вызывает конструктор T на памяти mem
} catch (...) { // Исключение в конструкторе
operator delete(mem);
throw;
}
}
// Примерный* эквивалент кода
// delete obj;
obj->~T(); // Вызов деструктора
operator delete(obj);
// *) При виртуальном деструкторе компилятор сгенерирует что-то похожее на
void* mem = obj->~T(); // псевдокод
operator delete(mem);
// Т.к. при множественном наследовании может быть mem != obj
Из кода выше видно, что new-выражение требует обе функции, operator new и operator delete
Функции operator new и operator delete в стандартной библиотеке
Обычный new
void* operator new(std::size_t size);
Использование:
T* p = new T;
assert(p != nullptr);
Примерная реализация:
void* operator new(std::size_t size) {
for (;;) {
void* mem = unspecified_alloc(size); // Может быть malloc, зависит от реализации.
if (mem) return mem;
// Пользовательская функция-обработчик нехватки памяти.
std::new_handler user_handler_fn = std::get_new_handler();
if (!user_handler_fn) throw std::bad_alloc();
user_handler_fn(); // Либо предоставляет память,
// либо бросает bad_alloc или завершает программу.
}
}
Пользователь может установить свою функцию new_handler при помощи функции std::set_new_handler
Не-бросающий исключений new
void* operator new(std::size_t size, const std::nothrow_t&) noexcept;
Использование:
T* p = new(std::nothrow) T;
if (!p) { /* не удалось выделить память */ }
Примерная реализация:
void* operator new(std::size_t size, const std::nothrow_t&) noexcept {
try {
return operator new(size);
} catch (const std::bad_alloc&) {
return nullptr;
}
}
Размещающий new
void* operator new(std::size_t size, void* ptr) noexcept { return ptr; }
Ничего не выделяет, возвращают аргумент ptr
Позволяют вызвать конструктор объекта.
char mem[sizeof(T)];
T* p = new(mem) T;
Размещающий operator delete также существует, и также ничего не делает. Нужен только потому что new-выражение требудет наличия operator delete
Динамические массивы, new[] и delete[]
Функции operator new[] и operator delete[] служат для выделения памяти, и ничем не отличаются от функций для одного объекта. Стандартные реализации просто вызывают operator new
void* operator new[](std::size_t size) { return operator new(size); }
Размер динамического массива сохраняется самим выражением new[], примерно так:
// Эквивалент кода (без учета выравнивания)
// T* arr = new T[100];
T* arr;
{
void* mem = operator new[](sizeof(std::size_t) + sizeof(T));
std::size_t& size = *reinterpret_cast
// Эквивалент кода (без учета выравнивания)
// delete[] arr;
{
std::size_t& size = reinterpret_cast
Массивы созданные при помощи выражения new T[N] могут быть освобождены только через delete[]
и наоборот, объекты созданные new T(), должны быть быть освобождены delete
Размещающий new[]
void* operator new[](std::size_t size, void* ptr) noexcept;
Он существует, но мы не знаем сколько байт будут резервироваться копилятором под размер объекта, поэтому не сдедует его использовать:
// char mem[100 * sizeof(T) + sizeof(std::size_t)];
// Компилятор может использовать больше чем sizeof(std::size_t), тогда мы выйдем за границы mem.
// new(mem) T[100];
Комментариев нет:
Отправить комментарий