Первые 27 лет своей жизни C прожил без логического типа. Это не упущение, а осознанное решение, последствия которого встречаются вам в первый же день, и один знаменитый баг, который следует из него напрямую.
Ноль — ложь, всё остальное — истина
В правилах вычисления условий в C нет никаких особых значений «истина» и «ложь». if, while, for, &&, || и ! задают ровно один вопрос: равно ли это значение нулю?
Отрицательные числа истинны. Дроби истинны. Ненулевой указатель истинен. Ложен только ноль — в любом типе.
Поэтому код на C полон идиом, которые выглядят слишком лаконично, пока вы не знаете правила:
if (count) /* если count не ноль */
if (!count) /* если count равен нулю */
if (p) /* если p не NULL */
if (!strcmp(a, b)) /* если строки равны - strcmp возвращает 0 при совпадении */
Последняя строка сбивает с толку. strcmp возвращает 0, когда строки совпадают, поэтому «равны» читается в if как «не». Запись if (strcmp(a, b) == 0) понятнее и делает ровно то же самое.
Сравнения дают int
Сравнение в C — не какое-то особое выражение. Оно вычисляется в обычный int со значением 1 или 0, и его можно вывести, сохранить или использовать в арифметике:
Оператор ! работает так же: он даёт 1, если его операнд равен нулю, и 0 в противном случае. Поэтому !!x — классическая идиома, приводящая любое значение ровно к 0 или 1.
bool, true и false (C99)
C99 добавил настоящий логический тип _Bool и заголовок, дающий ему читаемое имя:
<stdbool.h> крошечный: он определяет bool как _Bool, true как 1 и false как 0. Это весь заголовок целиком. Но с ним намерение становится видимым так, как int flag = 1; не покажет никогда.
У _Bool есть одно по-настоящему отличающееся поведение: он хранит только 0 или 1. Присваивание любого ненулевого значения превращается ровно в 1:
В printf по-прежнему нет спецификатора %b для логических значений — выводите их через %d или преобразуйте в текст сами:
В C23 всё стало ещё проще: bool, true и false стали настоящими ключевыми словами, так что подключать заголовок больше не требуется. Но сегодня большинство кода всё ещё ориентируется на C17, поэтому оставляйте #include <stdbool.h>.
Возврат логического значения из функции
Именно здесь bool оправдывает себя нагляднее всего:
Возвращаемый тип bool сообщает вызывающей стороне, что означает значение. Возврат int из функции с именем check_file может быть логическим значением, счётчиком или кодом ошибки — и по традициям библиотек C это чаще всего третье, где 0 означает успех. Названный тип избавляет от догадок.
Баг с = вместо ==
Поскольку присваивание — это выражение, дающее присвоенное значение, а любое ненулевое значение истинно, вот это компилируется:
Здесь сразу две беды. Условие присвоило x значение 5, уничтожив прежнее, а затем проверило 5, которая не равна нулю, поэтому ветка выполнилась. Программа не выдаёт ни ошибки, ни предупреждения по умолчанию.
Хуже того, вариант с 0 не выполняет свою ветку никогда:
if (found = 0) { /* никогда не выполнится - и found теперь 0 */ }
Три способа защиты, по убыванию полезности:
Компилируйте с предупреждениями. -Wall это ловит:
warning: suggest parentheses around assignment used as truth value
Довести число предупреждений до нуля — это и есть настоящее решение, и стоит оно одного флага.
Пишите константу первой. «Условие Йоды» превращает опечатку в ошибку компиляции:
if (5 == x) /* верно */
if (5 = x) /* ошибка: это не lvalue - компилятор вас остановит */
Одни команды это обожают, другим кажется, что так нечитаемо; обе позиции защитимы, но приём действительно работает.
Пишите так осознанно, когда вы этого хотите. Присваивание внутри условия по-настоящему полезно при чтении ввода:
int c;
while ((c = getchar()) != EOF) { ... }
Дополнительные скобки вокруг c = getchar() нужны из-за приоритета операций, и заодно они сигнализируют, что присваивание сделано намеренно. Это и есть идиома: если ваше присваивание в условии выглядит иначе, это скорее всего опечатка.
Частые ошибки с логическими значениями
Сравнение с true. if (flag == true) работает вместе с stdbool.h, но ломается в тот момент, когда flag оказывается int со значением 42, потому что 42 == 1 — ложь. Пишите if (flag).
Цепочки сравнений. if (0 < x < 10) истинно всегда: 0 < x даёт 0 или 1, и оба меньше 10. Пишите if (x > 0 && x < 10).
Побитовые операторы вместо логических. & и | — это не && и ||. Они не используют сокращённое вычисление и комбинируют биты:
Оба операнда «истинны», и всё же a & b равно 0. Использование & там, где имелось в виду &&, порождает условие, которое неверно лишь при некоторых значениях, — а это самый трудноуловимый вид бага.
Сравнение чисел с плавающей точкой на равенство. if (0.1 + 0.2 == 0.3) ложно. Сравнивайте с допуском — почему, объясняется в разделе типы данных.
Часто задаваемые вопросы
Есть ли в C логический тип?
Начиная с C99 — да: _Bool встроен в язык, а подключение <stdbool.h> даёт более удобные написания bool, true и false. До C99 логического типа не было вовсе — программы использовали int, где 0 означал ложь, а 1 истину; так по-прежнему можно писать, и так до сих пор часто пишут.
Что считается истиной в C?
Любое ненулевое значение. 1, -5, 0.01 и ненулевой указатель — всё это истина в условии; ложью являются только 0, 0.0 и нулевой указатель. Отдельной логической проверки нет: if (x) просто выясняет, отличается ли x от нуля.
Как пользоваться типом bool в C?
Добавьте #include <stdbool.h>, и bool ready = true; заработает так, как вы и ожидаете. Под капотом bool — это _Bool, который хранит только 0 или 1: присваивание любого ненулевого значения записывает в него ровно 1.
В чём разница между = и == в C?
= присваивает, == сравнивает. if (x = 5) присваивает x пятёрку, а затем проверяет 5, которая не равна нулю, поэтому ветка выполняется всегда. if (x == 5) проверяет равенство. Первый вариант компилируется без ошибки — именно поэтому существуют и -Wall, и привычка писать if (5 == x).