5 Обработка исключений

5.1 Исключения

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

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

Будет также охарактеризован пакет signal.h для ANSI C, который обрабатывает асинхронные исключения аппаратных средств. Эти исключения зависят от системы и обычно выдаются аппаратными средствами при обнаружении специального условия. Файл signal.h также обрабатывает синхронные исключения с использованием функции raise().

Использование assert.h

То, что вычисление завершилось с правильным результатом, частично можно рассматривать как доказательство того, что программа правильна. Кроме того, правильный результат зависит от правильного ввода, поэтому тот, кто активизировал вычисления, несет ответственность за обеспечение правильного ввода. Это было предусловие. Вычисление, если оно завершилось успешно, удовлетворяет постусловие. Усилие по обеспечению полностью формального доказательства правильности - идеализация, которая обычно не выполняется. Несмотря на это, такие подтверждения могут контролироваться во время выполнения для того, чтобы предоставить весьма полезную диагностику. Действительно, дисциплина продуманных подтверждений часто заставляет программиста избегать ошибок и “ловушек”.

Сообщества пользователей С и С++ все больше и больше подчеркивают важность использования таких подтверждений. Стандартный библиотечный assert.h обеспечивает макрокоманду.

void assert(int expression);

Если expression оценивается как неправильное, тогда выполнение прерывается с выводом диагностики. Подтверждения отбрасываются, если определяется макрокоманда NDEBUG.

Рассмотрим конструктор с одним параметром безопасного массива.

vect::vect(int n) {

if(n<1) { cerr << “ illegal vect size “ << n; exit(1); }

size=n; p=new int[size];

}

Заменим это на

vect::vect(int n) {

assert(n > 0); // оговоренное предусловие

size=n; p=new int[size];

assert(p!=NULL); // оговоренное постусловие

}

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

Использование signal.h

Файл signal.h обеспечивает стандартный механизм для непосредственной обработки исключений, определенных системой. Исключения определяются внутри этой библиотеки и представляют собой зависимый от системы значения целых чисел. Например,

#define SIGINT 2 /* сигнал прерывания */

#define SIGFPE 8 /* исключение операций с плавающей точкой */

#define SIGABRT 22 /* сигнал аварийного прекращения */

Эти исключения может устанавливать система. Например, нажатие control+C на клавиатуре в большинстве систем генерирует прерывание, обычно завершая текущий процесс пользователя.

Для того, чтобы генерировать явное исключение, может использоваться функция raise(), прототип которой находится в signal.h

raise(SIGFPE); // установка сигнала исключения, для операций с плавающей точкой.

Эти исключения могут обрабатываться с использованием функции signal(). Она связывает функцию-обработчик с сигналом. Также она может использоваться для того, чтобы игнорировать сигнал или повторно устанавливать действие по умолчанию.

signal(SIGABRT,my_abort); //вызвать my_abort(), если установлен SIGABRT

signal(SIGABRT,SIG_DFL); //действие по умолчанию, если установлен SIGABRT

signal(SIGFPE,SIG_IGN); // игнорировать SIGFPE

Это называется установкой обработчика. Она заменяет обычный системный обработчик, определяемый пользователем. Используем эти идеи при написании цикла, который прерывается от клавиатуры. После прерывания обработчик запрашивает пользователь, должна ли программа продолжать выполнение.

//Обработка прерывания с использованием signal.h

#include <signal.h>

#include <time.h>

void cntrl_c_handler(int sig);

main() {

int i=0,j;

cout << “Count to J million, Enter j: “; cin >>j; j*=1000000;

signal(SIGINT, cntrl_c_handler); // Функция обработчик связывается с прерыванием

// SIGINT. После обнаружения следующего прерывания вместо действия //системы по умолчанию система вызывает cntrl_c_handler

cout << (double)clock()/CLOCKS_PER_SEC << “start time\n”; //

while(1) {

j++;

if(i > j) {

cout << (double)clock()/CLOCKS_PER_SEC <<“end loop\n”;

cout << “HIT “ << j/1000000 << “MILLION” << endl;

raise(SIGINT); // Сигнал прерывания устанавливается явным вызовом.

// Неявно вызывается cntrl_c_handler

cout << “\nENter j: “; cin >> j; j*=1000000; i=0;

cout << (double)clock()/CLOCKS_PER_SEC << “start loop\n”;

}}}

// Эта подпрограмма обрабатывает исключение SIGINT

void cntrl_c_handler(int sig) {

char c;

// Было обнаружено прерывание и пользователя спрашивают, должна ли //программа продолжаться

cout << “ INTERRUPT” << “ type y to continue :”; cin >> c;

if(c==‘y’) signal(SIGINT,cntrl_c_handler);

// По этому запросу продолжения выполнения

// повторно устанавливается программа реакции на особую ситуацию. Без //повторной установки обработчика система будет возвращаться к //обработке прерывания по умолчанию.

else exit(0); // Чтобы завершить выполнение

}

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

const int SIGHEAP=SIGUSR1;

const int SIGSIZE = SIGUSR2;

vect::vect(int n) {

if(n<=0) { raise(SIGSIZE); cout << “ Enter vect size n: “; cin >> n; }

p= new int[n];

if(p==NULL) { raise(SIGHEAP); p=new int[n]; }

}

Теперь можно определять обработчики, которые могут обеспечивать соответствующее действие. Например,

void vect_size_handler(int sig) {

char c;

cout << “SIZE ERROR, Enter y to continue with default:”; cin >> c;

if(c==‘y’) { signal(SIGSIZE,vect_size_handler); }

else exit(0);

}

