C try catch типы ошибок

ГЛАВА 13. Обработка исключительных ситуаций

Исключительная ситуация, или просто исключение,
происходит во время выполнения. Используя под­
систему обработки исключительных ситуаций в С#,
можно обрабатывать структурированным и контроли­
руемым образом ошибки, возникающие при выполнении
программы. Главное преимущество обработки исключи­
тельных ситуаций заключается в том, что она позволяет ав­
томатизировать получение большей части кода, который
раньше приходилось вводить в любую крупную програм­
му вручную для обработки ошибок. Так, если программа
написана на языке программирования без обработки ис­
ключительных ситуаций, то при неудачном выполнении
методов приходится возвращать коды ошибок, которые не­
обходимо проверять вручную при каждом вызове метода.
Это не только трудоемкий, но и чреватый ошибками про­
цесс. Обработка исключительных ситуаций рационализи­
рует весь процесс обработки ошибок, позволяя определить
в программе блок кода, называемый обработчиком исклю­
чений и выполняющийся автоматически, когда возникает
ошибка. Эго избавляет от необходимости проверять вруч­
ную, насколько удачно или неудачно завершилась конкрет­
ная операция либо вызов метода. Если возникнет ошибка,
она будет обработана соответствующим образом обработ­
чиком ошибок.

Обработка исключительных ситуаций важна еще и по­
тому, что в С# определены стандартные исключения для
типичных программных ошибок, например деление на
нуль или выход индекса за границы массива. Для реаги­
рования на подобные ошибки в программе должно быть
организовано отслеживание и обработка соответствующих
исключительных ситуаций. Ведь в конечном счете для успешного программирования
на C# необходимо научиться умело пользоваться подсистемой обработки исключи­
тельных ситуаций.

Класс System.Exception

В C# исключения представлены в виде классов. Все классы исключений должны
быть производными от встроенного в C# класса Exception, являющегося частью про­
странства имен System. Следовательно, все исключения являются подклассами класса
Exception.

К числу самых важных подклассов Exception относится класс SystemException.
Именно от этого класса являются производными все исключения, генерируемые испол­
няющей системой C# (т.е. системой CLR). Класс SystemException ничего не добавляет
к классу Exception, а просто определяет вершину иерархии стандартных исключений.

В среде .NET Framework определено несколько встроенных исключений, являю­
щихся производными от класса SystemException. Например, при попытке выпол­
нить деление на нуль генерируется исключение DivideByZeroException. Как будет
показано далее в этой главе, в C# можно создавать собственные классы исключений,
производные от класса Exception.

Основы обработки исключительных ситуаций

Обработка исключительных ситуаций в C# организуется с помощью четырех клю­
чевых слов: try, catch, throw и finally. Они образуют взаимосвязанную подсистему,
в которой применение одного из ключевых слов подразумевает применение другого.
На протяжении всей этой главы назначение и применение каждого из упомянутых
выше ключевых слов будет рассмотрено во всех подробностях. Но прежде необходимо
дать общее представление о роли каждого из них в обработке исключительных ситуа­
ций. Поэтому ниже кратко описан принцип их действия.

Операторы программы, которые требуется контролировать на появление исключе­
ний, заключаются в блок try. Если внутри блока try возникает исключительная ситуа­
ция, генерируется исключение. Это исключение может быть перехвачено и обработано
каким-нибудь рациональным способом в коде программы с помощью оператора, обо­
значаемого ключевым словом catch. Исключения, возникающие на уровне системы,
генерируются исполняющей системой автоматически. А для генерирования исключе­
ний вручную служит ключевое слово throw. Любой код, который должен быть непре­
менно выполнен после выхода из блока try, помещается в блок finally.

Применение пары ключевых слов try и catch

Основу обработки исключительных ситуаций в C# составляет пара ключевых слов
try и catch. Эти ключевые слова действуют совместно и не могут быть использованы
порознь. Ниже приведена общая форма определения блоков try/catch для обработ­
ки исключительных ситуаций:

try {
    // Блок кода, проверяемый на наличие ошибок.
}
catch (ExcepType1 exOb) {
    // Обработчик исключения типа ExcepTypel.
}
catch (ExcepType2 exOb) {
    // Обработчик исключения типа ExcepType2.
}

где ЕхсерТуре — это тип возникающей исключительной ситуации. Когда исключение
генерируется оператором try, оно перехватывается составляющим ему пару опера­
тором catch, который затем обрабатывает это исключение. В зависимости от типа
исключения выполняется и соответствующий оператор catch. Так, если типы гене­
рируемого исключения и того, что указывается в операторе catch, совпадают, то вы­
полняется именно этот оператор, а все остальные пропускаются. Когда исключение
перехватывается, переменная исключения exOb получает свое значение.

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

Следует, однако, иметь в виду, что если исключение не генерируется, то блок опера­
тора try завершается как обычно, и все его операторы catch пропускаются. Выполне­
ние программы возобновляется с первого оператора, следующего после завершающе­
го оператора catch. Таким образом, оператор catch выполняется лишь в том случае,
если генерируется исключение.

Простой пример обработки исключительной ситуации

Рассмотрим простой пример, демонстрирующий отслеживание и перехватывание
исключения. Как вам должно быть уже известно, попытка индексировать массив за его
границами приводит к ошибке. Когда возникает подобная ошибка, система CLR гене­
рирует исключение IndexOutOfRangeException, которое определено как стандарт­
ное для среды .NET Framework. В приведенной ниже программе такое исключение
генерируется намеренно и затем перехватывается.

// Продемонстрировать обработку исключительной ситуации.
using System;

class ExcDemol {
    static void Main() {
        int[] nums = new int[4];

        try {
            Console.WriteLine("До генерирования исключения.");

            // Сгенерировать исключение в связи с выходом индекса за границы массива.
            for(int i=0; i < 10; i++) {
                nums[i] = i;
                Console.WriteLine("nums[(0)]: {1}", i, nums[i]);
            }
            Console.WriteLine("He подлежит выводу");
        }
        catch (IndexOutOfRangeException) {
            // Перехватить исключение.
            Console.WriteLine("Индекс вышел за границы массива!");
        }
        Console.WriteLine("После блока перехвата исключения.");
    }
}

При выполнении этой программы получается следующий результат.

До генерирования исключения.
nums[0]: 0
nums[1]: 1
nums[2]: 2
nums[3]: 3
Индекс вышел за границы массива!
После блока перехвата исключения.

В данном примере массив nums типа int состоит из четырех элементов. Но в цикле
for предпринимается попытка проиндексировать этот массив от 0 до 9, что и приво­
дит к появлению исключения IndexOutOfRangeException, когда происходит обра­
щение к элементу массива по индексу 4.

Несмотря на всю свою краткость, приведенный выше пример наглядно демон­
стрирует ряд основных моментов процесса обработки исключительных ситуаций.
Во-первых, код, который требуется контролировать на наличие ошибок, содержится в
блоке try. Во-вторых, когда возникает исключительная ситуация (в данном случае —
при попытке проиндексировать массив nums за его границами в цикле for), в блоке
try генерируется исключение, которое затем перехватывается в блоке catch. В этот
момент выполнение кода в блоке try завершается и управление передается блоку
catch. Это означает, что оператор catch не вызывается специально, а выполнение
кода переходит к нему автоматически. Следовательно, оператор, содержащий метод
WriteLine() и следующий непосредственно за циклом for, где происходит выход
индекса за границы массива, вообще не выполняется. А в задачу обработчика исклю­
чений входит исправление ошибки, приведшей к исключительной ситуации, чтобы
продолжить выполнение программы в нормальном режиме.

Обратите внимание на то, что в операторе catch указан только тип исключения
(в данном случае — IndexOutOfRangeException), а переменная исключения отсут­
ствует. Как упоминалось ранее, переменную исключения требуется указывать лишь
в том случае, если требуется доступ к объекту исключения. В ряде случаев значение
объекта исключения может быть использовано обработчиком исключений для по­
лучения дополнительной информации о самой ошибке, но зачастую для обработки
исключительной ситуации достаточно просто знать, что она произошла. Поэтому
переменная исключения нередко отсутствует в обработчиках исключений, как в рас­
сматриваемом здесь примере.

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

