Лекция 5. Логические выражения

Логический тип данных

Коснёмся подробнее логических выражений. Важнейшим отличием Си от, скажем, Pascal или Python, которые имеют встроенный логический тип данных, является то, что логические выражения в Си – просто целые числа типа int.

0
ложь
любое ненулевое число
истина

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

Вооружившись этим знанием, можно показать, что код

if (x != 0) {
    printf("x is not zero\n");
}

полностью эквивалентен коду

if (x) {
    printf("x is not zero\n");
}

Да, целое число может запросто стоять в условии if-а, ведь оно того же типа, что и результат логического оператора !=.

Операторы сравнения

<, >, <=, >= (сравнения)

== (равенство)

!= (неравенство)

Все эти операторы возвращают 0 в случае неверного сравнения и 1 в случае верного. Этот результат мы можем потом использовать в другом выражении или сохранить в переменной.

int a;
a = 5 > 10; // Сохранили в переменную a 0 (ложь)
a = 5 < 10; // 1 (истина)
a = 5 < 10; // 1 (истина)
a = 5 * 5 < 10; // Умножение имеет более высокий приоритет, чем оператор <,
                // в a будет записан результат 25 < 10, то есть 0.

// Используем скобки, чтобы изменить порядок выполнения операций
a = 5 * (5 < 10); // 5
a = 5 * (5 < 10); // 0

Примечание. Ошибки, связанные с опертором ==

Рассмотрим следующий, на первый взгляд простой и безобидный, оператор if:

if (a == 0) {
    // код выполнится, если a равно нулю
}

Так как в математике для сравнения и присвоения используется один и тот же оператор, то часто можно не заметить ошибки и написать в условии одинарное равенство вместо двойного. Такая ошибка приведёт к весьма драматическим последствиям. Код по-прежнему продолжит компилироваться, однако работать он будет совсем не так, как вы предполагаете.

if (a = 0) {
    // код здесь никогда не выполнится,
    // так как результатом присвоения является то,
    // что было присвоено. В данном случае это 0,
    // то есть ложь.
}
// В этой точке программы в a лежит ноль.

Как видим, у нас из ниоткуда взялись сразу две проблемы. Главная ветка if-а никогда не выполнится, альтернативная (если она есть) будет выполняться, наоборот, всегда. И ещё в переменную a записан ноль, хотя мы этого не планировали.

Если вместо нуля стоит какая-нибудь ненулевая константа (логическая истина), то после её присвоения переменной всегда будет выполняться альтернативная ветвь и никогда главная.

Если присваивается переменная или выражение, то будет наблюдаться одно из описанных выше ошибочных поведений.

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

Есть два способа существенно уменьшить вероятность такой ошибки:

  1. Если вы пользуетесь компилятором gcc, всегда компилируйте свои программы с флагом компиляции -Wall (он включает все предупреждения компилятора, сокращение от Warnings All). Компилятор начнёт выдавать предупреждения в тех местах, где нет ошибок, но которые компилятор считает подозрительными. В частности, компилятор попросит вас поставить лишнюю пару скобок вокруг присваивания в if-ах, если вы действительно хотите присваивания. Увидев это предупреждение, можно исправить такую ошибку.

  2. Если сравнение происходит с константой, то можно константу поставить слева, а переменную справа. Тогда, если забыть один из знаков =, компилятор выдаст ошибку, ведь константе нельзя присвоить новое значение:

    if (0 == a) {
        // ...
    }
    

    К сожалению, этот способ не работает, когда с обоих сторон от == стоят переменные. Но тем не менее, он всё же позволяет отловить много ошибок.

Логические операторы

Логические операторы принимают в качестве операндов целые числа. Ноль расценивается как ложь, любое ненулевое число – как истина. Как результат всегда возвращается либо ноль (если результат ложен), либо единица (результат истинен).

Логическое отрицание ведёт себя стандартно:

x !x
0 1
1 0

Примеры некоторых выражений:

!0      // 1
!1      // 0
!!123   // 1
!!0     // 0

Логическое “и”, оператор &&, истинно, когда оба оператора истинны:

x y x && y
0 0 0
0 1 0
1 0 0
1 1 1

Примеры:

0 && 0       // 0
12 && 0      // 0
0 && 123     // 0
123 && 456   // 1

Логическое “или”, оператор ||, истинно, когда оба оператора истинны:

x y x || y
0 0 0
0 1 1
1 0 1
1 1 1
0 && 0       // 0
12 && 0      // 1
0 && 123     // 1
123 && 456   // 1

Приоритеты операторов

Приоритеты операторов показаны на рисунке. Как видно, круглые скобки имеют наибольший приоритет, так как предназначены для повышения приоритетов других операторов. Далее по порядку уменьшения приоритета идут * и /, как в математике, затем + -. Далее идут операторы сравнения, которые выполняются до любых логических операторов. Далее, как в алгебре логики, в порядке уменьшения приоритета: !, &&, ||. Наименьший приоритет имеет присваивание.

_images/priorities.png

На рисунке выше разобран порядок выполнения подвыражений в выражении.

Также обратите внимание, что выражения вида x > y > z, x == y == z вычисляются совсем не так, как в математике. Сначала вычисляется левый оператор, на его место подставляется 0 или 1, затем вычисляется правый. Например:

1000 > 100 > 10     // -->>   1 > 10    -->>    0   (ложь)

10 == 10 == 10      // -->>   1 == 10   -->>    0   (ложь)

Правильной записью для математических выражений в этом случае было бы x > y && y > z и x == y && y == z.

Ленивые операции

Операторы && и || в Си являются “ленивыми”: если после вычисления левого операнда результат операции известен, правый операнд не вычисляется.

Например, выражение1 && выражение2 сначала вычислит выражение1, и, если его результат 0, то второе выражение не вычисляется и результатом логического “и” является 0 (ложь). Так делается потому, что если ложно выражение1, то вне зависимости от результата выражения2 результат всего логического “и” будет ложен.

Точно так же, для кода выражение || выражение2, если результат выражения1 не равен 0, то есть истинен, выражение2 не вычисляется и результат логического “или” равен 1.

Цикл do-while

Цикл while часто называют циклом с предусловием, так как условие в нём вычисляется до тела цикла. Кроме него в Си есть цикл с постусловием, который вычисляет условия после выполнения цикла:

do {
    // Тело цикла
} while(условие);

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

Оператор break

_images/break.png

Break выходит из цикла for, while или do-while. Если он вложен в несколько циклов, то он выйдет только из одного цикла, того, в который он непосредственно вложен.

Бесконечный цикл

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

while(1) {
    // Инструкции в бесконечном цикле
}

Обычно из бесконечного цикла предусмотрен выход где-нибудь в середине тела цикла через break.

Еще один способ записи тела цикла – не указать второе выражение в операторе for:

for(;;) {
    // Инструкции в бесконечном цикле
}

Если у вас случайно получился бесконечный цикл из-за ошибки в условии продолжения цикла, то можно выйти из него, нажав Ctrl+C.