Консольный калькулятор на 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 — обработчики нажатий.



