Урок №182. Обработка исключений. Операторы throw, try и catch

  Юрий  | 

  Обновл. 31 Окт 2020  | 

 41413

 ǀ   7 

На предыдущем уроке мы говорили о необходимости и пользе исключений. Исключения в языке C++ реализованы с помощью 3-х ключевых слов, которые работают в связке друг с другом: throwtry и catch.

Генерация исключений

Мы постоянно используем сигналы в реальной жизни для обозначения того, что произошли определенные события. Например, во время игры в баскетбол, если игрок совершил серьезный фол, то арбитр свистит, и игра останавливается. Затем идет штрафной бросок. Как только штрафной бросок выполнен, игра возобновляется.

В языке C++ оператор throw используется для сигнализирования о возникновении исключения или ошибки (аналогия тому, когда свистит арбитр). Сигнализирование о том, что произошло исключение, называется генерацией исключения (или «выбрасыванием исключения»).

Для использования оператора throw применяется ключевое слово throw, а за ним указывается значение любого типа данных, которое вы хотите задействовать, чтобы сигнализировать об ошибке. Как правило, этим значением является код ошибки, описание проблемы или настраиваемый класс-исключение. Например:

Каждая из этих строк сигнализирует о том, что возникла какая-то ошибка, которую нужно обработать.

Поиск исключения


Выбрасывание исключений — это лишь одна часть процесса обработки исключений. Вернемся к нашей аналогии с баскетболом: как только просвистел арбитр, что происходит дальше? Игроки останавливаются, и игра временно прекращается. Обычный ход игры нарушен.

В языке C++ мы используем ключевое слово try для определения блока стейтментов (так называемого «блока try»). Блок try действует как наблюдатель в поисках исключений, которые были выброшены каким-либо из операторов в этом же блоке try, например:

Обратите внимание, блок try не определяет, КАК мы будем обрабатывать исключение. Он просто сообщает компилятору: «Эй, если какой-либо из стейтментов внутри этого блока try сгенерирует исключение — поймай его!».

Обработка исключений

Пока арбитр не объявит о штрафном броске, и пока этот штрафной бросок не будет выполнен, игра не возобновится. Другими словами, штрафной бросок должен быть обработан до возобновления игры.

Фактически, обработка исключений — это работа блока(ов) catch. Ключевое слово catch используется для определения блока кода (так называемого «блока catch»), который обрабатывает исключения определенного типа данных.

Вот пример блока catch, который обрабатывает (ловит) исключения типа int:

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

Как только исключение было поймано блоком try и направлено в блок catch для обработки, оно считается обработанным (после выполнения кода блока catch), и выполнение программы возобновляется.

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

Как и в случае с функциями, если параметр не используется в блоке catch, то имя переменной можно не указывать:

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

Использование throw, try и catch вместе


Вот полная программа, которая использует throw, try и несколько блоков catch:

Результат выполнения программы:

We caught an int exception with value -1
Continuing our way!

Оператор throw используется для генерации исключения -1 типа int. Затем блок try обнаруживает оператор throw и перемещает его в соответствующий блок catch, который обрабатывает исключения типа int. Блок catch типа int и выводит соответствующее сообщение об ошибке.

После обработки исключения, программа продолжает свое выполнение и выводит на экран Continuing our way!.

Резюмируем

Обработка исключений, на самом деле, довольно-таки проста, и всё, что вам нужно запомнить, размещено в следующих двух абзацах:

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

   Если подходящих обработчиков catch не существует, то выполнение программы переходит к следующему блоку try. Если до конца программы не найдены соответствующие обработчики catch, то программа завершает свое выполнение с ошибкой исключения.

Обратите внимание, компилятор не выполняет неявные преобразования при сопоставлении исключений с блоками catch! Например, исключение типа char не будет обрабатываться блоком catch типа int, исключение типа int, в свою очередь, не будет обрабатываться блоком catch типа float.

Это действительно всё, что вам нужно запомнить.

Исключения обрабатываются немедленно


Вот маленькая программа, которая демонстрирует, что исключения обрабатываются немедленно:

Рассмотрим выполнение этой программы пошагово:

   Оператор throw — это первый оператор, который выполняется. Это приводит к генерации исключения типа double.

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

   Затем проверяются обработчики catch на соответствие типу данных. Поскольку у нас исключение типа double, то компилятор ищет обработчик catch типа double. У нас такой есть, поэтому он и выполняется.

