40. Свободная память (перегрузка new и delete). Явное указание размещения. Размещение, не вырабатывающее исключений.

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

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

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

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

// функция, в которой возможно возникновение исключения
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 можно перегрузить. Для этого есть несколько причин:

  • Можно увеличить производительность за счёт кеширования: при удалении объекта не освобождать память, а сохранять указатели на свободные блоки, используя их для вновь конструируемых объектов.
  • Можно выделять память сразу под несколько объектов.
  • Можно реализовать собственный "сборщик мусора" (garbage collector).
  • Можно вести лог выделения/освобождения памяти.

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

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

Оператор 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.

// Для примитивных типов вызываются глобальные ::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 в случае, если не удаётся выделить достаточно памяти.

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 освобождается память, занимаемая массивом.
// У класса A должен быть конструктор по умолчанию
A *a = new A[4];
// ...
delete [] a;

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

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

Placement new (Явное указание размещения)

Можно вызвать new на уже выделенной памяти. Такая форма оператора new называется 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;

results matching ""

    No results matching ""