Предыдущая Оглавление Следующая

Обработка исключений при динамическом выделении памяти

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

Достаточно распространенной особой ситуацией, требующей специальных действий на этапе выполнения программы, является невозможность выделить нужный участок памяти при ее динамическом распределении. Стандартное средство для такого запроса памяти - это операция new или перегруженные операции, вводимые с помощью операций-функций operator new () и operator new [] (). По умолчанию, если операция new не может выделить требуемое количество памяти, то она возвращает нулевое значение (NULL) и одновременно формирует исключение типа xalloc. Кроме того, в реализацию ВС++ включена специальная глобальная переменная _new_handler, значением которой служит указатель на функцию, которая запускается на выполнение при неудачном завершении операции-функции operator new (). По умолчанию функция, адресуемая указателем _new_handler, завершает выполнение программы.

Функция set_new_handler () позволяет программисту назначить собственную функцию, которая будет автоматически вызываться при невозможности выполнить операцию new.

Функция set_new_handler () принимает в качестве параметра указатель my_handler на ту функцию, которая должна автоматически вызываться при неудачном выделении памяти операцией new.

Параметр my_handler специфицирован как имеющий тип new_handler, определенный в заголовочном файле new.h таким образом:

typedef void (new *new_handler) () throw (xalloc);

В соответствии с приведенным форматом new_handler - это указатель на функцию без параметров, не возвращающую значения (void) и, возможно, порождающую исключение типа xalloc. Тип xalloc - это класс, определенный в заголовочном файле except.h.

Объект класса xalloc, созданный как исключение, передает информацию об ошибке при обработке запроса на выделение памяти. Класс xalloc создан на базе класса xmsg, который выдает сообщение, определяющее сформированное исключение. Определение xmsg в заголовочном файле except.h выглядит так:

class xmsg
{
public:
  xmsg(const string &msg);
  xmsg(const xmsg &msg);
  ~xmsg();
  const string & why() const;
  void raise() throw (xmsg);
  xmsg operator =(const xmsg &src);
private:
  string _FAR *str;
};

Класс xmsg не имеет конструктора по умолчанию. Общедоступный (public) конструктор: xmsg (string msg) предполагает, что с каждым xmsg-объектом должно быть связано конкретное явно заданное сообщение типа string. Тип string определен в заголовочном файле cstring.h.

Общедоступные (public) компонентные функции класса:

void raise () throw (xmsg);

вызов raise () приводит к порождению исключения xmsg. В частности, порождается *this.

inline const string _FAR &xmsg::why() const
{
  return *str;  
};

выдает строку, использованную в качестве параметра конструктором класса xmsg. Поскольку каждый экземпляр (объект) класса xmsg обязан иметь собственное сообщение, все его копии должны иметь уникальные сообщения.

Класс xalloc описан в заголовочном файле except.h следующим образом:

class xalloc : public xmsg
{
public:
  xalloc(const string &msg, size_t size); // Конструктор
  size_t requested() const;
  void raise() throw (xalloc);
private:
  size_t siz;
};

Класс xalloc не имеет конструктора по умолчанию, поэтому каждое определение объекта xalloc должно включать сообщение, которое выдается в случае, если не может быть выделено size байт памяти. Тип string определен в заголовочном файле cstring.h.

Общедоступные (public) компонентные функции класса xalloc:

void xalloc::raise() throw (xalloc);

Вызов raise() приводит к порождению исключения типа xalloc. В частности, порождается *this.

inline size_t xalloc::requested() const
{
  return siz;
}

Функция возвращает количество запрошенной для выделения памяти.

Если операция new не может выделить требуемого количества памяти, вызывается последняя из функций, установленных с помощью set_new_handler(). Если не было установлено ни одной такой функции, new возвращает значение 0. Функция my_handler() должна описывать действия, которые необходимо выполнить, если new не может удовлетворить требуемый запрос.

Определяемая программистом функция my_handler() должна выполнить одно из следующих действий:

Рассмотрим особенности перечисленных вариантов. Вызов функции abort() рассматривался ранее.

Для демонстрации передачи управления исходному обработчику рассмотрим следующую программу.

#include <iostream.h> // Описание потоков ввода/вывода
#include <new.h>      // Описание функции set_new_handler()
#include <stdlib.h>   // Описание функции abort()

// Прототип функции - старого обработчика ошибок памяти:
void (*old_new_handler)();
void new_new-handler() // Функция для обработки ошибок
{
  cerr << "Ошибка при выделении памяти!";
  of (old_new_handler) (*old_new_handler)();
  abort(); // "Abnormal program termination"
}

int main (void)
{
  //  Устанавливаем собственный обработчик ошибок;
  old_new_handler = set_new_handler(new_new_handler);
  //  Цикл с ограничением количества попыток выделения памяти:
  for (int n = 1; n <= 1000; n++)
  {
    cout << n << «: »;
    new char [61440U]; // Пытаемся выделить 60 Кбайт
    cout << "Успех!" << endl;
  }
  return 0;
}

При установке собственного обработчика ошибок адрес старого (стандартного) обработчика сохраняется как значение указателя old_new_handler. Этот сохраненный адрес используется затем в функции для обработки ошибок new_new_handler. С его помощью вместо библиотечной функции abort() вызывается "старый" обработчик.

Результаты выполнения программы в среде Windows:

1:  Успех! . . . 244:  Успех! Ошибка при выделении памяти!

и затем сообщение в окне: "Program Aborted".

Если my_handler возвращает управление программе, new пытается снова выделить требуемое количество памяти. Наилучшим выходом из ситуации нехватки памяти будет, очевидно, освобождение внутри функции my_handler требуемого количества памяти и возвращение управления программе. В этом случае new сможет удовлетворить запрос, и выполнение программы будет продолжено.

В следующем примере при нехватки памяти освобождаются блоки памяти, выделенной ранее, и управление возвращается программе. Для этого в программе определена глобальная переменная-указатель на блок (массив) символов (char *ptr).

#include <iostream.h> // Описание потоков ввода-вывода
#include <new.h>      // Описание функции set_new_handler

char *ptr;            //  Указатель на блок (массив) символов

//  Функция для обработки ошибок при выполнении операции new:
void new_new_handler()
{
  cerr << "Ошибка при выделении памяти! ";
  delete ptr; // Если выделить невозможно, удаляем последний блок
}
int main(void)
{
  // Устанавливаем собственный обработчик ошибок:
  set_new_handler (new_new_handler);
  // Цикл с ограничением количества попыток выделения памяти:
  for (int n = 1; n <= 1000; n++)
  {
    cout << n << ": ";
    //  Пытаемся выделить 60 Кбайт:
    ptr = new char [61440U];
    cout << "Успех! " << endl;
  }
  set_new_handler(0); // Отключаем все обработчики
  return 0;
}

Результаты выполнения этой программы будет следующим (при запуске из командной строки DOS):

1:  Успех!
. . .
6:  Успех!
7:  Ошибка при выделении памяти! Успех!

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

Если показанное в последнем примере освобождение памяти невозможно, функция my_handler() обязана либо вызвать исключение, либо завершить программу. В противном случае программа, очевидно, зациклится (после возврата из new_new_handler() попытка new выделить память опять окончится неудачей, снова будет вызвана new_new_handler(), которая, не очистив память, вновь вернет управление программе и т.д.)

Последняя из перечисленных задач, решаемых функцией, назначенной для обработки неудачного завершения операции new, предусматривает генерацию исключения xalloc. Это исключение формирует и функция, которая по умолчанию обрабатывает неудачное завершение операции new. Рассмотрим на примере, какую информацию передает исключение типа xalloc и как эту информацию можно использовать.

#include <except.h>   // Описание класса xalloc
#include <iostream.h> // Описание потоков ввода/вывода
#include <cstring.h>  // Описание класса string

int main (void)
{
  try
  {
    for (int n = 1; n <= 1000; n++)
    {
      cout << n << ": ";
      new char [61440U]; // Пытаемся выделить 60 Кбайт
      cout << "Успех!" << endl;
    }
  }
  catch (xalloc X)
  {
    cout << "При выделении памяти обнаружено ";
    cout << "исключение "; << X.why();
  }
  return 0;
}

Результат выполнения программы (из командной строки DOS):

1:  Успех!
. . .
6: Успех!
7: При выделении памяти обнаружено исключение Out of memory

К сожалению, стандартный обработчик ошибок выделения памяти не заносит количество не хватившей памяти в компоненте siz класса xalloc, поэтому даже если в тело обработчика исключений в последнем примере вставить дополнительно вызов функции requested, возвращающей siz, т.е.:

cout << "Обнаружено исключение " << X.why();
cout << " при выделении ";
cout << X.request() << "байт памяти";