for(int i=0; i < 10; i++) {

на строку

for(int i=0; i < nums.Length; i++) {

Теперь индексирование массива не выходит за его границы в цикле for. Следова­
тельно, никакого исключения не генерируется и блок catch не выполняется.

Второй пример обработки исключительной ситуации

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

В качестве еще одного примера рассмотрим следующую программу, где блок try
помещается в методе Main(). Из этого блока вызывается метод GenException(), в ко­
тором и генерируется исключение IndexOutOfRangeException. Это исключение не
перехватывается методом GenException(). Но поскольку метод GenException() вы­
зывается из блока try в методе Main(), то исключение перехватывается в блоке catch,
связанном непосредственно с этим блоком try.

/* Исключение может быть сгенерировано одним методом
и перехвачено другим. */
using System;

class ExcTest {
    // Сгенерировать исключение.
    public static void GenException() {
        int[] nums = new int[4];

        Console.WriteLine("До генерирования исключения.");

        // Сгенерировать исключение в связи с выходом индекса за границы
        массива.
        for(int i=0; i < 10; i++) {
            nums[i] = i;
            Console.WriteLine("nums [{0}] : {1}", i, nums[i]);
        }

        Console.WriteLine("He подлежит выводу");
    }
}

class ExcDemo2 {
    static void Main() {
        try {
            ExcTest.GenException();
        }
        catch (IndexOutOfRangeException) {
            // Перехватить исключение.
            Console.WriteLine("Индекс вышел за границы массива!");
        }
        Console.WriteLine("После блока перехвата исключения.");
    }
}

Выполнение этой программы дает такой же результат, как и в предыдущем
примере.

До генерирования исключения.
nums[0]: 0
nums[1]: 1
nums[2]: 2
nums[3]: 3
Индекс вышел за границы массива!
После блока перехвата исключения.

Как пояснялось выше, метод GenException() вызывается из блока try, и поэтому
генерируемое им исключение перехватывается не в нем, а в блоке catch внутри мето­
да Main(). А если бы исключение перехватывалось в методе GenException(), оно не
было бы вообще передано обратно методу Main().

Последствия неперехвата исключений

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

// Предоставить исполняющей системе C# возможность самой обрабатывать ошибки.
using System;

class NotHandled {
    static void Main() {
        int[] nums = new int[4];

        Console.WriteLine("До генерирования исключения.");

        // Сгенерировать исключение в связи с выходом индекса за границы массива.
        for(int i=0; i < 10; i++) {
            nums[i] = i;
            Console.WriteLine("nums[{0}]: {1}", i, nums[i]);
        }
    }
}

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

Необработанное исключение: System.IndexOutOfRangeException:
        Индекс находился вне границ массива.
    в NotHandled.Main() в <имя_файла>:строка 16

Это сообщение уведомляет об обнаружении в методе NotHandled.Main() необра­
ботанного исключения типа System.IndexOutOfRangeException, которое связано
с выходом индекса за границы массива.

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

Как упоминалось ранее, тип генерируемого исключения должен соответствовать
типу, указанному в операторе catch. В противном случае исключение не будет пере­
хвачено. Например, в приведенной ниже программе предпринимается попытка пере­
хватить ошибку нарушения границ массива в блоке catch, реагирующем на исключе­
ние DivideByZeroException, связанное с делением на нуль и являющееся еще одним
стандартным исключением. Когда индексирование массива выходит за его границы,
генерируется исключение IndexOutOfRangeException, но оно не будет перехвачено
блоком catch, что приведет к аварийному завершению программы.

// Не сработает!
using System;

class ExcTypeMismatch {
    static void Main() {
        int[] nums = new int[4];
        try {
            Console.WriteLine("До генерирования исключения.");
            // Сгенерировать исключение в связи с выходом индекса за границы массива.
            for(int i=0; i < 10; i++) {
                nums[i] = i;
                Console.WriteLine("nums[{0}]: {1}", i, nums[i]);
            }
            Console.WriteLine("He подлежит выводу");
        }
        /* Если перехват рассчитан на исключение DivideByZeroException,
        то перехватить ошибку нарушения границ массива не удастся. */
        catch (DivideByZeroException) {
            // Перехватить исключение.
            Console.WriteLine("Индекс вышел за границы массива!");
        }
        Console.WriteLine("После блока перехвата исключения.");
    }
}

Вот к какому результату приводит выполнение этой программы.

До генерирования исключения.
nums[0]: 0
nums[1]: 1
nums[2]: 2
nums[3]: 3
Необработанное исключение: System.IndexOutOfRangeException:
        Индекс находился вне границ массива
    в ExcTypeMismatch.Main() в <имя_файла>:строка 18

Как следует из приведенного выше результата, в блоке catch, реагирующем
на исключение DivideByZeroException, не удалось перехватить исключение
IndexOutOfRangeException.

Обработка исключительных ситуаций — “изящный” способ устранения программных ошибок

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

// Изящно обработать исключительную ситуацию и продолжить выполнение программы.
using System;

class ExcDemo3 {
    static void Main() {
        int[] numer = { 4, 8, 16, 32, 64, 128 };
        int[] denom = { 2, 0, 4, 4, 0, 8 };

        for(int i=0; i < numer.Length; i++) {
            try {
                Console.WriteLine(numer[i] + " / " +
                                denom[i] + " равно " +
                                numer[i]/denom[i]);
            }
            catch (DivideByZeroException) {
                // Перехватить исключение.
                Console.WriteLine("Делить на нуль нельзя!");
            }
        }
    }
}

Ниже приведен результат выполнения этой программы.

4/2 равно 2
Делить на нуль нельзя!
16/4 равно 4
32/4 равно 8
Делить на нуль нельзя!
128 / 8 равно 16

Из данного примера следует еще один важный вывод: как только исключение обра­
ботано, оно удаляется из системы. Поэтому в приведенной выше программе проверка
ошибок в блоке try начинается снова на каждом шаге цикла for, при условии, что все
предыдущие исключительные ситуации были обработаны. Это позволяет обрабаты­
вать в программе повторяющиеся ошибки.

Применение нескольких операторов catch

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

// Использовать несколько операторов catch.
using System;

class ExcDemo4 {
    static void Main() {
        // Здесь массив numer длиннее массива denom.
        int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int[] denom = { 2, 0, 4, 4, 0, 8 };

        for(int i=0; i < numer.Length; i++) {
            try {
                Console.WriteLine(numer[i] + " / " +
                                denom[i] + " равно " +
                                numer[i]/denom[i]);
            }
            catch (DivideByZeroException) {
                Console.WriteLine("Делить на нуль нельзя!");
            }
            catch (IndexOutOfRangeException) {
                Console.WriteLine("Подходящий элемент не найден.");
            }
        }
    }
}

Вот к какому результату приводит выполнение этой программы.

4/2 равно 2
Делить на нуль нельзя!
16/4 равно 4
32/4 равно 8
Делить на нуль нельзя!
128 / 8 равно 16
Подходящий элемент не найден.
Подходящий элемент не найден.

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

Вообще говоря, операторы catch выполняются по порядку их следования в про­
грамме. Но при этом выполняется только один блок catch, в котором тип исклю­
чения совпадает с типом генерируемого исключения. А все остальные блоки catch
пропускаются.

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

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

catch {
    // обработка исключений
}

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

Ниже приведен пример такого «универсального» обработчика исключений. Об­
ратите внимание на то, что он перехватывает и обрабатывает оба исключения,
IndexOutOfRangeException и DivideByZeroException, генерируемых в программе.

// Использовать "универсальный" обработчик исключений.
using System;

class ExcDemo5 {
    static void Main() {
        // Здесь массив numer длиннее массива denom.
        int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int[] denom = { 2, 0, 4, 4, 0, 8 );

        for(int i=0; i < numer.Length; i++) {
            try {
                Console.WriteLine(numer[i] + " / " +
                                denom[i] + " равно " +
                                numer[i]/denom[i]);
            }
            catch { // "Универсальный" перехват.
                Console.WriteLine("Возникла некоторая исключительная ситуация.");
            }
        }
    }
}

При выполнении этой программы получается следующий результат.

4/2 равно 2
Возникла некоторая исключительная ситуация.
16/4 равно 4
32/4 равно 8
Возникла некоторая исключительная ситуация.
128 / 8 равно 16
Возникла некоторая исключительная ситуация.
Возникла некоторая исключительная ситуация.

Применяя «универсальный» перехват, следует иметь в виду, что его блок должен
располагаться последним по порядку среди всех блоков catch.

ПРИМЕЧАНИЕ
В подавляющем большинстве случаев «универсальный» обработчик исключений (не при­
меняется. Как правило, исключения, которые могут быть сгенерированы в коде, обрабаты­
ваются по отдельности. Неправильное использование “универсального” обработчика может
привести к тому, что ошибки, перехватывавшиеся при тестировании программы, маскируют­
ся. Кроме того, организовать надлежащую обработку всех исключительных ситуаций в одном
обработчике не так-то просто. Иными словами, “универсальный» обработчик исключений
может оказаться пригодным лишь в особых случаях, например в инструментальном средстве
анализа кода во время выполнения.

Вложение блоков try

Один блок try может быть вложен в другой. Исключение, генерируемое во вну­
треннем блоке try и не перехваченное в соответствующем блоке catch, передается во
внешний блок try. В качестве примера ниже приведена программа, в которой исклю­
чение IndexOutOfRangeException перехватывается не во внутреннем, а во внешнем
блоке try.

// Использовать вложенный блок try.
using System;

class NestTrys {
    static void Main() {
        // Здесь массив numer длиннее массива denom.
        int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int[] denom = ( 2, 0, 4, 4, 0, 8 );

        try { // внешний блок try
            for(int i=0; i < numer.Length; i++) {
                try { // вложенный блок try
                    Console.WriteLine(numer[i] + " / " +
                                    denom[i] + " равно " +
                                    numer[i]/denom[i]);
                }
                catch (DivideByZeroException) {
                    Console.WriteLine("Делить на нуль нельзя!");
                }
            }
        }
        catch (IndexOutOfRangeException) {
            Console.WriteLine("Подходящий элемент не найден.");
            Console.WriteLine("Неисправимая ошибка - программа прервана.");
        }
    }
}

Выполнение этой программы приводит к следующему результату.

4/2 равно 2
Делить на нуль нельзя!
16/4 равно 4
32/4 равно 8
Делить на нуль нельзя!
128 / 8 равно 16
Подходящий элемент не найден.
Неисправимая ошибка - программа прервана.

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

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

Генерирование исключений вручную

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

где в качестве exceptOb должен быть обозначен объект класса исключений, произво­
дного от класса Exception.

Ниже приведен пример программы, в которой демонстрируется применение опе­
ратора throw для генерирования исключения DivideByZeroException.

// Сгенерировать исключение вручную.
using System;

class ThrowDemo {
    static void Main() {
        try {
            Console.WriteLine("До генерирования исключения.");
            throw new DivideByZeroException();
        }
        catch (DivideByZeroException) {
            Console.WriteLine("Исключение перехвачено.");
        }
        Console.WriteLine("После пары операторов try/catch.");
    }
}

Вот к какому результату приводит выполнение этой программы.

До генерирования исключения.
Исключение перехвачено.
После пары операторов try/catch.

Обратите внимание на то, что исключение DivideByZeroException было сге­
нерировано с использованием ключевого слова new в операторе throw. Не следует
забывать, что в данном случае генерируется конкретный объект, а следовательно, он
должен быть создан перед генерированием исключения. Это означает, что сгенериро­
вать исключение только по его типу нельзя. В данном примере для создания объекта
DivideByZeroException был автоматически вызван конструктор, используемый по
умолчанию, хотя для генерирования исключений доступны и другие конструкторы.

Повторное генерирование исключений

Исключение, перехваченное в одном блоке catch, может быть повторно сгенери­
ровано в другом блоке, чтобы быть перехваченным во внешнем блоке catch. Наиболее
вероятной причиной для повторного генерирования исключения служит предоставле­
ние доступа к исключению нескольким обработчикам. Допустим, что один обработчик
оперирует каким-нибудь одним аспектом исключения, а другой обработчик — другим
его аспектом. Для повторного генерирования исключения достаточно указать опера­
тор throw без сопутствующего выражения, как в приведенной ниже форме.

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

В приведенном ниже примере программы демонстрируется повтор­
ное генерирование исключения. В данном случае генерируется исключение
IndexOutOfRangeException.

// Сгенерировать исключение повторно.
using System;

class Rethrow {
    public static void GenException() {
        // Здесь массив numer длиннее массива denom.
        int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int[] denom = { 2, 0, 4, 4, 0, 8 };

        for(int i=0; i<numer.Length; i++) {
            try {
                Console.WriteLine(numer[i] + " / " +
                                denom[i] + " равно " +
                                numer[i]/denom[i]);
            }
            catch (DivideByZeroException) {
                Console.WriteLine("Делить на нуль нельзя!");
            }
            catch (IndexOutOfRangeException) {
                Console.WriteLine("Подходящий элемент не найден.");
                throw; // сгенерировать исключение повторно
            }
        }
    }
}

class RethrowDemo {
    static void Main() {
        try {
            Rethrow.GenException();
        }
        catch(IndexOutOfRangeException) {
            // перехватить исключение повторно
            Console.WriteLine("Неисправимая ошибка - программа прервана.");
        }
    }
}

В этом примере программы ошибки из-за деления на нуль обрабатываются локаль­
но в методе GenException(), но ошибка выхода за границы массива генерируется
повторно. В данном случае исключение IndexOutOfRangeException обрабатывается
в методе Main().

Использование блока finally

Иногда требуется определить кодовый блок, который будет выполняться после вы­
хода из блока try/catch. В частности, исключительная ситуация может возникнуть
в связи с ошибкой, приводящей к преждевременному возврату из текущего метода.
Но в этом методе мог быть открыт файл, который нужно закрыть, или же установлено
сетевое соединение, требующее разрывания. Подобные ситуации нередки в програм­
мировании, и поэтому для их разрешения в C# предусмотрен удобный способ: вос­
пользоваться блоком finally.

Для того чтобы указать кодовый блок, который должен выполняться после блока
try/catch, достаточно вставить блок finally в конце последовательности операторов
try/catch. Ниже приведена общая форма совместного использования блоков try/
catch и finally.

try {
    // Блок кода, предназначенный для обработки ошибок.
}
catch (ExcepType1 exOb) {
    // Обработчик исключения типа ExcepType1.
}
catch (ExcepType2 ехОb) {
    // Обработчик исключения типа ЕхсерТуре2.
}
finally {
    // Код завершения обработки исключений.
}

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

Ниже приведен пример применения блока finally.

// Использовать блок finally.
using System;

class UseFinally {
    public static void GenException(int what) {
        int t;
        int[] nums = new int[2];

        Console.WriteLine("Получить " + what);
        try {
            switch(what) {
                case 0:
                    t = 10 / what; // сгенерировать ошибку из-за деления на нуль
                    break;
                case 1:
                    nums[4] = 4; // сгенерировать ошибку индексирования массива
                    break;
                case 2:
                    return; // возврат из блока try
            }
        }
        catch (DivideByZeroException) {
            Console.WriteLine("Делить на нуль нельзя!");
            return; // возврат из блока catch
        }
        catch (IndexOutOfRangeException) {
            Console.WriteLine("Совпадающий элемент не найден.");
        }
        finally {
            Console.WriteLine("После выхода из блока try.");
        }
    }
}

class FinallyDemo {
    static void Main() {
        for(int i=0; i < 3; i++) {
            UseFinally.GenException(i);
            Console.WriteLine();
        }
    }
}

Вот к какому результату приводит выполнение этой программы.

Получить 0
Делить на нуль нельзя
После выхода из блока try.

Получить 1
Совпадающий элемент не найден.
После выхода из блока try.

Получить 2
После выхода из блока try.

Как следует из приведенного выше результата, блок finally выполняется независи­
мо от причины выхода из блока try.

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

Подробное рассмотрение класса Exception

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

В классе Exception определяется ряд свойств. К числу самых интересных отно­
сятся три свойства: Message, StackTrace и TargetSite. Все эти свойства доступны
только для чтения. Свойство Message содержит символьную строку, описывающую
характер ошибки; свойство StackTrace — строку с вызовами стека, приведшими к ис­
ключительной ситуации, а свойство TargetSite получает объект, обозначающий ме­
тод, сгенерировавший исключение.

Кроме того, в классе Exception определяется ряд методов. Чаще всего приходится
пользоваться методом ToString(), возвращающим символьную строку с описанием
исключения. Этот метод автоматически вызывается, например, при отображении ис­
ключения с помощью метода WriteLine().

Применение всех трех упомянутых выше свойств и метода из класса Exception
демонстрируется в приведенном ниже примере программы.

// Использовать члены класса Exception.
using System;
class ExcTest {
    public static void GenException() {
        int[] nums = new int[4];
        Console.WriteLine("До генерирования исключения.");
        // Сгенерировать исключение в связи с выходом за границы массива.
        for(int i=0; i < 10; i++) {
            nums[i] = i;
            Console.WriteLine("nums[{0}]: (1)", i, nums[i]);
        }
        Console.WriteLine("He подлежит выводу");
    }
}

class UseExcept {
    static void Main() {
        try {
            ExcTest.GenException();
        }
        catch (IndexOutOfRangeException exc) {
            Console.WriteLine("Стандартное сообщение таково: ");
            Console.WriteLine(exc); // вызвать метод ToString()
            Console.WriteLine("Свойство StackTrace: " + exc.StackTrace);
            Console.WriteLine("Свойство Message: " + exc.Message);
            Console.WriteLine("Свойство TargetSite: " + exc.TargetSite);
        }
        Console.WriteLine("После блока перехвата исключения.");
    }
}

При выполнении этой программы получается следующий результат.

До генерирования исключения.
nums[0]: 0
nums[1]: 1
nums[2]: 2
nums[3]: 3
Стандартное сообщение таково: System.IndexOutOfRangeException: Индекс находился
вне границ массива.
    в ExcTest.genException() в <имя_файла>:строка 15
    в UseExcept.Main() в <имя_файла>:строка 29
Свойство StackTrace:в ExcTest.genException() в <имя_файла>:строка 15
    в UseExcept.Main()в <имя_файла>:строка 29
Свойство Message: Индекс находился вне границ массива.
Свойство TargetSite: Void genException()
После блока перехвата исключения.

В классе Exception определяются четыре следующих конструктора.

public Exception()
public Exception(string сообщение)
public Exception(string сообщение, Exception внутреннее_исключение)
protected Exception(System.Runtime.Serialization.SerializationInfo информация,
    System.Runtime.Serialization.StreamingContext контекст)

Первый конструктор используется по умолчанию. Во втором конструкторе ука­
зывается строка сообщение, связанная со свойством Message, которое имеет отно­
шение к генерируемому исключению. В третьем конструкторе указывается так на­
зываемое внутреннее исключение. Этот конструктор используется в том случае, когда
одно исключение порождает другое, причем внутреннее_исключение обозначает
первое исключение, которое будет пустым, если внутреннее исключение отсутствует.
(Если внутреннее исключение присутствует, то оно может быть получено из свойства
InnerException, определяемого в классе Exception.) И последний конструктор об­
рабатывает исключения, происходящие дистанционно, и поэтому требует десериали­
зации.

Следует также заметить, что в четвертом конструкторе класса Exception типы
SerializationInfo и StreamingContext относятся к пространству имен System.
Runtime.Serialization.

Наиболее часто используемые исключения

В пространстве имен System определено несколько стандартных, встроенных ис­
ключений. Все эти исключения являются производными от класса SystemException,
поскольку они генерируются системой CLR при появлении ошибки во время выпол­
нения. В табл. 13.1 перечислены некоторые наиболее часто используемые стандартные
исключения.

Таблица 13.1. Наиболее часто используемые исключения, определенные в пространстве имен System

Исключение Значение
ArrayTypeMismatchException Тип сохраняемого значения несовместим с типом массива
DivideByZeroException Попытка деления на нуль
IndexOutOfRangeException Индекс оказался за границами массива
InvalidCastException Неверно выполнено динамическое приведение типов
OutOfMemoryException Недостаточно свободной памяти для дальнейшего выполнения программы. Это исключение может быть, например, сгенерировано, если для создания объекта с помощью оператора new не хватает памяти
OverflowException Произошло арифметическое переполнение
NullReferenceException Попытка использовать пустую ссылку, т.е. ссылку, которая не указывает ни на один из объектов

Большинство исключений, приведенных в табл. 13.1, не требует особых пояснений,
кроме исключения NullReferenceException. Это исключение генерируется при по­
пытке использовать пустую ссылку на несуществующий объект, например, при вы­
зове метода по пустой ссылке. Пустой называется такая ссылка, которая не указывает
ни на один из объектов. Для того чтобы создать такую ссылку, достаточно, например,
присвоить явным образом пустое значение переменной ссылочного типа, используя
ключевое слово null. Пустые ссылки могут также появляться и другими, менее оче­
видными путями. Ниже приведен пример программы, демонстрирующий обработку
исключения NullReferenceException.

// Продемонстрировать обработку исключения NullReferenceException.
using System;

class X {
    int x;

    public X(int a) {
        x = a;
    }

    public int Add(X o) {
        return x + o.x;
    }
}

// Продемонстрировать генерирование и обработку
// исключения NullReferenceException.
class NREDemo {
    static void Main() {
        X p = new X(10);
        X q = null; // присвоить явным образом пустое значение переменной q
        int val;
        try {
            val = p.Add(q); // эта операция приведет к исключительной ситуации
        } catch (NullReferenceException) {
            Console.WriteLine("Исключение NullReferenceException!");
            Console.WriteLine("Исправление ошибки...n");
            // А теперь исправить ошибку.
            q = new X(9);
            val = p.Add(q);
        }
        Console.WriteLine("Значение val равно {0}", val);
    }
}

Вот к какому результату приводит выполнение этой программы.

Исключение NullReferenceException!
Исправление ошибки...

Значение val равно 19

В приведенном выше примере программы создается класс X, в котором определя­
ются член х и метод Add(), складывающий значение члена х в вызывающем объекте
со значением члена х в объекте, передаваемом этому методу в качестве параметра. Оба
объекта класса X создаются в методе Main(). Первый из них (переменная р) инициа­
лизируется, а второй (переменная q) — нет. Вместо этого переменной q присваивается
пустое значение. Затем вызывается метод р.Add() с переменной q в качестве аргумен­
та. Но поскольку переменная q не ссылается ни на один из объектов, то при попытке
получить значение члена q.х генерируется исключение NullReferenceException.

Получение производных классов исключений

Несмотря на то что встроенные исключения охватывают наиболее распространен­
ные программные ошибки, обработка исключительных ситуаций в C# не ограничива­
ется только этими ошибками. В действительности одна из сильных сторон принятого
в C# подхода к обработке исключительных ситуаций состоит в том, что в этом языке
допускается использовать исключения, определяемые пользователем, т.е. тем, кто про­
граммирует на С#. В частности, такие специальные исключения можно использовать
для обработки ошибок в собственном коде, а создаются они очень просто. Для этого
достаточно определить класс, производный от класса Exception. В таких классах со­
всем не обязательно что-то реализовывать — одного только их существования в систе­
ме типов уже достаточно, чтобы использовать их в качестве исключений.

ПРИМЕЧАНИЕ
В прошлом специальные исключения создавались как производные от класса
Application.Exception, поскольку эта иерархия классов была первоначально зарезер­
вирована для исключений прикладного характера. Но теперь корпорация Microsoft не реко­
мендует этого делать, а вместо этого получать исключения, производные от класса Exception.
Именно по этой причине данный подход и рассматривается в настоящей книге.

Создаваемые пользователем классы будут автоматически получать свойства и мето­
ды, определенные в классе Exception и доступные для них. Разумеется, любой из этих
членов класса Exception можно переопределить в создаваемых классах исключений.
Когда создается собственный класс исключений, то, как правило, желательно, что­
бы в нем поддерживались все конструкторы, определенные в классе Exception. В про­
стых специальных классах исключений этого нетрудно добиться, поскольку для этого
достаточно передать подходящие аргументы соответствующему конструктору класса
Exception, используя ключевое слово base. Но формально нужно предоставить толь­
ко те конструкторы, которые фактически используются в программе.

Рассмотрим пример программы, в которой используется исключение специального
типа. Напомним, что в конце главы 10 был разработан класс RangeArray, поддержи­
вающий одномерные массивы, в которых начальный и конечный индексы определяют­
ся пользователем. Так, например, вполне допустимым считается массив, индексируе­
мый в пределах от -5 до 27. Если же индекс выходил за границы массива, то для обра­
ботки этой ошибки в классе RangeArray была определена специальная переменная.
Такая переменная устанавливалась и проверялась после каждой операции обращения
к массиву в коде, использовавшем класс RangeArray. Безусловно, такой подход к об­
работке ошибок «неуклюж» и чреват дополнительными ошибками. В приведенном
ниже улучшенном варианте класса RangeArray обработка ошибок нарушения границ
массива выполняется более изящным и надежным способом с помощью специально
генерируемого исключения.

// Использовать специальное исключение для обработки
// ошибок при обращении к массиву класса RangeArray.
using System;

// Создать исключение для класса RangeArray.
class RangeArrayException : Exception {
/* Реализовать все конструкторы класса Exception. Такие конструкторы просто
реализуют конструктор базового класса. А поскольку класс исключения
RangeArrayException ничего не добавляет к классу Exception, то никаких
дополнительных действий не требуется. */
public RangeArrayException() : base)) { }

public RangeArrayException(string str) : base(str) { }

public RangeArrayException(
string str, Exception inner) : base (str, inner) { }

protected RangeArrayException(
System.Runtime.Serialization.SerializationInfo si,
System.Runtime.Serialization.StreamingContext sc) :
base(si, sc) { }

// Переопределить метод ToString() для класса исключения RangeArrayException.
public override string ToString() {
    return Message;
}

}

// Улучшенный вариант класса RangeArray.
class RangeArray {
// Закрытые данные.
int[] a; // ссылка на базовый массив
int lowerBound; // наименьший индекс
int upperBound; // наибольший индекс

// Автоматически реализуемое и доступное только для чтения свойство Length.
public int Length { get; private set; }
// Построить массив по заданному размеру

public RangeArray(int low, int high) {
    high++;
    if(high <= low) {
        throw new RangeArrayException("Нижний индекс не меньше верхнего.");
    }
    а = new int[high - low];
    Length = high - low;
    lowerBound = low;
    upperBound = --high;
}

// Это индексатор для класса RangeArray.
public int this[int index] {
    // Это аксессор get.
    get {
        if(ok(index)) {
            return a[index - lowerBound];
        } else {
            throw new RangeArrayException("Ошибка нарушения границ.");
        }
    }
    // Это аксессор set.
    set {
        if(ok(index)) {
            a[index - lowerBound] = value;
        }
        else throw new RangeArrayException("Ошибка нарушения границ.");
    }
}

// Возвратить логическое значение true, если
// индекс находится в установленных границах.
private bool ok(int index) {
    if(index >= lowerBound S index <= upperBound) return true;
    return false;
}

}

// Продемонстрировать применение массива с произвольно
// задаваемыми пределами индексирования.
class RangeArrayDemo {
static void Main() {
try {
RangeArray ra = new RangeArray(-5, 5);
RangeArray ra2 = new RangeArray(1, 10);

        // Использовать объект ra в качестве массива.
        Console.WriteLine("Длина массива ra: " + ra.Length);
        for(int i = -5; i <= 5; i++)
            ra[i] = i;

        Console.Write("Содержимое массива ra: ");
        for(int i = -5; i <= 5; i++)
            Console.Write(ra[i] + " ");

        Console.WriteLine("n");

        // Использовать объект ra2 в качестве массива.
        Console.WriteLine("Длина массива ra2: " + ra2.Length);
        for(int i = 1; i <= 10; i++)
            ra2[i] = i;

        Console.Write("Длина массива ra2: ");
        for(int i = 1; i <= 10; i++)
            Console.Write(ra2[i] + " ");

        Console.WriteLine("n");
    } catch (RangeArrayException exc) {
        Console.WriteLine(exc);
    }

    // А теперь продемонстрировать обработку некоторых ошибок.
    Console.WriteLine("Сгенерировать ошибки нарушения границ.");

    // Использовать неверно заданный конструктор.
    try {
        RangeArray ra3 = new RangeArray(100, -10); // Ошибка!
    } catch (RangeArrayException exc) {
        Console.WriteLine(exc);
    }

    // Использовать неверно заданный индекс.
    try {
        RangeArray ra3 = new RangeArray(-2, 2);

        for(int i = -2; i <= 2; i++)
            ra3[i] = i;

        Console.Write("Содержимое массива ra3: ");
        for(int i = -2; i <= 10; i++) // сгенерировать ошибку нарушения границ
            Console.Write(ra3[i] + " ");
    } catch (RangeArrayException exc) {
        Console.WriteLine(exc);
    }
}

}

После выполнения этой программы получается следующий результат.

Длина массива ra: 11
Содержимое массива ra: -5 -4 -3 -2 -1 0 1 2 3 4 5

Длина массива ra2: 10
Содержимое массива ra2: 1 2 3 4 5 6 7 8 9 10

Сгенерировать ошибки нарушения границ.
Нижний индекс не меньше верхнего.
Содержимое массива ra3: -2 -1 0 1 2 Ошибка нарушения границ.

Когда возникает ошибка нарушения границ массива класса RangeArray, генери­
руется объект типа RangeArrayException. В классе RangeArray это может произой­
ти в трех следующих местах: в аксессоре get индексатора, в аксессоре set индексатора
и в конструкторе класса RangeArray. Для перехвата этих исключений подразумева­
ется, что объекты типа RangeArray должны быть сконструированы и доступны из
блока try, что и продемонстрировано в приведенной выше программе. Используя
специальное исключение для сообщения об ошибках, класс RangeArray теперь дей­
ствует как один из встроенных в C# типов данных, и поэтому он может быть полностью
интегрирован в механизм обработки ошибок, обнаруживаемых в программе.

Обратите внимание на то, что в теле конструкторов класса исключения
RangeArrayException отсутствуют какие-либо операторы, но вместо этого они про­
сто передают свои аргументы классу Exception, используя ключевое слово base. Как
пояснялось ранее, в тех случаях, когда производный класс исключений не дополняет
функции базового класса, весь процесс создания исключений можно поручить кон­
структорам класса Exception. Ведь производный класс исключений совсем не обяза­
тельно должен чем-то дополнять функции, наследуемые от класса Exception.

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

## Перехват исключений производных классов
При попытке перехватить типы исключений, относящихся как к базовым, так
и к производным классам, следует особенно внимательно соблюдать порядок следо­
вания операторов catch, поскольку перехват исключения базового класса будет со­
впадать с перехватом исключений любых его производных классов. Например, класс
Exception является базовым для всех исключений, и поэтому вместе с исключением
типа Exception могут быть перехвачены и все остальные исключения производных от
него классов. Конечно, для более четкого перехвата всех исключений можно воспользо­
ваться упоминавшейся ранее формой оператора catch без указания конкретного типа
исключения. Но вопрос перехвата исключений производных классов становится весьма
актуальным и в других ситуациях, особенно при создании собственных исключений.

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

В приведенном ниже примере программы создаются два класса исключений:
ExceptA и ExceptB. Класс ExceptA является производным от класса Exception,
а класс ExceptB — производным от класса ExceptA. Затем в программе генерируются
исключения каждого типа. Ради краткости в классах специальных исключений предо­
ставляется только один конструктор, принимающий символьную строку, описываю­
щую исключение. Но при разработке программ коммерческого назначения в классах
специальных исключений обычно требуется предоставлять все четыре конструктора,
определяемых в классе Exception.

// Исключения производных классов должны появляться до
// исключений базового класса.
using System;

// Создать класс исключения.
class ExceptA : Exception {
public ExceptA(string str) : base(str) { }

public override string ToString() {
    return Message;
}

}

// Создать класс исключения, производный от класса ExceptA.
class ExceptB : ExceptA {
public ExceptB(string str) : base(str) { }

public override string ToString() {
    return Message;
}

}

class OrderMatters {
static void Main() {
for(int x = 0; x < 3; x++) {
try {
if(x==0) throw new ExceptA(«Перехват исключения типа ExceptA»);
else if(x==1) throw new ExceptB(«Перехват исключения типа
ExceptB»);
else throw new Exception));
}
catch (ExceptB exc) {
Console.WriteLine(exc);
}
catch (ExceptA exc) {
Console.WriteLine(exc);
}
catch (Exception exc) {
Console.WriteLine(exc);
}
}
}
}

