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

Немного об исключениях
Особенности работы операторов new и delete
Перегрузка операторов new и delete для отдельных классов
Переопределение глобальных операторов new и delete
Оператор new и исключения
Операторы new[] и delete[]
Placement new
Оператор new, не бросающий исключение
Другие формы оператора new

Немного об исключениях

Функция может сигнализировать о возникшей ошибке с помощью возвращаемого значения. Однако, у этого способа есть ряд недостатков:

Поэтому в синтаксис языка C++ введены исключения. Можно сообщить о возникновении исключительной ситуации с помощью оператора throw <значение>. При этом значение, передаваемое оператору throw, может содержать дополнительную информацию о возникшей ошибке и иметь примитивный или объектный тип. После вызова оператора throw выполнение функции прерывается и выполняется раскрутка стека до ближайшего блока try-catch, в котором обрабатывается исключение переданного типа. Код, в котором возможно возникновение исключения, заключается в блок try, после которого может следовать несколько операторов catch, принимающих исключения определённого типа. Если блок catch должен принимать все исключения, используется оператор catch(...).

Листинг 1. Механизм обработки исключений.
// функция, в которой возможно возникновение исключения
int foo() {
    //...
    throw 1;
    //...
}

//...

void bar() {
    try {
        // при нормальном ходе выполнения возвращаемое значение сохраняется в локальную переменную
        int i = foo();
        // дальнейшее выполнение функции
    } catch (int e) {
        // код обработки исключения
    }
}

Особенности работы операторов new и delete

Переменная объектного типа в динамической памяти создаётся в два этапа:

  1. Выделяется память с помощью оператора new.
  2. Вызывается конструктор класса.

Удаляется такая переменная тоже в два этапа:

  1. Вызывается деструктор класса.
  2. Освобождается память с помощью оператора delete.

Перегрузка операторов new и delete для отдельных классов

Операторы new и delete можно перегрузить. Для этого есть несколько причин:

Операторы new и delete имеют следующие сигнатуры:

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

Оператор new принимает размер памяти, которую необходимо выделить, и возвращает указатель на выделенную память.

Оператор delete принимает указатель на память, которую нужно освободить.

Листинг 2. Пример реализации операторов new и delete.
class A {

public:
    void *operator new(size_t size);
    void operator delete(void *p);
};

void *A::operator new(size_t size) {
    printf("Allocated %d bytes\n", size);
    return malloc(size);
}

void A::operator delete(void *p) {
    free(p);
}

Вместо функций malloc и free можно использовать глобальные операторы ::new и ::delete.

Рекомендуется не производить в операторе new (особенно в глобальном) какие-либо операции с объектами, которые могут вызвать оператор new. Например, для вывода текста используется функция printf, а не объект std::cout.

Операторы new и delete, объявленные внутри класса, функционируют подобно статическим функциям и вызываются для данного класса и его наследников, для которых эти операторы не переопределены.

Переопределение глобальных операторов new и delete

В некоторых случаях может потребоваться перегрузить глобальные операторы new и delete. Они находятся не в пространстве имён std, а в глобальном пространстве имён.

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

Листинг 3. Вызов операторов new и delete для разных типов.
// Для примитивных типов вызываются глобальные ::new и ::delete
int *i = new int;
delete i;

// Для класса A вызываются переопределённые A::new и A::delete
A *a = new A;
delete a;

// Для класса C операторы new и delete не переопределены,
// поэтому вызываются глобальные ::new и ::delete
C *c = new C;
delete c;

Оператор new и исключения

Стандартный оператор new бросает исключение std::bad_alloc в случае, если не удаётся выделить достаточно памяти.

Листинг 4. Пример реализации оператора new, бросающего исключение при нехватке памяти.

void *operator new(size_t size) {
    void *p = malloc(size);
    if (p == NULL) {
        throw std::bad_alloc();
    }
    return p;
}

Если конструктор класса бросает любое исключение, память, выделенная под объект автоматически освобождается.

Операторы new[] и delete[]

При создании массива вызывается другая форма операторов new и delete. Они имеют следующие сигнатуры:

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

Параметры этих функций аналогичны параметрам обычных операторов new и delete.

Массив объектов создаётся в два этапа:

  1. Выделяется память, необходимая для размещения массива объектов.
  2. Для каждого из элементов массива вызывается конструктор по умолчанию.
Удаляется массив объектов тоже в два этапа:
  1. Для всех элементов массива вызываются деструкторы.
  2. С помощью оператора delete освобождается память, занимаемая массивом.

Листинг 5. Пример использования операторов new[] и delete[].

// У класса A должен быть конструктор по умолчанию
A *a = new A[4];
// ...
delete [] a;

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

К примеру, механизм создания и удаления массивов объектов может быть реализован следующим образом (рис. 1). С помощью функции operator new выделяется размер памяти, равный суммарному размеру всех элементов массива и размеру служебной информации. Оператор new выделяет память и возвращает, допустим, указатель p. По этому указателю записывается служебная информация, а в программу возвращается указатель на первый элемент массива. Аналогично, при вызове delete, в функцию operator delete передаётся не указатель на первый элемент массива, а указатель, на начало блока, выделенного оператором new.

Рис. 1. Пример выделения памяти для массива объектов.

Placement new

Можно вызвать new на уже выделенной памяти. Такая форма оператора new называется placement new. Она может понадобиться в операционных системах реального времени и встраиваемых системах, чтобы жёстко закрепить адрес объекта.

Листинг 6. Пример создания объекта с помощью placement new.

// p - указатель на некий статический буфер

// Явный вызов конструктора
A *a = new(p) A;
// Явный вызов деструктора
a->A::~A();

Реализация placement new выглядит следующим образом:

void *operator new(size_t size, void *p) {
    return p;
}

Оператор new, не бросающий исключение

Если требуется, чтобы оператор new не бросал исключение в случае нехватки памяти, а возвращал ноль, можно использовать оператор new с параметром std::nothrow. Этот параметр имеет тип std::nothrow_t.

// Объявление
void *operator new(size_t size, const std::nothrow_t &nt);

// Пример использования
A *a = new(std::nothrow) A;

Другие формы оператора new

Синтаксис языка C++ позволяет создавать собственные формы оператора new.

// Объявление
void *operator new(size_t size, std::string &str);

// Пример использования
A *a = new("Object a") A;