Как сделать исключение в c

Обновлено: 05.07.2024

В этой статье (перевод [1]) раскрываются следующие вопросы, касающиеся обработки исключений (Exception):

[Что такое исключение?]

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

Приложения используют логику обработки исключений (exception handling) для явной поддержки кодом каких-то неординарных событий. Причины исключений могут быть самые разные - от печально известного NullReferenceException до таймаута обращения к базе данных.

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

Пример 1: базовый блок "try catch finally"

Ниже приведен простой пример метода, который выбрасывает исключение, и как нужно правильно использовать блок try catch finally для обработки ошибки.

Пример 2: фильтры исключений

[Как создать свои собственные пользовательские типы исключений]

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

• Вызов кода может осуществлять пользовательскую обработку Custom Exception Type
• Возможность пользовательского мониторинга вокруг этого Custom Exception Type

Пример Custom Exception Type:

Код, наподобие приведенного ниже, очень часто встречается в приложениях. Этот код может выбросить (throw) тысячи исключений в минуту, и никто никогда про это бы не узнал. Этот код из приложения, который показывал серьезные проблемы производительности из-за плохой обработки исключений. Исключения произойдут, если reader равен null, columnName равен null, columnName не существует в результатах, значение столбца было null, или если value неправильная для DateTime. Настоящее минное поле ошибок.

Чтобы получить доступ к настройкам исключений, перейдите в меню Debug -> Windows -> Exception Settings (Отладка -> Исключения. это меню доступно при активной сессии отладчика). Под "Common Language Runtime Exceptions" Вы можете выбрать типы исключений, на которых отладчик должен автоматически поставить точку останова. Хорошей мыслью будет поставить здесь везде галочки. Как только код остановится на исключении, Вы можете указать ему игнорировать этот определенный тип исключений, если хотите.

Visual Studio Exception Settings

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

Почему запись в лог недостаточна. Лог исключений в файле хорошее решение, но это мало подходит для коммерческих приложений, работающих у пользователя. Если Вы не будете отслеживать каждый свой сервер ежедневно, то не узнаете про возникновение исключений.

Служба мониторинга ошибок - ключевой инструмент для любой команды разработки. Она позволяет централизованно собирать все Ваши исключения. Решение [8] предоставляет для этого следующие возможности:

• Централизованный лог исключений
• Просмотр и поиск всех исключений по всем серверам и приложениям
• Уникально идентифицировать каждое исключение
• Принимать оповещения на email о возникновении новых исключений или в случае слишком частого появления ошибок

(C) Dale, 01.02.2011 — 02.02.2011.

Конечно, язык, основы которого закладывались еще в конце 1960-х годов, не может быть безупречным по сегодняшним меркам. Хотя он и развивался все это время, о чем свидетельствует последовательность стандартов языка, но в узких рамках совместимости с предыдущими версиями (не забываем, что главный конек C — это совместимость и переносимость), то есть умеренно. Поэтому у него до сих пор есть ряд очевидных слабостей, которые приходится преодолевать программистам.

Один из наиболее скользких вопросов программирования на C — это, безусловно, обработка ошибок. Вообще говоря, обработка ошибок — это показатель мастерства и профессиональной зрелости программиста. Плохие программисты пишут программы, которые работают неправильно. Посредственные — пишут программы, которые работают правильно, но в тепличных условиях (оборудование исправно, все файлы на своем месте, данные имеют корректный формат и т.д.). Настоящие мастера пишут программы, которые надежны в любых условиях — если все гладко, они выдают правильный результат, а если по ходу работы возникли проблемы, они либо пытаются бороться с ними по мере возможностей, либо выдают внятную и недвусмысленную диагностику с описанием причин, по которым работа невозможна.

К счастью, C располагает ценным средством для компенсации своих многочисленных недостатков и слабостей — препроцессором. Инструмент этот довольно опасен, но при умелом использовании дает замечательные результаты. Сегодня мы рассмотрим одно из таких успешных применений макросов — реализацию механизма исключений на языке ANSI C. А для этого мы попробуем решить очень простую, но весьма типичную задачу.

