Калькулятор на C: пишем консольную программу с проверкой ввода

Калькулятор на C: пишем консольную программу с проверкой ввода Полезное

Консольный калькулятор на C — это программа, которая читает два числа и знак операции, выбирает действие оператором switch и печатает результат. Задача учебная, но на ней видны три вещи, на которых спотыкаются новички: проверка того, что scanf действительно прочитал ввод, забытый break и деление на ноль.

Ниже — готовая программа целиком, разбор по строкам, типичные ошибки с реальным выводом, версия с циклом и расширение до выражений со скобками вроде (2 + 3) * 4. Все примеры скомпилированы и запущены на Apple clang 21 (macOS, arm64) и GCC 13.3 (Ubuntu 24.04) с флагами -std=c17 -Wall -Wextra, проверка 23.09.2026; код использует только стандартную библиотеку C и не зависит от ОС.

Полная программа: одно выражение

#include <stdio.h>

int main(void) {
    double a, b;
    char op;

    printf("Введите выражение, например 7 / 2: ");
    if (scanf("%lf %c %lf", &a, &op, &b) != 3) {
        printf("Ошибка: нужно число, знак и число\n");
        return 1;
    }

    switch (op) {
    case '+':
        printf("%g\n", a + b);
        break;
    case '-':
        printf("%g\n", a - b);
        break;
    case '*':
        printf("%g\n", a * b);
        break;
    case '/':
        if (b == 0) {
            printf("Ошибка: деление на ноль\n");
            return 1;
        }
        printf("%g\n", a / b);
        break;
    default:
        printf("Ошибка: неизвестная операция '%c'\n", op);
        return 1;
    }
    return 0;
}

Сохраните код в calc.c, соберите и запустите:

cc -std=c17 -Wall -Wextra -o calc calc.c
./calc

Результаты прогона (ввод -> ответ программы, в скобках код возврата):

7 / 2    -> 3.5                                  (0)
7/2      -> 3.5                                  (0)
2.5 * 4  -> 10                                   (0)
5 / 0    -> Ошибка: деление на ноль              (1)
3 % 2    -> Ошибка: неизвестная операция '%'     (1)
abc      -> Ошибка: нужно число, знак и число    (1)

Разбор по строкам

Тип double, а не int. Калькулятор должен уметь 7 / 2 = 3.5. Если объявить числа как int, деление станет целочисленным и дробная часть отбросится (пример ниже).

Проверка scanf(...) != 3. Функция возвращает число успешно прочитанных полей. Три поля — два числа и знак. При вводе букв вернется меньше трех, и считать с такими переменными нельзя.

Пробел перед %c в формате. Спецификатор %c, в отличие от %lf, не пропускает пробелы сам. Пробел в строке формата означает «пропусти любые пробельные символы», поэтому работают и 7 / 2, и 7/2.

switch и break. switch сравнивает op с каждой меткой case. break выходит из switch; без него выполнение «проваливается» в следующую ветку. Ветка default ловит все, что не попало в case.

%g в printf. Печатает 10, а не 10.000000; по умолчанию до 6 значащих цифр, больше дает %.15g.

Сравнение b == 0. Корректно для введенного делителя; для результатов вычислений с дробями == ненадежно.

Переполнение. Проверка на ноль не спасает от выхода за диапазон double: 1e308 * 10 печатает inf, код возврата 0, а scanf примет и 1e999 как бесконечность. Ошибкой это не считается; если нужно, результат проверяют макросом isfinite из <math.h>, как в разборщике выражений ниже.

return 1 при ошибке. Код возврата 0 означает успешное завершение, ненулевой — ошибку. Это соглашение видят скрипты и оболочка (echo $?), значения true/false тут ни при чем.

Типичные ошибки: код -> результат -> исправление

Забытый break

#include <stdio.h>

int main(void) {
    double a = 6, b = 3;
    char op = '+';
    switch (op) {
    case '+':
        printf("%g\n", a + b);
    case '-':
        printf("%g\n", a - b);
        break;
    }
    return 0;
}

Результат: программа печатает 9, затем 3 — после сложения выполнилось и вычитание. Предупреждение зависит от компилятора. GCC включает его в -Wextra:

warning: this statement may fall through [-Wimplicit-fallthrough=]

Clang с -Wall -Wextra молчит; у него предупреждение появляется только с отдельным флагом -Wimplicit-fallthrough:

