Функции operator new() и operator delete()
Время жизни объекта определяется областью действия его имени. В зависимости от расположения оператора определения объекта, он может располагаться в глобальном или локальном сегменте памяти. При определении глобального объекта соответствующие конструкторы объявляются со спецификатором public, поскольку они должны быть доступны фактически до начала выполнения программы. Глобальные объекты существуют в течение всего времени выполнения программы.
Локальные объекты создаются в соответствующих сегментах памяти в ходе выполнения операторов определения, после передачи управления в функцию или вложенный блок операторов. По возвращении из вложенного блока или функции, имя объекта оказывается вне области действия имени. Сам же объект уничтожается в момент освобождения соответствующего сегмента памяти. Важная роль при этом отводится деструкторам.
Можно избежать преждевременной гибели объекта, расположив его в динамической памяти. В этом случае память для объекта выделяется с помощью выражения размещения. Значением этого выражения является адрес области памяти, выделенной для размещения объекта в результате выполнения выражения. Очевидно, что это значение можно присвоить переменной типа указатель на объект данного класса.
Динамическая память не опустошается автоматически. "Гибель" локального указателя, настроенного на выделенную область динамической памяти означает всего лишь потерю доступа к этой области памяти. В этом случае уничтожается указатель, но освобождения памяти не происходит.
Для освобождения памяти используется операция (операторная функция) delete. Подобно операторной функции new, delete также является статическим членом класса.
В контексте выражений размещения и удаления могут быть использованы стандартные операции C++ new и delete, а может быть обеспечен вызов операторных функций operator new и operator delete.
Согласно грамматике C++, основным операндом для символа операции new в выражении размещения является заключённое в круглые скобки ИмяТипа, либо ИмяТипаNew (без скобок), которое разворачивается в конструкцию, содержащую информацию о размерах размещаемого массива (константные выражения в квадратных скобках):
ВыражениеРазмещения
::= [::] new [Размещение] ИмяТипаNew [ИнициализаторNew] ::= [::] new [Размещение] (ИмяТипа) [ИнициализаторNew]
ИмяТипаNew ::= СписокСпецификаторовТипа [ОписательNew]
ОписательNew ::= [СписокCVОписателей] [ОписательNew] ::= [ОписательNew] [Выражение] ::= *****
При этом можно определить несколько различных вариантов операторной функции operator new. Перегруженные операторные функции будут различаться списками параметров. В C++ предусмотрены специальные средства передачи значений параметров подобным перегруженным операторным функциям. С этой целью используется так называемое Размещение, которое является необязательным составным элементом выражения размещения. Заключённый в круглые скобки список выражений располагается в выражении размещения непосредственно перед именем операторной функции new.
Мы объявляем простой класс, содержащий определения операторных функций распределения динамической памяти. И размещаем это объявление в заголовочном файле с именем TypeX.h.
// TypeX.h #ifndef TYPEX #define TYPEX /* Инструкции препроцессора используются для предотвращения многократного объявления класса в программном модуле. Даже если в исходном файле появится несколько инструкций препроцессора, обеспечивающих включение заголовочного файла TypeX.h, в исходном файле окажется всего лишь одно объявление класса TypeX. */ // Объявление класса TypeX. class TypeX { public: /* Встроенный конструктор */ TypeX() { cout "Это TypeX()" endl; } /* Встроенный конструктор с параметром */ TypeX(int x) { cout "Это TypeX(" x ")" endl; } /* Встроенный деструктор */ ~TypeX() { cout "Это ~TypeX()" endl; } /* Встроенная операторная функция operator new() */ void *operator new(size_t size) { cout "Это void *operator new(" size ")" endl; return new char(size); } /* Операторная функция operator new() с дополнительным параметром */ void *operator new(size_t size, int xPar) { cout "void *operator new(" size "," xPar ")" endl; return new char(size); } /* Встроенная операторная функция operator delete() */ void operator delete(void *cPoint, size_t size) { cout "Это void operator delete(" size ")" endl; if (cPoint) delete cPoint; }; }; #endif
Сложная семантика выражений C++ проявляется на простых примерах. Небольшие программы позволят выявить принципиальные моменты алгоритмов трансляции, свойства операций динамического распределения памяти, особенности операторных функций operator new() и operator delete(). В программе следует обратить внимание на второе выражение размещения, которое позволяет активизировать конструктор с параметрами.
#include iostream.h #include "TypeX.h" void main() { TypeX *xPoint = NULL, *xPointP = NULL, *xxPointP = NULL; xPoint = new TypeX; xPointP = new TypeX(25); // Выражение размещения может содержать параметры. // Так осуществляется управление конструктором. xxPointP = new (125+25) TypeX(50); // Выражение размещения может включать размещение. // Этот одноэлементный список выражений обеспечивает передачу // значений параметров операторной функции operator new. // Альтернативные формы вызова операторных функций: // ИмяТипа в круглых скобках. // xPoint = new (TypeX); // xPointP = new (TypeX)(25); // xxPointP = new (125+25) (TypeX)(50); delete xPoint; delete xPointP; delete xxPointP; cout "OK" endl; }
В ходе трансляции распознаются выражения размещения и освобождения, и делается всё необходимое для своевременного вызова конструкторов и деструктора. Если к тому же, в объявлении класса обнаружены объявления соответствующих операторных функций, эти выражения преобразуются транслятором в вызовы операторных функций.
Так что транслируем, запускаем и наблюдаем результаты:
Это void *operator new(1) Это TypeX() Это void *operator new(1) Это TypeX(25) Это void *operator new(1, 150) Это TypeX(50) Это ~TypeX() Это void operator delete(1) Это ~TypeX() Это void operator delete(1) Это ~TypeX() Это void operator delete(1) OK
В ходе выполнения этой программы на дисплей выводится сообщение о работе операторной функции operator new(), которая вызывается в результате определения значения выражения размещения.
После этого, появляется сообщение о работе конструкторов, запуск которых обеспечивается транслятором в результате выполнения выражений размещения.
Затем, непосредственно перед выполнением выражения освобождения, выполняется деструктор, о запуске которого также заботится транслятор.
Наконец, управление передаётся операторной функции operator delete(). Жизненный цикл безымянных объектов, размещённых в динамической памяти в результате выполнения выражений размещения и адресуемых посредством указателей xPoint и xPointP, завершён.
Недоступный и скрытый от программиста механизм запуска конструктора, достаточно сложен. В этом можно убедиться, изменив операторную функцию operator new() в классе TypeX следующим образом:
/* Встроенная операторная функция operator new() */ void *operator new(size_t size) { cout "Это void *operator new(" size ")" endl; return NULL; }
Новая операторная функция даже не пытается использовать операцию выделения памяти. Она возвращает пустое значение указателя. При этом значением выражения размещения в операторе xPoint = new TypeX;
оказывается нулевой адрес. И в результате запуск конструктора отменяется:
Это void *operator new(1) OK
Аналогичным образом работает программный код, который обеспечивает вызов деструктора: непосредственно перед запуском деструктора производится проверка значения указателя.
Мы возвращаем операторную функцию к исходному состоянию, после чего подвергнем исходную программу небольшой модификации. Расположим непосредственно перед символами операций new и delete (символ операции не обязательно представляет операцию!) разделители :: (именно разделители, поскольку они служат для модификации операции, а не используются в сочетании с операндами).
#include iostream.h #include "TypeX.h" void main() { TypeX *xPoint = NULL; xPoint = ::new TypeX; ::delete xPoint; cout "OK" endl; }
В результате выполнения новой версии нашей программы мы получаем следующий результат:
Это TypeX() Это ~TypeX() OK
Операторные функции не вызываются, однако память выделяется и производится запуск конструктора, а затем и деструктора.
Это означает, что помеченные разделителем :: выражения размещения и освобождения исправно работают, выделяя и освобождая необходимую память. Символы операций ::new и ::delete воспринимаются транслятором как символы собственных "глобальных" операций выделения и освобождения памяти языка C++.
К аналогичному результату мы приходим, исключив из объявления класса TypeX объявления операторных функций operator new() и operator delete(). В этом случае перед символами операций new и delete даже не требуется располагать разделители. В этом случае транслятор их однозначно воспринимает как символы операций.
Мы снова восстанавливаем файл с объявлением класса TypeX и очередной раз модифицируем нашу программу. На этот раз мы заменим выражения размещения и освобождения выражениями явного вызова операторных функций.
#include iostream.h #include "TypeX.h" void main() { TypeX *xPoint = NULL; xPoint = (TypeX *) TypeX::operator new (sizeof(TypeX)); TypeX::operator delete(xPoint, sizeof(TypeX)); // delete xPoint; cout "OK" endl; }
В результате выполнения этой версии программы на дисплей будут выведены следующие сообщения:
Это void *operator new(1) Это void operator delete(1) OK
Операторные функции работают успешно, память выделяется и освобождается, однако управление конструктору и деструктору не передаётся. Выражение вызова операторных функций operator new() и operator delete() не обеспечивают вызова конструктора и деструктора. Мы уже знаем, что в C++, за исключением весьма странного выражения явного вызова, вызов конструктора и деструктора обеспечивается транслятором в контексте ограниченного множества выражений. Нет соответствующего выражения, - нет и вызова конструктора.