Рассмотрим простую задачу. Треугольник задан тремя сторонами a, b, c. Нам нужно определить его площадь.

double triangleArea ( double a , double b , double c )
<
double p = ( a + b + c ) / 2.0 ;
double result = sqrt ( p * ( p - a ) * ( p - b ) * ( p - c ) ) ;
return result ;
>

Впрочем, идеальным это решение не назовешь. Функция реализует вычисление площади по формуле Герона, не заботясь о корректности значений переданных ей параметров. Во-первых, отрицательное значение длины стороны треугольника лишено физического смысла. Во-вторых, не каждые три отрезка положительной длины образуют треугольник. Помимо бессмысленного результата, можно получить и более серьезную проблему в виде отрицательного значения под корнем.

Самое простое — возложить ответственность за корректность параметров на клиента, вызывающего нашу функцию. Но такое решение сродни заметанию мусора под ковер. К тому же, если функция будет вызываться достаточно часто (а ведь именно для этого и придуманы функции), проверку параметров перед вызовом придется производить во многих местах. И даже это не дает гарантии: достаточно забыть проверить параметры в одном месте программы — и благодатная почва для потенциального сбоя подготовлена.

double triangleArea ( double a , double b , double c )
<
if ( ( a 0.0 )
|| ( b 0.0 )
|| ( c 0.0 )
|| ( a fabs ( b - c ) )
|| ( a >= ( b + c ) )
)
; // do something?
double p = ( a + b + c ) / 2.0 ;
double result = sqrt ( p * ( p - a ) * ( p - b ) * ( p - c ) ) ;
return result ;
>

Вот только не совсем понятно, что делать, если параметры некорректны. Конечно, можно вернуть нулевое или отрицательное значение вызывающей программе как указание на то, что при выполнении возникла нештатная ситуация:

double triangleArea ( double a , double b , double c )
<
if ( ( a 0.0 )
|| ( b 0.0 )
|| ( c 0.0 )
|| ( a fabs ( b - c ) )
|| ( a >= ( b + c ) )
)
return - 1.0 ;
double p = ( a + b + c ) / 2.0 ;
double result = sqrt ( p * ( p - a ) * ( p - b ) * ( p - c ) ) ;
return result ;
>

Как вариант, можно вообще прекратить выполнение программы при ошибке, чтобы заведомо избежать нежелательных последствий:

double triangleArea ( double a , double b , double c )
<
if ( ( a 0.0 )
|| ( b 0.0 )
|| ( c 0.0 )
|| ( a fabs ( b - c ) )
|| ( a >= ( b + c ) )
)
exit ( EXIT_FAILURE ) ;
double p = ( a + b + c ) / 2.0 ;
double result = sqrt ( p * ( p - a ) * ( p - b ) * ( p - c ) ) ;
return result ;
>

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

Это одна из причин, по которым язык C сегодня многие не воспринимают всерьез, считая, что на нем невозможно писать изящные программы. Зачастую такое мнение оказывается ошибочным: C может очень многое, гораздо больше, чем кажется на первый взгляд. Нужно лишь воспользоваться одним из многочисленных средств для расширения его возможностей. Одним из таких средств является CException.

Пользоваться CException очень просто. Его основу образуют три макроса: Try, Catch и Throw, а также тип CEXCEPTION_T.

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

Этот макрос начинает защищенный блок. Синтаксис его подобен синтаксису обычного if: за ним следует либо единичный оператор, либо блок. За ним обязательно должен следовать блок Catch (если вы забудете об этом, получите ошибку компиляции).

Блоки Try могут быть вложены друг в друга. При необходимости можно повторно выбросить исключение, чтобы обработать его во внешнем блоке Try.