то результат и в этом случае будет не очень информативным:

7:  Обнаружено исключение Out of memory при выделении 0 байт памяти.

Самым радикальным способом устранения этой некорректности реализации будет, вероятно, перегрузка операции new. Покажем, как можно реализовать обработку ошибок операции 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));
}
int main (void)
{
  // Устанавливаем собственный обработчик ошибок:
  set_new_handler (new_new_handler);
  try // Контролируемый блок
  {
    for (int n = 1; n <= 1000; n++)
    {
      cout << n << "; ";
      new char [SIZE]; // Пытаемся выделить 60 Кбайт
      cout << "Успех! "  << endl;
    }
  }
  catch (xalloc X) //  Обработчик исключений
  {
    cout << "Обнаружено исключение  " << X.why ();
    cout << "привыделении";
    cout << X.requested () << "байт памяти. ";
  }
  return 0;
}

Результат выполнения программы (из командной строки DOS):

1:  Успех!
. . .
7:  Обнаружено исключение Memory full при выделении 61440 байт памяти.

Функции, глобальные переменные и классы поддержки механизма исключений

Функция обработки неопознанного исключения. Функция void terminate() вызывается в случае, когда отсутствует процедура для обработки некоторого сформированного исключения. По умолчанию terminate() вызывает библиотечную функцию abort(), что влечет выдачу сообщения "Abnormal program termination" и завершение программы. Если такая последовательность действий программиста не устраивает, он может написать собственную функцию (terminate_function) и зарегистрировать ее с помощью функции set_terminate(). В этом случае terminate() будет вызывать эту новую функцию вместо функции abort().

Функция set_terminate() позволяет установить функцию, определяющую реакцию программы на исключение, для обработки которого нет специальной процедуры. Эти действия определяются в функции, поименованной ниже как terminate_func(). Указанная функция специфицируется как функция типа terminate_function. Такой тип в свою очередь определен в файле except.h как указатель на функцию без параметров, не возвращающую значения:

typedef void (*terminate_function)();
terminate_function set_terminate(terminate_function terminate_func);

Функция set_terminate() возвращает указатель на функцию, которая была установлена с помощью set_terminate() ранее.

Следующая программа демонстрирует общую схему применения собственной функции для обработки неопознанного исключения:

#include <stdlib.h>   // Для функции abort()
#include <except.h>   // Для функции поддержки исключений
#include <iostream.h> // Для потоков ввода-вывода
// Указатель на предыдущую функцию terminate:
void (*old_terminate)();
// Новая функция обработки неопознанного исключения:
void new_terminate()
{
  cout << "\nВызвана функция new_terminate()";
  // ... Действия, которые необходимо выполнить
  // ... до завершения программы
  abort (); // Завершение программы
}
int main (void)
{
  // Установка своей функции обработки:
  old_terminate = set_terminate(new_terminate);
  // Генерация исключения вне контролируемого блока:
  throw (25);
  return 0;
}

Результат выполнения программы:

Вызвана функция new_terminate()

Вслед за этим программа завершается и выводит в окно сообщение: "Program Aborted!".

Вводимая программистом функция для обработки неопознанного исключения, во- первых, не должна формировать новых исключений, во-вторых, эта функция должна завершать программу и не возвращать управление вызвавшей ее функции terminate(). Попытка такого возврата приведет к неопределенным результатам.

Функция void unexpected () вызывается, когда некоторая функция порождает исключение, отсутствующее в списке ее исключений. В свою очередь функция unexpected () по умолчанию вызывает функцию, зарегистрированную пользователем с помощью функции set_unexpected(). Если такая функция отсутствует, unexpected() вызывает функцию terminate(). Функция unexpected() не возвращает значения, однако может сама порождать исключения.

Функция set_unexpected() позволяет установить функцию, определяющую реакцию программы на неизвестное исключение. Эти действия определяются в функции, которая ниже поименована как unexpected_func(). Указанная функция специфицируется как функция типа unexpected_function. Этот тип определен в файле except.h как указатель на функцию без параметров, не возвращающую значения:

typedef void   (*unexpected_function)();
unexpected_function sеt_unexpected (unexpected_function unexpected_func);

По умолчанию, неожиданное (неизвестное для функции) исключение вызывает функцию unexpected(), которая, в свою очередь вызывает либо unexpected_func() (если она определена), либо terminate() (в противном случае).

