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
Переменная объектного типа в динамической памяти создаётся в два этапа:
- Выделяется память с помощью оператора new.
- Вызывается конструктор класса.
Удаляется такая переменная тоже в два этапа:
- Вызывается деструктор класса.
- Освобождается память с помощью оператора 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.
Массив объектов создаётся в два этапа:
- Выделяется память, необходимая для размещения массива объектов.
- Для каждого из элементов массива вызывается конструктор по умолчанию.
Удаляется массив объектов тоже в два этапа:
- Для всех элементов массива вызываются деструкторы.
- С помощью оператора 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;