void vect_heap_handler(int sig) { // Возможные действия:

// вернуть память в кучу или изящно завершиться

}

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

5.2 Исключения в С++

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

Исключения обычно сигнализируют об ошибочных ситуациях при выполнении программы, например, недостатке памяти или отсутствии некоторого устройства.

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

· Прекращение работы программы с помощью макроса ASSERT. Такой способ применяется обычно при отладке программы, так как при выполнении работающей программы остановка ее часто нежелательна.

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

· Установка функций - обработчиков ошибок. Именно в эти функции передается управление при возникновении ошибки. Такой прием удобно использовать совместно с предыдущим способом. Примером такого приема является установка функции - обработчика ошибок при динамическом выделении памяти c помощью set_new_handler(). Функция-обработчик, установленная с помощью set_new_handler(), может повторно выделить память и при неудачной попытке выдать сообщение об ошибке.

· Использование глобального перехода с помощью setjmp() и longjmp(). Это функции С позволяют программисту "раскрутить" стек для того, чтобы вернуть управление в точку, помеченную setjmp().

· Использование сигналов. Сигнал - это синхронное прерывание, требующее от системы вызова специального обработчика прерывания. Установка и обработка сигналов выполняется с помощью функция языка С, описанных в signal.h.

В С++ обработка исключений реализуется более гибким и удобным способом. Когда обнаруживается исключительная ситуация, программа генерирует ( выбрасывает ) исключение. Это выполняется с помощью оператора throw. Оператор throw помещается в блок try-catch. После выбрасывания исключения выполняется глобальный переход к обработке исключения. Для этого выброшенное исключение перехватывается соответствующим оператором catch в зависимости от типа исключения.

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

try {
//try - блок, в котором используются операторы
//throw тип_ошибки
}
catch ( тип_ошибки_1 ) {
//обработка исключений с типом ошибки 1
}
catch ( тип_ошибки_2 ) {
//обработка исключений с типом ошибки 2
} ...
catch (... ) {
//обработка любого исключения
}

Просмотр catch выполняется в порядке их записи после блока try. Если тип выброшенного исключения не подходит ни к одному catch, то при наличии catch с многоточием именно он перехватывает такие исключения. В приведенном ниже примере нет операторов if или switch, код более нагляден и прост:

const int CACHE_ERROR = -1;
const signed char RANGE_ERROR = -2;

void WriteCacheBlock(unsigned char * buf, long size) {
if ( size < 0 )

throw RANGE_ERROR;
if ( size > 10000 )
throw CACHE_ERROR;
// запись данных из буфера
}
void WriteCache(unsigned char * cache, long size) {
try { WriteCacheBlock(cache,size); }
catch ( int ) { cout << "Cache error\n"; }
catch (signed char ) { cout << "Range error\n"; }
}

Особенно удобна обработка исключений при использовании классов. Дело в том, что если обработчика данного типа исключения нет в операторах catch, приводимых за блоком try, то компилятор организует выход из данной функции и возврат в вызывающую функцию, при этом тип выброшенного исключения сохраняется, и осуществляется поиск catch c соответствующим типом исключения на более высоком уровне. Поднимаясь по стеку вызовов функций в поисках нужного catch, компилятор сам вызывает деструкторы всех локальных объектов, тем самым обеспечивая нормальное освобождение памяти и дальнейшую работу программы.

Для исключений удобно использовать иерархию классов, когда есть много типов исключений. В обработчиках исключений удобно использовать виртуальные функции. Например, рассмотрим функцию для записи информации на гибкий диск. Здесь возможно появление множества ошибок, таких как неформатированный диск, диск защищен от записи, пустой дисковод и т.д. Иерархия ошибок может быть реализована в виде классов:

class DiskError {
public:
virtual char * ErrorMessage()
{ return "Unknown disk error";}
};
class DiskWriteError: public DiskError {
public:
char * ErrorMessage() { return "Disk write error";}
};
class DiskWriteProtectedError: public DiskError {
public:
char * ErrorMessage()
{ return "Disk error: disk is protected";}
};
void WriteDisk() { try {
// здесь выполняется проверка доступа к диску и
// выбрасывается одно из исключений
// throw new DiskWriteError; или
// throw new DiskWriteProtectedError;
}
catch ( DiskError & err) { cout << err.ErrorMessage()<<endl; }

}

Здесь для любого типа исключения будет вырабатываться правильное сообщение. Важно то, что все исключения порождены от одного класса DiskError, а функция ErrorMessage() -виртуальная.

Вопросы для самопроверки.

1. Приведите пример использования макроса ASSERT. Как выглядит макрорасширение такого макроса?

2. Как обрабатываются ошибки в библиотеке stdio.h? Какая глобальная переменная используется для определения кода ошибки в библиотеке stdio.h?

3. Приведите пример использования функции set_new_handler(). Проверьте, как выполняется обработка ошибок при ошибке выделения динамической памяти.

4. Приведите пример использования функций setjmp() и longjmp().

5. Проверьте, как выполняется обработка сигналов на Вашей операционной системе. Приведите пример программы.

6. Что такое "раскрутка стека"? Приведите пример.

7. Что дает вызов деструкторов локальных объектов при раскрутке стека?

8. Можно ли выбрасывать исключения типов пользователя?

9. Назовите стандартные исключения в С++.

10. В последнем примере приведена иерархия классов исключений. Проверьте работоспособность такой иерархии. Как корректно освободить занятую под объекты исключений память?