Нижегородский государственный университет им. Н.И.Лобачевского
Опубликован: 25.11.2008 | Доступ: свободный | Студентов: 9593 / 1296 | Оценка: 4.06 / 3.66 | Длительность: 21:16:00
Лекция 17:

Прерывания, события, обработка исключений

< Лекция 16 || Лекция 17
Аннотация: Материалы данной лекции посвящены прерываниям, событиям и обработке исключений. Приводятся практические примеры и основные принципы обработки исключений

16.1. Аппаратные и программные прерывания

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

Основным механизмом, помогающим операционной системе в этой работе, является аппарат прерываний (в англоязычной технической литературе для его обозначения используется термин interrupt ). Не вдаваясь в тонкости технической реализации, действие этого механизма можно представить себе следующим образом. Для фиксации возникающих событий используется специальный регистр прерываний, в котором каждый разряд связан с определенным событием (англ. event – событие). При возникновении этого события разряд регистра прерываний взводится в "1", после чего должна сработать специальная системная функция, реагирующая на это событие. В операционной системе MS-DOS для реализации подобного механизма был предусмотрен участок в начале оперативной памяти под названием "вектор прерываний". Каждая компонента этого вектора представляла собой команду передачи управления на функцию обработки соответствующего события. При возникновении того или иного события аппаратно останавливалось выполнение текущей программы, автоматически запоминалось состояние центрального процессора (содержимое регистров, установка различных флажков) и управление передавалось вектору, индекс которого соответствовал номеру события. Разряд в регистре прерываний при этом сбрасывался в "0". После обработки события состояние прерванного процесса восстанавливалось, и работа продолжалась. Некоторые события требовали безотлагательного вмешательства операционной системы, другие могли "подождать". Для предотвращения зацикливания имелась возможность заблокировать прием других сигналов прерывания на время обработки срочного события.

Причины, по которым возникали те или иные события, можно разделить на две категории. К первой относились события, связанные с нормальным функционированием тех или иных устройств компьютера – сигналы датчиков времени, сигналы, поступающие при нажатии клавиш клавиатуры или других устройств управления (мышь, джойстик), сигналы драйверов, завершивших выполнение порученных им операций, критическое изменение уровня питающего напряжения. Эту группу можно отнести к аппаратным прерываниям. Причина других событий кроется в ненормальном функционировании приложения, которое предпринимает попытку разделить на нуль, извлечь квадратный корень из отрицательного аргумента, выйти за пределы отведенной ему памяти, передать управление по несуществующему адресу, нарушить границы того или иного массива. Одним словом, речь идет об ошибках, которые могут возникнуть во время исполнения программы. Такого рода прерывания называют программными. Для их обработки существуют две возможности. Если приложение не позаботилось об индивидуальной реакции на программные ошибки, то операционная система сообщит о возникшей ситуации и прервет работу приложения. Вторая возможность, которая может продолжить работу приложения, связана с запрограммированной реакцией самого приложения на те или иные нештатные ситуации.

Одним из первых алгоритмических языков, в которых появилась возможность организовать индивидуальную реакцию на ошибки периода выполнения программы, был Бейсик. И хотя на первых порах профессионалы его просто проигнорировали, уже в ранних версиях Бейсика были предусмотрены операторы типа ON ERROR GOTO ... и ON ERROR GOSUB... . Они позволяли включить в программу пользователя те фрагменты, которые могли реагировать на динамически возникающие ошибки. Для анализа возникшей ситуации приложение могло использовать системные переменные типа ERR (код программной ошибки), ERL (номер строки исходной программы, при выполнении которой была обнаружена ошибка) и др. С тех пор прошел не один десяток лет, пока создатели языка C++ не удосужились включить в состав языковых средств аналогичные конструкции для обработки особых ситуаций (англ. Exceptionисключение).

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

16.2. Исключения

Для обработки нештатных ситуаций в язык C++ внесены следующие служебные слова – try (попробуй, проверь), catch (перехвати) и throw (имитируй событие). В версии BC 3.1 эти средства еще не были задействованы, поэтому содержимое настоящего раздела распространяется на среду Borland C++ Builder.

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

try { контролируемый участок программы }

Если на контролируемом участке программы возникает та или иная особая ситуация, то для ее анализа надо предусмотреть одну или несколько ловушек, каждая из которых начинается со служебного слова catch:

catch(тип_события_1 значение_события_1) { блок обработки события_1 }
catch(тип_события_2 значение_события_2) { блок обработки события_2 }
.........................................................

