Перегрузка операторов new и delete

Операторы new и delete, предназначенные для динамического управления памятью, также можно перегрузить. Допускается перегружать все форматы операторов.

Выделение памяти под один объект

Для выделения памяти под один объект и освобождения памяти используется следующий синтаксис:

<Указатель> = new <Название класса>;
<Указатель> = new <Название класса>(<Значения для конструктора>);
delete <Указатель>;

Чтобы перегрузить эти форматы операторов необходимо создать «операторные» методы, имеющие следующие прототипы:

void *operator new(size_t count);
void operator delete(void *p);

Внутри метода operator new() для выделения памяти обычно используется функция malloc(), а для освобождения памяти внутри метода operator delete() — функция free(). Количество байтов, необходимое для хранения объекта, доступно через параметр count. При неудачном выделении памяти функция malloc() вернет нулевой указатель. В этом случае внутри метода operator new() следует сгенерировать исключение, являющее объектом класса bad_alloc (определен в заголовочном файле new). Генерация исключения выглядит так:

// #include <new>
std::bad_alloc err;         // Создаем объект исключения
throw err;                  // Генерируем исключение

Или так:

throw std::bad_alloc();     // Генерируем исключение

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

Листинг 14.8. Перегрузка операторов new и delete

#include <iostream>
#include <cstdlib>
#include <new>

class C {
   int x_;
public:
   C(int x) { x_ = x; std::cout << "C()" << std::endl; }
   ~C() { std::cout << "~C()" << std::endl; }
   void dump() const { std::cout << x_ << std::endl; }

   void *operator new(size_t count);
   void operator delete(void *p);
};

int main() {
   C *obj = new C(10); // Выделяем память для объекта
   obj->dump();        // Пользуемся памятью
   delete obj;         // Освобождаем память
   obj = nullptr;      // Обнуляем указатель
   return 0;
}

void *C::operator new(size_t count) {
   std::cout << "new count=" << count << std::endl;
   void *p = std::malloc(count);   // Выделяем память
   if (!p) throw std::bad_alloc(); // Проверка и генерация исключения
   return p;                       // Возвращаем указатель
}
void C::operator delete(void *p) {
   std::cout << "delete" << std::endl;
   std::free(p);                   // Освобождаем память
}

Результат выполнения:

new count=4
C()
10
~C()
delete

Выделение памяти под массив

Для выделения памяти под массив объектов и освобождения памяти используется следующий синтаксис:

<Указатель> = new <Название класса>[<Количество объектов>];
delete [] <Указатель>;

Чтобы перегрузить эти форматы операторов необходимо создать «операторные» методы, имеющие следующие прототипы:

void *operator new[](size_t count);
void operator delete[](void *p);

Внутри метода operator new[]() для выделения памяти обычно используется функция malloc(), а для освобождения памяти внутри метода operator delete[]() — функция free(). Количество байтов, необходимых для хранения нескольких объектов, доступно через параметр count. Обратите внимание: не количество объектов, а количество байтов, причем это значение дополнительно учитывает размер служебной информации. При неудачном выделении памяти следует сгенерировать исключение класса bad_alloc (объявлен в заголовочном файле new).

Пример перегрузки операторов new[] и delete[] с обработкой ошибок, а также последовательность вызова «операторных» методов, конструкторов и деструкторов класса приведены в листинге 14.9.

Листинг 14.9. Перегрузка операторов new[] и delete[]

#include <iostream>
#include <cstdlib>
#include <new>

class C {
   int x_;
public:
   C() { x_ = 0; std::cout << "C()" << std::endl; }
   ~C() { std::cout << "~C()" << std::endl; }
   void setX(int x) { x_ = x; }
   int getX() const { return x_; }

   void *operator new[](size_t count);
   void operator delete[](void *p);
};

int main() {
   C *obj = nullptr;
   try {               // Обрабатываем исключение
      obj = new C[2];  // Выделяем память для двух объектов
   }
   catch (std::bad_alloc &err) {
      std::cout << "Error" << std::endl;
      return 1;        // Выходим при ошибке
   }
   // std::cout << obj << std::endl; // 0x366e58
   obj[0].setX(10);    // Пользуемся памятью
   obj[1].setX(20);
   std::cout << obj[0].getX() << std::endl;
   std::cout << obj[1].getX() << std::endl;
   delete [] obj;      // Освобождаем память
   obj = nullptr;      // Обнуляем указатель
   return 0;
}

void *C::operator new[](size_t count) {
   std::cout << "new[]" << std::endl;
   // std::cout << sizeof(C) << std::endl; // 4
   // std::cout << count << std::endl;     // 16
   // 8 (служебная информация) + 4 (sizeof(C)) * 2 (количество)
   void *p = std::malloc(count);   // Выделяем память
   if (!p) {                       // Проверяем на корректность
      throw std::bad_alloc();      // Генерируем исключение
   }
   // std::cout << p << std::endl; // 0x366e50
   return p;                       // Возвращаем указатель
}
void C::operator delete[](void *p) {
   std::cout << "delete[]" << std::endl;
   // std::cout << p << std::endl; // 0x366e50
   std::free(p);                   // Освобождаем память
}

Результат выполнения:

new[]
C()
C()
10
20
~C()
~C()
delete[]

Как видно из результата, при успешном выделении памяти автоматически вызываются конструкторы. При вызове оператора delete[] вначале вызываются деструкторы всех объектов, а затем управление передается методу operator delete[](). Не забывайте о том, что если для выделения памяти используется оператор new[], то при освобождении памяти следует вызывать оператор delete[], а не delete. И, наоборот, если для выделения памяти используется оператор new, то при освобождении памяти следует вызывать оператор delete, а не delete[].

Обратите внимание: деструкторы вызываются до вызова метода operator delete[](). А откуда известно количество объектов? При вызове метода operator new[]() через параметр доступно количество байтов, необходимых под массив. Это значение больше, чем количество объектов умноженное на размер объекта. Посмотрите на значения в комментариях листинга 14.9: размер объекта 4 байта, количество объектов 2, итого 8, а значение параметра count равно 16. Вот в этих дополнительных восьми байтах и хранится служебная информация о количестве объектов. Давайте посмотрим на адреса в комментариях. Внутри методов operator new[]() и operator delete[]() имеем адрес 0x366e50, а при выделении памяти внутри функции main() — 0x366e58. Итого на размер служебной информации больше, т. е. количество объектов расположено перед массивом, а оператор new[] возвращает указатель на первый элемент массива. При использовании оператора new этой служебной информации нет, поэтому при работе с массивом освобождать память следует с помощью оператора delete[], а не delete. Иначе будет вызван деструктор только первого объекта.

Глобальные операторы new и delete

Если внутри класса перегружены операторы new и delete, то при выделении и освобождении памяти под объекты этого класса будут вызываться «операторные» методы этого класса. При выделении памяти под другие объекты вызываются их перегруженные операторы или глобальные операторы при отсутствии перегруженных версий. Чтобы отказаться от перегруженных операторов и обратиться к глобальному оператору следует написать так:

C *obj = ::new C;  // Обращение к глобальному оператору new
// ...
::delete obj;      // Обращение к глобальному оператору delete

В этом примере перед операторами new и delete расположен оператор разрешения области видимости ::, поэтому вместо перегруженных версий будут вызваны глобальные операторы new и delete.

Перегружать операторы new и delete допускается не только для отдельного класса, но и глобально для всех объектов, включая встроенные типы данных. Для этого следует создать глобальные «операторные» функции без привязки к конкретному классу. В этом случае стандартные операторы игнорируются и вызываются перегруженные версии. Если в каком-либо классе операторы new и delete перегружены, то вызываются именно перегруженные версии, а не глобальные.

Начиная со стандарта C++14, при глобальной перегрузке операторов delete и delete[] может быть указан второй параметр типа size_t, задающий количество байтов:

void operator delete(void *p, size_t count) noexcept;
void operator delete[](void *p, size_t count) noexcept;

