14.8. Процедура завершения и обработка ошибок программ

We use cookies. Read the Privacy and Cookie Policy

Если действия написанной программы вступают в противоречие с вычислительными возможностями ПЭВМ или требуют невозможного от ее ресурсов, то они, как правило, прерываются с выдачей сообщения типа «Runtime Error NNN». В таком случае мы говорим о фатальной ошибке времени счета (или во время счета). Такие ошибки могут возникать при запросах на размещение динамических переменных при малом объеме свободной памяти, попытках записи на переполненный диск, чтении с пустых дисководов, делениях на нуль или при нарушениях области допустимых значений аргументов функций Sqrt, Ln и т.д. Список этот можно продолжить. Общим для всех этих причин является то, что неизвестно, где и как они могут

- 308 -

возникнуть. Как правило, ошибки, не связанные с вводом-выводом, возникают из-за недосмотров в логическом построении программ. Причем хорошо, если все закончится выдачей текста «Runtime Error...», ведь программа может «зависнуть» так, что разблокировать ПЭВМ можно будет только полным перезапуском...

Все ошибки времени счета можно разделить на условно и безусловно фатальные. Условно фатальные ошибки — это те, которые могут блокироваться соответствующими режимами компиляции. Например, ошибка при проверке диапазонов с кодом 201 (Range Check Error) может появиться лишь, если программа откомпилирована в режиме {$R+}, ошибка 202 — переполнение стека (Stack Overflow) — в режиме {$S+}. К условно фатальным можно отнести все ошибки, связанные с вводом-выводом (коды ошибок 2-199), подробно рассмотренные в гл. 12. Отключение соответствующих режимов контроля ошибок вовсе не повышает безошибочность программ. Оно всего лишь загоняет «болезнь» программы внутрь и дает лишний повод усомниться в корректности выдаваемых программой ответов. Безусловно фатальные ошибки — это такие, которые нельзя ничем заблокировать. Сюда относятся все ошибки вычислений с плавающей точкой и некоторые другие.

Мы уже обсуждали способы обработки ошибок ввода-вывода и ошибок при распределении памяти (см. разд. 12.11, 11.5.6). Ниже будет рассмотрен способ обработки фатальных ошибок.

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

Механизм подстановки процедуры несложен. Вот его алгоритм:

1. Пишется процедура завершения программы. Это вполне обычная процедура. Требования к ней таковы: она не должна иметь параметров и должна быть откомпилирована в режиме {$F+}. Кроме того, в ней необходимо предпринять ряд действий по обработке ошибок и восстановлению системных адресов.

2. Объявляется переменная, например, с именем OldExitProc, имеющая тип Pointer.

- 309 -

3. В самом начале программы запоминается значение предопределенной системной переменной ExitProc — адрес стандартной процедуры завершения. Оно записывается в объявленную ранее переменную (у нас — в OldExitProc). А в ExitProc записывается значение адреса новой процедуры выхода.

4. Тело новой процедуры завершения должно начинаться с восстановления старого значения ExitProc. Далее, необходимо как бы обнулить системный указатель на фатальную ошибку (даже если ее не будет в действительности, это не повредит), записав в системную переменную ErrorAddr значение nil. Если этого не сделать, то после выполнения анализа ошибки и других действий на экран может вылезти уже не нужное «Runtime Error...».

Подставленная таким образом процедура всегда будет выполняться при завершении программы: будь то естественное завершение, выход по команде Halt или останов из-за ошибки.

Хотя по определению процедура выхода не содержит параметров, ей доступно значение кода ошибки и кодов завершения, посылаемых оператором Halt(N). Они хранятся в системной переменной ExitCode типа Integer. Совместно со значением адреса ErrorAddr переменная ExitCode определяет причину остановки программы (N — значение ExitCode):

ExitCode=0

ExitCode<>0

ErrorAddr= nil

Естественное завершение

Выход по Halt(N)

ErrorAddr<> nil

Не может быть

Ошибка N

Если программа была прервана пользователем с помощью комбинации клавиш Ctrl+Break, то в ExitCode запишется значение 255. Рассмотрим пример подстановки процедуры завершения (рис. 14.7).

Программа на рис. 14.7 всегда будет чистить за собой экран и заканчивать свою работу выдачей одного из предписанных сообщений. Если выполнимый код программы создан в режимах $R- и $I-, то варианты 2..199 и 201 могут никогда не сработать. Можно было использовать условную компиляцию для изъятия их из текста при необходимости.

В принципе возможно создать целую цепочку процедур завершения. Для этого надо лишь в каждой процедуре присваивать

- 310 -

| USES CRT; { используется модуль CRT }

| VAR

| OldExitProc : Pointer; { здесь запомнится ExitProc }

| {$F+} { режим компиляции $F+ }

| PROCEDURE NewExit; { новая процедура выхода }

| BEGIN

| ExitProc := OldExitProc; { восстановление пути выхода }

| TextAttr := White; { задание белого цвета (CRT) }

| ClrScr; { очистка всего экрана (CRT) }

| if ErrorAddr <> nil { Выход из-за ошибки? }

| then { Да. Обрабатывается ее код. }

| case ExitCode of

| 2..199:WriteLn('Ошибка ввода-вывода.' );

| 200 :WriteLn('ДЕЛЕНИЕ НА 0.Проверьте входные данные');

| 201 :WriteLn('Переполнение диапазона.' );

| 203 :begin

| WriteLn('HE ХВАТАЕТ СВОбОДНОЙ ПАМЯТИ.' );

| WriteLn('Освободите память и повторите запуск');

| end;

| 205 :WriteLn('Переполнение в вещественном числе.');

| 206 :WriteLn('Потеря порядка в вещественном числе.' );

| else WriteLn ('Ошибка ', ExitCode, '. ',

| 'Смотрите описание TURBO PASCAL 5.5')

| end {case и then}

| else (Нет. Нормальное завершение }

| case ExitCode of

| 255 :WriteLn('Программа прервана.');

| else WriteLn('Код завершения программы : ', ExitCode )

| end; {case и else}

| ErrorAddr := nil; { обнуление адреса ошибки }

| END; {$F-} { можно вернуть режим $F- }

| { == ОСНОВНОЙ БЛОК ПРОГРАММЫ == }

| BEGIN

| OldExitProc:= ExitProc; {запоминается адрес ExitProc }

| ExitProc:= @NewExit; {назначается новая процедура }

| {...любые остальные действия программы... }

| END.

Рис. 14.7

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

- 311 -

Иногда удобно оформлять процедуры завершения как модули. Программу на рис. 14.7 очень легко переделать в модуль. Надо лишь вписать слова unit Имя, interface и implementation. Тогда при его подключении к основной программе инициализирующая часть настроит процедуру завершения еще до начала выполнения основной программы. Подключать такой модуль надо будет одним из первых в списке раздела USES.