Урок №191. Винятки, Функції і Розкручування стеку

  Юрій  | 

  Оновл. 26 Бер 2021  | 

 35

На попередньому уроці ми розглянули, як, використовуючи ключові слова 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 Зірок (Немає Оцінок)
Loading...

Залишити відповідь

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