Сравнения, условия и выбор

В языке C++ есть такое понятие, как логическая величина, которая принимает два значения: правда и ложь, true и false, 1 и 0. Для хранения логических величин у нас есть тип данных boolean (bool), который может принимать значения 0 (false) или 1 (true). Логические переменные часто называют флагами: если переменная равна true - флаг поднят, если false - опущен.

Сравнение


Два числа можно сравнить при помощи операторов сравнения:

  • == равенство (a == b)
  • != неравенство (a != b)
  • >= больше или равно (a >= b)
  • <= меньше или равно (a <= b)
  • > больше (a > b)
  • < меньше (a < b)

В рассмотренных выше абстрактных примерах с a и b мы получаем логическое значение, которое является результатом сравнения чисел. Пусть a = 10 и b = 20, тогда скобка (a > b) вернёт значение false, потому что a меньше b. А вот (a != b) вернёт true, т.к. а действительно не равно b.

Для связи нескольких логических величин используются логические операторы:

  • ! логическое НЕ, отрицание. Есть аналог - оператор not
  • && логическое И. Есть аналог - оператор and
  • || логическое ИЛИ. Есть аналог - оператор or
byte a = 10, b = 20;
(a > b);  // false
(a != b); // true

boolean flag = true;
flag;   // true
!flag;  // false
!(a > b); // true

//flagA = true, flagB = false;
(flagA && flagB); // false, т.к. B false
//flagA = true, flagB = true;
(flagA and flagB);  // true, т.к. оба true

//flagA = true, flagB = false;
(flagA || flagB); // true, т.к. хотя бы А true
//flagA = false, flagB = false;
(flagA or flagB);  // false, т.к. ни А ни В не true

Двойное неравенство


В языке Си не предусмотрено "двойных неравенств", то есть запись вида 0 < a < 10 приведёт к ошибке компиляции. Для записи таких условий нужно разделить неравенство на одиночные и соединить их оператором И, вот исправленный пример 0 < a && a < 10.

Сравнение float


Со сравнением float чисел всё не так просто из за особенности самой модели "чисел с плавающей точкой" - вычисления иногда производятся с небольшой погрешностью, из за этого сравнение может работать неверно! Пример из урока про вычисления:

float val1 = 0.1;
// val1 == 0.100000000

float val2 = 1.1 - 1.0;
// val2 == 0.100000023 !!!

// казалось бы, val1 == val2
// но сравнение вернёт false
if (val1 == val2);  // false

Будьте внимательны при сравнении float чисел, особенно со строгими операциями <=: результат может быть некорректным и нелогичным!

true/false и 1/0


Для программы ключевое слово true это 1, а false это 0, и их можно равноценно друг на друга заменять. Зачем тогда их придумали, если можно использовать 1 и 0 и получить более короткую запись? Эти обозначения нужны в первую очередь для удобства программиста, чтобы в большой программе сориентироваться по типам данных, т.к. true и false принято присваивать только к логическим переменным типа bool. Например, если в коде встретится var = 0; - то var может быть любым численным типом данных, а если var = false; - то мы сразу понимаем, что это флаг, и это понимание помогает быстрее сориентироваться в чужом и своём коде.

Условный оператор if


Условный оператор if (англ. "если") позволяет разветвлять выполнение программы в зависимости от логических величин, т.е. результатов работы операторов сравнения и логических переменных.

if (лог. величина) {
  // выполняется, если лог. величина - true
}

Оператор else (англ. "иначе") работает в паре с оператором if и позволяет предусмотреть действие на случай невыполнения if:

if (лог. величина) {
  // выполняется, если лог. величина - true
} else {
  // выполняется, если лог. величина - false
}

Также есть третья конструкция, позволяющая ещё больше разветвить код, называется она else if:

if (лог. величина 1) {
  // выполняется, если лог. величина 1 - true
} else if (лог. величина 2) {
  // выполняется, если лог. величина 2 - true
} else {
  // выполняется иначе 
}

Посмотрим на все эти операторы в действии в большом примере:

// при выполнения одного действия 
// внутри условия, {} не обязательны
if (a > b) c = 10;  // если a больше b, то c = 10
else c = 20;        // если нет, то с = 20

// вместо сравнения можно использовать лог. переменную
boolean myFlag, myFlag2;
// если myFlag true, то c присвоить 10
if (myFlag) c = 10;

// сложные условия
// если оба флага true, то c присвоить 10
if (myflag && myFlag2) c = 10;

// при выполнении двух и более действий
// внутри условия, {} обязательны!
if (myFlag) {
  с = 10;
  b = c;
} else {
  с = 20;
  b = a;
}

byte buttonState;
if (buttonState == 1) a = 10;       // если buttonState 1
else if (buttonState == 2) a = 20;  // если нет, но если buttonState 2
else a = 30;                        // если и это не верно, то вот

Оператор if позволяет управлять программой и создавать разветвлённые действия в зависимости от разных условий. Обратите внимание на последний блок в примере выше, там где используется else if для выбора действия в зависимости от значения одной и той же переменной. Существует оператор выбора switch, позволяющий сделать код более красивым. О нём поговорим чуть ниже.

Особенность bool


В уроке о типах данных я упоминал о том, что bool принимает значение true, если присвоить ему отличное от нуля число, то есть оператору if можно скормить любое число, и он вернёт true в любом случае, кроме нуля. Это бывает удобно в некоторых случаях, но также может и приводить к ошибкам, которые трудно отловить. if (50) {} - код в фигурных скобках будет выполнен.

Порядок условий


Порядок условий играет очень большую роль: логические выражения и переменные проверяются слева направо, и если результат всего выражения в скобках будет однозначно определён после проверки первого выражения - остальные выражения проверяться не будут. Например если в выражении if (a && b && c) хотя бы а имеет значение false, проверка остальных выражений (b и c) уже не выполняется, потому что всё выражение заведомо будет false.

Или наоборот: если в выражении if (a || b || c) хотя бы а будет true - всё выражение также будет true и b с c не будут проверяться.

Это может помочь в оптимизации кода: например, есть какой-то флаг и выражение, которое вычисляется прямо в условии и сразу проверяется. Если флаг опущен, микроконтроллер не будет тратить время на лишние вычисления и сразу покинет условие. Например:

if (flag && analogRead(0) > 500) {
  // делать что то
}

Тернарный оператор ?


Оператор знак вопроса ?, или тернарный оператор, является более коротким аналогом для записи конструкции if else. Действие с оператором ? имеет следующий вид:

условие ? выражение1 : выражение2

Это работает так: вычисляется условие, если оно истинно, то всё действие возвращает значение выражения 1, а если оно ложно, то всё действие возвращает значение выражения 2. Пример:

byte a, b;
a = 10;

// если а > 9, b получает значение 100
// иначе b получает значение 200
b = (a > 9) ? 100 : 200;

Аналогичная конструкция на if-else

a = 10;
if (a > 9) b = 100;
else b = 200;

Ещё вариант с вычислением:

byte a = 10, b = 5;
// прибавим к b результат выражения a*10 если а > 10
// иначе прибавим a+10
b += (a > 9) ? (a * 10) : (a + 10);

Аналогичным образом можно использовать оператор ? для вывода данных и текста в последовательный порт (подробнее о нём в другом уроке):

Serial.println((a > 9) ? "больше 9" : "меньше 9");

Важный момент: если результат присваивается или передаётся в функцию, тип данных должен быть одинаковый! То есть код Serial.println((a > 9) ? 9 : "меньше 9"); приведёт к ошибке, так как мы пытаемся передать или int или char*, что не будет работать.

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

a = condition ? func1() : func2();

А можно ли сделать на операторе ? более сложную конструкцию, типа else if? Можно!

void setup() {
  Serial.begin(9600);
  // код выводит "размер" переменной value
  
  byte value = 5;
  
  // конструкция на if-else
  if (value > 19) Serial.println("> 19");
  else if (value > 9) Serial.println("> 9");
  else Serial.println("< 9");

  // на операторах ?
  Serial.println(( (value > 9) ? ( (value > 19) ? "> 19" : "> 9" ) : "< 9" ));
}

Оператор выбора


Оператор выбора switch позволяет создать разветвление кода в зависимости от значения одной переменной. Синтаксис такой:

switch (значение) {
  case 0:
    // выполнить, если значение == 0
    break;
  case 1:
    // выполнить, если значение == 1
    break;
  case 2:
  case 3:
  case 4:
    // выполнить, если значение == 2, 3 или 4
    break;
  default:
    // выполнить, если значение не совпадает ни с одним из case
    break;
}

Наличие оператора default необязательно. Наличие оператора break обязательно, иначе сравнение пойдёт дальше, как показано для case 2, 3 и 4.

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

Значения case обязательно должны быть константами, так как программа не сравнивает значение, а сразу переходит на нужный блок кода

Также конструкция на switch работает быстрее, чем аналогичная на else if: происходит выбор варианта и моментальный переход на нужный блок кода, а в else if в каждой строке производится проверка логического выражения.

ВАЖНОЕ ЗАМЕЧАНИЕ


Нужно быть крайне внимательным при работе с оператором switch, потому что код, находящийся внутри фигурных скобок switch() { }, является одним блоком кода для всех кейсов. Соответственно кейсы case - всего лишь ярлыки перехода между участками этого блока. Почему это так важно: все кейсы находятся в одной области видимости, то есть внутри switch не могут быть объявлены локальные переменные с одинаковыми именами:

switch (mode) {
  case 0:
    long val = 100;
    break;
  case 1:
    long val = 100;  // приведёт к ошибке
    break;
  case 2:
    break;
}

Более того, крайне не рекомендуется создавать локальные переменные внутри кейсов, так как это может сломать код!

switch (mode) {
  case 0:
    break;
  case 1:
    // локальная переменная
    long val = 100;
    break;
  case 2:
    // при mode == 2 будет выводиться кек
    // только если убрать локальную переменную выше!
    Serial.println("kek");
    break;
}

Что делать, если очень хочется? Обернуть содержимое кейса в блок при помощи фигурных скобок:

switch (mode) {
  case 0:
    break;
  case 1:
    {
      long val = 100;
    }
    break;
  case 2:
    Serial.println("kek");
    break;
}

Диапазон значений


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

switch (a) {
  case   0 ... 100: b = 1; break;
  case 101 ... 200: b = 2; break;
  case 201 ... 300: b = 3; break;
  case 301 ... 400: b = 4; break;
  case 401 ... 500: b = 5; break;
  case 501 ... 600: b = 6; break;
}

По сути данный код равносилен вот такой конструкции на else if:

if (a > 0 && a < 100) b = 1;
else if (a < 200) b = 2;
else if (a < 300) b = 3;
else if (a < 400) b = 4;
else if (a < 500) b = 5;
else if (a < 600) b = 6;

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

Видео


 

Полезные страницы


5 1 голос
Рейтинг статьи
Подписаться
Уведомить о
guest

24 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх