§ 5. Управляющие конструкции

§ 5.1 Операторы сравнения и логические связки

Для сравнения чисел будем пользоваться следующими операторами.

Оператор

Описание

==

Равно

!=

Не равно

>

Больше

>=

Больше или равно

<

Меньше

<=

Меньше или равно

Условия могут содержать логические операторы.

Оператор

Описание

&&

И

||

ИЛИ

!

НЕ

В языке C до стандарта C99 не было логического типа, поэтому условились считать 0 логической ложью, а все остальные числа — истиной. В C99 можно подключить заголовочный файл <stdbool.h> и пользоваться типом bool, значениями которого являются true и false, как в C++.

§ 5.2 Ветвления

Как и во многих языках, для этого служит if.

if (foo > 0)
{
    printf("Больше нуля"); // Если foo больше 0, то будет выведено "Больше нуля".
}

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

if (foo > 0)
    printf("Больше нуля");

Блок «иначе» оформляется следующим образом.

if (foo > 0)
{
    printf("Больше нуля");
}
else
{
    printf("Меньше или равно нулю");
}

Можно строить и более сложные конструкции.

if (foo == 0)
{
    printf("Равно нулю");
}
else if (foo > 0)
{
    printf("Больше нуля");
}
else
{
    printf("Меньше нуля");
}

Вполне распространена ситуация, когда в зависимости от условия нужно присвоить переменной то или иное значение.

if (foo > 0)
{
    bar = 1;
}
else
{
    bar = -1;
}

В таком случае помогает тернарный условный оператор.

bar = (foo > 0 ? 1 : -1);

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

switch (month)
{
case 1:  printf("Январь");   break;
case 2:  printf("Февраль");  break;
case 3:  printf("Март");     break;
case 4:  printf("Апрель");   break;
case 5:  printf("Май");      break;
case 6:  printf("Июнь");     break;
case 7:  printf("Июль");     break;
case 8:  printf("Август");   break;
case 9:  printf("Сентябрь"); break;
case 10: printf("Октябрь");  break;
case 11: printf("Ноябрь");   break;
case 12: printf("Декабрь");  break;
default: printf("Мартабрь"); break;
}

Сравните с «классическим» if.

if (month == 1)
{
    printf("Январь");
}
else if (month == 2)
{
    printf("Февраль");
}
else if (month == 3)
{
    printf("Март");
}
else if (month == 4)
{
    printf("Апрель");
}
else if (month == 5)
{
    printf("Май");
}
else if (month == 6)
{
    printf("Июнь");
}
else if (month == 7)
{
    printf("Июль");
}
else if (month == 8)
{
    printf("Август");
}
else if (month == 9)
{
    printf("Сентябрь");
}
else if (month == 10)
{
    printf("Октябрь");
}
else if (month == 11)
{
    printf("Ноябрь");
}
else if (month == 12)
{
    printf("Декабрь");
}
else
{
    printf("Мартабрь");
}

Что будет, если пропустить слово break?

switch (month)
{
case 1:  printf("Январь");
case 2:  printf("Февраль");
case 3:  printf("Март");
case 4:  printf("Апрель");
case 5:  printf("Май");
case 6:  printf("Июнь");
case 7:  printf("Июль");
case 8:  printf("Август");
case 9:  printf("Сентябрь");
case 10: printf("Октябрь");
case 11: printf("Ноябрь");
case 12: printf("Декабрь");
default: printf("Мартабрь");
}

Если month равно 9, то будет напечатано СентябрьОктябрьНоябрьДекабрьМартабрь. То есть выполнение не остановится, пока не будет найден первый break.

§ 5.3 Циклы

Цикл с предусловием выполняется следующим образом: сперва проверяется значение выражения в круглых скобках, если оно истинно, тело цикла (оно заключено в фигурные скобки) выполняется. Напечатаем все числа от 0 до 9.

int n = 10;
int i = 0;
while (i < n)
{
    printf("%d ", i);
    ++i; // Увеличиваем i на 1.
}