Аргумент оператора catch можно рассматривать как специфический объект некоторого класса. Этот объект может быть создан как в результате аварийной ситуации, фиксируемой операционной системой, так и в результате выполнения программой оператора throw. Вторая возможность позволяет программе пользователя генерировать особые ситуации в случае выполнения условий, запланированных в работе алгоритма, и структурировать обработку возникающих событий в блоках catch.

Рассмотрим в качестве примера защищенную функцию fact(n), вычисляющую n!, которая возвращает значение типа double. По определению аргумент функции факториал не может быть отрицательным, а из диапазона представления вещественных данных типа double следует, что аргумент n не должен превосходить величины 1754, т.к. 1755! > 1e+308. Конечно, проверки аргумента на принадлежность допустимому интервалу можно было предусмотреть в теле функции, например, следующим образом:

double fact(int n)
{ if(0>n || n>1754)
    { cerr<<"fact: недопустимый аргумент"<<endl;
      getch(); exit(1)
    }
  if(n==0)return 1;
  else return n*fact(n-1);
}

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

double fact(int n)
{ try
    { if(n<0) throw "fact: Argument < 0";
      if(n>1754) throw "fact: Argument too big";
      if(n==0)return 1;
      else return n*fact(n-1);
    }
  catch(const char *s)
    { cerr<<s<<endl;    //вывод сообщения об ошибке 
      return -1;        //возврат несуществующего значения функции
    }
}

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

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

Project ... raised exception class char * with message 'Exception Object Address: ...'
Process Stopped. Use Step or Run to continue.

После этого нажатие клавиши F9 (команда Run ) или F8 (команда Step ) передает управление блоку catch, который выдает сообщение, сгенерированное соответствующим оператором throw. В автоматическом режиме функция fact выдаст несуществующее значение факториала и программа продолжит свою работу:

void main()
{ int x=-5;
  double z=fact(x);
  cout<<"fact("<<x<<")="<<z<<endl;
  getch();
}
//=== Результат работы ===
fact: Argument < 0
fact(-5)=-1

Система Visual C++ в такой ситуации не выдает системное предупреждение и не останавливает процесс – происходит просто автоматическая передача управления блоку catch.

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

double fact(int n)
{ try
    { if(n<0) throw EInvalidArgument("fact: Argument < 0");
      if(n>1754) throw EInvalidArgument("fact: Argument too big");
      if(n==0)return 1;
      else return n*fact(n-1);
    }
  catch(EInvalidArgument &s)
    { cerr<<s.Message.c_str()<<endl;   //вывод сообщения об ошибке 
      return -1;        //возврат несуществующего значения функции
    }
}

Для правильной работы такой функции нам понадобится подключение заголовочного файла math.hpp, в котором находится описание класса EInvalidArgument. В модифицированном варианте функции оператор throw использует конструктор этого класса для генерации ссылки на соответствующий объект. После обработки события в блоке catch созданный объект будет автоматически уничтожен. Обратите внимание на то, что все объекты класса Exception (и порожденных классов тоже) обладают свойством Message, значение которого представлено строкой типа AnsiString. Именно поэтому при выводе сообщения его приходится преобразовывать в обычную строку с помощью функции c_str.

Использование классов типа Exception существенно расширяет многообразие нестандартных ситуаций, которое может предусмотреть программа пользователя. Если бы для индикации событий мы использовали только переменные базовых типов, то количество различных ситуаций не превышало бы десятка ( char*, unsigned char*, short int, unsigned short int, ...).

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

В программах, связанных с обработкой исключений, можно встретить ловушку catch с аргументом в виде трех точек ( catch(...) ). Такая ловушка перехватывает исключения любого типа.

< Лекция 16 || Лекция 17
Alexey Ku
Alexey Ku

Попробуйте часть кода до слова main заменить на 

#include "stdafx.h" //1

#include <iostream> //2
#include <conio.h>

using namespace std; //3

Александр Талеев
Александр Талеев

#include <iostream.h>
#include <conio.h>
int main(void)
{
int a,b,max;
cout << "a=5";
cin >> a;
cout <<"b=3";
cin >> b;
if(a>b) max=a;
else max=b;
cout <<" max="<<max;
getch();
return 0;
}

при запуске в visual express выдает ошибки 

Ошибка    1    error C1083: Не удается открыть файл включение: iostream.h: No such file or directory    c:\users\саня\documents\visual studio 2012\projects\проект3\проект3\исходный код.cpp    1    1    Проект3

    2    IntelliSense: не удается открыть источник файл "iostream.h"    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    1    1    Проект3

    3    IntelliSense: идентификатор "cout" не определен    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    6    1    Проект3

    4    IntelliSense: идентификатор "cin" не определен    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    7    1    Проект3

при создании файла я выбрал пустой проект. Может нужно было выбрать консольное приложение?