Menu

Логический тип в C: stdbool.h, _Bool и истинность значений

До C99 в C не было логического типа. Ноль — ложь, всё остальное — истина, сравнения возвращают int, а stdbool.h добавляет сверху bool, true и false. Плюс знаменитый баг с = вместо ==, который порождён этой схемой.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Первые 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).

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