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

  Юрий  | 

  Обновл. 1 Ноя 2020  | 

 14589

 ǀ   3 

На предыдущем уроке мы говорили о том, как, используя ключевые слова 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 (104 оценок, среднее: 4,95 из 5)
Загрузка...

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

  1. Аватар Виктор:

    Если бы были рисунки показывающие, как это происходит в программе, было бы шикарно.

  2. Аватар korvell:

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

    1. Юрий Юрий:

      Пожалуйста 🙂

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

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