Защита от дурака. Ввод числовых типов данных в C/C++ с проверкой на корректность

Превью к статье об организации защиты от дурака в программах

Большинство программ должны взаимодействовать с пользователем посредством ввода определённых данных, будь то ФИО, рост, вес для внесния в базу данных или геометрические размеры какого-то объекта, для которого нужно что-то рассчитать. Все эти данные вводит пользователь - человек, а значит в ответ может придти всё что угодно. Что выдаст программа, если вместо требуемого ей возраста пользователь напишет его словом? Скорее всего программа аварийно завершится или зависнет, но только не в том случае, если в ней предусмотрена "защита от дурака".

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

Реализация защиты от дурака на языке C

Чтобы реализовать хорошую защиту от дурака для ввода различных числовых (int, double...) данных, необходимо считывать не сами числа, а всю вводимую строку и уже потом анализировать ввод. В языке C есть очень хорошая функция sscanf(const char *, const char *, args), которая работает аналогично функции scanf(const char *, args), возвращая количество успешно считанных аргументов, только чтение данных происходит не из стандартного потока ввода, а из переданной ей первым аргументом строки.

Рассмотрим несколько примеров функций, которые реализуют проверку на дурака, используя функцию sscanf.

Ввод целого числа с проверкой на некорректный ввод

int get_integer(const char *msg) {
    char answer[256]; // строка для чтения
    int n; // итоговое целое число

    printf("%s", msg); // выводим приглашение ко вводу
    fgets(answer, sizeof(answer), stdin); // считываем строку

	// пока не будет считано целое число
    while (sscanf(answer, "%d", &n) != 1) {
        printf("Incorrect input. Try again: "); // выводим сообщение об ошибке
        fgets(answer, sizeof(answer), stdin); // и заново считываем строку
    }

    return n; // возвращаем корректное целое число
}

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

Ввод вещественного числа с проверкой на некорректный ввод

double get_double(const char *msg) {
    char answer[256];  // строка для чтения
    double x; // итоговое вещественное число

    printf("%s", msg); // выводим приглашение ко вводу
    fgets(answer, sizeof(answer), stdin); // считываем строку

	// пока не будет считано вещественное число
    while (sscanf(answer, "%lf", &x) != 1) {
        printf("Incorrect input. Try again: "); // выводим сообщение об ошибке
        fgets(answer, sizeof(answer), stdin); // и заново считываем строку
    }

    return x; // воозвращаем корректное вещественное число
}

Ввод точки на координатной плоскости (структура с двумя вещественными полями)

// описание структуры даных
typedef struct point_t {
    double x; // координата x
    double y; // координата y
} point_t;

point_t get_point(const char *msg) {
    char answer[256]; // строка для чтения
    point_t point; // итоговая точка

    printf("%s", msg); // выводим приглашение ко вводу
    fgets(answer, sizeof(answer), stdin); // считываем строку

    // пока не будут считаны обе координаты точки
    while (sscanf(answer, "(%lf,%lf)", &point.x, &point.y) != 2) {
        printf("Incorrect input. Try again: "); // выводим сообщение об ошибке
        fgets(answer, sizeof(answer), stdin); // и заново считываем строку
    }

    return point; // возвращаем корректную точку
}

Как видно из примеров, особенность возвращения функцией sscanf числа считанных аргументов позволяет контролировать корректность введённых данных в указанном формате, а считывание всей строки защищает от того, что в потоке ввода останутся символы пробела или переноса строки '\n', которые уж очень часто заставляют потратить на поиск ошибки ни один час или даже день.