Этот макрос начинает блок обработки ошибок. Он выполняется лишь в том случае, если в блоке Try возникло исключение. Исключение может возникнуть как непосредственно в коде внутри блока Try, так и в функции, вызываемой в блоке Try на любом уровне вложенности.

Catch получает идентификатор исключения типа CEXCEPTION_T, который вы можете использовать при обработке ошибки. Если в процессе обработки возникнет исключение, оно будет обработано внешним блоком Try.

Используется для того, чтобы выбросить исключение. Принимает единственный аргумент — идентификатор исключения, который будет передан Catch в качестве причины ошибки. Как уже упоминалось ранее, в процессе обработки ошибки можно опять выбрасывать исключение, в том числе и повторно то же самое, которое обрабатывается в данный момент.

.
// Защищенный блок
Try
.
потенциально_опасный код
if (возникла_ошибка)
Throw(e1); // e1 - код конкретной ошибки
.
вызов_потенциально_опасной_функции()
.
>
// Сюда мы попадаем только в случае возникновения ошибки
Catch(e) // здесь e - код возникшей ошибки
.
код_обработки_ошибки
.
>
// Конец защищенного блока
.

потенциально_опасная_функция()
.
if (возникла_ошибка)
Throw(e2); // e2 - код конкретной ошибки
.
>

Теперь мы можем сосредоточить весь код обработки ошибок в единственном месте — блоке Catch, не загромождая основной поток выполнения программы.

enum ERRORCODE_T
<
NEGATIVE_SIDE ,
BAD_TRIANGLE
> ;

extern double triangleArea ( double a , double b , double c ) ;

double triangleArea ( double a , double b , double c )
<
if ( ( a 0.0 )
|| ( b 0.0 )
|| ( c 0.0 )
)
Throw ( NEGATIVE_SIDE ) ;

if ( ( a fabs ( b - c ) )
|| ( a >= ( b + c ) )
)
Throw ( BAD_TRIANGLE ) ;

double p = ( a + b + c ) / 2.0 ;
double result = sqrt ( p * ( p - a ) * ( p - b ) * ( p - c ) ) ;
return result ;
>

int main ( int argc , char * argv [ ] )
<
CEXCEPTION_T e ;
double a , b , c ;
a = 10.0 ;
b = 2.0 ;
c = 2.0 ;
printf ( "a=%f b=%f c=%f \n " , a , b , c ) ;

Try
<
double area = triangleArea ( a , b , c ) ;
printf ( "area=%f \n " , area ) ;
>
Catch ( e )
<
switch ( e )
<
case NEGATIVE_SIDE :
printf ( "One of triangle sides is negative. \n " ) ;
break ;

case BAD_TRIANGLE :
printf ( "Triangle cannot be made of these sides. \n " ) ;
break ;

default :
printf ( "Unknown error: %d \n " , e ) ;
>
>

system ( "PAUSE" ) ;
return 0 ;
>

Улучшение структуры кода сразу бросается в глаза: функция сообщает о возникновении ошибки, а инфраструктура CException передает управление обработчику ошибок без участия программиста. Больше нет необходимости загромождать текст программы условными операторами (не забываем, что по-хорошему программу нужно еще тестировать, а наличие каждого ветвления усложняет эту процедуру).

Конечно, CException — не панацея, и его применение не решит всех проблем. Например, CEXCEPTION_T — это всего лишь целочисленная константа, и с ее помощью невозможно сообщить обработчику все детали об ошибке. Кроме того, обработчик Catch в отличие от настоящих обработчиков, встроенных в современные языки программирования, перехватывает все исключения, поэтому вполне возможно, что некоторые из них, не предназначенные данному обработчику, придется выбросить повторно. Но все же плюсов от использования CException гораздо больше чем минусов, и программы на C, написанные с использованием механизма исключений, окажутся проще и надежнее своих старорежимных собратьев.

Рассмотрим канонический пример того, когда работа с программой приводит к генерации исключения — деление на ноль. Вот такой может быть наша программа:

Теперь запустим программу и введем число 0 . В итоге, в Visual Studio мы увидим ошибку:

Мы получили исключение типа System.DivideByZeroException (деление на ноль) и наше приложение аварийно завершило свою работу. Кроме этого, в таком простом, казалось бы, приложении имеется ещё одна уязвимость — пользователь может ввести совсем не то, что от него требуется и вместо числа введет, например, строку. В этом случае мы, опять же, получим в Visual Studio исключение:

Блок try…catch…finally

Неправильный ввод значения

Выполнили блок finally

Приложение так же, как и в предыдущем примере, дошло до строки

В конструкции try. catch. finally обязательным является блок try . Блоки catch или finally могут отсутствовать, при этом следует отметить, что, если отсутствует блок catch , то исключение будет возбуждено и программа аварийно завершит работу. Варианты использования конструкции try. finally. catch могут быть такими:

Блок finally обычно используется для выполнения очистки ресурсов выделенных в блоке try . Блок finally не выполниться в том случае, если в блоке catch также, как и в try возникнет какое-либо исключение.

Перехват и обработка исключений в блоке catch

  1. ввести 0 (исключение System.DivideByZeroException )
  2. ввести вместо целого числа строку (исключение System.FormatException )
  3. ввести вместо целого числа число с плавающей запятой (исключение System.FormatException )
  4. ввести число, превышающее максимальное значение int (исключение System.OverflowException )

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

здесь мы добавили сразу три блока catch в каждом из которых происходит обработка исключений определенного типа. Для того, чтобы обработать исключение определенного типа мы использовали рядом с catch круглые скобки, в которых указали тип обрабатываемого исключения и соотнесли этот тип с именем исключения, которое в нашем случае было e .

Следует также отметить, что далеко не всегда удается на этапе разработки предугадать абсолютна все типы исключений. Что, например, произойдет, если мы уберем из нашего кода блок, обрабатывающий System.OverflowException ? Правильно, мы снова нарвемся на аварийное завершение работы программы, так как компилятор пройдет по всем блокам catch и не сможет соотнести тип исключение с именем. Чтобы такого не произошло, можно также предусмотреть при обработке исключений общий блок catch в котором будет обрабатываться всё, что не попало в другие блоки. Например, мы можем сделать обработку двух типов исключений, а третий — обработаем в общем блоке:

Необходимо отметить, что важен не только факт наличия, но и порядок написания блоков catch . Универсальный блок catch должен находиться в самом низу кода. Об этом, кстати, Visual Studio сообщает. Если вы перенесете общий блок catch и поставите его, например, над блоком, обрабатывающим исключение DivideByZeroException , то Visual Studio выдаст ошибку:

Ошибка CS1017 Конструкции catch не могут использоваться после универсальной конструкции catch оператора try

Несмотря на то, что использование конструкции try..catch..finally прекрасно позволяет перехватывать и обрабатывать различного типа исключения, её использование не всегда может быть оправдано, а некоторые исключения могут быть предвидены разработчиком и обработаны с использованием обычных логических операций. Например, в случае, если пользователь вводит не число, а непонятно что, можно было бы обойтись вот такой конструкцией:

Здесь метод int.TryParse() пробует преобразовать строку в целое число и, если преобразование прошло успешно, то возвращает true . Таким образом, мы избежали использования конструкции try. catch , которая, кстати, с точки зрения производительности более накладна, чем обычный условный оператор if .

Итого

В программах на C++ могут возникать ошибки. Различают три типа ошибок, которые могут возникать в программах:

Данная тема освещает применение механизма перехвата ошибок времени выполнения.

2. Понятие исключительной ситуации

Исключительная ситуация – это событие, которое привело к сбою в работе программы. В результате возникновения исключительной ситуации программа не может корректно продолжить свое выполнение.

Примеры действий в программе, которые могут привести к возникновению исключительных ситуаций:

  • деление на нуль;
  • нехватка оперативной памяти при использовании оператора new для ее выделения (или другой функции);
  • доступ к элементу массива за его пределами (ошибочный индекс);
  • переполнение значения для некоторого типа;
  • взятие корня из отрицательного числа;
  • другие ситуации.
3. Понятие исключения

В языке C++ исключение – это специальный объект класса или значение базового типа, который описывает (определяет) конкретную исключительную ситуацию и соответствующим образом обрабатывается.

При написании программы система описания исключительных ситуаций выбирается программистом по собственному усмотрению. Можно создать свою квалификацию ошибок, которые могут возникать в программе. Например, программист может квалифицировать разные типы ошибок числовым (целочисленным) значением или разработать собственную иерархию классов описывающих исключительные ситуации. Кроме того, можно использовать возможности классов C++, которые являются производными от класса exception.

4. Средства языка C++ для обработки исключительных ситуаций. Общая форма конструкции try…catch . Назначение

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

Для перехвата и обработки исключительных ситуаций в языке C++ введена конструкция try…catch, которая имеет следующую общую форму:

  • type1 , type2 , …, typeN – соответственно тип аргументов argument1 , argument2 , …, argumentN .

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

Если в блоке try возникнет исключительная ситуация, которая не предусмотрена блоком catch , то вызывается стандартная функция terminate() , которая по умолчанию вызовет функцию abort() . Эта стандартная функция останавливает выполнение программы.

5. Оператор throw . Назначение

Чтобы в блоке try сгенерировать исключительную ситуацию, нужно использовать оператор throw . Оператор throw может быть вызван внутри блока try или внутри функции, которая вызывается из блока try .

Общая форма оператора throw следующая

В результате выполнения оператора throw генерируется исключение некоторого типа. Это исключение должно быть обработано в блоке catch .

6. Примеры использования блока try…catch

Пример 1. Демонстрируется использование блока try…catch для обработки выражения:

В данном выражении в трех случаях может возникнуть исключительная ситуация:

Поэтому, в блоке try…catch нужно обработать эти три случая.

Текст программы типа Console Application следующий:

Результат работы программы

После применения блока try..catch работа программы не прекращается.

Пример 2. Другой вариант обработки выражения из примера 1. Здесь блок try…catch содержит два оператора catch .

7. Пример использования блока try…catch в функции

Условие задачи. Написать функцию вычисления значения по заданной строке символов, которая есть записью этого числа в десятичной системе исчисления. Предусмотреть случай выхода за границы диапазона, определяемого типом int . Необходимо использовать механизм исключений.

Текст программы для приложения типа Console Application следующий

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

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

Если вызов функции StrToInt2() перенести за пределы оператора try

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

что означает, что исключение необработано.

8. Пример программы, которая генерирует исключение в одной функции, а перехватывает его в другой функции

В примере, в функции нижнего уровня GenerateException() генерируется исключение типа const char* . Функция проверяет допустимые границы входного параметра index .

В функции верхнего уровня ProcessException() происходит вызов функции GenerateException() . Этот вызов взят в блок try .

Текст программы следующий:

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

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

Бывают случаи, когда нужно перехватить все исключительные ситуации подряд. Для этого, в C++ используется блок catch(…) , который имеет следующую общую форму

Пример. В примере демонстрируется использование блока catch(…) для обработки ситуаций любого типа.

В программе реализованы:

  • функция DivNumbers() , которая возвращает результат деления двух чисел, введенных из клавиатуры. В функции генерируется исключение типа int , если значение делителя равно 0;
  • функция SqRoot() , возвращающая корень из отрицательного числа. В функции генерируется исключение типа const char* , если значение параметра number отрицательно;
  • функция ProcessException() . Эта функция демонстрирует работу функций DivNumbers() и SqRoot() . В функции используется инструкция try…catch() .

Как видно из текста функции ProcessException() вызов функций DivNumbers() и SqRoot() взят в блок try…catch

Читайте также: