На любом языке программирования, при разработке программы, в какой-то момент, мы гарантированно столкнемся с ошибкой.
Все ошибки можно разделить на два типа:
- Ошибки времени компиляции: это ошибки, возникающие во время компиляции. Наиболее распространенными ошибками времени компиляции являются синтаксические ошибки, неправильные включения/импорты или неправильные ссылки.
- Ошибки времени выполнения: эти ошибки возникают во время выполнения и называются исключениями.
Ошибки, возникающие во время выполнения, создают серьезные проблемы и, в большинстве случаев, мешают нормальному выполнению программы. Вот почему мы должны уметь обрабатывать эти ошибки. Эта обработка ошибок для поддержания нормального хода выполнения программы известна как «обработка исключений».
Обработка исключений
В C++ обработка исключений обеспечивается с помощью трех конструкций или ключевых слов: try, catch и throw.
Блок кода, обеспечивающий способ обработки исключения, называется «обработчиком исключений».
Общий синтаксис типичного обработчика исключений:
1 2 3 4 5 |
try{ //код, который, вероятно, выдаст исключение }catch(exception e){ //код для обработки исключения e } |
Код, который может генерировать исключения, заключен в блок try. Блок try также может содержать оператор «throw», который используется для явного генерирования исключений. Оператор «throw» состоит из ключевого слова «throw», за которым следует параметр, являющийся именем исключения. Затем этот параметр передается в блок catch.
Исключение, созданное в блоке «try», передается в блок «catch». Блок catch содержит код для обработки сгенерированного исключения. Он может содержать просто сообщение или целый блок кода для обработки исключения, чтобы не мешать нормальному ходу программы.
Необязательно, чтобы за каждым блоком try следовал только один блок catch. Если код нашего блока try содержит операторы, которые могут генерировать более одного исключения, то у нас может быть несколько блоков catch, в которых каждый блок catch обеспечивает обработку каждого из исключений.
В этом случае структура обработчика исключений выглядит так, как показано ниже:
1 2 3 4 5 6 7 8 9 |
try{ }catch(exception ex1) {} catch(exception ex2) {} ….. catch(exception exn) {} |
Далее мы покажем исключения, поддерживаемые C++, вместе с их описанием.
Стандартные исключения C++
На следующей диаграмме показана иерархия классов исключений std::exception, поддерживаемых в C++:
В приведенной ниже таблице отображено описание каждого из приведенных выше исключений:
Исключение | Описание |
std::exception | Это родительский класс для всех стандартных исключений C++. |
std::bad_alloc | Вызывается оператором ‘new’, когда выделение памяти идет неправильно. |
std::bad_cast | Возникает, когда ‘dynamic_cast’ идет не так. |
std::bad_typeid | Это исключение вызывается typeid. |
std::bad_exception | Это исключение используется для обработки непредвиденных исключений в программе. |
std::runtime_error | Исключения, возникающие во время выполнения и не определяемые простым чтением кода. |
std::overflow_error | Это исключение возникает, когда происходит математическое переполнение. |
std::underflow_error | Это исключение выдается, когда возникает математическая потеря значимости. |
std::range_error | Когда программа сохраняет значение, выходящее за пределы допустимого диапазона, возникает это исключение. |
std::logic_error | Эти исключения можно вывести, прочитав код. |
std::out_of_range | Исключение возникает, когда мы обращаемся/вставляем элементы, которые не находятся в диапазоне. Например, в случае векторов или массивов. |
std::length_error | Это исключение возникает в случае превышения длины переменной. Например, когда создается строка большой длины для типа std::string. |
std::domain_error | Генерируется при использовании математически недопустимого домена. |
std::invalid_argument | Исключение обычно возникает для недопустимых аргументов. |
Использование try, catch и throw
Вы уже поняли, как операторы try, catch и throw используются при обработке исключений в C++. Теперь давайте посмотрим на пример программирования, чтобы лучше понять их работу.
Здесь мы представили классический случай ошибки времени выполнения «деление на ноль». Программа скомпилируется нормально, так как логических ошибок нет. Но во время выполнения, поскольку указанный знаменатель равен «0», программа обязательно выйдет из строя.
Чтобы предотвратить это, мы помещаем код разделения в блок try и обрабатываем исключение в блоке catch, чтобы программа выполнялась нормально.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <iostream> using namespace std; int main() { int numerator = 20, denominator = 0; try{ if(denominator == 0) throw "Exception - Division by zero!!"; else cout<<"Result = "<<numerator/denominator; } catch(const char* error) { cout << error << endl; } } |
Вывод данных:
1 |
Exception – Division by zero! |
Вышеприведенная программа является простой иллюстрацией «try throw catch» на C++. Как видим, в программе мы предоставляем значения числителя и знаменателя. Далее проверяем, равен ли знаменатель нулю. Если да, то выбрасываем исключение, иначе печатаем результат. В блоке catch мы печатаем сообщение об ошибке, вызванное исключением.
Это гарантирует, что программа не выйдет из строя или не завершится аварийно, когда она столкнется с делением на ноль.
Как остановить бесконечный цикл в обработке исключений
Рассмотрим еще один случай бесконечного цикла.
Если код внутри должен генерировать исключение, то нам нужно рассмотреть фактическое место, где мы должны поместить наш блок try-catch. То есть должен ли блок try-catch быть внутри цикла или цикл должен быть внутри блока try-catch.
Нет гарантированного ответа относительно размещения блока try-catch, однако это зависит исключительно от ситуации. Одно из соображений, которое мы должны учитывать, состоит в том, что блок try-catch обычно приводит к расширению программы. Поэтому, если это не требуется, мы должны отказаться от кода обработки исключений в бесконечном цикле.
Рассмотрим пример, показанный ниже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> #include <stdexcept> using namespace std; int main (void) { int Sum=0, num; cout<<"Please enter number you wish to add(-99 to exit):"<<endl; while( true ) { try{ cin>>num; if(num == -99) throw -99; Sum+=num; } catch(...) { cout <<" Aborting the program..."<<endl; break; } } cout << "Sum is:" << Sum << endl; return 0; } |
Здесь у нас есть бесконечный цикл while. Внутри цикла мы читаем входное число и добавляем его к сумме. Чтобы выйти из цикла, нам нужно указать условие завершения внутри цикла. Мы указали -99 в качестве конечного условия.
Как можете видеть, мы поместили этот код в блок try, и когда введенное число равно -99, мы генерируем исключение, которое перехватывается в блоке catch.
Теперь в приведенной выше ситуации, если мы поместим весь цикл while внутри блока try, тогда неизбежно возникнут неудобства, поскольку цикл while является бесконечным. Таким образом, лучшее место для размещения блока try-catch — внутри цикла.
Программа выдает следующий вывод данных:
1 2 3 4 |
Please enter the number you wish to add(-99 to exit): -99 Aborting the program… Sum is:0 |
Обратите внимание, что управление передается блоку catch после ввода -99 в качестве входных данных.
Обратите внимание, что мы не указали какой-либо конкретный объект исключения в качестве аргумента для перехвата. Вместо этого мы предоставили (…). Это указывает на то, что мы перехватываем обобщенное исключение.
Раскручивание стека
Вы уже знаете, что всякий раз, когда программа имеет модульную структуру и задействованы вызовы функций, текущее состояние программы сохраняется в стеке. Когда функция возвращается обратно, состояние восстанавливается и выполнение программы продолжается в обычном режиме.
Раскручивание стека обычно связано с обработкой исключений. Это процесс, в котором записи функций удаляются из стека вызовов во время выполнения. В случае C++ всякий раз, когда возникает исключение, стек вызовов, в котором хранятся записи функций, линейно ищется обработчиком исключения.
Все записи удаляются из стека до тех пор, пока не будет найдена запись функции с обработчиком исключения. Это означает, что раскручивание стека происходит, когда исключение не обрабатывается в той же функции, т.е. функция выдает исключение, но не предоставляет обработчик перехвата.
Давайте разберемся с процессом раскручивания стека на примере программирования:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
#include <iostream> using namespace std; void last_f() { cout << "last_f :: start\n"; cout << "last_f:: throw int exception\n"; throw -1; cout << "last_f :: end\n"; } void third_f() { cout<< "third_f :: Start\n"; last_f(); cout << "third_f :: End\n"; } void second_f() { cout << "second_f :: Start\n"; try { third_f(); } catch(double) { cout << "second_f :: catch double exception\n"; } cout << "second_f :: End\n"; } void first_f() { cout << "first_f :: Start\n"; try { second_f(); } catch (int) { cout << "first_f :: catch int exception\n"; } catch (double) { cout << "first_f :: catch double exception\n"; } cout << "first_f :: End\n"; } int main() { cout << "main :: Start\n"; try { first_f(); } catch (int) { cout << "main :: catch int exception\n"; } cout << "main :: End\n"; return 0; } |
Вывод данных:
1 2 3 4 5 6 7 8 9 |
main :: Start first_f :: Start second_f :: Start third_f :: Start last_f :: start last_f:: throw int exception first_f :: catch int exception first_f :: End main :: End |
В приведенной выше программе у нас есть четыре функции, которые вызываются друг из друга.
Путь вызова функции: main=>first_f()=>second_f()=> Third_f()=>last_f().
Следовательно, первые пять строк вывода говорят сами за себя.
В функции last_f выбрасывается исключение типа int. Но в этой функции нет блока catch и поэтому стек начинает раскручиваться. Таким образом, last_f завершается, и управление передается вызывающей функции, которая является Third_f. В этой функции также нет обработчика исключений, поэтому Third_f завершается и управление передается second_f.
В функции second_f есть обработчик исключения, но он не совпадает с исключением int и функция завершается.
Далее управление переходит к вызывающей функции first_f. В этой функции исключение int, сгенерированное функцией last_f, совпадает с предоставленным обработчиком исключений. Этот обработчик catch вызывается, и сообщение печатается. Затем печатается оператор, следующий за обработчиком catch, и управление возвращается к основной функции.
В main также есть обработчик исключения int, но поскольку он уже завершен, управление переходит к последнему оператору в основной функции, и печатается последний оператор.
Следовательно, в выводе программы вы можете видеть, что, поскольку не было обработчика исключения, который соответствовал бы исключению int, стек раскрутился до функции first_f. В результате функции, предшествующие функции first_f, были завершены.
Таким образом, благодаря раскрутке стека C++ дает нам преимущество в размещении обработчика исключений в подходящем месте. Таким образом, даже если функция просто выдает исключение и не хочет его обрабатывать, исключение будет распространяться до тех пор, пока не найдет подходящий обработчик исключения.
Исключение вне диапазона
Исключение « out_of_range » является наиболее распространенным исключением, с которым сталкивается программист. Следовательно, он нуждается в особом упоминании в этой статье.
Иногда, когда мы используем контейнеры, такие как массивы или векторы, мы определяем их размер или количество элементов, которые они содержат, во время компиляции. Поэтому, когда программист пытается вставить или получить доступ к элементам за пределами этого размера, возникает исключение «out_of_range».
Рассмотрим следующий пример программирования, чтобы это понять:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> #include <stdexcept> #include <vector> using namespace std; int main (void) { vector<int> myvector(10); try { myvector.at(11)=11; } catch (const out_of_range& oor_exce) { cout << "Out of Range Exception: " << oor_exce.what() << '\n'; } return 0; } |
Вывод данных:
1 |
Out of Range Exception: vector::_M_range_check: __n (which is 11) >= this->size() (which is 10) |
В приведенной выше программе у нас есть векторный контейнер размером 10. Затем мы пытаемся вставить элемент 11 в 11 -е место в векторе. Поскольку размер вектора объявлен равным 10, возникает исключение, выходящее за пределы диапазона, которое перехватывается в блоке catch программы, и точная причина, заданная функцией «what», выводится вместе с сообщением.
Итог
В этой статье мы рассмотрели обработку исключений в C++. Вы должны знать, что обработка исключений является неотъемлемой частью программы. Обработка исключений обеспечивает нормальное выполнение программы, чтобы при возникновении ошибки или исключительной ситуации программа не выходила из строя.
Таким образом, при использовании исключений нужно уметь использовать их с умом, чтобы сделать код надежным и эффективным. В следующей статье мы поговорим об аргументах командной строки в C++.
С Уважением, МониторБанк