Как программа выбирает, что делать, и как она отказывается работать с неверными данными
if, if … else, цепочкой else if и switchscanf, диапазоны, сообщение и код завершения 1gcc -std=c11 -Wall -Wextra -pedantic файл.c -o файл.Условие — выражение, значение которого понимается как «да» или «нет». В C нет отдельного логического значения в обычных выражениях: ноль — ложь, любое другое значение — истина. Операции сравнения дают int: 1, если верно, 0, если нет.
| Операция | Смысл | Пример при a = 5 |
|---|---|---|
== | равно | a == 5 → 1 |
!= | не равно | a != 5 → 0 |
< <= | меньше, не больше | a < 5 → 0, a <= 5 → 1 |
> >= | больше, не меньше | a > 3 → 1 |
Для читаемости есть <stdbool.h>: тип bool и значения true (1) и false (0). bool is_leap = year % 4 == 0; читается лучше, чем int с тем же смыслом. Курс использует bool для переменных-флагов.
if, else и блокif (условие) {
инструкции, если условие истинно
} else {
инструкции, если ложно /* ветка else может отсутствовать */
}
Фигурные скобки объединяют инструкции в блок (составной оператор): для if блок — одна инструкция, сколько бы строк в нём ни было. Пример — корни квадратного уравнения ax² + bx + c = 0 при a ≠ 0:
/* Корни квадратного уравнения */
#include <stdio.h>
#include <math.h>
int main(void)
{
double a = 0, b = 0, c = 0;
printf("a b c: ");
if (scanf("%lf %lf %lf", &a, &b, &c) != 3 || a == 0) {
printf("Error: need three numbers, a != 0\n");
return 1;
}
double d = b * b - 4 * a * c;
if (d < 0) {
printf("No real roots\n");
} else if (d == 0) {
printf("One root: %.4f\n", -b / (2 * a));
} else {
double root = sqrt(d);
printf("Two roots: %.4f %.4f\n", (-b - root) / (2 * a), (-b + root) / (2 * a));
}
return 0;
}
a b c: 1 -3 2 -> Two roots: 1.0000 2.0000
a b c: 1 2 1 -> One root: -1.0000
a b c: 1 0 1 -> No real roots
a b c: 0 2 1 -> Error: need three numbers, a != 0
Переменная root объявлена внутри блока и видна только в нём: после закрывающей скобки её нет. Объявлять переменную там, где она нужна, — правило курса: так её не перепутать с другой и не использовать там, где у неё нет смысла.
d == 0 для вещественных. Здесь сравнение с нулём допустимо только потому, что пример учебный: при a, b, c, вычисленных с погрешностью, дискриминант «почти ноль» превратится в два почти равных корня. В практической работе сравнение вещественных — с допуском (раздел 3, пункт 04): fabs(d) < EPS.if относится только первая инструкция; вторая, добавленная позже с тем же отступом, выполнится всегда.| Операция | Значение | Пример |
|---|---|---|
&& (и) | 1, если истинны оба операнда | x >= 1 && x <= 10 — x в отрезке [1; 10] |
|| (или) | 1, если истинен хотя бы один | x < 1 || x > 10 — x вне отрезка |
! (не) | 1, если операнд ложен | !(x >= 1 && x <= 10) — то же, что строкой выше |
Математическая запись 1 ≤ x ≤ 10 в C — только через &&. 1 <= x <= 10 собирается, но значит другое: (1 <= x) <= 10, а результат сравнения — 0 или 1, что всегда ≤ 10. gcc предупреждает: comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses].
Сокращённое вычисление. && и || вычисляют левый операнд первым и не вычисляют правый, если результат уже ясен: у && — когда левый ложен, у || — когда левый истинен. На этом строятся защитные условия:
if (count != 0 && sum / count > 100) { … } /* деления на 0 не будет */
if (scanf("%d", &n) != 1 || n < 1) { … } /* n проверяется, только если прочитано */
Порядок важен: в sum / count > 100 && count != 0 деление выполнится раньше проверки. Законы де Моргана помогают переписать отрицание: !(a && b) = !a || !b, !(a || b) = !a && !b. «Не (x от 1 до 10)» — «x меньше 1 или больше 10», а не «и».
Високосный год — классическое условие из трёх частей: год делится на 4 и не делится на 100, или делится на 400:
bool is_leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
2024 — високосный, 2100 — нет (делится на 100), 2000 — да (делится на 400), 2023 — нет. && выполняется раньше ||, поэтому скобки здесь не обязательны — но с ними условие читается без таблицы приоритетов, и gcc без них предупреждает suggest parentheses around '&&' within '||'.
else if и вложенные ветвленияКогда вариантов больше двух, ветвления выстраивают в цепочку: условия проверяются сверху вниз, выполняется ветка первого истинного, остальные пропускаются. Оценка по баллам по шкале курса:
/* Оценка по баллам (0..100) */
#include <stdio.h>
int main(void)
{
int score = 0;
printf("Score: ");
if (scanf("%d", &score) != 1 || score < 0 || score > 100) {
printf("Error: score must be 0..100\n");
return 1;
}
int mark;
if (score >= 85) {
mark = 5;
} else if (score >= 70) {
mark = 4;
} else if (score >= 50) {
mark = 3;
} else {
mark = 2;
}
printf("Mark: %d\n", mark);
return 0;
}
Score: 85 -> Mark: 5 Score: 84 -> Mark: 4 Score: 50 -> Mark: 3
Score: 49 -> Mark: 2 Score: 0 -> Mark: 2 Score: 101 -> Error: score must be 0..100
Во второй ветке не нужно писать score >= 70 && score < 85: до неё доходят только баллы меньше 85. Поэтому порядок проверок важен — если начать с score >= 50, все от 50 до 100 получат «3». Тесты — на границах каждого условия: 85 и 84, 70 и 69, 50 и 49, 0 и 100, и за пределами: −1 и 101.
Вложенное ветвление — if внутри ветки другого if. Без фигурных скобок else относится к ближайшему if, а не к тому, под которым он стоит по отступу:
if (x > 0)
if (y > 0)
printf("first quarter\n");
else /* относится к if (y > 0)! */
printf("x <= 0\n");
При x = 1, y = −1 программа выведет x <= 0. gcc предупреждает: suggest explicit braces to avoid ambiguous 'else' [-Wdangling-else]. Скобки у каждой ветки снимают проблему.
switch: выбор по значениюКогда действие выбирается по конкретному значению целого выражения (номер дня, код команды, символ операции), удобен switch:
/* Калькулятор: число, операция, число */
#include <stdio.h>
int main(void)
{
double a = 0, b = 0;
char op = 0;
printf("a op b: ");
if (scanf("%lf %c %lf", &a, &op, &b) != 3) {
printf("Error: expected a op b\n");
return 1;
}
switch (op) {
case '+':
printf("%g\n", a + b);
break;
case '-':
printf("%g\n", a - b);
break;
case '*':
case 'x': /* два значения — одно действие */
printf("%g\n", a * b);
break;
case '/':
if (b == 0) {
printf("Error: division by zero\n");
return 1;
}
printf("%g\n", a / b);
break;
default:
printf("Error: unknown operation '%c'\n", op);
return 1;
}
return 0;
}
a op b: 7 / 2 -> 3.5
a op b: 3 x 4 -> 12
a op b: 1 / 0 -> Error: division by zero
a op b: 2 ^ 3 -> Error: unknown operation '^'
switch — целое (в том числе char); метки case — константы. Строки, double и диапазоны («от 1 до 10») в case не пишутся — для них цепочка else if.switch переходит к подходящей метке и выполняет всё до break, проходя через следующие метки. Без break выполнение «проваливается» в следующий case. Намеренное проваливание — несколько меток подряд без инструкций между ними, как '*' и 'x'.default — ветка для всех остальных значений; в курсе она обязательна.break — самая частая ошибка switch: при op = '+' без первого break программа выведет и сумму, и разность. gcc с -Wextra предупреждает this statement may fall through [-Wimplicit-fallthrough=]. Если проваливание после инструкций задумано, перед следующей меткой ставят комментарий /* fall through */ — gcc его понимает и не предупреждает, а читатель видит, что break пропущен не случайно.?:условие ? значение1 : значение2 — выражение: если условие истинно, его значение — значение1, иначе значение2. Удобно, когда нужно выбрать значение, а не действие:
int max = (a > b) ? a : b;
printf("%d item%s\n", count, (count == 1) ? "" : "s"); /* 1 item, 5 items */
Вложенные ?: читаются плохо: a > b ? (a > c ? a : c) : (b > c ? b : c) — это максимум из трёх, но понять это можно только трассировкой. Если выбор сложнее двух значений — if.
С этого раздела каждая программа курса проверяет ввод: результат scanf и все ограничения задачи. При ошибке — сообщение и завершение с кодом 1 (раздел 1, пункт 05): так скрипт или другая программа, запустившая эту, узнает о неудаче.
/* Шаблон: ввод с проверкой */
int students = 0, seats = 0;
printf("Students and seats: ");
if (scanf("%d %d", &students, &seats) != 2) {
printf("Error: two integers expected\n");
return 1;
}
if (students < 1 || seats < 1) {
printf("Error: both numbers must be >= 1\n");
return 1;
}
/* дальше данные гарантированно допустимы */
seats < 1 — её прямой перевод.| Ошибка | Что происходит | Предупреждение gcc |
|---|---|---|
if (x = 5) вместо == | x становится 5, условие всегда истинно | suggest parentheses around assignment used as truth value [-Wparentheses] |
if (x > 0); — точка с запятой | пустое тело; блок ниже выполняется всегда | suggest braces around empty body in an 'if' statement [-Wempty-body] |
1 <= x <= 10 | всегда истинно | comparisons like 'X<=Y<=Z' … [-Wparentheses] |
else без скобок у вложенного if | else относится к ближайшему if | -Wdangling-else |
нет break в case | выполняется и следующий case | -Wimplicit-fallthrough |
d == 0.3 для double | ложь из-за погрешности | нет (есть -Wfloat-equal, не входит в -Wall) |
неверный порядок в цепочке else if | нижние ветки недостижимы | нет |
граница > вместо >= | неверный результат ровно на границе | нет |
bool&& и || не вычисляют правый операнд, если результат ясен по левому — так строятся защитные условияx >= a && x <= b, не a <= x <= belse if выполняется первая истинная ветка; порядок проверок важенswitch — по значению целого выражения; break в каждом case, default обязателенscanf и ограничения до вычислений; при ошибке — сообщение и код 1double, char как число; раздел 4 — выражения, приоритеты, результат scanf. Нужен для: раздела 6 — условие цикла, проверка ввода в цикле, break; всех практических работ дальше — проверка ввода обязательна.