MyTetra Share
Делитесь знаниями!
Ключевое слово new, оператор new и их различные формы: стандартный, размещающий, и другие
Время создания: 01.12.2023 16:29
Автор: Дмитрий Пономарев
Текстовые метки: язык, си++, c++, new, delete, оператор, ключевое слово, функция, форма, выделение, память, массив, размер, динамический, произвольный
Раздел: Компьютер - Программирование - Язык C++ (Си++)
Запись: xintrea/mytetra_syncro/master/base/1701437390u8dh7neti1/text.html на raw.github.com

В языке C++ операторы new и delete служат для размещения и удаления объектов в куче. После размещения объекта в куче, возвращается указатель на объект. Так же есть операторы new[] и delete[], которые позволяют размещать в куче целые масивы объектов. Причем, что важно, размер создаваемых таким образом массивов не обязательно должен быть константой. Обычно на этом объяснения особенностей работы заканчивается. В данной статье описаны различные варианты операторов new/delete, которые были изначально в языке и которые появились в стандартах C++11 и выше, как они устроены внутри, как их переопределять.


Примечание об обозначениях. Когда идет речь об операторах new[] или delete[], то на самом деле имеется в виду синтаксис new T[длина массива] и delete[] ptr, а форма new[] и delete[] - это по-сути, просто сокращение. Когда пишется оператор new или delete то имеется в виду создание/удаление единичного объекта в куче. Когда речь идет об операторе new[] или delete[] то имеется в виду создание/удаление массива объектов в куче. Точный синтаксис, конечно, будет более полным.



Внимание! Описанные здесь операторы создания массивов не являются операторами создания динамических массивов. Динамические массивы - это массивы, размер которых может изменяться динамически по ходу работы программы. Здесь же речь идет всего лишь об операторах, позволяющих создать массив произвольного размера (а не только размера, заранее заданного в программе как константа).


К сожалению, в некоторых источниках, массивы в C++, создаваемые в куче (т. е. в динамической памяти) именуют динамическими массивами. Но по-факту такие массивы динамическими не являются.



1. Стандартные формы операторов new/delete

C++ поддерживает несколько вариантов операторов new/delete. Их можно разделить на:


  • основные стандартные,
  • дополнительные стандартные
  • пользовательские.


В этом разделе и разделе 2 рассматриваются стандартные формы, пользовательские формы будут рассмотрены в разделе 3.


1.1. Основные стандартные формы операторов new/delete

Основные стандартные формы операторов new/delete, используемые при создании и удалении объекта и массива типа T следующие:


new T(аргументы конструктора) // Создание единичного объекта типа T в куче

new T[длина массива] // Создание массива объектов типа T в куче

delete ptr; // Удаление объекта из кучи

delete[] ptr; // Удаление массива объектов из кучи


Их работу можно описать следующим образом. При вызове оператора new сначала выделяется память для объекта. Если выделение прошло успешно, то вызывается конструктор. Если конструктор выбрасывает исключение, то выделенная память освобождается. При вызове оператора delete все происходит в обратном порядке: сначала вызывается деструктор, потом освобождается память. Деструктор не должен выбрасывать исключений.

Когда оператор new[] используется для создания массива объектов, то сначала выделяется память для всего массива. Если выделение прошло успешно, то вызывается конструктор по умолчанию (или другой конструктор, если есть инициализатор) для каждого элемента массива начиная с нулевого. Если какой-нибудь конструктор выбрасывает исключение, то для всех созданных элементов массива вызывается деструктор в порядке, обратном вызову конструктора, затем выделенная память освобождается. Для удаления массива надо вызвать оператор delete[], при этом для всех элементов массива вызывается деструктор в порядке, обратном вызову конструктора, затем выделенная память освобождается.

Следует еще раз повторить: в форме new T[длина массива] длинна массива может быть любым динамически вычисленным числом, а не только константой как в стандартных массивах, создаваемых не через new.


Внимание! Необходимо вызывать правильную форму оператора delete в зависимости от того, удаляется одиночный объект или массив. Это правило надо соблюдать неукоснительно, иначе можно получить неопределенное поведение, то есть может случиться все, что угодно: утечки памяти, аварийное завершение и т.д. Подробнее см. [Meyers1].


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

Стандартные функции выделения памяти при невозможности удовлетворить запрос выбрасывают исключение типа std::bad_alloc. Но это исключение можно перехватить, для этого надо установить глобальный перехватчик с помощью вызова функции set_new_handler(), подробнее см. [Meyers1].

