Сравнение


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

  • == равенство (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 

Результаты операторов сравнения можно использовать для работы с условиями, а также для цикла while (речь о нём пойдёт в следующем уроке)

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


Условный оператор 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), позволяющий сделать код более красивым. О нём поговорим чуть ниже.

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

Порядок условий играет очень большую роль при оптимизации кода и попытке сделать его более быстрым в некоторых случаях. Суть очень проста: логические выражения/величины проверяются слева направо, и если хоть одно значение делает всё выражение неверным (ложью), дальнейшая проверка условий прекращается. Например если в выражении

if (a && b && c) {
  // делать что то
}

хотя бы а имеет значение false, проверка остальных выражений (b и c) уже не выполняется. Когда это может быть важно: например, есть какой-то флаг и выражение, которое вычисляется прямо в условии и сразу проверяется. В таком случае если флаг опущен, микроконтроллер не будет тратить время на лишние вычисления. Например:

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

Если флаг опущен, микроконтроллер не будет тратить лишние 100 мкс на работу с АЦП, и сразу проигнорирует остальные логические выражения. Это конечно очень мало, но иногда и 100 мкс решают, просто помните о том, что порядок условий имеет значение.

Оператор ?


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

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

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

byte a, b;
a = 10;

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

Вот для сравнения аналогичная конструкция на if-else

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

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

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

А можно ли сделать на операторе ? более сложную конструкцию, типа 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. Рассмотрим более живой пример:

switch (val) {
  case 1:
  case 2:
  case 3:
  case 4:
    // выполнить, если val == 1, 2, 3 или 4
    break;
  case 5:
    // выполнить, если val == 5
    break;
}

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

Условные директивы #if #else


Помимо директивы #define, сообщающей препроцессору о необходимости замены набора символов набором символов, есть ещё условные директивы, позволяющие заниматься так называемой условной компиляцией: обладая такой же логикой, как if-else, данные конструкции позволяют делать некоторый выбор перед компиляцией самого кода. Отличным примером является само “ядро” Ардуино – большинство функций написаны со спецификой каждого процессора, и перед компиляцией кода из множества вариантов реализации функции выбирается тот, который соответствует текущему выбранному микроконтроллеру. Проще говоря, условная компиляция позволяет по условиям включать или исключать тот или иной код из основной компиляции, т.е. сначала препроцессор анализирует код, что-то в него включается, что-то нет, и затем проходит компиляция.

Также например мы не можем объявить какую-либо константу или макро через #define более одного раза, это приведёт к ошибке. Условная компиляция позволяет сделать ветвящуюся конструкцию, где такое возможно. Для условной компиляции нам доступны директивы #if, #elif, #else, #endif, #ifdef, #ifndef

  • #if – аналог if в логической конструкции
  • #elif – аналог else if в логической конструкции
  • #else – аналог else в логической конструкции
  • #endif – директива, завершающая условную конструкцию
  • #ifdef – если “определено”
  • #ifndef – если “не определено”
  • defined – оператор, который не подсвечивается в коде, но работает так: возвращает true если указанное в скобках слово “определено” через #define, и false – если нет

Как ими пользоваться давайте посмотрим на примере:

#define TEST 1    // определяем TEST как 1
#if (TEST == 1)   // если TEST 1
#define VALUE 10  // определить VALUE как 10
#elif (TEST == 0) // TEST 0
#define VALUE 20  // определить VALUE как 20
#else             // если нет
#define VALUE 30  // определить VALUE как 30
#endif            // конец условия

Таким образом мы получили задефайненную константу VALUE, которая зависит от “настройки” TEST. Конструкция позволяет включать или исключать куски кода перед компиляцией, вот например кусочек про отладку:

#define DEBUG 1

void setup() {
#if (DEBUG == 1)
  Serial.begin(9600);
  Serial.println("Hello!");
#endif
}

Таким образом при помощи настройки DEBUG можно включить или исключить любой кусок кода.

Есть у препроцессора ещё две директивы: #ifdef, #ifndef, они позволяют включать или исключать участки кода по условию: ifdef – определено ли? ifndef – не определено ли? Определено или не определено – речь идёт конечно же о #define

#define TEST      // определяем TEST 

#ifdef TEST       // если TEST определено 
#define VALUE 10  // определить VALUE как 10 
#else             // если закоммент. #define TEST
#define VALUE 20  // определить VALUE как 20 
#endif            // конец условия

Именно на условной компиляции строится вся универсальность библиотек для Arduino, ведь при выборе автоматически “создаётся” дефайн на название микроконтроллера, выглядят они так:

  • __AVR_ATmega32U4__
  • __AVR_ATmega2560__
  • __AVR_ATmega328P__
  • И далее в этом стиле

Это позволяет создавать универсальный код, используя конструкцию с #ifdef или #if defined:

#if defined(__AVR_ATmega32U4__)
// код для Leonardo/Micro/Pro Micro

#elif defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
// код для Mega (1280 или 2580)

#elif defined(__AVR_ATmega328P__)
// код для UNO/Nano/Pro Mini

#endif

Таким образом микроконтроллерам (платам Arduino) разных моделей будет доступен персональный кусок кода, который будет передан компилятору при выборе этой платы из списка плат.

Видео


Важные страницы


  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макро, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
Последнее обновление Сентябрь 04, 2019
2019-09-04T17:05:29+03:00