warning: unannotated fall-through between switch labels [-Wimplicit-fallthrough]

Исправление: break в конце каждой ветки case.

Целочисленное деление

#include <stdio.h>

int main(void) {
    int a = 7, b = 2;
    printf("%d\n", a / b);
    printf("%g\n", (double)a / b);
    return 0;
}

Результат: 3 и 3.5. Деление двух int отбрасывает дробную часть. Исправление: хранить числа в double или привести один операнд к double до деления.

Деление на ноль у int — неопределенное поведение: в зависимости от процессора и компилятора программа может упасть или вернуть произвольное число. Поэтому делитель проверяют до операции.

Бесконечный цикл на букве

#include <stdio.h>

int main(void) {
    double a;
    while (scanf("%lf", &a) != EOF) {
        printf("прочитано: %g\n", a);
    }
    return 0;
}

На ввод 5 abc программа бесконечно печатает прочитано: 5. scanf не смог прочитать abc как число, вернул 0 (не EOF) и оставил буквы во входном потоке — на следующей итерации все повторяется. Исправление — в следующем разделе.

Калькулятор в цикле с устойчивым вводом

Надежнее читать строку целиком через fgets и разбирать ее sscanf: неудачная строка пропускается, мусор в потоке не застревает.

#include <stdio.h>

int main(void) {
    char line[128];
    double a, b;
    char op;
    int used;

    while (printf("> "), fgets(line, sizeof line, stdin) != NULL) {
        if (line[0] == 'q')
            break;
        used = 0;
        if (sscanf(line, "%lf %c %lf %n", &a, &op, &b, &used) != 3
            || line[used] != '\0') {
            printf("Не понял строку, формат: 7 / 2\n");
            continue;
        }
        switch (op) {
        case '+': printf("%g\n", a + b); break;
        case '-': printf("%g\n", a - b); break;
        case '*': printf("%g\n", a * b); break;
        case '/':
            if (b == 0) printf("Деление на ноль\n");
            else printf("%g\n", a / b);
            break;
        default: printf("Неизвестная операция '%c'\n", op);
        }
    }
    return 0;
}

Прогон на вводе 7 / 2, abc, 2 + 3x, 5 / 0, 10 * 1.5, q:

> 3.5
> Не понял строку, формат: 7 / 2
> Не понял строку, формат: 7 / 2
> Деление на ноль
> 15
>

%n записывает, сколько символов разобрано. Если после выражения остался хвост (2 + 3x), line[used] не равен '\0' и строка отклоняется. Первая программа такой хвост молча игнорирует и выдает 5. Граница: строка длиннее 127 символов будет прочитана по частям.

Способ ввода Что делает с мусором Когда брать
scanf оставляет в потоке, в цикле возможно зацикливание одно чтение, учебный пример
fgets + sscanf строка отбрасывается целиком диалог в цикле
fgets + strtod видно, где остановился разбор свой разбор выражений

Расширение: выражения со скобками

Формат «число, знак, число» не справится с 2 + 3 * 4. Для выражений с приоритетом операций пишут рекурсивный спуск: на каждый уровень приоритета — своя функция. expr складывает и вычитает, term умножает и делит, factor читает число, скобки или унарный минус.

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <math.h>

static const char *p;   /* текущая позиция в строке */
static int error;       /* 1, если разбор не удался */

static double expr(void);

static void skip_spaces(void) {
    while (isspace((unsigned char)*p)) p++;
}

/* число | ( выражение ) | -множитель */
static double factor(void) {
    skip_spaces();
    if (*p == '(') {
        p++;
        double v = expr();
        skip_spaces();
        if (*p == ')') p++; else error = 1;
        return v;
    }
    if (*p == '-') { p++; return -factor(); }
    char *end;
    double v = strtod(p, &end);
    if (end == p) error = 1;
    p = end;
    return v;
}

/* множитель { (* | /) множитель } */
static double term(void) {
    double v = factor();
    for (;;) {
        skip_spaces();
        if (*p == '*') { p++; v *= factor(); }
        else if (*p == '/') {
            p++;
            double d = factor();
            if (d == 0) error = 1; else v /= d;
        }
        else return v;
    }
}

/* слагаемое { (+ | -) слагаемое } */
static double expr(void) {
    double v = term();
    for (;;) {
        skip_spaces();
        if (*p == '+') { p++; v += term(); }
        else if (*p == '-') { p++; v -= term(); }
        else return v;
    }
}

int main(void) {
    const char *tests[] = { "2 + 3 * 4", "(2 + 3) * 4", "10 / 4 - 1",
                            "-(1 + 2) * 2", "1 / 0", "2 + * 3", "(1 + 2",
                            "1e308 * 10" };
    for (size_t i = 0; i < sizeof tests / sizeof tests[0]; i++) {
        p = tests[i];
        error = 0;
        double v = expr();
        skip_spaces();
        if (error || *p != '\0' || !isfinite(v))
            printf("%-14s -> ошибка\n", tests[i]);
        else
            printf("%-14s -> %g\n", tests[i], v);
    }
    return 0;
}

Вывод:

2 + 3 * 4      -> 14
(2 + 3) * 4    -> 20
10 / 4 - 1     -> 1.5
-(1 + 2) * 2   -> -6
1 / 0          -> ошибка
2 + * 3        -> ошибка
(1 + 2         -> ошибка
1e308 * 10     -> ошибка

Умножение выполнилось раньше сложения, потому что term вызывается изнутри expr: приоритет задан вложенностью функций. Проверка isfinite в main отсекает переполнение (1e308 * 10 дает бесконечность, а не ошибку операции) и заодно введенные inf и nan: strtod принимает их как числа, так же как 1e3 и шестнадцатеричное 0x10. Это учебная версия: глобальное состояние, без степени, а слишком глубокая вложенность скобок или длинная цепочка унарных минусов переполнит стек (с -fsanitize=address на 200 000 минусах — stack-overflow). Десятичный разделитель в strtod зависит от локали, если программа вызывает setlocale.

Степень добавляется функцией pow из <math.h>. На Linux с GCC и glibc для нее нужен флаг -lm при сборке (без него на Ubuntu 24.04 — undefined reference to 'pow'; исключение — вызов с константами, который компилятор вычисляет сам), на macOS математическая библиотека подключена по умолчанию. Макросу isfinite флаг не нужен.

Если не получилось

  • Компилятор пишет, что не найден conio.h: это нестандартный заголовок компиляторов под DOS/Windows (Borland, MSVC), в Linux и macOS его нет, а clrscr() был только у Borland. Для калькулятора он не нужен — уберите #include <conio.h>, getch() и clrscr().
  • Ошибка компоновки со ссылкой на pow: добавьте -lm в конец команды сборки.
  • Вместо 3.5 выводится 3: числа объявлены как int.
  • Программа зацикливается после ввода буквы: scanf в цикле без очистки ввода, переходите на fgets + sscanf.
  • Выполняются две операции подряд: пропущен break.

Выводы

  • Калькулятор на C строится из трех частей: ввод с проверкой результата scanf, выбор операции через switch, вывод через printf.
  • Числа храните в double, иначе 7 / 2 даст 3; делитель проверяйте до операции.
  • У каждой ветки case нужен break; GCC предупреждает о проваливании уже с -Wextra, Clang — только с -Wimplicit-fallthrough.
  • Для диалога в цикле читайте строку через fgets и разбирайте sscanf — так мусорный ввод не зацикливает программу.
  • Выражения с приоритетом и скобками разбираются рекурсивным спуском: по функции на уровень приоритета.

Где применяется / связь с практикой

Освойте тему на практике

Разбор ввода, обработка ошибок и коды возврата — то же, что в утилитах командной строки и парсерах конфигов; рекурсивным спуском устроены многие разборщики форматов. Если хочется двигаться от учебных программ к системному коду на C — работе с памятью, процессами, файлами и сетью в Linux, — посмотрите курс «Системное программирование». Разобрать отдельные темы бесплатно можно на открытых уроках Otus.

FAQ

Почему scanf("%d", &x) без проверки считается плохой практикой?
Если ввод не число, scanf не изменит переменную: в неинициализированной останется мусор, и программа продолжит считать с ним. Возвращаемое значение показывает, сколько полей реально прочитано.

Как добавить остаток от деления?
Для целых — оператор % (только с целыми типами), для double — функция fmod из <math.h>. Делитель также проверяйте на ноль.

Можно ли сделать калькулятор с окном и кнопками на C?
Да, через библиотеку интерфейса, например GTK. Логика расчета та же, вместо scanf — обработчики нажатий.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)