Урок №183. Исключения, Функции и Раскручивание стека

  Юрий Ворон  | 

    | 

  Обновлено 28 Ноя 2018  | 

 428

 ǀ   1 

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

Генерация исключений за пределами блока try

В предыдущем уроке операторы throw помещались непосредственно в блок try. Если бы это было обязательным условием, то, согласитесь, обработка исключений не была бы гибкой вообще.

На самом деле стейтменты throw вовсе не обязаны находиться непосредственно в блоке try, благодаря выполнению такой операции как «раскручивание стека» (детальнее о том, что такое стек). Это дает нам необходимую гибкость в разделении общего потока выполнения кода программы от обработки исключений. Продемонстрируем это, переписав программу из предыдущего урока, вынеся генерацию исключения и вычисление квадратного корня в отдельную функцию:

Здесь мы переместили генерацию исключения и операцию вычисления квадратного корня в отдельную функцию mySqrt(). Затем мы вызываем эту функцию в блоке try. Убедимся, что всё работает, как нужно:

Enter a number: -3
Error: Can not take sqrt of negative number



Ура!

Однако, давайте вернемся к моменту генерации исключения и рассмотрим ход выполнения программы. Во-первых, при генерации исключения компилятор смотрит, можно ли сразу же обработать это исключение (для этого нужно, чтобы исключение выбрасывалось внутри блока try). Поскольку точка выполнения не находится внутри блока try, то и обработать исключение немедленно не получится. Таким образом, выполнение функции mySqrt() приостанавливается и программа смотрит, может ли caller (который и вызывает mySqrt()) обработать это исключение. Если нет, то компилятор завершает выполнение caller-а и переходит на уровень выше: к caller-у, который вызывает текущего caller-а, чтобы проверить, сможет ли тот обработать исключение. И так последовательно до тех пор, пока не будет найден соответствующий обработчик исключения, или пока main() не завершится без обработки исключения. Этот процесс называется раскручиванием стека.

Теперь рассмотрим детальнее, как это относится к нашей программе. Сначала компилятор проверяет, генерируется ли исключение внутри блока try. В нашем случае – нет, поэтому стек начинает раскручиваться. При этом функция mySqrt() завершает своё выполнение и точка выполнения перемещается обратно в main(). Теперь компилятор проверяет снова, находимся ли мы внутри блока try. Поскольку вызов mySqrt() был выполнен из блока try, то компилятор начинает искать соответствующий обработчик catch. Он находит обработчик типа const char*, и исключение обрабатывается блоком catch внутри main().

Подводя итог, mySqrt() генерирует исключение, но блоки try/catch, которые находятся в main(), ловят и обрабатывают это исключение. Другими словами, блок try ловит исключения не только внутри себя, но и внутри функций, которые вызываются в блоке try.

Самое интересное здесь в том, что mySqrt() как бы сообщает: «Эй, компилятор, здесь проблема!», но обрабатывать эту проблему mySqrt() отказывается. Это, по сути, делегирование ответственности за обработку исключения на caller-а (эквивалентно тому, как использование кодов завершения перекладывает ответственность за обработку ошибок обратно на caller-а).

Сейчас, некоторые из вас, вероятно, спросят: «Зачем передавать ошибки обратно в caller? Почему бы просто не заставить mySqrt() обрабатывать собственные исключения?». Проблема в том, что разные программы обрабатывают ошибки/исключения по-разному. Консольная программа выводит сообщение об ошибке. Приложение Windows выводит диалоговое окно с ошибкой. В одной программе это может быть фатальной ошибкой, а в другой — нет. Передавая ошибку обратно в стек, каждое приложение может обрабатывать исключение mySqrt() таким образом, который является наиболее подходящим по контексту! В конечном счете, это позволяет отделить функционал mySqrt() от кода обработки исключений, который можно разместить в других (менее важных) частях кода.

Еще один пример раскручивания стека

Здесь у нас стек уже побольше. Хотя всё кажется слишком сложным, но, на самом деле, всё просто:

   main() вызывает one();

   one() вызывает two();

   two() вызывает three();

   three() вызывает last();

   last() выбрасывает исключение.



Смотрим:

Взгляните на эту программу еще раз. Можете ли вы понять, что выведется, а что нет? Результат:

Start main
Start one
Start two
Start three
Start last
last throwing int exception
one caught int exception
End one
End main

Рассмотрим ход выполнения программы детальнее. Объяснять вывод строчек «Start», думаю, не нужно. Функция last() выводит last throwing int exception, а затем выбрасывает исключение типа int. Вот где начинается самое интересное.

Поскольку last() не обрабатывает исключения самостоятельно, то стек начинает раскручиваться. Функция last() немедленно завершает своё выполнение и точка выполнения возвращается обратно в caller (в функцию three()).

Функция three() не обрабатывает какие-либо исключения, поэтому стек раскручивается дальше, выполнение функции three() прекращается и точка выполнения возвращается в two().

Функция two() имеет блок try, в котором находится вызов three(), поэтому компилятор пытается найти обработчик исключений типа int, но так как его не находит, то точка выполнения возвращается обратно в one(). Обратите внимание, компилятор не выполняет неявного преобразования, чтобы сопоставить исключение типа int с обработчиком типа double.

Функция one() также имеет блок try с вызовом two() внутри, поэтому компилятор смотрит, есть ли подходящий обработчик catch. Есть! Функция one() обрабатывает исключение и выводит one caught int exception.

Поскольку исключение было обработано, то точка выполнения перемещается в конец блока catch внутри one(). Это означает, что one() выводит End one, а затем завершает своё выполнение как обычно.

Точка выполнения возвращается обратно в main(). Хотя main() имеет обработчик исключений типа int, но наше исключение уже было обработано функцией one(), поэтому блок catch внутри main() не выполняется. main() выводит End main, а затем завершает своё выполнение.

Из этой программы можно сделать несколько интересных выводов:

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

   Во-вторых, если блок try не имеет обработчика catch соответствующего типа, то раскручивание стека происходит так же, как если бы этого блока try не было вообще. В примере выше two() не обрабатывает исключение, потому что соответствующего обработчика catch у него нет.

   В-третьих, когда исключение обработано, выполнение кода продолжается как обычно, начиная с конца блока catch (в котором это исключение было обработано). В примере выше функция one() обработала исключение, а затем продолжила своё выполнение выводом строчки End one. К тому времени, как точка выполнения возвращается обратно в main(), исключение уже было сгенерировано и обработано, main() выполняется так, как если бы этого исключения не было вообще!

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

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

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (5 оценок, среднее: 5,00 из 5)
Загрузка...

Комментариев: 1

  1. korvell:

    Спасибо за переводы, в этом месяце особенно много!

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *