Шаг 10.
Примеры программной обработки исключений при динамическом выделении памяти

    На этом шаге мы рассмотрим способы программной обработки исключений при динамическом выделении памяти.

    Итак, поговорив о классах 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();
  }
}
Текст этой программы можно взять здесь. Результат выполнения программы (из командной строки DOS):


Рис.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[]() для определения каких-либо дополнительных проверок.

    К оглавлению

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




Предыдущий шаг Содержание Следующий шаг