Следовательно, результат выполнения программы:

We caught a double of value: 7.4

Обратите внимание, строка This never prints никогда не выводится, так как генерация исключения заставило точку выполнения программы немедленно перейти к обработчику исключений типа double.

Еще один пример

Рассмотрим более популярный пример:

Здесь мы просим пользователя ввести число. Если пользователь ввел положительное число, то стейтмент if не выполняется, исключение не генерируется, и пользователь получает квадратный корень из числа. Поскольку исключение не генерируется, то код внутри блока catch никогда не выполняется.

Результат:

Enter a number: 16
The sqrt of 16 is 4

Если же пользователь ввел отрицательное число, то генерируется исключение типа const char*. Поскольку мы уже находимся в блоке try, то компилятор ищет соответствующий обработчик catch типа const char*, и точка выполнения немедленно перемещается в этот блок.

Результат:

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

Что обычно делают блоки catch?


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

   Во-первых, блок catch может вывести сообщение об ошибке (либо в консоль, либо в лог-файл).

   Во-вторых, блок catch может возвратить значение или код ошибки обратно в caller.

   В-третьих, блок catch может сгенерировать другое исключения. Поскольку блок catch не находится внутри блока try, то новое сгенерированное исключение будет обрабатываться следующим блоком try.

Сейчас вы уже должны были получить основное представление об исключениях. На следующем уроке мы рассмотрим еще несколько примеров для проверки того, насколько гибкими исключения могут быть.

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

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

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

  1. Аватар Woland:

    Странно, что в C++ нет такого дополнения к try catch как finally. Даже в таком недопиле как Windows Script Host который встроен в винду такое есть и нормально работает. Очень полезно как раз для работы с ФС. Закрытие файла часто можно задавать именно в блоке finally, потому как он выполнится в любом случае.

    1. Аватар Александр:

      А зачем он нужен?
      При нормальной компоновке кода у Вас все равно не будут вводиться в try динамические переменные, а статические и так умирают "правильно" (в том числе закрываются файлы).

      У меня не большой опыт, конечно, но пока я не видел ни разу пример кода, в котором finally был бы действительно хорошим решением. Обычно это либо совсем уж высосанные из пальца примеры, либо "говнокод", после рефакторинга которого finally становится не нужным.

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

  2. Аватар Валерий:

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

    После прочтения возникает мысль, что если рядом существует конструкция try-catch, то управление будет передано ей. Согласен, что изучение следующих уроков может помочь понять логику обработки. Но может и не помочь. Может стоит поменять формулировку. Например "При выбрасывании исключения (оператор throw), точка выполнения программы немедленно возвращается к ближайшему блоку try. (возможно находящемуся выше по стеку возвратов)"

    И может какой-то пример типа:

    Будет понятно, что исключение ошибки передается не в обработчик в функции, а отдано в вызывающий блок try. А вот исключение тестовое обработалось в функции.

  3. Аватар Ксения:

    То есть, я правильно понимаю, что исключения все равно ничего не меняют в логике программы? Они нужны только для того, чтобы отследить возможные ошибки "на ходу"?
    Например, если мы в программе делим на ноль, то обработка такого события исключением все равно завершится тем, что программа вылетит с ошибкой или выдаст неверный результат, но перед этим, нам будет сообщено о том, что мы поделили на ноль. И это все, зачем нужны исключения. Так?

    1. Юрий Юрий:

      В некоторых случаях они могут менять логику выполнения программы — всё зависит от вас как от разработчика. Если делить на ноль запрещено, то вы в любом случае получите ошибку на выходе, но если, например, у вас в программе нельзя передавать в функцию строку (грубо говоря), то если вы не сделаете соответствующую обработку исключений, то логика выполнения будет отличаться от той, если бы обработка исключений была.

      Нет обработки исключений — программа может не завершать своё выполнение с какой-либо ошибкой (строка передастся и всё будет ок), есть обработка исключений — логика уже другая (программа либо обрабатывает ошибку, меняя русло выполнения программы, либо обрабатывает ошибку, не меняя русло выполнения программы, либо выдаёт ошибку и гудбай).

  4. Евгений Евгений:

    Здравствуйте, сайт очень качественный, но могли бы вы добавить "Непереведенные статьи", выделенные серым цветом, что-бы знать что ты изучишь следующим?

    1. Юрий Юрий:

      Привет, хорошая идея. Подумаю.

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

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