Любую форму оператора delete безопасно применять к нулевому указателю.

При создании массива оператором new[] размер может быть установлен нулевым.

Обе формы оператора new допускают использование инициализаторов в фигурных скобках:



int *value = new int{42};

int *values = new int[8]{1,2,3,4};

...

delete value;

delete[] values;



Для доступа к элементам массива можно использовать как синтаксис масивов, так и операцию разыменования:



// Работа с элементами через синтаксис массивов

values[0] = 10; // 0-й элемент

values[1] = 20; // 1-й элемент

std::cout << values[0] << std::endl;

std::cout << values[1] << std::endl;

// Работа с элементами через операцию разыменования

*values = 30; // 0-й элемент

*(values+1) = 40; // 1-й элемент, работает арифметика указателей

std::cout << *values << std::endl;

std::cout << *(values+1) << std::endl;


1.2. Дополнительные стандартные формы операторов new/delete

При подключении заголовочного файла <new> становятся доступными еще 4 стандартные формы оператора new:


new(ptr) T(аргументы конструктора);

new(ptr) T[длина массива];


new(std::nothrow) T(аргументы конструктора);

new(std::nothrow) T[длина массива];


Первые две из них называются размещающим оператором new (non-allocating placement new). Аргумент ptr — это указатель на область памяти, размер которой достаточен для размещения экземпляра или массива. Также область памяти должна иметь соответствующее выравнивание. Этот вариант оператора new не выделяет памяти, он обеспечивает только вызов конструктора. Таким образом данный вариант позволяет разделить фазу (этап) выделения памяти и инициализации объектов. Эта возможность активно используется в стандартных контейнерах.


Оператор delete для объектов, созданных таким способом, вызывать, конечно, нельзя. Для удаление объекта надо прямо вызвать деструктор, а затем освободить память способом, зависящим от способа выделения памяти.


Вторые два варианта называются не выбрасывающим исключений оператором new (nothrow new) и отличаются тем, что при невозможности удовлетворить запрос возвращают nullptr, а не выбрасывают исключение типа std::bad_alloc. Удаление объекта происходит с помощью основного оператора delete. Эти варианты считаются устаревшими и не рекомендованы для использования.


1.3. Функции выделения и освобождения памяти

Стандартные формы операторов new/delete используют, по факту, следующие функции выделения и освобождения памяти (allocation and deallocation functions):


// Для стандартной формы new, создающей единичный объект

void* operator new(std::size_t size);

void operator delete(void* ptr);



// Для стандартной формы new, создающей масив

void* operator new[](std::size_t size);

void operator delete[](void* ptr);



// Для размещающей формы оператора new - единичной и создающей массив

void* operator new(std::size_t size, void* ptr);

void* operator new[](std::size_t size, void* ptr);


// Специальный оператор delete для размещаемой формы new отсутсвует,

// так как, как было сказано выше, при использовании размещающего

// конструктора необходимо напрямую вызывать деструктор, а потом

// давать команды освобождения памяти



// Для не выбрасывающего исключение оператора new - единичной и создающей массив

void* operator new(std::size_t size, const std::nothrow_t& nth);

void* operator new[](std::size_t size, const std::nothrow_t& nth);


// Специальный оператор delete для не выбрасывающего исключение

// оператора new отсутсвует, так как можно использовать обычный

// оператор delete


Эти функции определены в глобальном пространстве имен. Функции выделения памяти для размещающих операторов new ничего не делают и просто возвращают ptr.

C++17 поддержал дополнительные формы функций выделения и освобождения памяти, с указанием выравнивания. Вот некоторые из них:


void* operator new(std::size_t size, std::align_val_t al);

void* operator new[](std::size_t size, std::align_val_t al);


Эти формы непосредственно пользователю недоступны, их использует компилятор для объектов у которых требования по выравниванию превосходят __STDCPP_DEFAULT_NEW_ALIGNMENT__, поэтому главная проблема состоит в том, чтобы пользователь случайно их не скрыл (см. раздел 2.2.1). Напомним, что в C++11 появилась возможность явно задавать выравнивание пользовательских типов.


struct alignas(32) X { /* ... */ };


2. Перегрузка стандартных форм операторов new/delete

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

После такого определения соответствующие операторы new/delete будут использовать новые функции, а не стандартные.


2.1. Перегрузка в глобальном пространстве имен

Перегрузка new/delete в глобальном пространстве имен сопряжена с рядом трудностей. Пусть, например, в некотором модуле в глобальном пространстве имен определены пользовательские функции:


void* operator new(std::size_t size)

{

// ...

}


void operator delete(void* ptr)

{

// ...

}


В этом случае произойдет фактически подмена (replacement) стандартных функций выделения и освобождения памяти для всех вызовов операторов new/delete для любых классов (в том числе и стандартных) во всем модуле. Это может привести к полному хаосу. Отметим, что описанный механизм подмены — это особый механизм, реализованный только для этого случая, а не какой-то общий механизм C++. В этом случае при реализации пользовательских функций выделения и освобождения памяти становится невозможным вызов соответствующих стандартных функций, они полностью скрыты (оператор :: не помогает) и при попытке их вызвать возникает рекурсивный вызов пользовательской функции.

Определенная в глобальном пространстве имен функция


void* operator new(std::size_t size, const std::nothrow_t& nth)

{

// ...

}


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

Такая же ситуация с функциями для массивов.

Перегрузка операторов new/delete в глобальном пространстве имен настоятельно не рекомендуется.


2.2. Перегрузка в классе

Перегрузка операторов new/delete в классе лишена описанных выше недостатков. Перегрузка действует только при создании и удалении экземпляров соответствующего класса независимо от контекста вызова операторов new/delete. При реализации пользовательских функций выделения и освобождения памяти с помощью оператора :: можно получить доступ к соответствующим стандартным функциям. Рассмотрим пример.


class X

{

// ...

public:

void* operator new(std::size_t size)

{

std::cout << "X new\n";

return ::operator new(size);

}


void operator delete(void* ptr)

{

std::cout << "X delete\n";

::operator delete(ptr);

}


void* operator new[](std::size_t size)

{

std::cout << "X new[]\n";

return ::operator new[](size);

}


void operator delete[](void* ptr)

{

std::cout << "X delete[]\n";

::operator delete[](ptr);

}

};


В этом примере к стандартным операциям просто добавляется трассировка. Теперь в выражениях new X() и new X[N] будут использоваться эти функции для выделения и освобождения памяти.

Эти функции формально являются статическими, и их можно объявлять как static. Но по существу они являются экземплярными, с вызова функции operator new() начинается создание экземпляра, а вызов функции operator delete() завершает его удаление. Эти функции никогда не вызываются для других задач. Более того, как будет показано ниже, функция operator delete() по существу является виртуальной. Так что правильнее объявлять их без static.


2.2.1. Доступ к стандартным формам операторов new/delete

Операторы new/delete можно использовать с дополнительным оператором разрешения области видимости, например ::new(p) X(). В этом случае функция operator new(), определенная в классе, будет игнорироваться, а будет использована соответствующая стандартная. Таким же способом можно использовать и оператор delete.


2.2.2. Сокрытие других форм операторов new/delete

Если теперь для класса X мы попробуем использовать размещающий или не выбрасывающий исключений new, то получим ошибку. Дело в том что, функция operator new(std::size_t size) будет скрывать (hide) другие формы operator new(). Проблему можно решить двумя способами. В первом надо добавить соответствующие варианты в класс (эти варианты должны просто делегировать операцию стандартной функции). Во втором надо использовать оператор new с оператором разрешения области видимости, например ::new(p) X().


2.2.3. Стандартные контейнеры

Если мы попробуем разместить экземпляры X в каком-нибудь стандартном контейнере, например std::vector<X>, то увидим, что наши функции для выделения и освобождения памяти не используются. Дело в том, что все стандартные контейнеры имеют собственный механизм выделения и освобождения памяти (специальный класс-аллокатор, являющийся шаблонным параметром контейнера), а для инициализации элементов используют размещающий оператор new.


2.2.4. Наследование

Функции для выделения и освобождения памяти наследуются. Если эти функции определены в базовом классе, а в производном нет, то для производного класса также будет перегружены операторы new/delete, и будут использованы функции для выделения и освобождения памяти, определенные в базовом классе.

Рассмотрим теперь полиморфную иерархию классов, где каждый класс перегружает операторы new/delete. Пусть теперь экземпляр производного класса удаляется с помощью оператора delete через указатель на базовый класс. Если деструктор базового класса виртуальный, то стандарт гарантирует вызов деструктора этого производного класса. В этом случае также гарантируется вызов функции operator delete(), определенной для этого производного класса. Таким образом функция operator delete() фактически является виртуальной.


2.2.5. Альтернативная форма функции operator delete()

В классе (особенно, когда используется наследование) иногда удобно применить альтернативную форму функции освобождения памяти:


void operator delete(void* p, std::size_t size);

void operator delete[](void* p, std::size_t size);


