Обработка ошибок

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

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

Возможные методы информирования об ошибке включают:

Многие функции стандартной библиотеки Си возвращают выделенное значение и вместе с этим устанавливают значение глобальной переменной errno. Например, функция fopen возвращает нулевой указатель, если запрашиваемый файл не удалось открыть. Возвращаемое значение говорит о том, что операция не была выполнена, а значение глобальной переменной позволяет получить код ошибки.

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

typedef enum { INT_OK=0, INT_LIMITS, INT_CONVERGENCE } ErrorCode;
typedef double (*RealFun)(double);
double integrate(RealFun f, double a, double b, double eps, ErrorCode *perr);

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

Теперь посмотрим, как может быть реализована функция integrate.

#include "integrate.h"
double integrate(RealFun f, double a, double b, double eps, ErrorCode *perr) {
    double result = 0.0;
    if(perr != NULL)
        *perr = INT_OK; // По умолчанию считаем, что ошибок нет
    // ...
    if(/* условие возникновения ошибки */) {
        if(perr)
            *perr = INT_CONVERGENCE;
        return result;
    }
    // ...
    return result;
}
Сначала проверяем, что значением perr явлется ненулевой адрес. Это означает, что вызывающая сторона хочет получить информацию об ошибках. Если в процессе вычислений наступает определенное условие, то по адресу perr записывается код ошибки и управление передается вызывающей функции.

Пример использования.

#include "integrate.h"
int main() {
    ErrorCode err;
    // Вызов с проверкой
    double res = integrate(sin, 0, 1, 0.001, &err);
    switch(err) {
    case INT_LIMITS:
        printf("Ошибка интегрирования. Неверные педелы. Код ошибки: %d\n", err);
        return -1;
    // ... обработка других кодов
    case INT_OK:
        // эту ветку можно не указывать
        break;
    }

    // Вызов БЕЗ ПРОВЕРКИ
    res = integrate(sin, 0, 1, 0.001, NULL);
    return 0;
}
Заметим, что если проверка на ошибки не требуется, то можно не объявлять переменную типа ErrorCode.

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