Вот к какому результату приводит выполнение этой программы.

Перехват исключения типа ExceptA.
Перехват исключения типа ExceptB.
System.Exception: Выдано исключение типа «System.Exception».
в OrderMatters.Main() в <имя_файла>:строка 36

Обратите внимание на порядок следования операторов catch. Именно в таком по­
рядке они и должны выполняться. Класс ExceptB является производным от класса
ExceptA, поэтому исключение типа ExceptB должно перехватываться до исключения
типа ExceptA. Аналогично, исключение типа Exception (т.е. базового класса для всех
исключений) должно перехватываться последним. Для того чтобы убедиться в этом,
измените порядок следования операторов catch. В итоге это приведет к ошибке во
время компиляции.

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

## Применение ключевых слов checked и unchecked
В C# имеется специальное средство, связанное с генерированием исключений, воз­
никающих при переполнении в арифметических вычислениях. Как вам должно быть
уже известно, результаты некоторых видов арифметических вычислений могут превы­
шать диапазон представления чисел для типа данных, используемого в вычислении.
В этом случае происходит так называемое переполнение. Рассмотрим в качестве при­
мера следующий фрагмент кода.

byte a, b, result;
а = 127;
b = 127;
result = (byte)(а * b);

В этом коде произведение значений переменных а и b превышает диапазон пред­
ставления чисел для типа byte. Следовательно, результат вычисления данного вы­
ражения приводит к переполнению для типа данных, сохраняемого в переменной
result.

В C# допускается указывать, будет ли в коде сгенерировано исключение при пере­
полнении, с помощью ключевых слов checked и unchecked. Так, если требуется ука­
зать, что выражение будет проверяться на переполнение, следует использовать клю­
чевое слово checked, а если требуется проигнорировать переполнение — ключевое
слово unchecked. В последнем случае результат усекается, чтобы не выйти за пределы
диапазона представления чисел для целевого типа выражения.

У ключевого слова checked имеются две общие формы. В одной форме проверя­
ется конкретное выражение, и поэтому она называется операторной. А в другой форме
проверяется блок операторов, и поэтому она называется блочной. Ниже приведены обе
формы:

checked (выражение)

checked {
// проверяемые операторы
}

где выражение обозначает проверяемое выражение. Если вычисление прове­
ряемого выражения приводит к переполнению, то генерируется исключение
OverflowException.

У ключевого слова unchecked также имеются две общие формы. В первой, опера­
торной форме переполнение игнорируется при вычислении конкретного выражения.
А во второй, блочной форме оно игнорируется при выполнении блока операторов:

unchecked (выражение)

unchecked {
// операторы, для которых переполнение игнорируется
}

где выражение обозначает конкретное выражение, при вычислении которого перепол­
нение игнорируется. Если же в непроверяемом выражении происходит переполнение,
то результат его вычисления усекается.

Ниже приведен пример программы, в котором демонстрируется применение клю­
чевых слов checked и unchecked.

// Продемонстрировать применение ключевых слов checked и unchecked.
using System;

class CheckedDemo {
static void Main() {
byte a, b;
byte result;
a = 127;
b = 127;
try {
result = unchecked((byte)(a * b));
Console.WriteLine(«Непроверенный на переполнение результат: » +
result);
result = checked((byte)(a * b)); // эта операция приводит к
// исключительной ситуации
Console.WriteLine(«Проверенный на переполнение результат: » +
result); //не подлежит выполнению
}
catch (OverflowException exc) {
Console.WriteLine(exc);
}
}
}

При выполнении этой программы получается следующий результат.

Непроверенный на переполнение результат: 1
System.OverflowException: Переполнение в результате
выполнения арифметической операции.
в CheckedDemo.Main() в <имя_файла>:строка 20

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

В представленном выше примере программы было продемонстрировано приме­
нение ключевых слов checked и unchecked в одном выражении. А в следующем при­
мере программы показывается, каким образом проверяется и не проверяется на пере­
полнение целый блок операторов.

// Продемонстрировать применение ключевых слов checked
// и unchecked в блоке операторов.
using System;

class CheckedBlocks {
static void Main() {
byte a, b;
byte result;
a = 127;
b = 127;
try {
unchecked {
a = 127;
b = 127;
result = unchecked((byte)(a * b));
Console.WriteLine(«Непроверенный на переполнение результат: » +
result);

            а = 125;
            b = 5;
            result = unchecked((byte)(a * b));
            Console.WriteLine("Непроверенный на переполнение результат: " +
                            result);
        }

        checked {
            a = 2;
            b = 7;
            result = checked((byte)(a * b)); // верно
            Console.WriteLine("Проверенный на переполнение результат: " +
                            result);

            а = 127;
            b = 127;
            result = checked((byte)(a * b)); // эта операция приводит к
            // исключительной ситуации
            Console.WriteLine("Проверенный на переполнение результат: " +
                            result); // не подлежит выполнению
        }
    }
    catch (OverflowException exc) {
        Console.WriteLine(exc);
    }
}

}

Результат выполнения этой программы приведен ниже.

Непроверенный на переполнение результат: 1
Непроверенный на переполнение результат: 113
Проверенный на переполнение результат: 14
System.OverflowException: Переполнение в результате
выполнения арифметической операции.
в CheckedDemo.Main() в <имя_файма>:строка 41

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

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

Содержание

  • Исключения (Exceptions) и инструкция try
  • Оговорка catch
  • Блок finally
  • Инструкция using
  • Выбрасывание исключений
  • Основные свойства System.Exception
  • Основные типы исключений
  • Директивы препроцессора
    • Pragma Warning
    • Атрибут Conditional
  • Классы Debug и Trace
    • TraceListener
    • Fail и Assert

Исключения, их обработка, и некоторые другие моменты, связанные с ошибками в приложении на C#.

Исключения (Exceptions) и инструкция try

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

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

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

В целом конструкция try выглядит следующим образом:

try

{

  ... // в пределах этого блока может быть выброшено исключение

}

catch (ExceptionA ex)

{

  ... // обработчик исключений типа ExceptionA

}

catch (ExceptionB ex)

{

  ... // обработчик исключений типа ExceptionB

}

finally

{

  ... // код очистки

}

Например, следующий код выбросит ошибку DivideByZeroException (поскольку делить на ноль нельзя) и наша программа завершить досрочно:

int x = 3, y = 0;

Console.WriteLine (x / y);

Чтобы этого избежать можно использовать конструкцию try:

try

{

  int x = 3, y = 0;

  Console.WriteLine (x / y);

}

catch (DivideByZeroException ex)

{

  Console.Write («y cannot be zero. «);

}

// выполнение программы продолжится отсюда

Обработка исключений довольно ресурсоёмкая операция, поэтому на практике для таких случаев как в примере ее лучше не использовать (лучше непосредственно перед делением проверить делить на равенство нулю).

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

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

Оговорка catch

В оговорке catch указывается какой тип исключения она должна перехватывать. Это может быть либо System.Exception, либо его производный класс. Перехватывая непосредственно System.Exception, мы перехватим все возможные ошибки.  Это может быть полезно в нескольких случаях:

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

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

Можно обработать несколько типов исключений с помощью нескольких оговорок catch:

try

{

  DoSomething();

}

catch (IndexOutOfRangeException ex) { ... }

catch (FormatException ex) { ... }

catch (OverflowException ex) { ... }

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

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

catch (StackOverflowException) // без переменной

{ ... }

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

Блок finally

Блок finally выполняется всегда, независимо от того выброшено исключение или нет. Блок finally обычно содержит код очистки.

Блок finally выполняется в следующих случаях:

  • после завершения блока catch
  • если выполнение блока try прервано jump-инструкциями: return, goto и т.д.
  • после выполнения блока try полностью, если исключений так и не было выброшено

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

static void ReadFile()

{

  StreamReader reader = null;

  try

  {

      reader = File.OpenText («file.txt»);

      if (reader.EndOfStream) return;

      Console.WriteLine (reader.ReadToEnd());

  }

  finally

  {

      if (reader != null) reader.Dispose();

  }

}

В пример для закрытия файла вызывается метод Dispose. Использование этого метода внутри блока finally является стандартной практикой. C# даже позволяет заменить всю конструкцию инструкцией using.

Инструкция using

Многие классы инкапсулируют неуправляемые ресурсы, такие как дескриптор файла, соединение с базой данных и т.д. Эти классы реализуют интерфейс System.IDisposable, который содержит единственный метод без параметров Dispose, освобождающий соответствующие машинные ресурсы. Инструкция using предусматривает удобный синтаксис вызова метода Dispose для объектов реализующих IDisposable внутри блока finally:

using (StreamReader reader = File.OpenText («file.txt»))

{

  ...

}

Что эквивалентно следующей конструкции:

StreamReader reader = File.OpenText («file.txt»);

try

{

  ...

}

finally

{

  if (reader != null) ((IDisposable)reader).Dispose();

}

Выбрасывание исключений

Исключение может быть выброшено автоматически во время выполнения программы либо явно в коде программы с помощью ключевого слова throw:

static void Display (string name)

{

  if (name == null)

  throw new ArgumentNullException («name»);

  Console.WriteLine (name);

}

Также исключение может быть выброшено повторно внутри блока catch:

try { ... }

catch (Exception ex)

{

  // логирование ошибки

  ...

  throw; // повторное выбрасывание того же самого исключения

}

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

Если throw заменить на throw ex, то пример по прежнему будет работать, но свойство исключения StackTrace не будет отражать исходную ошибку.

Другой распространенный сценарий использования повторного выбрасывания исключения — повторное выбрасывание более специфического и конкретного типа исключения, чем было перехвачено ранее:

try

{

  ... // парсинг даты рождения из xml-данных

}

catch (FormatException ex)

{

  throw new XmlException («Неправильная дата рождения», ex);

}

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

Основные свойства System.Exception

К наиболее важным свойствам класса System.Exception можно отнести:

  • StackTrace — строка, представляющая все методы, которые были вызваны, начиная с того, в котором было выброшено исключение, и заканчивая тем, в котором содержится блок catch, перехвативший исключение;
  • Message — строка с описанием ошибки;
  • InnerException — содержит ссылку на объект Exeption, который вызвал текущее исключение (например, при повторном выбрасывании исключения).

Основные типы исключений

Следующие типы исключений являются наиболее распространенными в среде CLR и .NET Framework. Их можно выбрасывать непосредственно или использовать как базовые классы для пользовательских типов исключений.

  • System.ArgumentException — выбрасывается при вызове функции с неправильным аргументом.
  • System.ArgumentNullException — производный от ArgumentException класс, выбрасывается если один из аргументов функции неожиданно равен null.
  • System.ArgumentOutOfRangeException — производный от ArgumentException класс, выбрасывается когда аргумент функции имеет слишком большое или слишком маленькое значение для данного типа (обычно касается числовых типов). Например, такое исключение будет выброшено если попытаться передать отрицательное число в функцию, которая ожидает только положительные числа.
  • System.InvalidOperationException — выбрасывается когда состояние объекта является неподходящим для нормального выполнения метода, например, при попытке прочесть не открытый файл.
  • System.NotSupportedException — выбрасывается, когда запрошенный функционал не поддерживается, например, если попытаться вызвать метод Add для коллекции доступной только для чтения (свойство коллекции IsReadOnly возвращает true).
  • System.NotImplementedException — выбрасывается, когда запрошенный функционал еще не реализован.
  • System.ObjectDisposedException — выбрасывается при попытке вызвать метод объекта, который уже был уничтожен (disposed).