Параметр size задает размер элемента (даже в варианте для массива). Такая форма позволяет использовать разные функции для выделения и освобождения памяти в зависимости от конкретного производного класса.



3. Пользовательские операторы new/delete

C++ может поддержать пользовательские формы оператора new следующего вида:


new(/* аргументы */) T(/* аргументы конструктора */)

new(/* аргументы */) T[/* длина массива */]


Для того, чтобы эти формы были поддержаны, необходимо определить соответствующие функции выделения и освобождения памяти:


void* operator new(std::size_t size, /* доп. параметры */);

void* operator new[](std::size_t size, /* доп. параметры */);

void operator delete(void* p, /* доп. параметры */);

void operator delete[](void* p, /* доп. параметры */);


Список дополнительных параметров функций выделения памяти должен быть не пуст и не состоять из одного void* или const std::nothrow_t&, то есть их сигнатура не должна совпадать с одной из стандартных. Списки дополнительных параметров в operator new() и operator delete() должны совпадать. Аргументы, передаваемые в оператор new, должны соответствовать дополнительным параметрам функций выделения памяти. Пользовательская функция operator delete() также может быть в форме с дополнительным параметром размера.

Эти функции можно определить в глобальном пространстве имен или в классе, но не в пространстве имен, отличном от глобального. Если они определены в глобальном пространстве имен, то они не подменяют, а перегружают стандартные функции выделения и освобождения памяти, поэтому их использование предсказуемо и безопасно, а стандартные функции всегда доступны. Если они определены в классе, то скрывают стандартные формы, но доступ к стандартным формам можно получить с помощью оператора ::, это подробно описано в разделе 2.2.

Пользовательские формы оператора new называют пользовательским размещающим оператором new (user-defined placement new). Их не надо путать со стандартным (non-allocating) размещающим оператором new, описанным в разделе 1.2.

Соответствующей формы оператора delete не существует. Удалять объект, созданный с помощью пользовательского оператора new, можно двумя способами. Если пользовательская функция operator new() делегирует операцию выделения памяти стандартные функции выделения памяти, то можно применять стандартный оператор delete. Если нет, то придется явно вызвать деструктор, а потом пользовательскую функцию operator delete(). Компилятор вызывает пользовательскую функцию operator delete() только в одном случае: когда в процессе работы пользовательского оператора new конструктор выбрасывает исключение.

Вот пример (в глобальной области видимости).


void* operator new(std::size_t size, int a, const char* b)

{

std::cout << "new " << a << " + " << b << "\n";

return ::operator new(size);

}

void operator delete(void* p, int a, const char* b)

{

std::cout << "delete " << a << " + " << b << "\n";

::operator delete(p);

}


class X {/* ... */};

X* p = new(42, "meow") X(); // вывод: new 42 + meow

delete p; // вызов стандартной ::operator delete()


4. Определение функций выделения памяти

В приведенных примерах пользовательские функции operator new() и operator delete() делегировали операцию соответствующей стандартной функции. Иногда и такой вариант полезен, но главная цель перегрузки new/delete является создание нового механизма выделения/освобождения памяти. Задача это не простая, и прежде, чем браться за нее, надо тщательно все продумать. Скотт Мейерс [Meyers1] обсуждает возможные мотивы для принятия подобного решения (конечно, главные из них — это эффективность). Также он обсуждает основные технические проблемы связанные с правильной реализацией пользовательских функций выделения и освобождения памяти (использование функции set_new_handler(), многопоточная синхронизация, выравнивание). В [Guntheroth] приведен пример реализации относительно простых пользовательских функций выделения и освобождения памяти. Прежде, чем создавать свой вариант, следует поискать готовые решения, в качестве примера можно привести библиотеку Pool из проекта Boost.


5. Классы-аллокаторы стандартных контейнеров

Как уже упоминалось выше, стандартные контейнеры используют специальные классы-аллокаторы для задач выделения и освобождения памяти. Эти классы являются шаблонными параметрами контейнеров и пользователь может определить свою версию такого класса. Мотивы для такого решения примерно те же, что и для перегрузки операторов new/delete. В [Guntheroth] описано, как создавать подобные классы.


Список литературы

[Guntheroth]

Гантерот, Курт. Оптимизация программ на C++. Проверенные методы для повышения производительности.: Пер. с англ. — СПб.: ООО «Альфа-книга», 2017.

[Meyers1]

Мэйерс, Скотт. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2014.


Так же в этом разделе:
 
MyTetra Share v.0.59
Яндекс индекс цитирования