Функция set_unexpected() возвращает указатель на функцию, которая была установлена с помощью set_unexpected() ранее. Устанавливаемая функция (unexpected_func) обработки неизвестного исключения не должна возвращать управление вызвавшей ее функции unexpected(). Попытка возврата приведет к неопределенным результатам.

Кроме всего прочего, unexpected_func() может вызывать функции abort(), exit() и terminate().

Глобальные переменные, относящиеся к исключениям:

Эти переменные определяются в файле except.h следующим образом:

extern char *__throwExceptionName;
extern char *__throwFileName;
extern unsigned __throwLineNumber;

Следующая программа демонстрирует возможности применения перечисленных глобальных переменных:

#include <except.h>    // Описание переменных   throwXXXX
#include <iostream.h>  // Описание потоков ввода-вывода
class A // Определяем класс A
{
public:
  void print () // Функция печати сведений об исключении
  {
    cout << "Обнаружено исключение ";
    cout << __throwExceptionName;
    cout << " в строке " << __throwLineNumber;
    cout << " файла " << __throwFileName << endl;
  }
}
class В : public A {};   // Класс В порождается из A
class С : public A {};   // Класс С порождается из А
С _с; // Создан объект класса С
void f()        // Функция может порождать любые исключения
{
  try
  { // Формируем исключение (объект класса С):
    throw (_с);
  }
  catch (В X)   // Здесь обрабатываются исключения типа В
  {
    X.print ();
  }
}
int main ()
{
  try
  { f(); }         // Контролируемый блок
  // Обрабатываются исключения типа А
  // (и порожденных от него):
  catch (A X)
  { X.print (); }      //  Обнаружено исключение
  return 0;
}

Комментарии в тексте программы достаточно подробно описывают ее особенности. В выводимом на экран результате используются значения глобальных переменных.

Конструкторы и деструкторы в исключениях

Когда выполнение программы прерывается возникшим исключением, происходит вызов деструкторов для всех автоматических объектов, появившихся с начала входа в контролируемый блок. Если исключение было порождено во время исполнения конструктора некоторого объекта, деструкторы вызываются лишь для успешно построенных объектов. Например, если исключение возникло при построении массива объектов, деструкторы будут вызваны только для полностью построенных объектов.

Приведем пример законченной программы, иллюстрирующий поведение деструкторов при обработке исключений.

#include <iostream.h>
#include <new.h>
#include <cstring.h>
class Memory
{
  char *ptr;
public:
  Memory ()     // Конструктор выделяет 60 Кбайт памяти
  { ptr = new char [61440U]; }
  ~Memory ()    // Деструктор очищает выделенную память
  { delete ptr; }
};
// Определение класса "Набор блоков памяти" :
class BigMemory
{
  static int nCopy;   // Счетчик экземпляров класса + 1
  // Указатель на класс Memory
  Memory *MemPtr;
public:
  // Конструктор с параметром по умолчанию
  BigMemory (int n = 3)
  {
    cout << endl << nCopy << ": ";
    MemPtr = new Memory [n];
    cout << "Успех!"; // Если память выделена успешно,
    ++nСору;          //  увеличиваем счетчик числа экземпляров
  }
  ~BigMemory () // Деструктор очищает выделенную память
  {
    cout << endl << --nCopy << ": Вызов деструктора";
    delete [] MemPtr;
  }
};
// Инициализация статического элемента:
int BigMemory::nCopy = 1;
// Указатель на старый обработчик для new:
void (*old_new_handler) ();
// Новый обработчик ошибок:
void new_new_handler () throw (xalloc)
{ // Печатаем сообщение ...
  cout << "Ошибка при выделении памяти!";
  // ... и передаем управление старому обработчику
  (*old_new_handler) ();
}
int main (void)
{ // Устанавливаем новый обработчик:
  old_new__handler = set_new_handler (new_new_handler);
  try // Контролируемый блок
  { // Запрашиваем 100 блоков по 60 Кбайт:
    BigMemory Request1 (100) ;
    // Запрашиваем 100 блоков по 60 Кбайт:
    BigMemory Request2 (100) ;
    // Запрашиваем 100 блоков по 60 Кбайт:
    BigMemory Requests (100) ;
  }
  catch (xmsg& X) // Передача объекта по ссылке
  {
    cout << "\nОбнаружено исключение " << X.why();
    cout << " класса " << __throwExceptionName;
  }
  set_new_handler (old_new_handler);
  return 0;
}
Предыдущая Оглавление Следующая