Директивы препроцессора

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

#define DEBUG

class MyClass

{

  int x;

  void Foo()

  {

      # if DEBUG

      Console.WriteLine («Testing: x = {0}», x);

      # endif

  }

}

В этом классе инструкции в методе Foo скомпилируются если определен символ DEBUG, а если его удалить — инструкции не скомпилируются. Символы препроцессора могут быть определены в исходном коде (как в примере), а могут быть переданы компилятору в командной строке с помощью параметра /define:symbol.

С директивами #if и #elif можно использовать операторы ||, && и ! с несколькими символами:

Директивы #error и #warning предотвращают некорректное использование условных директив, заставляя компилятор генерировать предупреждение или ошибку при передаче неверного набора символов.

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

  • условное включение атрибута
  • изменение типа, объявляемого для переменной
  • переключение между разными пространствами имен или псевдонимами типа в директиве using:

    using TestType =

      #if V2

          MyCompany.Widgets.GadgetV2;

      #else

          MyCompany.Widgets.Gadget;

      #endif

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

Полный список директив препроцессора:

  • #define symbol — определяет символ
  • #undef symbol — удаляет символ
  • #if symbol [оператор symbol2]... — условная компиляция; допустимые операторы ==, !=, && и ||
  • #else — выполняет код после #endif
  • #elif symbol [оператор symbol2] — объединяет #else и #if
  • #endif — конец условных директив
  • #warning text — текст предупреждения, которое появится в выдаче компилятора
  • #error text — текст ошибки, которая появится в выдаче компилятора
  • #line [число["файл"] | hidden]число указывает номер строки в исходном коде; файл — имя файла, которое появится в выдаче компилятора; hidden — дает указание дебагеру пропустить код от этой точки до следующей директивы #line
  • #region name — отмечает начало области
  • #endregion — отмечает конец области
  • #pragma warning

Pragma Warning

Компилятор генерирует предупреждения, когда что-то в коде ему кажется неуместным (но корректным). В отличии от ошибок предупреждения не препятствуют компиляции программы. Предупреждения компилятора могут быть очень полезны при поиске багов в программе. Однако часто предупреждения оказываются ложными, поэтому целесообразно иметь возможность получать предупреждения только о действительных багах. С этой целью компилятор дает возможность выборочно подавить предупреждения с помощью директивы #pragma warning.

public class Foo

{

  static void Main() { }

  #pragma warning disable 414

  static string Message = «Hello»;

  #pragma warning restore 414

}

В примере мы указываем компилятору не выдавать предупреждения о том, что поле Message не используется.

Если не указывать номер директива #pragma warning отменит или восстановит вывод всех предупреждений.

Если скомпилировать программу с параметром /warnaserror, то все не отмененные директивой #pragma warning предупреждения будут расцениваться компилятором как ошибки.

Атрибут Conditional

Атрибут Conditional указывает компилятору на необходимость игнорировать все обращения к определенному классу или методу, если заданный символ не был определен:

[Conditional («LOGGINGMODE»)]

static void LogStatus (string msg)

{

  ...

}

Это равносильно тому, что каждый вызов метода будет окружен условными директивами:

#if LOGGINGMODE

LogStatus («Message Headers: « + GetMsgHeaders());

#endif

Классы Debug и Trace

Статические классы Debug и Trace предлагают базовые возможности логирования. Оба класса схожи, отличие заключается в их назанчении. Класс Debug предназначен для отладочных сборок, класс Trace — для отладочных и финальных. В связи с этим все методы класса Debug определены с атрибутом [Conditional("DEBUG")], а методы класса Trace — с атрибутом [Conditional("TRACE")]. Это значит, что все обращения к Debug и Trace будут подавляться компилятором, пока не определен символ DEBUG или TRACE.

Класс Debug и Trace определяют методы Write, WriteLine и WriteIf. По умолчанию они отправляют сообщения в окно вывода отладчика:

Debug.Write («Data»);

Debug.WriteLine (23 * 34);

int x = 5, y = 3;

Debug.WriteIf (x > y, «x is greater than y»);

Класс Trace также содержит методы TraceInformation, TraceWarning и TraceError. Их действия зависят от зарегистрированных прослушивателей.

TraceListener

Классы Debug и Trace имеют свойство Listeners, которое представляет собой статическую коллекцию экземпляров TraceListener. Они отвечают за обработку данных, возвращаемых методами Write, Fail и Trace.

По умолчанию коллекция Listeners обоих классов включает единственный прослушиватель — DefaultTraceListener — стандартный прослушиватель, имеющий две ключевые возможности:

  • при подключении к отладчику (например, Visual Studio) сообщения записываются в окно вывода отладчика, во всех остальных случаях сообщения игнорируются
  • при вызове метода Fail отображается диалоговое окно, запрашивающее у пользователя дальнейшие действия: продолжить, прервать или повторить отладку (независимо от того, подключен ли отладчик)

Это поведение можно изменить или дополнить, удалив (на обязательно) стандартный прослушиватель и/или добавив один или более собственных прослушивателей.

Прослушиваетли трассировки можно написать с нуля (создав производный класс от TraceListener) или воспользоваться готовыми классами:

  • TextWriterTraceListener записывает в Stream или TextWriter или добавляет в файл; имеет четыре подкласса: ConsoleTraceListener, DelimitedListTraceListener, XmlWriterTraceListener и EventSchemaTraceListener
  • EventLogTraceListener записывает в журнал событий Windows
  • EventProviderTraceListener записывает в систему трассировки событий Windows (Event Tracing for Windows — ETW)
  • WebPageTraceListener выводит на веб-страницу ASP.NET

Ни один из этих прослушивателе не отображает диалоговое окно при вызове Fail, это делает только DefaultTraceListener.

// Удалить стандартный прослушиватель, очистив коллекцию прослушивателей:

Trace.Listeners.Clear();

// Добавить средство записи в файл trace.txt:

Trace.Listeners.Add (new TextWriterTraceListener («trace.txt»));

// Добавит средство записи в консоль:

System.IO.TextWriter tw = Console.Out;

Trace.Listeners.Add (new TextWriterTraceListener (tw));

// Добавить средство записи в журнал событий Windows:

if (!EventLog.SourceExists («DemoApp»))

  EventLog.CreateEventSource («DemoApp», «Application»);

Trace.Listeners.Add (new EventLogTraceListener («DemoApp»));

В случае журнала событий Windows сообщения, отправляемые с помощью Write, Fail или Assert, записываются как сведения, а сообщения методов TraceWarning и TraceError записываются как предупреждения или ошибки.

Каждый экземпляр TraceListener имеет свойство Filter и TraceFilter, с помощью которых можно управлять, будет ли сообщение записано в этот прослушиватель. Для этого необходимо создать экземпляр классов EventTypeFilter или SourceFilter (производных от TraceFilter) или создать свой класс, наследующий от TraceFilter и переопределить в нем метод ShouldTrace.

В TraceListener также определены свойства IndentLevel и IndentSize для управления отступами и свойство TraceOutputOptions для записи дополнительных данных:

TextWriterTraceListener tl = new TextWriterTraceListener (Console.Out);

tl.TraceOutputOptions = TraceOptions.DateTime | TraceOptions.Callstack;

// Это применяется при использовании метода Trace:

Trace.TraceWarning («Orange alert»);

DiagTest.vshost.exe Warning: 0 : Orange alert

DateTime=20070308T05:57:13.6250000Z

Callstack= at System.Environment.GetStackTrace(Exception e, Boolean

needFileInfo)

at System.Environment.get_StackTrace() at ...

Прослушиватели, которые записывают данные в поток, кэшируются. По этой причине данные не появляются в потоке немедленно, а также поток перед завершением приложения должен быть закрыт, или хотя бы сброшен, чтоб не потерять данные в кэше. Для этой цели классы Trace и Debug содержат статические методы Close и Flush, которые вызывают Close и Flush во всех прослушивателях (а они в свою очередь закрывают или сбрасывают все потоки). Метод Close вызывает метод Flush, закрывает файловые дескрипторы и предотвращает дальнейшую запись.

Классы Trace и Debug также определяют свойство AutoFlush, которое если равно true вызывает Flush после каждого сообщения.

Fail и Assert

Классы Debug и Trace содержат методы Fail и Assert.

Метод Fail отправляет сообщения каждому TraceListener:

Debug.Fail («File data.txt does not exist!»);

Метод Assert вызывает Fail если аргумент типа bool равен false. Это называется созданием утверждения и указывает на ошибку, если оно нарушено. Можно также создать необязательное сообщение об ошибке:

Debug.Assert (File.Exists («data.txt»), «File data.txt does not exist!»);

var result = ...

Debug.Assert (result != null);

Методы Write, Fail и Assert также могут принимать категорию в виде строки ,которая может быть использована при обработке вывода.

One of the advantages of C++ over C is Exception Handling. Exceptions are runtime anomalies or abnormal conditions that a program encounters during its execution. There are two types of exceptions: a)Synchronous, b)Asynchronous (i.e., exceptions which are beyond the program’s control, such as disc failure, keyboard interrupts etc.). C++ provides the following specialized keywords for this purpose:
try: Represents a block of code that can throw an exception.
catch: Represents a block of code that is executed when a particular exception is thrown.
throw: Used to throw an exception. Also used to list the exceptions that a function throws but doesn’t handle itself.

Why Exception Handling? 
The following are the main advantages of exception handling over traditional error handling:

1) Separation of Error Handling code from Normal Code: In traditional error handling codes, there are always if-else conditions to handle errors. These conditions and the code to handle errors get mixed up with the normal flow. This makes the code less readable and maintainable. With try/catch blocks, the code for error handling becomes separate from the normal flow.

2) Functions/Methods can handle only the exceptions they choose: A function can throw many exceptions, but may choose to handle some of them. The other exceptions, which are thrown but not caught, can be handled by the caller. If the caller chooses not to catch them, then the exceptions are handled by the caller of the caller. 
In C++, a function can specify the exceptions that it throws using the throw keyword. The caller of this function must handle the exception in some way (either by specifying it again or catching it).

3) Grouping of Error Types: In C++, both basic types and objects can be thrown as exceptions. We can create a hierarchy of exception objects, group exceptions in namespaces or classes and categorize them according to their types.
 

C++ Exceptions:

When executing C++ code, different errors can occur: coding errors made by the programmer, errors due to wrong input, or other unforeseeable things.

When an error occurs, C++ will normally stop and generate an error message. The technical term for this is: C++ will throw an exception (error).

C++ try and catch:

Exception handling in C++ consists of three keywords: try, throw and catch:

The try statement allows you to define a block of code to be tested for errors while it is being executed.

The throw keyword throws an exception when a problem is detected, which lets us create a custom error.

The catch statement allows you to define a block of code to be executed if an error occurs in the try block.

The try and catch keywords come in pairs:

We use the try block to test some code: If the value of a variable “age” is less than 18, we will throw an exception, and handle it in our catch block.

In the catch block, we catch the error if it occurs and do something about it. The catch statement takes a single parameter. So, if the value of age is 15 and that’s why we are throwing an exception of type int in the try block (age), we can pass “int myNum” as the parameter to the catch statement, where the variable “myNum” is used to output the value of age.

If no error occurs (e.g. if age is 20 instead of 15, meaning it will be greater than 18), the catch block is skipped.

Exception Handling in C++

1) The following is a simple example to show exception handling in C++. The output of the program explains the flow of execution of try/catch blocks. 

CPP

#include <iostream>

using namespace std;

int main()

{

   int x = -1;

   cout << "Before try n";

   try {

      cout << "Inside try n";

      if (x < 0)

      {

         throw x;

         cout << "After throw (Never executed) n";

      }

   }

   catch (int x ) {

      cout << "Exception Caught n";

   }

   cout << "After catch (Will be executed) n";

   return 0;

}

Output: 

Before try
Inside try
Exception Caught
After catch (Will be executed)

2) There is a special catch block called the ‘catch all’ block, written as catch(…), that can be used to catch all types of exceptions. For example, in the following program, an int is thrown as an exception, but there is no catch block for int, so the catch(…) block will be executed. 

CPP

#include <iostream>

using namespace std;

int main()

{

    try  {

       throw 10;

    }

    catch (char *excp)  {

        cout << "Caught " << excp;

    }

    catch (...)  {

        cout << "Default Exceptionn";

    }

    return 0;

}

Output: 

Default Exception

3) Implicit type conversion doesn’t happen for primitive types. For example, in the following program, ‘a’ is not implicitly converted to int. 

CPP

#include <iostream>

using namespace std;

int main()

{

    try  {

       throw 'a';

    }

    catch (int x)  {

        cout << "Caught " << x;

    }

    catch (...)  {

        cout << "Default Exceptionn";

    }

    return 0;

}

Output: 

Default Exception

4) If an exception is thrown and not caught anywhere, the program terminates abnormally. For example, in the following program, a char is thrown, but there is no catch block to catch the char.  

CPP

#include <iostream>

using namespace std;

int main()

{

    try  {

       throw 'a';

    }

    catch (int x)  {

        cout << "Caught ";

    }

    return 0;

}

Output: 

terminate called after throwing an instance of 'char'

This application has requested the Runtime to terminate it in an 
unusual way. Please contact the application's support team for 
more information.

We can change this abnormal termination behavior by writing our own unexpected function.
5) A derived class exception should be caught before a base class exception. See this for more details.
6) Like Java, the C++ library has a standard exception class which is the base class for all standard exceptions. All objects thrown by the components of the standard library are derived from this class. Therefore, all standard exceptions can be caught by catching this type
7) Unlike Java, in C++, all exceptions are unchecked, i.e., the compiler doesn’t check whether an exception is caught or not (See this for details). So, it is not necessary to specify all uncaught exceptions in a function declaration. Although it’s a recommended practice to do so. For example, the following program compiles fine, but ideally the signature of fun() should list the unchecked exceptions. 

CPP

#include <iostream>

using namespace std;

void fun(int *ptr, int x)

{

    if (ptr == NULL)

        throw ptr;

    if (x == 0)

        throw x;

}

int main()

{

    try {

       fun(NULL, 0);

    }

    catch(...) {

        cout << "Caught exception from fun()";

    }

    return 0;

}

Output: 

Caught exception from fun()

A better way to write the above code: 

CPP

#include <iostream>

using namespace std;

void fun(int *ptr, int x) throw (int *, int)

{

    if (ptr == NULL)

        throw ptr;

    if (x == 0)

        throw x;

}

int main()

{

    try {

       fun(NULL, 0);

    }

    catch(...) {

        cout << "Caught exception from fun()";

    }

    return 0;

}

Note : The use of Dynamic Exception Specification has been deprecated since C++11. One of the reasons for it may be that it can randomly abort your program. This can happen when you throw an exception of another type which is not mentioned in the dynamic exception specification. Your program will abort itself because in that scenario, it calls (indirectly) terminate(), which by default calls abort().

Output: 

Caught exception from fun()

8) In C++, try/catch blocks can be nested. Also, an exception can be re-thrown using “throw; “. 

CPP

#include <iostream>

using namespace std;

int main()

{

    try {

        try {

            throw 20;

        }

        catch (int n) {

            cout << "Handle Partially ";

            throw;

        }

    }

    catch (int n) {

        cout << "Handle remaining ";

    }

    return 0;

}

Output: 

Handle Partially Handle remaining

A function can also re-throw a function using the same “throw; ” syntax. A function can handle a part and ask the caller to handle the remaining.
9) When an exception is thrown, all objects created inside the enclosing try block are destroyed before the control is transferred to the catch block.

CPP

#include <iostream>

using namespace std;

class Test {

public:

    Test() { cout << "Constructor of Test " << endl; }

    ~Test() { cout << "Destructor of Test " << endl; }

};

int main()

{

    try {

        Test t1;

        throw 10;

    }

    catch (int i) {

        cout << "Caught " << i << endl;

    }

}

Output: 

Constructor of Test
Destructor of Test
Caught 10

10) You may like to try Quiz on Exception Handling in C++.
Please write comments if you find anything incorrect, or you want to share more information about the topic discussed above.
 

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

Конструкция try..catch..finally

Последнее обновление: 30.12.2021

Иногда при выполнении программы возникают ошибки, которые трудно предусмотреть или предвидеть, а иногда и вовсе невозможно. Например, при передачи файла по сети может неожиданно оборваться сетевое подключение.
такие ситуации называются исключениями. Язык C# предоставляет разработчикам возможности для обработки таких ситуаций. Для этого
в C# предназначена конструкция try…catch…finally.

try
{
	
}
catch
{
	
}
finally
{
	
}

При использовании блока try…catch..finally вначале выполняются все инструкции в блоке try. Если в
этом блоке не возникло исключений, то после его выполнения начинает выполняться блок finally. И затем конструкция try..catch..finally
завершает свою работу.

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

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