При этом в MinGW глобальная перегрузка операторов delete и delete[] с одним параметром не вызывается. Чтобы код работал при использовании разных стандартов, следует перегрузить два варианта «операторной» функции:

// Глобальная перегрузка операторов new и delete
void *operator new(size_t count) {
   std::cout << "new global " << count << std::endl;
   void *p = std::malloc(count);   // Выделяем память
   if (!p) throw std::bad_alloc(); // Проверка и генерация исключения
   return p;                       // Возвращаем указатель
}
void operator delete(void *p) {
   std::cout << "delete global" << std::endl;
   std::free(p);                   // Освобождаем память
}
#if __cplusplus > 201103L
void operator delete(void *p, size_t count) noexcept {
   std::cout << "delete global size_t " << count << std::endl;
   std::free(p);                   // Освобождаем память
}
#endif

Выделение памяти без возбуждения исключения

При использовании следующих форматов операторов необходимо не генерировать исключение, а возвращать нулевой указатель:

<Указатель> = new (std::nothrow) <Название класса>;
<Указатель> = new (std::nothrow) <Название класса>(
                                     <Значения для конструктора>);
<Указатель> = new (std::nothrow) 
                   <Название класса>[<Количество объектов>];

Чтобы перегрузить эти форматы операторов необходимо создать «операторные» методы, имеющие следующие прототипы:

void *operator new(size_t count, const std::nothrow_t &);
void *operator new[](size_t count, const std::nothrow_t &);
void operator delete(void *p, const std::nothrow_t &);
void operator delete[](void *p, const std::nothrow_t &);

Параметр nothrow_t внутри методов можно проигнорировать. Обратите внимание: перегруженные версии операторов delete и delete[] с параметром nothrow_t вызываются только в случае, если внутри конструктора возникло исключение. В обычных случаях вызываются «операторные» методы с одним параметром.

Прочие способы перегрузки

Вместо std::nothrow внутри круглых скобок при вызове операторов new и new[] можно указать несколько значений через запятую:

<Указатель> = new (<Значения>) <Название класса>;
<Указатель> = new (<Значения>) <Название класса>(<Начальное значение>);
<Указатель> = new (<Значения>) 
                   <Название класса>[<Количество элементов>];

Чтобы перегрузить эти форматы операторов необходимо создать «операторные» методы, имеющие следующие прототипы:

void *operator new(size_t count, <Тип> <Параметр 1>
                                [, ..., <Тип> <Параметр N>]);
void operator delete(void *p, <Тип> <Параметр 1>
                                [, ..., <Тип> <Параметр N>]);
void *operator new[](size_t count, <Тип> <Параметр 1>
                                [, ..., <Тип> <Параметр N>]);
void operator delete[](void *p, <Тип> <Параметр 1>
                                [, ..., <Тип> <Параметр N>]);

Тип и количество дополнительных параметров может быть произвольным. Обратите внимание на то, что операторы выделения памяти и операторы освобождения памяти отличаются только первым параметром. Помимо этих методов дополнительно следует создать версии методов для перегрузки операторов delete и delete[] с одним параметром, т. к. методы operator delete() и operator delete[]() с дополнительными параметрами вызываются только при возникновении исключения внутри конструктора.

Например, если определены следующие методы:

void *operator new(size_t count, int x, double y);
void operator delete(void *p);
void operator delete(void *p, int x, double y);

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

C *obj = new (10, 25.8) C;      // Выделяем память
// ...
delete obj;                     // Освобождаем память

Значения 10 и 25.8, указанные внутри круглых скобок при выделении памяти, будут доступны в методе operator new() через параметры x и y соответственно.

Учебник C++ (MinGW-W64)
Учебник C++ (MinGW-W64) в формате PDF

Помощь сайту

ЮMoney (Yandex-деньги): 410011140483022

ПАО Сбербанк:
Счет: 40817810855006152256
Реквизиты банка:
Наименование: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК
Корреспондентский счет: 30101810500000000653
БИК: 044030653
КПП: 784243001
ОКПО: 09171401
ОКОНХ: 96130
Скриншот реквизитов