Если тело цикла состоит из одного выражения, то фигурные скобки ставить, опять же, необязательно.

Цикл с постусловием работает иначе: сперва выполняется тело, и только затем проверяется значение условного выражения. Для примера будем выполнять тело до тех пор, пока пользователь не введёт число больше нуля.

do
{
    scanf("%d", &foo);
} while (foo <= 0);

А вот цикл с параметром (как он традиционно называется) является синтаксическим сахаром. Синтаксический сахар — это такие конструкция языка, которые позволяют делать то, что язык уже и так умеет, но более коротким или наглядным способом. Постарайтесь запомнить, как записывается этот цикл, он используется довольно часто. Напечатаем все числа от 0 до 9.

int n = 10;
for (int i = 0; i < n; ++i)
{
    printf("%d ", i);
}

В скобках три части, разделённые точкой с запятой. Первая часть выполняется один раз в самом начале. Вторая часть — условное выражение. Третья часть выполняется после тела цикла.

Бывает так, что выполнять цикл дальше нет смысла. Для прекращения выполнения используется слово break. Если становится понятно, что продолжать текущую итерацию не нужно, то можно сразу перейти к следующей с помощью слова continue.

Иногда после каждой итерации нужно не только увеличить (уменьшить) счётчик цикла, но и совершить схожее действие с другой переменной. В этом случае поможет оператор запятая ,.

...
int n = 10;
for (int i = 0; i < n; ++i, ++j)
{
    printf("%d ", i);
}

В данном примере это не имеет значения, но не будет лишним знать, что оператор запятая , возвращает значение правого операнда.

Вот небольшая программа, которая вычисляет сумму цифр числа, пользуясь тем фактом, что остаток от деления на 10 — это самая правая цифра числа, а при делении на 10 последняя цифра числа отсекается.

int number = 0;
scanf("%d", number);
int sum = 0;
while (number > 0)
{
    sum += number % 10;
    number /= 10;
}
printf("%d", sum);

§ 5.4 Метки и goto

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

Но в код можно добавлять метки (со знаком двоеточие) и переходы между этими метками (goto).

    int i = 0;
body:
    printf("%d ", i);
    ++i;
    if (i < 10)
    {
        goto body;
    }

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

§ 5.5 Пустой оператор

Пустой оператор не является управляющей конструкцией, но с ним можно столкнуться не без последствий.

int foo = 853;
if (foo == 0);
{
    printf("Равно нулю");
}

То же может быть записано и в таком виде.

int foo = 853;
if (foo == 0);
    printf("Равно нулю");

Несмотря на форматирование, интерпретироваться код будет следующим образом. То есть надпись «Равно нулю» будет появляться всегда, независимо от значения условного выражения.

int foo = 853;
if (foo == 0)
{
    ;
}
printf("Равно нулю");

§ 5.6 Одинарное равно в условии

Отдельного упоминания заслуживает оператор = в условных выражениях (не путать с ==). Представим, что мы сделали опечатку.

int foo = 0;
if (foo = 65535) // Незамеченная опечатка.
{
    printf("Равно 65535");
}

Это покажется странным, но будет выведено «Равно 65535».

Следует различать, что делает оператор и что он возвращает. Оператор присваивания =, понятно, копирует значение. Возвращает он то же значение. Поскольку он присваивал 65535, то и вернёт 65535. А всё, что не 0, считается истиной.

Чтобы предостеречь себя от такого рода ошибок, нужно обращать внимание на предупреждения компилятора. Некоторые записывают условия зеркально (это называется нотацией Йоды). Тогда один знак равенства вызовет ошибку компиляции, ведь числу нельзя ничего присвоить.

int foo = 0;
if (65535 = foo) // Ошибка компиляции.
{
    printf("Равно 65535");
}

Почему тогда язык вообще позволяет такие конструкции? В некоторых случаях, которые мы увидим позже, они позволяют писать лаконичный код.

14 июня