Рассмотрим следующий пример:

int x = 5;
int y = x / 0;
Console.WriteLine($"Результат: {y}");
Console.WriteLine("Конец программы");

В данном случае происходит деление числа на 0, что приведет к генерации исключения. И при запуске приложения в
режиме отладки мы увидим в Visual Studio окошко, которое информирует об исключении:

Исключения в C#

В этом окошке мы видим, что возникло исключение, которое представляет тип System.DivideByZeroException,
то есть попытка деления на ноль. С помощью пункта View Details можно посмотреть более детальную информацию об исключении.

И в этом случае единственное, что нам остается, это завершить выполнение программы.

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

try
{
	int x = 5;
	int y = x / 0;
	Console.WriteLine($"Результат: {y}");
}
catch
{
	Console.WriteLine("Возникло исключение!");
}
finally
{
	Console.WriteLine("Блок finally");
}
Console.WriteLine("Конец программы");

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

int y = x / 0;

выполнение программы остановится. CLR найдет блок catch и передаст управление этому блоку.

После блока catch будет выполняться блок finally.

Возникло исключение!
Блок finally
Конец программы

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

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

try
{
	int x = 5;
	int y = x / 0;
	Console.WriteLine($"Результат: {y}");
}
catch
{
	Console.WriteLine("Возникло исключение!");
}

И, наоборот, при наличии блока finally мы можем опустить блок catch и не обрабатывать исключение:

try
{
	int x = 5;
	int y = x / 0;
	Console.WriteLine($"Результат: {y}");
}
finally
{
	Console.WriteLine("Блок finally");
}

Однако, хотя с точки зрения синтаксиса C# такая конструкция вполне корректна, тем не менее, поскольку CLR не сможет найти нужный блок
catch, то исключение не будет обработано, и программа аварийно завершится.

Обработка исключений и условные конструкции

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

Square("12"); // Квадрат числа 12: 144
Square("ab"); // !Исключение

void Square(string data)
{
    int x = int.Parse(data);
    Console.WriteLine($"Квадрат числа {x}: {x * x}");
}

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

Square("12"); // Квадрат числа 12: 144
Square("ab"); // Некорректный ввод

void Square(string data)
{
    if (int.TryParse(data, out var x))
    {
        Console.WriteLine($"Квадрат числа {x}: {x * x}");
    }
    else
    {
        Console.WriteLine("Некорректный ввод");
    }
}

Метод int.TryParse() возвращает true, если преобразование можно осуществить, и false — если нельзя. При допустимости преобразования переменная x
будет содержать введенное число. Так, не используя try...catch можно обработать возможную исключительную ситуацию.

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

На 32-м уроке по C# мы начнем обзор обработки исключений. При возникновении непредвиденного события необработанные исключения приводят к аварийному завершению работы программы. Правильная обработка исключений позволяет выйти из ошибки без поломки программы или даже сеанса системы.

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

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

Иерархия классов исключений

При возникновении исключения нормальная обработка кода прекращается. Создается объект, содержащий информацию, относящуюся к ошибке. Этот объект является экземпляром класса исключений. Самым основным из классов исключений является Exception, определенный в пространстве имен System.

Классы исключений организованы в иерархию с исключением наверху. Под классом исключения находятся SystemException и ApplicationException. Класс SystemException имеет дополнительные производные подклассы, каждый из которых представляет определенный тип исключения, который может быть вызван в ответ на системную ошибку. Класс ApplicationException является базовым классом, из которого могут быть получены пользовательские исключения приложения.

Примечание: подкласс и базовый класс-это термины объектно-ориентированного программирования.

Системные исключения

Существует множество системных исключений, которые могут возникать или выбрасываться при возникновении проблемы. Они вызывают создание объекта на основе его собственного специализированного класса исключений, производного от SystemException. Примеры включают в себя:

  • ArithmeticException. Выбрасывается при возникновении ошибки во время арифметической операции, приведения или преобразования.
  • DivideByZeroException. Возникает при попытке разделить значение на ноль. Является более специализированной версией исключения выше.
  • OverflowException. Выбрасывается, когда ошибка возникает во время арифметической операции или во время литья или преобразования, потому что результирующее значение слишком велико или мало. Исключение OverflowException является производным от исключения ArithmeticException.
  • OutOfMemoryException. Выбрасывается, когда доступной памяти недостаточно для продолжения выполнения.

Приведенный выше список дает небольшую выборку классов исключений в иерархии. Гораздо более полный список можно найти на странице иерархии системных исключений веб-сайта Microsoft MSDN.

Исключения приложений

Исключения приложений — это те, которые вы определяете. Они могут быть универсальными, например WordProcessorException, или специализированными, например LeftMarginTooSmallException. Конечно, ни одно из этих исключений не существует в .NET framework; они должны быть созданы перед использованием. Исключения приложений не будут обсуждаться до следующей статьи, в которой исследуются программные исключения.

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

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

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

Основной блок Try / Catch

C# предоставляет структуру кода, известную как блок try / catch, которая позволяет обрабатывать исключения. Основной блок try / catch имеет два элемента. Раздел try содержит одну или несколько выполняемых команд, удерживаемых в символах фигурной скобки { и }. Раздел catch содержит код для выполнения, если во время обработки раздела try происходит исключение. Основной синтаксис выглядит следующим образом:

try
{
    // команды, выполняющиеся в обычном режиме
}
catch
{
    // команды для выполнения в случае возникновения исключения
}

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

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

static void Main(string[] args)
{
    int value = 50;
    int divisor = 0;
    int calculated;
 
    try
    {
        calculated = value / divisor;
    }
    catch
    {
        Console.WriteLine("Произошла ошибка во время деления.");
        calculated = int.MaxValue;
    }
 
    Console.WriteLine("Result = {0}", calculated);
}
 
/* OUTPUT
 
Произошла ошибка во время деления 
Result = 2147483647
 
*/

Извлечение информации об исключении

Как написано выше, создание исключения включает в себя создание объекта исключения. Объект имеет свойства, содержащие информацию, описывающую ошибку. Это может быть прочитано кодом в блоке catch путем добавления объявления исключения к оператору catch. Следующий пример расширяет предыдущий код, объявляя объект наиболее обобщенного класса исключений. Все исключения будут пойманы, но теперь информация об исключении может быть использована при создании сообщения об ошибке.

static void Main(string[] args)
{
    int value = 50;
    int divisor = 0;
    int calculated;
 
    try
    {
        calculated = value / divisor;
    }
    catch (Exception ex)                        // Ловим
    {
        Console.WriteLine("Произошла ошибка во время деления.");
        Console.WriteLine(ex.Message);          // Вывод
        calculated = int.MaxValue;
    }
 
    Console.WriteLine("Result = {0}", calculated);
}
 
/* OUTPUT
 
Произошла ошибка во время деления 
Attempted to divide by zero.
Result = 2147483647
 
*/

Приведенный выше пример улавливает любое исключение и заполняет объект класса Exception . Свойство объекта Message используется для вывода описания ошибки. Это одно из нескольких свойств, предоставляемых всеми классами исключений. Некоторые из самых полезных свойств являются:

  • Message. Строка, содержащая описание исключения.
  • Source. Строка, содержащая имя программы или объекта, вызвавшего исключение.
  • TargetSite. Объект, содержащий сведения о методе, вызвавшем исключение.
  • StackTrace. Строка, содержащая полный стек вызовов, которые привели к исключению. Эта строка позволяет программисту просматривать каждый вызов метода, выполненный до возникновения исключения. Это особенно полезно во время тестирования и отладки.
  • InnerException. Когда одно исключение возникает как прямой результат другого, начальное исключение может содержаться в этом свойстве. Внутреннее исключение содержит все стандартные свойства, включая, возможно,еще одно внутреннее исключение. Если нет внутреннего исключения, это свойство имеет значение null.

Примечание: более специализированные типы исключений включают дополнительную релевантную информацию. Например, исключение ArgumentException включает свойство ParamName, детализирующее рассматриваемый параметр.

static void Main(string[] args)
{
    int value = 50;
    int divisor = 0;
    int calculated;
 
    try
    {
        calculated = value / divisor;
    }
    catch (Exception ex)
    {
        Console.WriteLine("Сообщение:    {0}n", ex.Message);
        Console.WriteLine("Источник:     {0}n", ex.Source);
        Console.WriteLine("Метод: {0}n", ex.TargetSite.Name);
        Console.WriteLine("StackTrace: {0}n", ex.StackTrace);
 
        calculated = int.MaxValue;
    }
 
    Console.WriteLine("Result = {0}", calculated);
}
 
/* Вывод
 
Сообщение:    Attempted to divide by zero.
 
Источник:     ConsoleApplication1
 
Метод: Void Main(System.String[])
 
StackTrace: at ConsoleApplication1.Program.Main(String[] args) in
            C:...Program.cs:line 17
 
Result = 2147483647
 
*/

Как ловить конкретные исключения

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

static void Main(string[] args)
{
    int value = 50;
    int divisor = 0;
    int calculated;
 
    try
    {
        calculated = value / divisor;
    }
    catch (DivideByZeroException ex)     // поймать только конкретное исключение
    {
        Console.WriteLine("произошло деление на ноль.");
        Console.WriteLine(ex.Message);          // сообщить об ошибке
        calculated = int.MaxValue;
    }
 
    Console.WriteLine("Result = {0}", calculated);
}
 
/* OUTPUT
 
произошло деление на ноль.
Attempted to divide by zero.
Result = 2147483647
 
*/

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

Примечание: если достаточно поймать тип исключения и нет необходимости опрашивать свойства исключения, то нет необходимости включать имя переменной для объекта исключения. Catch в приведенном выше примере может быть сокращен до catch (DivideByZeroException) в такой ситуации.

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

При разработке сложных процедур возможно возникновение различных типов исключений. Каждое из этих исключений может потребовать обработки по-разному. Чтобы разрешить это, несколько блоков catch могут быть добавлены к одному блоку try. Каждый блок catch обрабатывает различные типы исключений, причем наиболее конкретные типы обрабатываются первыми, а наиболее общие исключения — последними.

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

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

static void Main(string[] args)
{
    int value = 50;
    int divisor = 0;
    int calculated;
 
    try
    {
        calculated = value / divisor;
    }
    catch (DivideByZeroException)            
    {
        Console.WriteLine("Division by zero occurred.");
        calculated = int.MaxValue;
    }
    catch (ArithmeticException)                 
    {
        Console.WriteLine("An arithmetic exception occurred.");
        calculated = int.MaxValue;
    }
    catch
    {
        Console.WriteLine("An unexpected exception occurred.");
        calculated = int.MaxValue;
    }
 
    Console.WriteLine("Result = {0}", calculated);
}

Блок Try / Catch / Finally

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

C# определяет блок добавления, который может быть добавлен в конец структуры try / catch. Это последний блок. Код в этом разделе гарантированно выполняется после блока try / catch, даже если возникает исключение или блок try содержит оператор return.

static void Main(string[] args)
{
    int value = 50;
    int divisor = 0;
    int calculated;
 
    try
    {
        calculated = value / divisor;
    }
    catch
    {
        Console.WriteLine("An error occurred during division.");
        calculated = int.MaxValue;
    }
    finally
    {
        Console.WriteLine("Clearing up any resources.");
    }
 
    Console.WriteLine("Result = {0}", calculated);
}

Можно попробовать использовать Finally вместе с блоками catch. Если при использовании такой структуры возникает исключение, оно остается необработанным и передается вызывающей подпрограмме или системе времени выполнения C#. Однако код в блоке finally выполняется независимо от того, вызвано исключение или нет.


Автор этого материала — я — Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML — то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегистатьи IT, уроки по си шарп, си шарп, исключения, ошибки

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

  • Урок 30. Условные операторы C#

Associates one or more exception handlers (catch-clauses) with a compound statement.

[edit] Syntax

try compound-statement handler-sequence

where handler-sequence is a sequence of one or more handlers, which have the following syntax:

catch ( attr (optional) type-specifier-seq declarator ) compound-statement (1)
catch ( attr (optional) type-specifier-seq abstract-declarator (optional) ) compound-statement (2)
catch ( ... ) compound-statement (3)
compound-statement brace-enclosed sequence of statements
attr (since C++11) any number of attributes, applies to the formal parameter
type-specifier-seq part of a formal parameter declaration, same as in a function parameter list
declarator part of a formal parameter declaration, same as in a function parameter list
abstract-declarator part of an unnamed formal parameter declaration, same as in function parameter list

1) Catch-clause that declares a named formal parameter

2) Catch-clause that declares an unnamed parameter

3) Catch-all handler, which is activated for any exception

try { /* */ } catch (...) { /* */ }

[edit] Explanation

See throw exceptions for more information about throw-expressions

A try-block is a statement, and as such, can appear anywhere a statement can appear (that is, as one of the statements in a compound statement, including the function body compound statement). See function-try-block for the try blocks around function bodies. The following description applies to both try-blocks and function-try-blocks.

The formal parameter of the catch clause (type-specifier-seq and declarator or type-specifier-seq and abstract-declarator) determines which types of exceptions cause this catch clause to be entered. It cannot be an incomplete type, abstract class type, rvalue reference type, (since C++11) or pointer to incomplete type (except that pointers to (possibly cv-qualified) void are allowed). If the type of the formal parameter is array type or function type, it is treated as the corresponding pointer type (similar to a function declaration).

When an exception is thrown by any statement in compound-statement, the exception object of type E is matched against the types of the formal parameters T of each catch-clause in handler-seq, in the order in which the catch clauses are listed. The exception is a match if any of the following is true:

  • E and T are the same type (ignoring top-level cv-qualifiers on T)
  • T is an lvalue-reference to (possibly cv-qualified) E
  • T is an unambiguous public base class of E
  • T is a reference to an unambiguous public base class of E
  • T is (possibly cv-qualified) U or const U& (since C++14), and U is a pointer or pointer to member type, and E is also a pointer or pointer to member type that is implicitly convertible to U by one or more of
  • a standard pointer conversion other than one to a private, protected, or ambiguous base class
  • a qualification conversion
  • T is a pointer or a pointer to member or a reference to a const pointer (since C++14), while E is std::nullptr_t.
(since C++11)
try
{
    f();
}
catch (const std::overflow_error& e)
{} // this executes if f() throws std::overflow_error (same type rule)
catch (const std::runtime_error& e)
{} // this executes if f() throws std::underflow_error (base class rule)
catch (const std::exception& e)
{} // this executes if f() throws std::logic_error (base class rule)
catch (...)
{} // this executes if f() throws std::string or int or any other unrelated type

The catch-all clause catch () matches exceptions of any type. If present, it has to be the last catch clause in the handler-seq. Catch-all block may be used to ensure that no uncaught exceptions can possibly escape from a function that offers nothrow exception guarantee.

If no matches are found after all catch-clauses were examined, the exception propagation continues to the containing try-block, as described in throw-expression. If there are no containing try-blocks left, std::terminate is executed (in this case, it is implementation-defined whether any stack unwinding occurs at all: throwing an uncaught exception is permitted to terminate the program without invoking any destructors).

When entering a catch clause, if its formal parameter is a base class of the exception type, it is copy-initialized from the base class subobject of the exception object. Otherwise, it is copy-initialized from the exception object (this copy is subject to copy elision).

try
{
    std::string("abc").substr(10); // throws std::length_error
}
// catch (std::exception e) // copy-initialization from the std::exception base
// {
//     std::cout << e.what(); // information from length_error is lost
// }
catch (const std::exception& e) // reference to the base of a polymorphic object
{
    std::cout << e.what(); // information from length_error printed
}

If the parameter of the catch-clause is a reference type, any changes made to it are reflected in the exception object, and can be observed by another handler if the exception is rethrown with throw;. If the parameter is not a reference, any changes made to it are local and its lifetime ends when the handler exits.

A goto or switch statement shall not be used to transfer control into a try block or into a handler.

Other than by throwing or rethrowing the exception, the catch-clause after a regular try block (not function-try-block) may be exited with a return, continue, break, goto, or by reaching the end of its compound-statement. In any case, this destroys the exception object (unless an instance of std::exception_ptr exists that refers to it).

[edit] Notes

The throw-expression throw NULL; is not guaranteed to be matched by a pointer catch clause, because the exception object type may be int, but throw nullptr; is assuredly matched by any pointer or pointer-to-member catch clause.

If a catch-clause for a derived class is placed after the catch-clause for a base class, the derived catch-clause will never be executed:

If goto is used to exit a try-block and if any of the destructors of block-scoped automatic variables that are executed by the goto throw exceptions, those exceptions are caught by the try blocks in which the variables are defined:

label:
    try
    {
        T1 t1;
        try
        {
            T2 t2;
            if (condition)
                goto label; // destroys t2, then destroys t1, then jumps to label
        }
        catch (...) {} // catches the exception from the destructor of t2
    }
    catch (...) {}     // catches the exception from the destructor of t1

[edit] Keywords

try,
catch,
throw

[edit] Example

The following example demonstrates several usage cases of the try-catch block

#include <iostream>
#include <vector>
 
