Урок №108. Обработка ошибок, cerr и exit()

  Юрий  | 

  Обновл. 8 Сен 2020  | 

 18427

 ǀ   5 

При написании программ возникновение ошибок почти неизбежно. Ошибки в языке C++ делятся на две категории: синтаксические и семантические.

Синтаксические ошибки

Синтаксическая ошибка возникает при нарушении правил грамматики языка C++. Например:

если 7 не равно 8, то пишем "not equal";

Хотя этот стейтмент нам (людям) понятен, компьютер не сможет его корректно обработать. В соответствии с правилами грамматики языка C++, корректно будет:

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

Семантические ошибки


Семантическая (или «смысловая») ошибка возникает, когда код синтаксически правильный, но выполняет не то, что нужно программисту. Например:

Возможно, программист хотел, чтобы вывелось 0 1 2 3, но на самом деле выведется 0 1 2 3 4.

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

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

Что произойдет, если x будет равен 4? Условие выполнится как true, а программа выведет x is greater than 4. Логические ошибки иногда бывает довольно-таки трудно обнаружить.

Другой распространенной семантической ошибкой является ложное предположение. Ложное предположение возникает, когда программист предполагает, что что-то будет истинным или ложным, а оказывается наоборот. Например:

Заметили потенциальную проблему здесь? Предполагается, что пользователь введет значение между 0 и длиной строки Hello, world!. Если же пользователь введет отрицательное число или число, которое больше длины указанной строки, то index окажется за пределами диапазона массива. В этом случае, поскольку мы просто выводим значение по индексу, результатом будет вывод мусора (при условии, что пользователь введет число вне диапазона). Но в других случаях ложное предположение может привести и к изменениям значений переменных, и к сбою в программе.

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

Определение ложных предположений

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

   При вызове функции, когда caller может передать некорректные или семантически бессмысленные аргументы.

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

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

Поэтому, придерживаясь безопасного программирования, нужно следовать следующим 3-м правилам:

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

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

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

Рассмотрим примеры проблем:

Проблема №1: При вызове функции caller может передать некорректные или семантически бессмысленные аргументы:

Можете ли вы определить потенциальную проблему здесь? Дело в том, что caller может передать нулевой указатель вместо допустимой строки C-style. Если это произойдет, то в программе будет сбой. Вот как правильно (с проверкой параметра функции на то, не является ли он нулевым):

Проблема №2: Возвращаемое значение может указывать на возникшую ошибку:

Можете ли вы определить потенциальную проблему здесь? Пользователь может ввести символ, который не находится в строке hello. Если это произойдет, то функция find() возвратит индекс -1, который и выведется. Правильно:

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

Вот как правильно (с проверкой пользовательского ввода):

Обратите внимание, здесь проверка двухуровневая:

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

   Во-вторых, это значение должно находиться в диапазоне массива.

Обработка ложных предположений


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

Но все же есть несколько способов обработки ложных предположений:

Способ №1: Пропустите код, который зависит напрямую от правильности предположения:

В примере, приведенном выше, если cstring окажется NULL, то мы ничего не будем выводить. Мы пропустили тот код, который напрямую зависит от значения cstring и который с ним работает (в коде мы просто выводим этот cstring). Это может быть хорошим вариантом, если пропущенный стейтмент не является критическим и не влияет на логику программы. Основной недостаток при этом заключается в том, что caller или пользователь не имеет возможности определить, что что-то пошло не так.

Способ №2: Из функции возвращайте код ошибки обратно в caller и позволяйте caller-у обработать эту ошибку:

Здесь функция возвратит -1, если caller передаст некорректный index. Возврат перечислителя в качестве кода ошибки будет еще лучшим вариантом.

Способ №3: Если нужно немедленно завершить программу, то используйте функцию exit(), которая находится в заголовочном файле cstdlib, для возврата кода ошибки обратно в операционную систему:

Если caller передаст некорректный index, то программа немедленно завершит свое выполнение и передаст код ошибки 2 обратно в операционную систему.

Способ №4: Если пользователь ввел данные не того типа, что нужно — попросите пользователя ввести данные еще раз:

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

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

Способ №6: Если вы работаете в какой-то графической среде, то распространенной практикой является вывод всплывающего окна с кодом ошибки, а затем немедленное завершение программы. То, как это сделать, зависит от конкретной среды разработки.

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

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

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

  1. Аватар Антонида:

    Большое спасибо за труд! Запустила код с проверкой пользовательского ввода и ввела число 3.5, вывод был следующий: "Letter 3 is l", хотя ожидалось, что программа попросит ввести число еще раз, так как 3.5 не является целым числом. Почему так происходит? cin привел это число к int?

    1. Аватар Кетчуп:

      std::cin сначала взял 3 и присвоил её переменной index, но внутри std::cin ещё осталось '.' и 5. Если вы потом попробуете поместить значение из std::cin в переменную типа char, то вам даже не предложат ничего вводить и возьмут то самое значение '.'. Чтобы такого избежать, в случае успеха с index нужно проигнорировать все хранимые значения в std::cin. Можно сделать это так:
      std::cin.ignore(std::numeric_limits <std::streamsize>::max(), '\n');

  2. Аватар Алексей:

    Отличный урок, очень пригодиться.

    Я только не одну неделю не могу найти способ определить пустую строку или пустой ввод.
    В bash это всего лишь "if ( -z $variable)". C++ вообще без понятия.

  3. Аватар Алексей:

    Вроде как без continue работает, нехватает очистки консоли.
    Сижу и думаю, массив, почему while >=13, потом присмотрелся "="…

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

    а где же throw-try-catch? или дальше будет?
    вроде как логичный и удобный способ как сообщать об ошибках, так и "ловить" их…

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

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