Булевый тип. Операции сравнения. Логические И, ИЛИ, НЕ

Практический курс по C/C++: https://stepik.org/course/193691

На этом занятии вы увидите, как в программах можно делать логические выводы на уровне:

  • true – истина;
  • false – ложь.

Так как компьютер – это вычислительная машина, то понятия «истина» и «ложь» должны выражаться на уровне чисел. В большинстве, а может быть во всех языках программирования, значение false определяется как 0, а true – как любое ненулевое значение. Поэтому константы true и false часто  определяют как:

true = 1; false = 0

До стандарта C99 язык Си не предполагал наличия какого-либо специального булевого типа данных. Вместо этого использовался любой целочисленный тип, и если значение переменной равнялось нулю, это означало false (ложь), а любое другое значение, отличное от нуля – true (истина). Например, так:

char fl_view = 0;        // false
int fl_open_file = 1;     // true

Но стандарт C99 предоставляет нам новый тип (новое ключевое слово):

_Bool

И булевы переменные стало возможно определять следующим образом:

_Bool fl_view = 0;           // false

На самом деле, тип _Bool определен на базе обычного целочисленного типа, который соответствует наименьшему допустимому размеру, то есть, char. В этом легко убедиться, воспользовавшись оператором sizeof:

#include <stdio.h>
 
int main(void)
{
    _Bool fl_view = 0;       // false
    printf("Size of _Bool: %d\n", sizeof(_Bool));
 
    return 0;
}

После запуска программы увидим строчку:

Size of _Bool: 1

К сожалению, ключевые слова true или false по-прежнему не определены. Поэтому булевой переменной fl_view нужно явно присваивать значение 0 в качестве false и 1 – для true. Причем, никакие другие значения она принимать не может. Если попытаться присвоить, например, число 10:

_Bool fl_view = 10;

то fl_view его преобразует к единице. И, вообще, любое ненулевое значение приравнивается единице. В этом ключевое отличие типа _Bool, например, от типа char, который позволяет представлять любые целые числа в диапазоне [0; 255].

А теперь внимательнее посмотрим на написание этого типа. Вначале идет символ подчеркивания, а затем, с заглавной буквы слово «Bool». Довольно неудобная и «корявая» запись. Дело в том, что это была первая официальная попытка определить булевый тип в языке Си. И никто тогда точно не мог сказать, как эта идея будет воспринята сообществом программистов. Кроме того, для совместимости с прежними компиляторами, этот тип можно было легко заменить на любой другой базовый целочисленный, например, char. И программы бы компилировались без проблем.

В действительности, именно в такой записи тип _Bool практически не использовался на практике. Позже его подменили более изящным словом bool. И, кроме того, такой тип официально появился в языке С++, который является естественным развитием языка Си. Так вот, чтобы в программах на языке Си в соответствии со стандартом C99 можно было бы использовать более приятную и общеупотребительную запись булевого типа bool, следует подключить заголовочный файл stdbool.h:

#include <stdbool.h>

В нем не только переопределен тип _Bool как bool, но и введены две константы:

true = 1; false = 0

Поэтому переменную fl_view в нашей программе теперь можно определить так:

bool fl_view = true;

Эта строчка выглядит естественнее и понятнее для программиста, чем корявый тип _Bool и числа 0 или 1.

Операции сравнения

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

Операция

Описание

==

(Два равно). Сравнение на равенство.

!=

Сравнение на неравенство.

<

Сравнение на меньше.

>

Сравнение на больше.

<=

Сравнение на меньше или равно.

>=

Сравнение на больше или равно.

Все эти операции являются бинарными, то есть, слева и справа от них прописываются выражения (операнды):

<левый операнд> <операция сравнения> <правый операнд>

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

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

0 – false; 1 – true.

Например:

#include <stdio.h>
#include <stdbool.h>
 
int main(void)
{
    double x = 5.67;
    bool fl_view = x < 0;
    
    printf("%d\n", fl_view);
 
    return 0;
}

Так как в нашем примере x больше нуля, то операция сравнения на отрицательное значение вернет 0, которое соответствует понятию «ложь» (false). А вот если вместо нуля указать, например, значение 10:

bool fl_view = x < 10;

то переменная fl_view будет равна 1, что соответствует понятию «истина» (true). И так работают все операции сравнения:

double x = 5.67;
int var_i = 7;
 
bool fl_view = x < 10;      // true
bool res_1 = 5 > 7;         // false
bool res_2 = x+2 >= 10.56;    // false
bool res_3 = var_i == 7;    // true
bool res_4 = var_i != 7;    // false

Причем, приоритет операций сравнения выше приоритета операции присваивания. Поэтому сначала выполняются сравнения и только потом – присваивания. А арифметические операции выше операций сравнения, поэтому x+2 будет выполнено до сравнения на больше или равно. Кроме того, обратите внимание на операцию сравнения на равенство. Она записывается как два символа равно (==). Как мы знаем, одно равно – это операция присваивания, поэтому для сравнения на равенство ввели обозначение из двух символов равно. И здесь начинающие программисты очень часто делают ошибку. Для сравнения на равенство значений они пишут не два, а, по привычке, одно равно, например, так:

bool res_3 = var_i = 7;

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

Также следует помнить, что операции:

<= и >=

нужно прописывать именно в таком виде. Иногда, правда редко, меняют местами символы и пытаются прописать:

=< и => (неверная запись)

Это приведет к синтаксической ошибке.

Давайте в качестве короткого примера приведу программу ввода числового значения и определение четности числа:

#include <stdio.h>
#include <stdbool.h>
 
int main(void)
{
    int digit;
    scanf("%d", &digit);
 
    bool even = digit % 2 == 0;
    printf("%d\n", even);
 
    return 0;
}

После запуска и ввода целого значения мы увидим 0 – для нечетных чисел и 1 – для четных.

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

Давайте теперь поставим более сложную задачу и определим, попадает ли число (значение переменной) в диапазон [-2; 5]? Какие логические умозаключения здесь нужно провести, чтобы ответить на этот вопрос? Для простоты представим, что у нас имеется переменная:

double y = 1.85;

Очевидно, чтобы она попадала в диапазон [-2; 5], нужно соблюдение двух условий:

y >= -2

и

y <= 5

В языке Си это можно реализовать следующим образом:

bool is_range = y >= -2 && y <= 5;

Здесь операция && означает логическое И. При этом общее условие истинно, если истинно каждое из подусловий: y >= -2 и y <= 5. Благодаря этому мы будем получать значение true (единица), если y принадлежит диапазону [-2; 5] и false (ноль) в противном случае.

А теперь сделаем противоположную проверку, что переменная y не попадает в диапазон [-2; 5]. Очевидно, это будет происходить, если:

y < -2 или y > 5

В языке Си такая составная операция сравнения может быть записана в виде:

bool is_not_range = y < -2 || y > 5;

Операция || означает логическое ИЛИ и возвращает истину (true), если истинно хотя бы одно из подусловий. В нашем примере, это проверка, что y или меньше -2 или больше 5. Часто начинающие программисты здесь делают логическую ошибку и подобное сравнение записывают с использованием операции И следующим образом:

bool is_not_range = y < -2 && y > 5;

В этом случае я прошу назвать число, которое одновременно меньше -2 и больше 5. Как правило, после этого в головах все встает на свои места. Не путайте принцип действия этих двух операций: логическое ИЛИ и логическое И.

На самом деле противоположную проверку непопадания в диапазон [-2; 5] можно было бы реализовать путем инвертирования ранее вычисленного значения is_range следующим образом:

bool is_not_range = !is_range;

Здесь восклицательный знак – это унарная операция НЕ, которая может быть применена к любому выражению. Принцип ее работы заключается в инвертировании булевого значения:

true -> false; false->true.

Приоритеты операций И, ИЛИ, НЕ

Приоритеты всех этих трех логических операций следующие:

Логическое ИЛИ (||)

1

Логическое И (&&)

2

Логическое НЕ (!)

3

То есть, наибольший приоритет имеет унарная операция НЕ, затем, операция И, и самый низкий – у операции ИЛИ.

Все эти приоритеты необходимо строго соблюдать для составления корректных условий. Например:

int x = 5;
bool is_correct = x % 2 == 0 || x % 3 == 0 && x > 5;

Это составное условие эквивалентно следующему:

bool is_correct = x % 2 == 0 || (x % 3 == 0 && x > 5);

то есть, сначала проверяется, что число x кратно 2 (четное) ИЛИ число кратно 3 и при этом больше 5. Обратите внимание здесь на два важных момента. Во-первых, стандартом языка Си определен строгий порядок проверок слева-направо при вычислении составных логических операций. Это значит, мы можем быть абсолютно уверены, что сначала выполнится проверка x % 2 == 0 и только после этого следующее подусловие x % 3 == 0 && x > 5. Причем, в нем также сначала проверяется первое x % 3 == 0 и только потом второе x > 5. Во-вторых, если в процессе проверки значение всей составной логической операции становится известным, то вычисления прерываются и не идут дальше. Например, в условии:

bool fl_digit = x != 0 && 10 / x > 1;

сначала будет проверено, что переменная x не равна нулю, и если это не так (то есть это первое подусловие равно false), то дальше проверки делать не имеет смысла, т.к. при любом булевом значении второго подусловия (10/x > 1) общее все равно будет принимать значение false. Благодаря такому поведению, мы можем без проблем вычислять деление 10 / x после проверки, что x != 0. Этим на практике довольно часто пользуются.

Но все это нужно применять очень аккуратно. Например, если прописать следующее составное условие:

bool is_read = x < 0 && scanf("%d", &x) == 1;

то функция scanf() не будет вызвана, если x больше или равен нулю.

И последнее, важный, хотя и очевидный момент. Если нам нужно поменять приоритеты операций, то для этого можно использовать все те же круглые скобки. Например:

bool is_correct = (x % 2 == 0 || x % 3 == 0) && x > 5;

Теперь это условие будет истинно, если x кратно 2 или 3 и больше 5.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме