На этом шаге мы рассмотрим способы программной обработки исключений при динамическом выделении памяти.
Итак, поговорив о классах xalloc и xmsg, возвратимся к функции set_new_handler(). Ее можно вызывать многократно, устанавливая разные функции для реакции на нехватку памяти. В качестве возвращаемого значения функция set_new_handler() возвращает указатель (тип new_handler) на функцию, которая была установлена с помощью set_new_handler() при ее предыдущем выполнении. Чтобы восстановить традиционную схему обработки ситуации нехватки памяти для операции new, следует вызвать функцию в виде set_new_handler(0). В этом случае все обработчики "отключаются" и накладывается запрет на генерацию исключений.
Итак, если операция new не может выделить требуемого количества памяти, вызывается последняя из функций, установленных с помощью set_new_handler(). Если не было установлено ни одной такой функции, new возвращает значение 0. Функция my_handler() должна описывать действия, которые необходимо выполнить, если new не может удовлетворить требуемый запрос.
Определяемая программистом функция my_handler() должна выполнить одно из следующих действий:
Рассмотрим особенности перечисленных вариантов.
Вызов функции abort() демонстрирует следующая программа:
//EXC10_1.СРР - завершение программы в функции my_handler(). #include <iostream.h> // Описание потоков ввода/вывода. #include <new.h> // Описание функции set_new_handler(). #include <stdlib.h> // Описание функции abort(). // Функция для обработки ошибок при выполнении // операции new: void new_new_handler() { cerr << "Error! "; abort(); // Если память выделить невозможнно, вызываем // функцию abort(), которая завершает программу // с выдачей сообщения "Abnormal program termination". } void main(void) { // Устанавливаем собственный обработчик ошибок: set_new_handler(new_new_handler); // Цикл с ограничением количества попыток выделения памяти: for (int n = 1; n <= 10000; n++) { cout << n << ": "; new char[6144000U]; // Пытаемся выделить 6000 Кбайт. cout << "OK!" << endl; } }
Откомпилируем эту программу под DOS и выполним ее. Для этого в среде программирования Borland C++ 4.5 нужно щелкнуть в окне кода правой клавишей мыши и в появившемся меню выбрать пункт Target Expert:
Рис.1. Выбор пункта меню Target Expert
Затем в одноименном окне установите настройки так, как показано на рисунке 2:
Рис.2. Содержимое окна Target Expert
По нажатию на кнопку OK вернитесь в среду программирования и выполните пункт меню Project | Build All для перекомпиляции приложения. В папке рядом с исходным файлом появится EXE-файл.
Из командной строки, например, из FAR-менеджера, выполните полученный EXE-файл. Должно получиться приблизительно следующее, приведенное на рисунке 3:
Рис.3. Результат выполнения программы
Для демонарации передачи управления исходному обработчику рассмотрим следующую программу (модификация программы EXC10_1.СРР):
//EXC10_2.СРР #include <iostream.h> // Описание потоков ввода/вывода. #include <new.h> // Описание функции set_new_handler(). #include <stdlib.h> // Описание функции abort(). // Прототип функции - старого обработчика ошибок // распределения памяти: void (*old_new_handler)(); // Функция для обработки ошибок при выполнении // операции new: void new_new_handler() { cerr << "Error! "; if (old_new_handler) (*old_new_handler)(); } void main(void) { // Устанавливаем собственный обработчик ошибок: old_new_handler = set_new_handler(new_new_handler); // Цикл с ограничением количества попыток выделения памяти: for (int n = 1; n <= 10000; n++) { cout << n << ": "; new char[6144000U]; // Пытаемся выделить 6000 Кбайт. cout << "OK!" << endl; } }
Ее отличие от предыдущей программы (EXC10_1.СРР) состоит в том, что при установке собственного обработчика ошибок адрес старого (стандартного) обработчика сохраняется как значение указателя old_new_handler. Этот сохраненный адрес используется затем в функции для обработки ошибок new_new_handler(). С его помощью вместо библиотечной функции abort() вызывается "старый" обработчик. Результат работы этой программы аналогичен предыдущему.
В следующем примере при нехватке памяти освобождаются блоки памяти, выделенной ранее, и управление возвращается программе. Для этого в программе определена глобальная переменная-указатель на блок (массив) символов (char *ptr;).
//EXC10_3.СРР - освобождение памяти в функции new_new_handler(). #include <iostream.h> // Описание потоков ввода/вывода. #include <new.h> // Описание функции set_new_handler(). #include <stdlib.h> // Описание функции abort(). char *ptr; // Указатель на блок (массив) символов. // Функция для обработки ошибок при выполнении // операции new: void new_new_handler() { cerr << "Error! "; delete ptr; // Если память выделить невозможно, удаляем // последний выделенный блок и возвращаем // управление программе. } void main(void) { // Устанавливаем собственный обработчик ошибок: set_new_handler(new_new_handler); // Цикл с ограничением количества попыток выделения памяти: for (int n = 1; n <= 10000; n++) { cout << n << ": "; ptr = new char[6144000U]; // Пытаемся выделить 6000 Кбайт. cout << "OK!" << endl; } set_new_handler(0); // Отключаем все обработчики. }
Результат выполнения этой программы будет следующим (при запуске из командной строки DOS):
1: OK! 2: OK! 3: OK! 4: OK! 5: OK! 6: OK! 7: Error! OK! 8: Error! OK! 9: Error! OK! 10: Error! OK! . . .
Действительно, результаты работы подтверждают сказанное: после неудачного выделения памяти освобождается последний выделенный блок, после чего операции new удается выделить очередной блок памяти такого же размера. Эта последовательность действий прерывается только по достижении конца цикла.
В случае компиляции для Windows была отмечена такая закономерность: если программа, выполняемая под Windows, не может получить требуемое количество памяти, то имеет смысл повторить попытки через некоторое время.
Если показанное в последнем примере освобождение памяти невозможно, функция new_new_handler() обязана либо вызвать исключение, либо завершить программу. В противном случае программа, очевидно, зациклится (после возврата из new_new_handler() попытка new выделить память опять окончится неудачей, снова будет вызвана new_new_handier(), которая, не очистив память, вновь вернет управление программе и т.д.)
Так, если в первом примере EXC10_1.СРР из функции new_new_handler() убрать вызов функции abort(), получившаяся программа зациклится.
Последняя из перечисленных задач, решаемых функцией, назначенной для обработки неудачного завершения операции new, предусматривает генерацию исключения xalloc. Это исключение формирует и функция, которая по умолчанию обрабатывает неудачное завершение операции new. Рассмотрим на примере, какую информацию передает исключение типа xalloc и как эту информацию можно использовать.
//EXC10_4.СРР - обработка исключения типа xalloc. #include <except.h> // Описание класса xalloc. #include <iostream.h> // Описание потоков ввода/вывода. #include <cstring.h> // Описание класса string. void main(void) { try { // Цикл с ограничением количества попыток выделения памяти: for (int n = 1; n <= 10000; n++) { cout << n << ": "; new char[6144000U]; // Пытаемся выделить 6000 Кбайт. cout << "OK!" << endl; } } catch (xalloc X) { cout << "Error! "; cout << "Exception: " << X.why(); } }
Рис.4. Результат работы приложения
К сожалению, стандартный обработчик ошибок выделения памяти не заносит количество нехватившей памяти в компоненте siz класса xalloc (на что, между прочим, намекается в документации), поэтому даже если в тело обработчика исключений в последнем примере вставить дополнительно вызов функции requested(), возвращающей siz, т.е.:
cout << "Error! "; cout << "Exception: " << X.why() <<" "; cout << X.request() << " bytes memory";
то результат и в этом случае будет не очень информативным:
Error! Exception: Out of memory 0 bytes memory
Самым радикальным способом устранения этой некорректности реализации будет, вероятно, перегрузка операции new. Впрочем, эту возможность предоставим читателю, а сейчас покажем, как можно реализовать обработку ошибок операции new с помощью установки своей функции, которая будет порождать исключение xalloc с соответствующими значениями компонентов.
//EXC10_5.СРР - "своя" функция обработки ошибок при //выполнении операции new и генерации xalloc. #include <except.h> // Описание класса xalloc. #include <iostream.h> // Описание потоков ввода/вывода. #include <new.h> // Описание функции set_new_handler. #include <cstring.h> // Описание класса string. #define SIZE 61440U // Функция для обработки ошибок при выполнении // операции new: void new_new_handler() throw(xalloc) { // Если память выделить не удалось, формируем исключение // xalloc с соответствующими компонентами: throw(xalloc(string("Memory full"),SIZE)); } void main(void) { // Устанавливаем собственный обработчик ошибок: set_new_handler(new_new_handler); try { // Цикл с ограничением количества попыток выделения памяти: for (int n = 1; n <= 10000; n++) { cout << n << ": "; new char[SIZE]; // Пытаемся выделить 60 Кбайт. cout << "OK!" << endl; } } catch (xalloc X) { cout << "Error! "; cout << "Exception: " << X.why()<<" "; cout << X.requested() << " bytes memory"; } }
Результат выполнения программы (из командной строки DOS):
Рис.5. Результат работы приложения
По-видимому, так по смыслу и должен работать встроенный обработчик ошибок выделения памяти. Кроме вышеописанных вариантов, также может использоваться перегрузка операций new() и new[]() для определения каких-либо дополнительных проверок.
Со следующего шага мы начнем рассматривать функции, используемые для поддержки механизма исключений.