int main()
{
    try
    {
        std::cout << "Throwing an integer exception...n";
        throw 42;
    }
    catch (int i)
    {
        std::cout << " the integer exception was caught, with value: " << i << 'n';
    }
 
    try
    {
        std::cout << "Creating a vector of size 5... n";
        std::vector<int> v(5);
        std::cout << "Accessing the 11th element of the vector...n";
        std::cout << v.at(10); // vector::at() throws std::out_of_range
    }
    catch (const std::exception& e) // caught by reference to base
    {
        std::cout << " a standard exception was caught, with message '"
                  << e.what() << "'n";
    }
}

Possible output:

Throwing an integer exception...
 the integer exception was caught, with value: 42
Creating a vector of size 5...
Accessing the 11th element of the vector...
 a standard exception was caught, with message 'out_of_range'

[edit] Defect reports

The following behavior-changing defect reports were applied retroactively to previously published C++ standards.

DR Applied to Behavior as published Correct behavior
CWG 98 C++98 a switch statement can transfer control
into a try block or into a handler
prohibited
CWG 210 C++98 the throw expression was matched against the catch clauses the exception object is matched
against the catch clauses
CWG 1166 C++98 the behavior was unspecified when a catch clause whose
exception type is a reference to an abstract class type is matched
abstract class types are not
allowed for catch clauses
CWG 1769 C++98 when the type of the exception declared in the catch-clause is a
base of the type of the exception object, a converting constructor
might be used for the initialization of the catch-clause parameter
the parameter is copy-initialized
from the corresponding base class
subobject of the exception object
CWG 2093 C++98 an exception object of pointer to object type could not match a
handler of pointer to object type through qualification conversion
allowed

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

11.1. Возбуждение исключения

Исключение – это аномальное поведение во время выполнения,
которое программа может обнаружить, например: деление на 0, выход
за границы массива или истощение свободной памяти. Такие исключения
нарушают нормальный ход работы программы, и на них нужно немедленно
отреагировать. В C++ имеются встроенные средства для их возбуждения и
обработки. С помощью этих средств активизируется механизм, позволяющий
двум несвязанным (или независимо разработанным) фрагментам программы
обмениваться информацией об исключении.

Когда встречается аномальная ситуация, та часть программы,
которая ее обнаружила, может сгенерировать, или возбудить,
исключение. Чтобы понять, как это происходит, реализуем по-новому
класс iStack, представленный в разделе 4.15, используя исключения для
извещения об ошибках при работе со стеком. Определение класса

#include
class iStack {
public:
iStack( int capacity )
: _stack( capacity ), _top( 0 ) { }

bool pop( int &top_value );
bool push( int value );

bool full();
bool empty();
void display();

int size();
private:
int _top;
vector< int > _stack;

iStack выглядит следующим образом:

};

Стек реализован на основе вектора из элементов типа int.
При создании объекта класса iStack его конструктор создает вектор из int,
размер которого (максимальное число элементов, хранящихся в стеке) задается с
помощью начального значения. Например, следующая инструкция создает объект myStack,
который способен содержать не более 20 элементов типа int:

iStack myStack(20);

При манипуляциях с объектом myStack могут возникнуть две ошибки:

  • запрашивается операция pop(), но стек пуст;
  • запрашивается операция push(), но стек полон.

Вызвавшую функцию нужно уведомить об этих ошибках посредством исключений. С
чего же начать?

Во-первых, мы должны определить, какие именно исключения могут быть возбуждены.
В C++ они чаще всего реализуются с помощью классов. Хотя в полном объеме классы
будут представлены в главе 13, мы все же определим здесь два из них, чтобы использовать
их как исключения для класса iStack. Эти определения мы поместим в заголовочный файл stackExcp.h:

// stackExcp.h
class popOnEmpty { /* ... */ };
class pushOnFull { /* ... */ };

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

Затем надо изменить определения функций-членов pop() и push() так, чтобы они
возбуждали эти исключения. Для этого предназначена инструкция throw,
которая во многих отношениях напоминает return. Она состоит из ключевого слова
throw, за которым следует выражение того же типа, что и тип возбуждаемого исключения.
Как выглядит инструкция throw для функции pop()? Попробуем такой вариант:

// увы, это не совсем правильно
throw popOnEmpty;

К сожалению, так нельзя. Исключение – это объект,
и функция pop() должна генерировать объект класса соответствующего типа.
Выражение в инструкции throw не может быть просто типом. Для создания нужного
объекта необходимо вызвать конструктор класса. Инструкция throw для функции pop()
будет выглядеть так:

// инструкция является вызовом конструктора
throw popOnEmpty();

Эта инструкция создает объект исключения типа popOnEmpty.

Напомним, что функции-члены pop() и push() были определены как
возвращающие значение типа bool: true означало, что операция завершилась
успешно, а false – что произошла ошибка. Поскольку теперь для извещения о
неудаче pop() и push() используют исключения, возвращать значение необязательно.
Поэтому мы будем считать, что эти функции-члены имеют тип void:

class iStack {
public:
// ...
// больше не возвращают значения
void pop( int &value );
void push( int value );
private:
// ...
};

Теперь функции, пользующиеся нашим классом iStack,
будут предполагать, что все хорошо, если только не возбуждено исключение;
им больше не надо проверять возвращенное значение, чтобы узнать, как завершилась
операция. В двух следующих разделах мы покажем, как определить функцию для обработки
исключений, а сейчас представим новые реализации функций-членов pop() и push() класса iStack:

#include "stackExcp.h"
void iStack::pop( int &top_value )
{
if ( empty() )
throw popOnEmpty();

top_value = _stack[ --_top ];

cout << "iStack::pop(): "<<top_value " endl;
}
void iStack::push( int value )
{
cout << "iStack::push( "<< value << " )n";

if ( full() )
throw pushOnFull( value );

_stack[ _top++ ] = value;
}

Хотя исключения чаще всего представляют собой объекты типа класса,
инструкция throw может генерировать объекты любого типа. Например, функция
mathFunc() в следующем примере возбуждает исключение в виде объекта-перечисления .
Это корректный код C++:

enum EHstate { noErr, zeroOp, negativeOp, severeError };

int mathFunc( int i ) {
if ( i == 0 )
throw zeroOp; // исключение в виде объекта-перечисления

// в противном случае продолжается нормальная обработка
}

Упражнение 11.1

Какие из приведенных инструкций throw ошибочны? Почему?
Для правильных инструкций укажите тип возбужденного исключения:

(a) class exceptionType { };
throw exceptionType();
(b) int excpObj;
throw excpObj;
(c) enum mathErr { overflow, underflow, zeroDivide };
throw mathErr zeroDivide();
(d) int *pi = excpObj;
throw pi;

Упражнение 11.2

У класса IntArray, определенного в разделе 2.3,
имеется функция-оператор operator[](), в которой используется assert()
для извещения о том, что индекс вышел за пределы массива. Измените определение
этого оператора так, чтобы в подобной ситуации он генерировал исключение.
Определите класс, который будет употребляться как тип возбужденного исключения.

11.2. Try-блок

В нашей программе тестируется определенный в предыдущем разделе
класс iStack и его функции-члены pop() и push(). Выполняется 50
итераций цикла for. На каждой итерации в стек помещается значение,
кратное 3: 3, 6, 9 и т.д. Если значение кратно 4 (4, 8, 12…), то
выводится текущее содержимое стека, а если кратно 10 (10, 20, 30…),
то с вершины снимается один элемент, после чего содержимое стека выводится
снова. Как нужно изменить функцию main(), чтобы она обрабатывала исключения,
возбуждаемые функциями-членами класса iStack?

#include
#include "iStack.h"

int main() {
iStack stack( 32 );

stack.display();
for ( int ix = 1; ix < 51; ++ix )
{
if ( ix % 3 == 0 )
stack.push( ix );

if ( ix % 4 == 0 )
stack.display();

if ( ix % 10 == 0 ) {
int dummy;
stack.pop( dummy );
stack.display();
}
}
return 0;
}

Инструкции, которые могут возбуждать исключения,
должны быть заключены в try-блок. Такой блок начинается с ключевого
слова try, за которым идет последовательность инструкций, заключенная в
фигурные скобки, а после этого – список обработчиков, называемых catch-предложениями.
Try-блок группирует инструкции программы и ассоциирует с ними обработчики исключений.
Куда нужно поместить try-блоки в функции main(), чтобы были обработаны исключения
popOnEmpty и pushOnFull?

for ( int ix = 1; ix < 51; ++ix ) {
try { // try-блок для исключений pushOnFull
if ( ix % 3 == 0 )
stack.push( ix );
}
catch ( pusOnFull ) { ... }

if ( ix % 4 == 0 )
stack.display();

try { // try-блок для исключений popOnEmpty
if ( ix % 10 == 0 ) {
int dummy;
stack.pop( dummy );
stack.display();
}
}
catch ( popOnEmpty ) { ... }
}

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

try {
for ( int ix = 1; ix < 51; ++ix )
{
if ( ix % 3 == 0 )
stack.push( ix );

if ( ix % 4 == 0 )
stack.display();

if ( ix % 10 == 0 ) {
int dummy;
stack.pop( dummy );
stack.display();
}
}
}
catch ( pushOnFull ) { ... }
catch ( popOnEmpty ) { ... }

С try-блоком ассоциированы два catch-предложения, которые
могут обработать исключения pushOnFull и popOnEmpty, возбуждаемые
функциями-членами push() и pop() внутри этого блока. Каждый catch-обработчик
определяет тип «своего» исключения. Код для обработки исключения помещается
внутрь составной инструкции (между фигурными скобками), которая является частью
catch-обработчика. (Подробнее catch-предложения мы рассмотрим в следующем разделе.)

Исполнение программы может пойти по одному из следующих путей:

  • если исключение не возбуждено, то выполняется код внутри try-блока, а ассоциированные
    с ним обработчики игнорируются. Функция main() возвращает 0;
  • если функция-член push(), вызванная из первой инструкции if внутри цикла for,
    возбуждает исключение, то вторая и третья инструкции if игнорируются,
    управление покидает цикл for и try-блок, и выполняется обработчик исключений
    типа pushOnFull;
  • если функция-член pop(), вызванная из третьей инструкции if внутри
    цикла for, возбуждает исключение, то вызов display() игнорируется,
    управление покидает цикл for и try-блок, и выполняется обработчик исключений
    типа popOnEmpty.

Когда возбуждается исключение, пропускаются все инструкции,
следующие за той, где оно было возбуждено. Исполнение программы
возобновляется в catch-обработчике этого исключения. Если такого
обработчика не существует, то управление передается в функцию terminate(),
определенную в стандартной библиотеке C++.
Try-блок может содержать любую инструкцию языка
C++: как выражения, так и объявления. Он вводит локальную
область видимости, так что объявленные внутри него переменные
недоступны вне этого блока, в том числе и в catch-обработчиках.
Например, функцию main() можно переписать так, что объявление переменной
stack окажется в try-блоке. В таком случае обращаться к этой переменной в
catch-обработчиках нельзя:

int main() {
try {
iStack stack( 32 ); // правильно: объявление внутри try-блока

stack.display();
for ( int ix = 1; ix < 51; ++ix )
{
// то же, что и раньше
}
}
catch ( pushOnFull ) {
// здесь к переменной stack обращаться нельзя
}
catch ( popOnEmpty ) {
// здесь к переменной stack обращаться нельзя
}

// и здесь к переменной stack обращаться нельзя
return 0;
}

Можно объявить функцию так, что все ее тело
будет заключено в try-блок. При этом не обязательно
помещать try-блок внутрь определения функции, удобнее
заключить ее тело в функциональный try-блок. Такая организация
поддерживает наиболее чистое разделение кода для нормальной обработки
и кода для обработки исключений. Например:

int main()
try {
iStack stack( 32 ); // правильно: объявление внутри try-блока

stack.display();
for ( int ix = 1; ix < 51; ++ix )
{
// то же, что и раньше
}

return 0;
}
catch ( pushOnFull ) {
// здесь к переменной stack обращаться нельзя
}
catch ( popOnEmpty ) {
// здесь к переменной stack обращаться нельзя
}

Обратите внимание, что ключевое слово
try находится перед фигурной скобкой, открывающей
тело функции, а catch-обработчики перечислены после
закрывающей его скобки. Как видим, код, осуществляющий
нормальную обработку, находится внутри тела функции и четко
отделен от кода для обработки исключений. Однако к переменным,
объявленным в main(), нельзя обратиться из обработчиков исключений.

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

Упражнение 11.3

Напишите программу, которая определяет объект
IntArray (тип класса IntArray рассматривался в разделеa 2.3)
и выполняет описанные ниже действия.

Пусть есть три файла, содержащие целые числа.

  1. Прочитать первый файл и поместить в объект IntArray первое, третье,
    пятое, …, n-ое значение (где n нечетно). Затем вывести содержимое
    объекта IntArray.
  2. Прочитать второй файл и поместить в объект IntArray пятое, десятое,
    …, n-ое значение (где n кратно 5). Вывести содержимое объекта.
  3. Прочитать третий файл и поместить в объект IntArray второе,
    четвертое, …, n-ое значение (где n четно). Вывести содержимое объекта.

Воспользуйтесь оператором operator[]() класса
IntArray, определенным в упражнении 11.2, для сохранения
и получения значений из объекта IntArray. Так как operator[]()
может возбуждать исключения, обработайте их, поместив необходимое
количество try-блоков и catch-обработчиков. Объясните, почему вы разместили
try-блоки именно так, а не иначе.

11.3. Перехват исключений

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

Catch-обработчик состоит из трех частей: ключевого слова
catch, объявления одного типа или одного объекта, заключенного
в круглые скобки (оно называется объявлением исключения), и составной
инструкции. Если для обработки исключения выбрано некоторое catch-предложение,
то выполняется эта составная инструкция. Рассмотрим catch-обработчики исключений
pushOnFull и popOnEmpty в функции main() более подробно:

catch ( pushOnFull ) {
cerr << "trying to push value on a full stackn";
return errorCode88;
}
catch ( popOnEmpty ) {
cerr << "trying to pop a value on an empty stackn";
return errorCode89;
}

В обоих catch-обработчиках есть
объявление типа класса; в первом это pushOnFull, а
во втором – popOnEmpty. Для обработки исключения выбирается тот
обработчик, для которого типы в объявлении исключения и в возбужденном
исключении совпадают. (В главе 19 мы увидим, что типы не обязаны совпадать точно:
обработчик для базового класса подходит и для исключений с производными классами.)
Например, когда функция-член pop() класса iStack возбуждает исключение popOnEmpty, то
управление попадает во второй обработчик. После вывода сообщения об ошибке в cerr, функция
main() возвращает код errorCode89.

А если catch-обработчики не содержат инструкции return,
с какого места будет продолжено выполнение программы? После
завершения обработчика выполнение возобновляется с инструкции,
идущей за последним catch-обработчиком в списке. В нашем примере
оно продолжается с инструкции return в функции main(). После того
как catch-обработчик popOnEmpty выведет сообщение об ошибке, main() вернет 0.

int main() {
iStack stack( 32 );

try {
stack.display();
for ( int x = 1; ix < 51; ++ix )
{
// то же, что и раньше
}
}
catch ( pushOnFull ) {
cerr << "trying to push value on a full stackn";
}
catch ( popOnEmpty ) {
cerr << "trying to pop a value on an empty stackn";
}

// исполнение продолжается отсюда
return 0;
}

Говорят, что механизм обработки исключений в C++
невозвратный: после того как исключение обработано, управление
не возобновляется с того места, где оно было возбуждено. В нашем
примере управление не возвращается в функцию-член pop(), возбудившую исключение.

11.3.1. Объекты-исключения

Объявлением исключения в catch-обработчике
могут быть объявления типа или объекта. В каких
случаях это следует делать? Тогда, когда необходимо
получить значение или как-то манипулировать объектом,
созданным в выражении throw. Если классы исключений спроектированы
так, что в объектах-исключениях при возбуждении сохраняется
некоторая информация и если в объявлении исключения фигурирует
такой объект, то инструкции внутри catch-обработчика могут
обращаться к информации, сохраненной в объекте выражением throw.

Изменим реализацию класса исключения
pushOnFull, сохранив в объекте-исключении то
значение, которое не удалось поместить в стек.
Catch-обработчик, сообщая об ошибке, теперь будет
выводить его в cerr. Для этого мы сначала модифицируем
определение типа класса pushOnFull следующим образом:

// новый класс исключения:
// он сохраняет значение, которое не удалось поместить в стек
class pushOnFull {
public:
pushOnFull( int i ) : _value( i ) { }
int value { return _value; }
private:
int _value;
};

Новый закрытый член _value
содержит число, которое не удалось поместить в
стек. Конструктор принимает значение типа int и сохраняет его в
члене _data. Вот как вызывается этот конструктор для сохранения
значения из выражения throw:

void iStack::push( int value )
{
if ( full() )
// значение, сохраняемое в объекте-исключении
throw pushOnFull( value );

// ...
}

У класса pushOnFull появилась также новая функция-член
value(), которую можно использовать в catch-обработчике для
вывода хранящегося в объекте-исключении значения:

catch ( pushOnFull eObj ) {
cerr << "trying to push value << "eObj.value()
<< "on a full stackn";
}

Обратите внимание, что в объявлении
исключения в catch-обработчике фигурирует объект eObj,
с помощью которого вызывается функция-член value() класса pushOnFull.

Объект-исключение всегда создается в
точке возбуждения, даже если выражение throw –
это не вызов конструктора и, на первый взгляд, не должно создавать объекта.

Например:

enum EHstate { noErr, zeroOp, negativeOp, severeError };
enum EHstate state = noErr;

int mathFunc( int i ) {
if ( i == 0 ) {
state = zeroOp;
throw state; // создан объект-исключение
}
// иначе продолжается обычная обработка
}

В этом примере объект state
не используется в качестве объекта-исключения.
Вместо этого выражением throw создается объект-исключение
типа EHstate, который инициализируется значением глобального
объекта state. Как программа может различить их? Для ответа на этот
вопрос мы должны присмотреться к объявлению исключения в catch-обработчике
более внимательно.

Это объявление ведет себя почти так же,
как объявление формального параметра. Если при
входе в catch-обработчик исключения выясняется,
что в нем объявлен объект, то он инициализируется копией
объекта-исключения. Например, следующая функция calculate()
вызывает определенную выше mathFunc(). При входе в catch-обработчик
внутри calculate() объект eObj инициализируется копией объекта-исключения,
созданного выражением throw.

void calculate( int op ) {
try {
mathFunc( op );
}
catch ( EHstate eObj ) {
// eObj - копия сгенерированного объекта-исключения
}
}

Объявление исключения в этом примере напоминает
передачу параметра по значению. Объект eObj инициализируется
значением объекта-исключения точно так же, как переданный по
значению формальный параметр функции – значением соответствующего
фактического аргумента. (Передача параметров по значению рассматривалась
в разделе 7.3)

Как и в случае параметров функции, в
объявлении исключения может фигурировать ссылка.
Тогда catch-обработчик будет напрямую ссылаться на
объект-исключение, сгенерированный выражением throw,
а не создавать его локальную копию:

void calculate( int op ) {
try {
mathFunc( op );
}
catch ( EHstate &eObj ) {
// eObj ссылается на сгенерированный объект-исключение
}
}

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

В последнем случае catch-обработчик сможет
модифицировать объект-исключение. Однако переменные,
определенные в выражении throw, остаются без изменения.
Например, модификация eObj внутри catch-обработчика не затрагивает
глобальную переменную state, установленную в выражении throw:

void calculate( int op ) {
try {
mathFunc( op );
}
catch ( EHstate &eObj ) {
// исправить ошибку, вызвавшую исключение
eObj = noErr; // глобальная переменная state не изменилась
}
}

Catch-обработчик переустанавливает eObj в
noErr после исправления ошибки, вызвавшей исключение.
Поскольку eObj – это ссылка, можно ожидать, что присваивание
модифицирует глобальную переменную state. Однако изменяется лишь
объект-исключение, созданный в выражении throw, поэтому модификация eObj
не затрагивает state.

11.3.2. Раскрутка стека

Поиск catch-обработчикадля возбужденного
исключения происходит следующим образом. Когда
выражение throw находится в try-блоке, все ассоциированные
с ним предложения catch исследуются с точки зрения того, могут
ли они обработать исключение. Если подходящее предложение catch
найдено, то исключение обрабатывается. В противном случае поиск
продолжается в вызывающей функции. Предположим, что вызов функции,
выполнение которой прекратилось в результате исключения, погружен в
try-блок; в такой ситуации исследуются все предложения catch, ассоциированные
с этим блоком. Если один из них может обработать исключение, то процесс
заканчивается. В противном случае переходим к следующей по порядку вызывающей
функции. Этот поиск последовательно проводится во всей цепочке вложенных
вызовов. Как только будет найдено подходящее предложение, управление передается
в соответствующий обработчик.

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

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

Если в программе нет предложения catch, способного
обработать исключение, оно остается необработанным. Но
исключение – это настолько серьезная ошибка, что программа
не может продолжать выполнение. Поэтому, если обработчик не
найден, вызывается функция terminate() из стандартной библиотеки C++.
По умолчанию terminate() активизирует функцию abort(), которая аномально
завершает программу. (В большинстве ситуаций вызов abort() оказывается
вполне приемлемым решением. Однако иногда необходимо переопределить действия,
выполняемые функцией terminate(). Как это сделать, рассказывается в книге [STROUSTRUP97].)

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

11.3.3. Повторное возбуждение исключения

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

throw;

которая вновь генерирует объект-исключение.
Повторное возбуждение возможно только внутри составной
инструкции, являющейся частью catch-обработчика:

catch ( exception eObj ) {
if ( canHandle( eObj ) )
// обработать исключение
return;
else
// повторно возбудить исключение, чтобы его перехватил другой
// catch-обработчик
throw;
}

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

enum EHstate { noErr, zeroOp, negativeOp, severeError };

void calculate( int op ) {
try {
// исключение, возбужденное mathFunc(), имеет значение zeroOp
mathFunc( op );
}
catch ( EHstate eObj ) {
// что-то исправить

// пытаемся модифицировать объект-исключение
eObj = severeErr;

// предполагалось, что повторно возбужденное исключение будет
// иметь значение severeErr
throw;
}
}

Так как eObj не является ссылкой, то catch-обработчик
получает копию объекта-исключения, так что любые модификации
eObj относятся к локальной копии и не отражаются на исходном
объекте-исключении, передаваемом при повторном возбуждении.
Таким образом, переданный далее объект по-прежнему имеет тип
zeroOp.

Чтобы модифицировать исходный объект-исключение,
в объявлении исключения внутри catch-обработчика должна фигурировать ссылка:

catch ( EHstate &eObj ) {
// модифицируем объект-исключение
eObj = severeErr;

// повторно возбужденное исключение имеет значение severeErr
throw;
}

Теперь eObj ссылается на объект-исключение, созданный выражением throw,
так что все изменения относятся непосредственно к исходному объекту.
Поэтому при повторном возбуждении исключения далее передается модифицированный объект.

Таким образом, другая причина для объявления ссылки в
catch-обработчике заключается в том, что сделанные внутри
обработчика модификации объекта-исключения в таком случае
будут видны при повторном возбуждении исключения. (Третья причина
будет рассмотрена в разделе 19.2, где мы расскажем, как catch-обработчик
вызывает виртуальные функции класса.)

11.3.4. Перехват всех исключений

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

void manip() {
resource res;
res.lock(); // захват ресурса

// использование ресурса
// действие, в результате которого возбуждено исключение

res.release(); // не выполняется, если возбуждено исключение
}

Если исключение возбуждено, то управление не
попадет на инструкцию, где ресурс освобождается. Чтобы
освободить ресурс, не пытаясь перехватить все возможные
исключения (тем более, что мы не всегда знаем, какие именно
исключения могут возникнуть), воспользуемся специальной конструкцией,
позволяющей перехватывать любые исключения. Это не что иное, как
предложение catch, в котором объявление исключения имеет вид (…)
и куда управление попадает при любом исключении.
Например:

// управление попадает сюда при любом возбужденном исключении
catch (...) {
// здесь размещаем наш код
}

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

void manip() {
resource res;
res.lock();
try {
// использование ресурса
// действие, в результате которого возбуждено исключение
}
catch (...) {
res.release();
throw;
}
res.release(); // не выполняется, если возбуждено исключение
}

Чтобы гарантировать освобождение ресурса в
случае, когда выход из manip() происходит в результате
исключения, мы освобождаем его внутри catch(…) до того,
как исключение будет передано дальше. Можно также управлять
захватом и освобождением ресурса путем инкапсуляции в класс
всей работы с ним. Тогда захват будет реализован в конструкторе,
а освобождение – в автоматически вызываемом деструкторе. (С этим
подходом мы познакомимся в главе 19.)

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

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

try {
stack.display();
for ( int ix = 1; ix < 51; ++x )
{
// то же, что и выше
}
}
catch ( pushOnFull ) { }
catch ( popOnEmpty ) { }
catch ( ... ) { } // должно быть последним в списке catch-обработчиков

Упражнение 11.4

Объясните, почему модель обработки исключений в C++ называется невозвратной.

Упражнение 11.5

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

(a) class exceptionType { };
catch( exceptionType *pet ) { }
(b) catch(...) { }
(c) enum mathErr { overflow, underflow, zeroDivide };
catch( mathErr &ref ) { }
(d) typedef int EXCPTYPE;
catch( EXCPTYPE ) { }

Упражнение 11.6

Объясните, что происходит во время раскрутки стека.

Упражнение 11.7

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

Упражнение 11.8

На основе кода, написанного вами в упражнении 11.3,
модифицируйте класс созданного исключения: неправильный индекс,
использованный в операторе operator[](), должен сохраняться в
объекте-исключении и затем выводиться catch-обработчиком. Измените
программу так, чтобы operator[]() возбуждал при ее выполнении исключение.

11.4. Спецификации исключений

По объявлениям функций-членов pop() и push()
класса iStack невозможно определить, что они возбуждают
исключения. Можно, конечно, включить в объявление подходящий
комментарий. Тогда описание интерфейса класса в заголовочном
файле будет содержать документацию возбуждаемых исключений:

class iStack {
public:
// ...

void pop( int &value ); // возбуждает popOnEmpty
void push( int value ); // возбуждает pushOnFull

private:
// ...
};

Но такое решение несовершенно. Неизвестно,
будет ли обновлена документация при выпуске следующих
версий iStack. Кроме того, комментарий не дает компилятору
достоверной информации о том, что никаких других исключений
функция не возбуждает. Спецификация исключений позволяет перечислить
в объявлении функции все исключения, которые она может возбуждать.
При этом гарантируется, что другие исключения функция возбуждать не будет.

Такая спецификация следует за списком формальных параметров
функции. Она состоит из ключевого слова throw, за которым идет
список типов исключений, заключенный в скобки. Например, объявления
функций-членов класса iStack можно модифицировать, добавив
спецификации исключений:

class iStack {
public:
// ...

void pop( int &value ) throw(popOnEmpty);
void push( int value ) throw(pushOnFull);

private:
// ...
};

Гарантируется, что при обращении к pop()
не будет возбуждено никаких исключений, кроме
popOnEmpty, а при обращении к push()–только pushOnFull.

Объявление исключения – это часть интерфейса функции,
оно должно быть задано при ее объявлении в заголовочном файле.
Спецификация исключений – это своего рода «контракт» между функцией
и остальной частью программы, гарантия того, что функция не будет
возбуждать никаких исключений, кроме перечисленных.

Если в объявлении функции присутствует спецификация
исключений, то при повторном объявлении этой же функции
должны быть перечислены точно те же типы. Спецификации
исключений в разных объявлениях одной и той же функции
не суммируются:

// два объявления одной и той же функции
extern int foo( int = 0 ) throw(string);

// ошибка: опущена спецификация исключений
extern int foo( int parm ) { }

Что произойдет, если функция возбудит
исключение, не перечисленное в ее спецификации?
Исключения возбуждаются только при обнаружении
определенных аномалий в поведении программы, и во время
компиляции неизвестно, встретится ли то или иное исключение
во время выполнения. Поэтому нарушения спецификации исключений
функции могут быть обнаружены только во время выполнения. Если
функция возбуждает исключение, не указанное в спецификации, то
вызывается unexpected() из стандартной библиотеки C++, а та по
умолчанию вызывает terminate(). (В некоторых случаях необходимо
переопределить действия, выполняемые функцией unexpected(). Стандартная
библиотека предоставляет механизм для этого. Подробнее см. [STRAUSTRUP97].)

Необходимо уточнить, что unexpected() не вызывается
только потому, что функция возбудила исключение, не указанное
в ее спецификации. Все нормально, если она обработает это
исключение самостоятельно, внутри функции. Например:

void recoup( int op1, int op2 ) throw(ExceptionType)
{
try {
// ...
throw string("we're in control");
}
// обрабатывается возбужденное исключение
catch ( string ) {
// сделать все необходимое
}
} // все хорошо, unexpected() не вызывается

Функция recoup() возбуждает исключение типа
string, несмотря на его отсутствие в спецификации.
Поскольку это исключение обработано в теле функции,
unexpected() не вызывается.

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

extern void doit( int, int ) throw(string, exceptionType);

void action ( int op1, int op2 ) throw(string) {
doit( op1, op2 ); // ошибки компиляции не будет
// ...
}

doit() может возбудить исключение типа exceptionType,
которое не разрешено спецификацией action(). Однако функция
компилируется успешно. Компилятор при этом генерирует код,
гарантирующий, что при возбуждении исключения, нарушающего
спецификацию, будет вызвана библиотечная функция unexpected().

Пустая спецификация показывает, что функция не возбуждает никаких исключений:

extern void no_problem () throw();

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

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

int convert( int parm ) throw(string)
{
//...
if ( somethingRather )
// ошибка программы:
// convert() не допускает исключения типа const char*
throw "help!";
}

Выражение throw в функции convert() возбуждает
исключение типа строки символов в стиле языка C. Созданный
объект-исключение имеет тип const char*. Обычно выражение
типа const char* можно привести к типу string. Однако спецификация
не допускает преобразования типов, поэтому если convert() возбуждает
такое исключение, то вызывается unexpected(). Для исправления ошибки
выражение throw можно модифицировать так, чтобы оно явно преобразовывало
значение выражения в тип string:

throw string( "help!" );

11.4.1. Спецификации исключений и указатели на функции

Спецификацию исключений можно задавать и при объявлении указателя на функцию.

Например:

void (*pf)( int ) throw(string);

В этом объявлении говорится, что pf указывает на
функцию, которая способна возбуждать только исключения типа
string. Как и для объявлений функций, спецификации исключений
в разных объявлениях одного и того же указателя не суммируются,
они должны быть одинаковыми:

extern void (*pf) ( int ) throw(string);
// ошибка: отсутствует спецификация исключения
void (*pf)( int );

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

void recoup( int, int ) throw(exceptionType);
void no_problem() throw();
void doit( int, int ) throw(string, exceptionType);

// правильно: ограничения, накладываемые на спецификации
// исключений recoup() и pf1, одинаковы
void (*pf1)( int, int ) throw(exceptionType) = &recoup;

// правильно: ограничения, накладываемые на спецификацию исключений no_problem(),
более строгие,
// чем для pf2
void (*pf2)( ) throw(string) = &no_problem;

// ошибка: ограничения, накладываемые на спецификацию
// исключений doit(), менее строгие, чем для pf3
//
void (*pf3)( int, int ) throw(string) = &doit;

Третья инициализация не имеет смысла. Объявление
указателя гарантирует, что pf3 адресует функцию, которая
может возбуждать только исключения типа string. Но doit()
возбуждает также исключения типа exceptionType. Поскольку
она не подходит под ограничения, накладываемые спецификацией
исключений pf3, то не может служить корректным инициализатором
для pf3, так что компилятор выдает ошибку.

Упражнение 11.9

В коде, разработанном для упражнения 11.8, измените
объявление оператора operator[]() в классе IntArray,
добавив спецификацию возбуждаемых им исключений.
Модифицируйте программу так, чтобы operator[]() возбуждал
исключение, не указанное в спецификации. Что при этом происходит?

Упражнение 11.10

Какие исключения может возбуждать функция, если ее
спецификация исключений имеет вид throw()? А если у нее нет такой спецификации?

Упражнение 11.11

Какое из следующих присваиваний ошибочно? Почему?

void example() throw(string);
(a) void (*pf1)() = example;

(b) void (*pf2) throw() = example;

11.5. Исключения и вопросы проектирования

С обработкой исключений в программах C++ связано несколько
вопросов. Хотя поддержка такой обработки встроена в язык, не стоит
использовать ее везде. Обычно она применяется для обмена информацией
об ошибках между независимо разработанными частями программы.
Например, автор некоторой библиотеки может с помощью исключений
сообщать пользователям об ошибках. Если библиотечная функция
обнаруживает аномальную ситуацию, которую не способна обработать
самостоятельно, она может возбудить исключение для уведомления
вызывающей программы.

В нашем примере в библиотеке определен класс iStack и его
функции-члены. Разумно предположить, что программист,
кодировавший main(), где используется эта библиотека,
не разрабатывал ее. Функции-члены класса iStack могут
обнаружить, что операция pop() вызвана, когда стек пуст,
или что операция push() вызвана, когда стек полон; однако
разработчик библиотеки ничего не знал о программе, пользующейся
его функциями, так что не мог разрешить проблему локально.
Не сумев обработать ошибку внутри функций-членов, мы решили
возбуждать исключения, чтобы известить вызывающую программу.

Хотя C++ поддерживает исключения, следует применять и другие
методы обработки ошибок (например, возврат кода ошибки) – там,
где это более уместно. Однозначного ответа на вопрос: «Когда
ошибку следует трактовать как исключение?» не существует.
Ответственность за решение о том, что считать исключительной
ситуацией, возлагается на разработчика. Исключения – это часть
интерфейса библиотеки, и решение о том, какие исключения она возбуждает, –
важный аспект ее дизайна. Если библиотека предназначена для использования в
программах, которые не должны аварийно завершаться ни при каких обстоятельствах,
то она обязана разбираться с аномалиями сама либо извещать о них вызывающую
программу, передавая ей управление. Решение о том, какие ошибки следует
обрабатывать как исключения, – трудная часть работы по проектированию
библиотеки.

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

void iStack::push( int value )
{
// если стек полон, увеличить размер вектора
if ( full() )
_stack.resize( 2 * _stack.size() );
_stack[ _top++ ] = value;
}

Аналогично следует ли функции pop() возбуждать исключение
при попытке извлечь значение из пустого стека? Интересно отметить,
что класс stack из стандартной библиотеки C++ (он рассматривался в
главе 6) не возбуждает исключения в такой ситуации. Вместо этого
постулируется, что поведение программы при попытке выполнения
подобной операции не определено. Разрешить программе продолжать
работу при обнаружении некорректного состояния признали возможным.
Мы уже упоминали, что в разных библиотеках определены разные исключения.
Не существует пригодного для всех случаев ответа на вопрос, что такое
исключение.

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

Еще один аспект проектирования программ заключается в том, что обработка исключений
обычно структурирована. Как правило, программа строится из компонентов, и каждый компонент
решает сам, какие исключения обрабатывать локально, а какие передавать на верхние
уровни. Что мы понимаем под компонентом? Например, система анализа текстовых
запросов, рассмотренная в главе 6, может быть разбита на три компонента, или слоя.
Первый слой – это стандартная библиотека C++, которая обеспечивает базовые операции
над строками, отображениями и т.д. Второй слой – это сама система анализа текстовых
запросов, где определены такие функции, как string_caps() и suffix_text(), манипулирующие
текстами и использующие стандартную библиотеку как основу. Третий слой – это программа,
которая применяет нашу систему. Каждый компонент строится независимо и должен принимать
решения о том, какие исключительные ситуации обрабатывать локально, а какие передавать
на более высокий уровень.

Не все функции должны уметь обрабатывать исключения. Обычно try-блоки и ассоциированные
с ними catch-обработчики применяются в функциях, являющихся точками входа в компонент.
Catch-обработчики проектируются так, чтобы перехватывать те исключения, которые не должны
попасть на верхние уровни программы. Для этого также используются спецификации исключений
(см. раздел 11.4).

Мы расскажем о других аспектах проектирования программ, использующих
исключения, в главе 19, после знакомства с классами и иерархиями
классов.

В C++ различают ошибки времени компиляции и ошибки времени выполнения. Ошибки первого типа обнаруживает компилятор до запуска программы. К ним относятся, например, синтаксические ошибки в коде. Ошибки второго типа проявляются при запуске программы. Примеры ошибок времени выполнения: ввод некорректных данных, некорректная работа с памятью, недостаток места на диске и т. д. Часто такие ошибки могут привести к неопределённому поведению программы.

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

Коды возврата и исключения

Рассмотрим функцию, которая считывает со стандартного потока возраст и возвращает его вызывающей стороне. Добавим в функцию проверку корректности возраста: он должен находиться в диапазоне от 0 до 128 лет. Предположим, что повторный ввод возраста в случае ошибки не предусмотрен.

int ReadAge() {
    int age;
    std::cin >> age;
    if (age < 0 || age >= 128) {
        // Что вернуть в этом случае?
    }
    return age;
}

Что вернуть в случае некорректного возраста? Можно было бы, например, договориться, что в этом случае функция возвращает ноль. Но тогда похожая проверка должна быть и в месте вызова функции:

int main() {
    if (int age = ReadAge(); age == 0) {
        // Произошла ошибка
    } else {
        // Работаем с возрастом age
    }
}

Такая проверка неудобна. Более того, нет никакой гарантии, что в вызывающей функции программист вообще её напишет. Фактически мы тут выбрали некоторое значение функции (ноль), обозначающее ошибку. Это пример подхода к обработке ошибок через коды возврата. Другим примером такого подхода является хорошо знакомая нам функция main. Только она должна возвращать ноль при успешном завершении и что-либо ненулевое в случае ошибки.

Другим способом сообщить об обнаруженной ошибке являются исключения. С каждым сгенерированным исключением связан некоторый объект, который как-то описывает ошибку. Таким объектом может быть что угодно — даже целое число или строка. Но обычно для описания ошибки заводят специальный класс и генерируют объект этого класса:

#include <iostream>

struct WrongAgeException {
    int age;
};

int ReadAge() {
    int age;
    std::cin >> age;
    if (age < 0 || age >= 128) {
        throw WrongAgeException(age);
    }
    return age;
}

Здесь в случае ошибки оператор throw генерирует исключение, которое представлено временным объектом типа WrongAgeException. В этом объекте сохранён для контекста текущий неправильный возраст age. Функция досрочно завершает работу: у неё нет возможности обработать эту ошибку, и она должна сообщить о ней наружу. Поток управления возвращается в то место, откуда функция была вызвана. Там исключение может быть перехвачено и обработано.

Перехват исключения

Мы вызывали нашу функцию ReadAge из функции main. Обработать ошибку в месте вызова можно с помощью блока try/catch:

int main() {
    try {
        age = ReadAge();  // может сгенерировать исключение
        // Работаем с возрастом age
    } catch (const WrongAgeException& ex) {  // ловим объект исключения
        std::cerr << "Age is not correct: " << ex.age << "n";
        return 1;  // выходим из функции main с ненулевым кодом возврата
    }
    // ...
}

Мы знаем заранее, что функция ReadAge может сгенерировать исключение типа WrongAgeException. Поэтому мы оборачиваем вызов этой функции в блок try. Если происходит исключение, для него подбирается подходящий catch-обработчик. Таких обработчиков может быть несколько. Можно смотреть на них как на набор перегруженных функций от одного аргумента — объекта исключения. Выбирается первый подходящий по типу обработчик и выполняется его код. Если же ни один обработчик не подходит по типу, то исключение считается необработанным. В этом случае оно пробрасывается дальше по стеку — туда, откуда была вызвана текущая функция. А если обработчик не найдётся даже в функции main, то программа аварийно завершается.

Усложним немного наш пример, чтобы из функции ReadAge могли вылетать исключения разных типов. Сейчас мы проверяем только значение возраста, считая, что на вход поступило число. Но предположим, что поток ввода досрочно оборвался, или на входе была строка вместо числа. В таком случае конструкция std::cin >> age никак не изменит переменную age, а лишь возведёт специальный флаг ошибки в объекте std::cin. Наша переменная age останется непроинициализированной: в ней будет лежать неопределённый мусор. Можно было бы явно проверить этот флаг в объекте std::cin, но мы вместо этого включим режим генерации исключений при таких ошибках ввода:

int ReadAge() {
    std::cin.exceptions(std::istream::failbit);
    int age;
    std::cin >> age;
    if (age < 0 || age >= 128) {
        throw WrongAgeException(age);
    }
    return age;
}

Теперь ошибка чтения в операторе >> у потока ввода будет приводить к исключению типа std::istream::failure. Функция ReadAge его не обрабатывает. Поэтому такое исключение покинет пределы этой функции. Поймаем его в функции main:

int main() {
    try {
        age = ReadAge();  // может сгенерировать исключения разных типов
        // Работаем с возрастом age
    } catch (const WrongAgeException& ex) {
        std::cerr << "Age is not correct: " << ex.age << "n";
        return 1;
    } catch (const std::istream::failure& ex) {
        std::cerr << "Failed to read age: " << ex.what() << "n";
        return 1;
    } catch (...) {
        std::cerr << "Some other exceptionn";
        return 1;
    }
    // ...
}

При обработке мы воспользовались функцией ex.what у исключения типа std::istream::failure. Такие функции есть у всех исключений стандартной библиотеки: они возвращают текстовое описание ошибки.

Обратите внимание на третий catch с многоточием. Такой блок, если он присутствует, будет перехватывать любые исключения, не перехваченные ранее.

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

Функции и классы стандартной библиотеки в некоторых ситуациях генерируют исключения особых типов. Все такие типы выстроены в иерархию наследования от базового класса std::exception. Иерархия классов позволяет писать обработчик catch сразу на группу ошибок, которые представлены базовым классом: std::logic_error, std::runtime_error и т. д.

Вот несколько примеров:

  1. Функция at у контейнеров std::array, std::vector и std::deque генерирует исключение std::out_of_range при некорректном индексе.

  2. Аналогично, функция at у std::map, std::unordered_map и у соответствующих мультиконтейнеров генерирует исключение std::out_of_range при отсутствующем ключе.

  3. Обращение к значению у пустого объекта std::optional приводит к исключению std::bad_optional_access.

  4. Потоки ввода-вывода могут генерировать исключение std::ios_base::failure.

Исключения в конструкторах

В главе 3.1 мы написали класс Time. Этот класс должен был соблюдать инвариант на значение часов, минут и секунд: они должны были быть корректными. Если на вход конструктору класса Time передавались некорректные значения, мы приводили их к корректным, используя деление с остатком.

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

class Time {
private:
    int hours, minutes, seconds;

public:
    // Заведём класс для исключения и поместим его внутрь класса Time как в пространство имён
    class IncorrectTimeException {
    };

    Time::Time(int h, int m, int s) {
        if (s < 0 || s > 59 || m < 0 || m > 59 || h < 0 || h > 23) {
            throw IncorrectTimeException();
        }
        hours = h;
        minutes = m;
        seconds = s;
    }

    // ...
};

Генерировать исключения в конструкторах — совершенно нормальная практика. Однако не следует допускать, чтобы исключения покидали пределы деструкторов. Чтобы понять причины, посмотрим подробнее, что происходит при генерации исключения.

Свёртка стека

Вспомним класс Logger из предыдущей главы. Посмотрим, как он ведёт себя при возникновении исключения. Воспользуемся в этом примере стандартным базовым классом std::exception, чтобы не писать свой класс исключения.

#include <exception>
#include <iostream>

void f() {
    std::cout << "Welcome to f()!n";
    Logger x;
    // ...
    throw std::exception();  // в какой-то момент происходит исключение
}

int main() {
    try {
        Logger y;
        f();
    } catch (const std::exception&) {
        std::cout << "Something happened...n";
        return 1;
    }
}

Мы увидим такой вывод:

Logger(): 1
Welcome to f()!
Logger(): 2
~Logger(): 2
~Logger(): 1
Something happened...

Сначала создаётся объект y в блоке try. Затем мы входим в функцию f. В ней создаётся объект x. После этого происходит исключение. Мы должны досрочно покинуть функцию. В этот момент начинается свёртка стека (stack unwinding): вызываются деструкторы для всех созданных объектов в самой функции и в блоке try, как если бы они вышли из своей области видимости. Поэтому перед обработчиком исключения мы видим вызов деструктора объекта x, а затем — объекта y.

Аналогично, свёртка стека происходит и при генерации исключения в конструкторе. Напишем класс с полем Logger и сгенерируем нарочно исключение в его конструкторе:

#include <exception>
#include <iostream>

class C {
private:
    Logger x;

public:
    C() {
        std::cout << "C()n";
        Logger y;
        // ...
        throw std::exception();
    }

    ~C() {
        std::cout << "~C()n";
    }
};

int main() {
    try {
        C c;
    } catch (const std::exception&) {
        std::cout << "Something happened...n";
    }
}

Вывод программы:

Logger(): 1  // конструктор поля x
C()
Logger(): 2  // конструктор локальной переменной y
~Logger(): 2  // свёртка стека: деструктор y
~Logger(): 1  // свёртка стека: деструктор поля x
Something happened...

Заметим, что деструктор самого класса C не вызывается, так как объект в конструкторе не был создан.

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

Пример с динамической памятью

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

void f() {
    Logger* ptr = new Logger();  // конструируем объект класса Logger в динамической памяти
    // ...
    g();  // вызываем какую-то функцию
    // ...
    delete ptr;  // вызываем деструктор и очищаем динамическую память
}

На первый взгляд кажется, что в этом коде нет ничего опасного: delete вызывается в конце функции. Однако функция g может сгенерировать исключение. Мы не перехватываем его в нашей функции f. Механизм свёртки уберёт со стека лишь сам указатель ptr, который является автоматической переменной примитивного типа. Однако он ничего не сможет сделать с объектом в памяти, на которую ссылается этот указатель. В логе мы увидим только вызов конструктора класса Logger, но не увидим вызова деструктора. Нам придётся обработать исключение вручную:

void f() {
    Logger* ptr = new Logger();
    // ...
    try {
        g();
    } catch (...) {  // ловим любое исключение
        delete ptr;  // вручную удаляем объект
        throw;  // перекидываем объект исключения дальше
    }
    // ...
    delete ptr;

}

Здесь мы перехватываем любое исключение и частично обрабатываем его, удаляя объект в динамической памяти. Затем мы прокидываем текущий объект исключения дальше с помощью оператора throw без аргументов.

Согласитесь, этот код очень далёк от совершенства. При непосредственной работе с объектами в динамической памяти нам приходится оборачивать в try/catch любую конструкцию, из которой может вылететь исключение. Понятно, что такой код чреват ошибками. В главе 3.6 мы узнаем, как с точки зрения C++ следует работать с такими ресурсами, как память.

Гарантии безопасности исключений

Предположим, что мы пишем свой класс-контейнер, похожий на двусвязный список. Наш контейнер позволяет добавлять элементы в хранилище и отдельно хранит количество элементов в некотором поле elementsCount. Один из инвариантов этого класса такой: значение elementsCount равно реальному числу элементов в хранилище.

Не вдаваясь в детали, давайте посмотрим, как могла бы выглядеть функция добавления элемента.

template <typename T>
class List {
private:
    struct Node {  // узел двусвязного списка
        T element;
        Node* prev = nullptr;  // предыдущий узел
        Node* next = nullptr;  // следующий узел
    };

    Node* first = nullptr;  // первый узел списка
    Node* last = nullptr;  // последний узел списка
    int elementsCount = 0;

public:
    // ...

    size_t Size() const {
        return elementsCount;
    }

    void PushBack(const T& elem) {
        ++elementsCount;

        // Конструируем в динамической памяти новой узел списка
        Node* node = new Node(elem, last, nullptr);

        // Связываем новый узел с остальными узлами
        if (last != nullptr) {
            last->next = node;
        } else {
            first = node;
        }
        last = node;
    }
};

Не будем здесь рассматривать другие функции класса — конструкторы, деструктор, оператор присваивания… Рассмотрим функцию PushBack. В ней могут произойти такие исключения:

  1. Выражение new может сгенерировать исключение std::bad_alloc из-за нехватки памяти.

  2. Конструктор копирования класса T может сгенерировать произвольное исключение. Этот конструктор вызывается при инициализации поля element создаваемого узла в конструкторе класса Node. В этом случае new ведёт себя как транзакция: выделенная перед этим динамическая память корректно вернётся системе.

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

#include <iostream>

class C;  // какой-то класс

int main() {
    List<C> data;
    C element;

    try {
        data.PushBack(element);
    } catch (...) {  // не получилось добавить элемент
        std::cout << data.Size() << "n";  // внезапно 1, а не 0
    }

    // работаем дальше с data
}

Наша функция PushBack сначала увеличивает счётчик элементов, а затем выполняет опасные операции. Если происходит исключение, то в классе List нарушается инвариант: значение счётчика elementsCount перестаёт соответствовать реальности. Можно сказать, что функция PushBack не даёт гарантий безопасности.

Всего выделяют четыре уровня гарантий безопасности исключений (exception safety guarantees):

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

  2. Строгая гарантия безопасности. Исключение может возникнуть, но от этого объект нашего класса не поменяет состояние: количество элементов останется прежним, итераторы и ссылки не будут инвалидированы и т. д.

  3. Базовая гарантия безопасности. При исключении состояние объекта может поменяться, но оно останется внутренне согласованным, то есть, инварианты будут соблюдаться.

  4. Отсутсвие гарантий. Это довольно опасная категория: при возникновении исключений могут нарушаться инварианты.

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

Переместим в нашей функции PushBack изменение счётчика в конец:

    void PushBack(const T& elem) {
        Node* node = new Node(elem, last, nullptr);

        if (last != nullptr) {
            last->next = node;
        } else {
            first = node;
        }
        last = node;

        ++elementsCount;  // выполнится только если раньше не было исключений
    }

Теперь такая функция соответствует строгой гарантии безопасности.

В документации функций из классов стандартной библиотеки обычно указано, какой уровень гарантии они обеспечивают. Рассмотрим, например, гарантии безопасности класса std::vector.

  • Деструктор, функции empty, size, capacity, а также clear предоставляют гарантию отсутствия сбоев.

  • Функции push_back и resize предоставляют строгую гарантию.

  • Функция insert предоставляет лишь базовую гарантию. Можно было бы сделать так, чтобы она предоставляла строгую гарантию, но за это пришлось бы заплатить её эффективностью: при вставке в середину вектора пришлось бы делать реаллокацию.

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

class C {
public:
    void f() noexcept {
        // ...
    }
};

С одной стороны, эта подсказка позволяет компилятору генерировать более эффективный код. С другой — эффективно обрабатывать объекты таких классов в стандартных контейнерах. Например, std::vector<C> при реаллокации будет использовать конструктор перемещения класса C, если он помечен как noexcept. В противном случае будет использован конструктор копирования, который может быть менее эффективен, но зато позволит обеспечить строгую гарантию безопасности при реаллокации.

Возможно, вам также будет интересно:

  • C builder если ошибка то
  • C 3102 ошибка konica minolta
  • C 1611 ошибка киа сид
  • C 1155 ошибка мазда 3
  • C 0213 konica minolta ошибка

  • Понравилась статья? Поделить с друзьями:
    0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии