А.В. Столяров - Введение в язык Си++ (1114949), страница 16
Текст из файла (страница 16)
В вышеупомянутом примере с редактором текстов в ошибке, допущенной пользователем,нет вовсе ничего страшного: нужно просто констатировать факт ошибкии попросить пользователя ввести другое имя файла. Точно так же можетне быть фатальной ошибка, связанная с установлением сетевого соединения, ведь во многих случаях можно обратиться к серверу-дублёру1. Даже ситуация с делением на ноль в некоторых случаях может оказатьсянефатальной, как, например, при исследовании некоторых некорректнопоставленных задач (обычно в этих случаях головная программа «знает», как можно скорректировать исходные данные, чтобы избежать деления на ноль, и нужную комбинацию ищет методом проб и ошибок).Наконец, заметим, что один и тот же код может использоваться в совершенно разных программах, причём для одних ошибка может оказатьсяфатальной, для других — не стоящей даже упоминания.
Например, многие программы выполняют поиск нужного файла в разных директориях,просто пытаясь его открыть и продолжая поиск дальше после каждойнеудачи; в этом случае ошибка вообще перестаёт быть ошибкой.Из всего вышесказанного можно сделать один очень серьёзный и важный вывод: принимать реш ения о том , что д ел ать при возникновении ошибочной ситуации — это п рерогати ва головной програм м ы . Завершать программу, а также и выполнять тем или иным1Такое возможно при обращениях к системе доменных имён (DNS), а в некоторыхслучаях — и при отправке электронной почты, и в других ситуациях тоже.72способом выдачу сообщений может только функция mainQ и те из вызываемых ею функций, которые для этого специально предназначены.Что же касается кода, не попавшего в эту категорию, то его дело в случае возникновения ошибки — оповестить об этом головную программу,и не более того.Звучит это достаточно просто, однако все, кто имеет опыт написаниядаже не очень сложных программ, знают, с каким трудом это воплощается на практике.
В простых языках программирования, таких как Си илиПаскаль, приходится из функций в случае возникновения ошибок возвращать специальные значения, а в точке вызова функции, соответственно,проверять, не вернула ли вызванная функция значение, соответствующее ошибке. В большинстве случаев при этом вызвавшая функция, всвою очередь, вынуждена возвращать признак ошибки и т. д. Представьте себе теперь, что функция f 1 () вызвала функцию f 2 (), та, в своюочередь, функцию f 3 () и т.д., а в функции, скажем, f 10() возможновозникновение ошибочной ситуации.
Тогда нужно предусмотреть значение, возвращаемое в случае этой ошибки, в функции f 9 () сделать проверку и в случае ошибки, в свою очередь, возвратить нечто ошибочное,и так во всех функциях.Теоретически именно так всё и должно делаться, все возможные ошибочные ситуации должны проверяться и т. п., но на практике аккуратноесоблюдение требований по обработке ошибок может оказаться настолько трудоёмким, что программисты прибегают к известному «алгоритмурешения всех проблем», а именно — (1) поднять вверх правую руку и (2)резко махнуть ею, одновременно произнося что-то вроде «а, ладно». Естьдаже шуточная фраза на эту тему: «никогда не проверяйте на ошибки,которые вы не знаете, как обработать».
Разумеется, такое обычно даромне проходит. Ошибка, которую не стали обрабатывать в надежде, чтоона никогда не произойдёт, проявится в самый неподходящий момент,причём чем дальше находится заказчик и чем больше денег он заплатил,тем выше вероятность, что наша программа сломается именно у него.Именно поэтому во многих языках предусмотрены специальные возможности для обработки ошибочных ситуаций. В языке С и + + соответствующий механизм называется обработкой исключений (англ, exceptionhandling).§3.2. О бщ ая и д ея м ехан и зм а исклю чен и йОтметим ещё один недостаток использования специальных возвращаемых значений для индикации ошибки.
В некоторых случаях среди всехзначений возвращаемого функцией типа просто нет ни одного неиспользуемого; отличный пример такого рода функции — a to iQ , переводящая73строковое представление целого числа в значение типа in t. Возвращаетона, естественно, число типа in t, а поскольку любое такое число имеетстроковое представление, то и вернуть (при отсутствии каких-либо ошибок) она может, вообще говоря, любое значение типа in t. В то же времявозможно, что в строке, поданной a to i на вход, содержится текст, не являющийся представлением какого-либо числа (например, состоящий избукв, а не из цифр).
В этой ситуации разработчики функции решили, занеимением лучшего, возвращать ноль, но это не решает проблему, ведьполучается, что функция возвращает одно и то же и для строки, содержащей белиберду, и для строки "О", которая является вполне корректным представлением числа. Это приводит нас к идее исключения, которое представляет собой особый способ завершения функции: если в языках, не поддерживающих механизм исключений, функция могла тольковернуть одно из возможных значений, то в языках, поддерживающихисключения, функция может либо вернуть значение, либо в о зб у д и т ьисклю чение.Отметим в этой связи, что исключения вовсе не являются составной частьюпарадигмы объектно-ориентированного программирования, как это почему-то часто утверждается.
Напротив, они скорее относятся к ф у н к ц и о н а л ь н о м у п р о г р а м м и р о в а н и ю : небезызвестная функция call/cc, поддерживаемая во многих функциональных языках, представляет собой ничто иное как обобщение механизмаисключений; в языке Common Lisp, не имеющем call/cc, при этом имеются, темне менее, конструкции, совершенно аналогичные конструкциям обработки исключений Си+-КВозбуждение исключения означает, на самом деле, переход программы в особое состояние, в котором все активные на настоящий моментфункции досрочно завершаются одна за другой, пока не найдётся такая,которая может справиться с данным типом исключительных ситуаций.Чтобы понять, о чём идёт речь, вспомним ситуацию, рассматривавшуюся в предыдущем параграфе: функция f 1 () вызвала функцию f 2 (), та,в свою очередь, функцию f 3() и т.
д. вплоть до f 10(), где и возникает ошибка. При этом нам совершенно не обязательно делать какие-либопроверки в функциях f 9, f8 и т. д., вполне достаточно пометить, например, функцию f l как умеющую справляться с данным типом ошибок.Тогда при возникновении ошибки во время исполнения f 10 будет завершена досрочно и она сама, и вызвавшая её функция f 9, и f 8, и так вплотьдо f l , которая и обработает ошибку. Иначе говоря, если некая функцияg () вызывает другую функцию h (), а эта последняя возбуждает исключение, для которого в g () нет обработчика, то это эквивалентно тому,как если бы функция g () сама возбудила то же самое исключение, невызывая h ().Иногда всю обработку ошибок делают в функции mainQ. Она дляэтого удобна, поскольку остаётся активной всё время исполнения про74граммы2 и, таким образом, может обработать исключение, возбуждённоев практически любой части программы.§3.3.
В озб уж д ен и е исклю ченийПри возникновении ситуации, трактуемой как ошибочная и требующая обработки в качестве исключительной, следует в о зб у д и т ь исклю чение с помощью оператора throw (англ, б р о с и т ь ). У этого оператораимеется один параметр, в качестве которого может выступать выражение, вообще говоря, произвольного типа, в том числе как любого базового(in t, f lo a t, ...), так и производного (указатель и т. и.), а равно и определённого пользователем (в том числе, что немаловажно, типа класс илиструктура). Тип выражения в операторе throw будем называть т и п о мисключения, а значение выражения — зн ачен и ем исключения.Для примера рассмотрим функцию, которая принимает на вход имя(текстового) файла, а возвращает количество строк (т.
е. символов перевода строки) в этом файле. Ситуация, когда файл не удалось открыть,для такой функции оказывается заведомо исключительной; в этом случаев языке Си нам пришлось бы возвращать специальное значение (например, -1), а при вызове проверять, не его ли вернула функция. В С и + +это можно сделать проще:unsigned in t lin e _ co u n t_ in _ file (co n st char *file_nam e){FILE * f = fopen(file_nam e, " r " ) ;i f (!f )throw "couldn’ t open the f i l e " ;in t n = 0;in t c = 0;w h ile((c = f g e t c ( f ) ) != EOF)i f ( c == ’ \ n ’ ) n++;f c lo s e ( f ) ;return n;}В этом примере мы в качестве исключения «бросили» строку (точнее, адрес её начала, т. е. указатель типа const char*).