Arduino define и const
Инструкция define в ардуино, как и в языке C++, нужна для того, чтобы упростить написание скетчей. Мы можем один раз определить название какого-то фрагмента кода, а затем везде использовать только это название. В этой статье мы на конкретных примерах разберемся с такими вопросами, как правильно использовать #define, что такое препроцессор, в каких случаях надо использовать define, а в каких – лучше const.
Синтаксис define ардуино
Синтаксис использования инструкции достаточно прост:
Обратите внимание, что в конце строки не нужно ставить знак точки с запятой.
Примеры использования define:
Тело макроса должно заканчиваться в той же строке. Но если мы хотим сделать многострочный блок, то добавляем символ “/” в конце. Например:
Описание define
#define является одной из инструкций препроцессора ардуино и C++. Само слово препроцессор означает, что работа с такими инструкциями проходит до основного процесса компиляции кода. Во время такого «нулевого» этапа препроцессор компилятора пробегает по исходному коду, находит все наши инструкции и производит замену прямо в том же исходном коде. Все это делается незаметно от нас, мы результатов этого отдельного шага не видим. И только после работы препроцессора запускается сам компилятор, который будет анализировать и собирать код с учетом тех изменений, который уже сделал препроцессор.
В чем-то это напоминает механизм макросов и шаблонов. Расставив определенные с помощью #define шаблоны по всему коду, мы даем возможность компилятору автоматически заменить их перед компиляцией на то, что нам нужно. Единственное исключение – если идентификатор находится внутри скобок “”. Тогда подстановки не будет.
Все это нужно для того, чтобы во время создания программы тратить меньше времени на написание команд и их изменение. Мы можем «закодировать» коротким словом большую «длинную» конструкцию и писать в коде ее («короткую»), а не длинную. А компилятор сам подставит «длинную» в нужные места перед компиляцией. Также мы можем один раз изменить значение подстановки в начале программы, и новое значение само подставится во всех нужных местах. Возможностей использования #define – много. Как это работает – посмотрим на примерах ниже.
Примеры define в arduino
define pin
Самое частое использование define в Arduino – это определение констант для номеров пинов. С помощью инструкции define можем «дать название» какому то числу, и потом везде в коде использовать именно это название. Вот самый простой фрагмент примера со светодиодами:
В данном случае мы определили новое слово PIN_LED и можем его использовать повсюду в коде. Перед тем, как компилировать программу ардуино пробежится по коду и везде, где встретит словосочетание PIN_LED, заменит его на цифру 13. Т.е. в конечном итоге команде pinMode все равно придет в параметрах номер пина 13. И функция digitalWrite тоже получит 13. Но при этом мы явно в коде цифру 13 не использовали. А это очень хорошо: если нам нужно будет переключить светодиод на другой порт (например, 12), нам не придется бегать в ужасе по коду и менять цифры.
В итоге мы просто поменяем цифру один раз (в блоке #define ) и теперь уже во все функции вместо PIN_LED будет передаваться цифра 12. Представьте, если у вас скетч длиной 1000 строк и в 50 разных местах вы обращаетесь к пину 13, а нужно поменять на 12. С помощью #define вы вместе с компилятором аруино сделаете все за 1 секунду. А вручную, да без ошибок, вам придется провозиться гораздо дольше.
define константы
С помощью #define мы можем определять «псевдо константы». Они будут работать как обычные константы, но самих переменных создаваться при этом не будет. Например, создав макрос BUTTON_UP со значением 1, мы можем использовать его в коде как константу. Например, как только мы определили, что была нажата кнопка вверх, мы можем запомнить код нажатой кнопки и передать его другим функциям, которые будут сравнивать этот код и делать какие-то действия. Сам код кнопки мы можем придумать любым: хотим – 1, хотим – 345 (такие цифры часто называют «магическими», т.к. их придумывают сами программисты и не всегда понятно, почему придумали именно так), главное, чтобы этот код оставался неизменным в тексте программы. Лучшим решением будет закодировать этот код в константе (например, BUTTON_UP) и использовать вместо цифры именно название константы.
Вложенные define
Вы вполне можете использовать в блоке инструкции define другие макросы. Например:
#define delay delay(pin)
Во второе определение вместо pin подставится цифра 13 и в итоге мы получим:
#define delay delay(13)
Ошибки define
Главное при работе define – это выбрать такое название макроса, которое не совпадает с другими конструкциями языка. Потому что ардуино найдет все совпадающие фрагменты и заменит тем, что мы определил в define. Компилятору все равно, какие конструкции он заменяет, поэтому он с легкостью выполнит подстановку любого кода. Результат будет непредсказуемым.
#define setup 12345
,вы заставите ардуино заменить команду setup и безобидная функция превратится в
В таком случае, когда ардуино приступит к компиляции, он уже не найдет обязательную функцию setup и очень рассердится. К тому же, он обнаружит соврешенно странную строку 12345, нарушающe правила наименование переменных и функций.
Вы также можете создать трудноуловимую проблему, просто нечаянно добавив символ точки с запятой в конец строки:
После подстановки эта конструкция превратится в digitalWrite(13;, HIGH). Т.е. внутри блока аргументов появится точка с запятой – а это, без сомнения, нарушение синтаксиса.
Еще одной серьезной ошибкой может стать несоответствие типов, которое вы не сможете сразу же распознать. Например, если вы определите
#define PIN_LED pin12
То при использовании в функции
#Ifdef, #ifndef и #endif – дополнительные команды препроцессора
Иногда бывает полезным знать, объявили ли мы уже в #define какую-либо инструкцию ранее. Это может быть полезным, если у нас много файлов в проекте и мы не всегда понимаем, в какой последовательности в итоге будут подключаться наши модули. Для всего этого мы и используем специальные инструкции #ifdef или #ifndef, которые проверят, было ли встречено данное определение ранее и, если было (или не было – для второго варианта), то оставит блок кода с последующей строки и до места встречи #endif.
#ifdef имя_макроса
последовательность команд, которые будут оставлены в коде, только если данный макрос был определен ранее инструкцией #define. В противном случае данный участок кода будет исключен
#endif
#ifndef имя_макроса
последовательность команд, которые будут оставлены в коде, только если до данного момента макрос не был определен
#endif
Мы проверяем, определили ли ранее в #define признак отладки, и если да, то код будет выполнен, если нет, то ничего выводиться не будет.
#define DEBUG 1 // Если закоментировать всю эту строку, то отладочные сообщения будут отключены
Пример для ifndef. Мы объявляем константу, только если не делали этого ранее.
Все достаточно понятно и похоже на обычные конструкции
define как альтернатива функциям
Сейчас мы рассмотрим более сложный вариант использования define – мы попробуем передавать внутрь конструкции define какие-то параметры. Но сначала давайте разберемся, зачем это нужно.
#define on digitalWrite(13, HIGH)
В этом примере мы достаточно длинную команду digitalWrite закодировали одним коротким словом on. Теперь везде в коде, где встретится on, компилятор (препроцессор компилятора) вставит строку digitalWrite(13, HIGH). И в конечном итоге функция loop будет выглядеть так:
void loop() <
digitalWrite(13, HIGH);
>
Все будет замечательно до того момента, когда мы захотим добавить второй светодиод и помигать им. Использовать инструкцию on уже не получится, т.к. на ее месте появится вызов для того же светодиода на 13 порту. И у нас есть два выхода:
Первый вариант – это путь в никуда. Потому что нам опять придется делать однообразную работу по копированию одного и того же текста. А вот второй вариант мы сейчас рассмотрим.
В препроцессоре С++ (и, следовательно, ардуино), можно определить параметры, которые препроцессор будет использовать при замене одного фрагмента на другой. Можно указать, что в первом случае on должен вставить digitalWrite для 13 пина, а во втором – для 12 и т.д. Для этого нужно сделать следующее:
#define on(pin) digitalWrite(pin, HIGH)
В инструкции #define мы ввели параметр, который сами назвали a(можно выбрать любое имя) и прописали его явно с помощью скобок: on(a). Во второй части инструкции мы просто используем название параметра в нужном месте: digitalWrite (pin, HIGH).
Теперь, встретив конструкцию on(13), препроцессор вставит слово digitalWrite (pin, HIGH), а вместо pin добавит то, что мы передали в скобках: цифру 13. В итоге в код вставится строка digitalWrite (13, HIGH).
Указав on(12), мы заставим вставить в код функцию digitalWrite (12, HIGH). И так далее, в зависимости от наших нужд.
Таким же образом мы можем сделать макрофункцию (псевдофункцию) off(pin), которая будет выполнять подстановку digitalWrite (pin, LOW).
В итоге код маячка со светодиодами может стать до чрезвычайности простым:
Можно сократить еще сильнее, потому что на вход макрофункции можно передавать несколько параметров:
Достаточно интересно, не правда ли? Также можно придумать очень много разных упрощающих инструкций. Но давайте немного остановимся и поговорим об ограничениях.
Все дело в том, что создавая такую «библиотеку» из собственных инструкций мы очень сильно рискуем:
Ради справедливости скажем и о плюсах:
В общем случае совет такой: используйте конструкцию #define для псевдофункций только в том случае, если структура вашего проекта проста, сама псевдофункция умещается в одну строчку, вызывается часто, вы не используете вложенные вызовы макросов и функций и всегда можете визуально оценить, какие изменения в коде вызовут подстановки.
Define или const
Альтернативой #define является использование констант. Константа в arduino и C++ – это переменная, которая определяется c модификатором const. Пример:
сonst int PIN_LED = 10;
То же самое можно сделать с помощью define:
Результат одинаковый – в коде вы просто используете конструкцию типа digitalRead(PIN_LED) и получите желаемое: в нужное место вставится цифра 10.
Но использование константы все-таки предпочтительней по следующим причинам:
Если оценивать объем памяти необходимой для скетча с использованием #define или const, то никаких преимуществ ни тот, ни другой способ не дают – компилятор выделит одинаковый объем памяти для переменной, объявленной явно или встроенной в виде макроса. Исходя из всего этого, старайтесь при использовании констант в коде все-таки брать за основу const.
Директивы препроцессора
Препроцессор
Процесс компиляции прошивки очень непростой и имеет несколько этапов, один из первых – работа препроцессора. Препроцессору можно давать команды, которые он выполнит перед компиляцией кода прошивки: это может быть подключение файлов, замена текста, условные конструкции и некоторые другие вещи. Также у препроцессора есть макросы, которые позволяют добавлять в код некоторые интересные вещи.
#include – подключить файл
Также можно указать путь к файлу, который нужно подключить. Например у нас в папке со скетчем есть папка libs, а в ней – файл mylib.h. Чтобы подключить такой файл, пишем:
Компилятор будет искать его в папке со скетчем, в подпапке libs.
#define / undef
Или быстрого и удобного отключения отладки в коде:
Или даже задефайнить целый кусок кода, используя переносы и обратный слэш
Если DEBUG задефайнен, то DEBUG_PRINT – это макро-функция, которая выводит значение в порт. А если не задефайнен – все вызовы DEBUG_PRINT просто убираются из кода и экономят память!
Если DEBUG_ENABLE задефайнен – все вызовы DEBUG() в коде будут заменены на вывод в порт. Если не задефайнен – они будут заменены НИЧЕМ, то есть просто “вырежутся” из кода! Также по DEBUG_ENABLE можно запустить сериал и получить полный контроль над отладкой: если она не нужна – убрали DEBUG_ENABLE и из кода убрался запуск порта и все выводы, что резко сокращает объём занимаемой памяти:
Проблемы


#if – условная компиляция
Условная компиляция является весьма мощным инструментом, при помощи которого можно вмешиваться в компиляцию кода и делать его очень универсальным как для пользователя, так и для железа. Рассмотрим директивы условной компиляции:
При помощи условной компиляции можно буквально включать и выключать целые части кода из компиляции, то есть из финальной версии программы, которая будет загружена в микроконтроллер. Рассмотрим несколько конструкция для примера: [fusion_accordion type=”” boxed_mode=”” border_size=”1″ border_color=”” background_color=”” hover_color=”” divider_line=”” title_font_size=”” icon_size=”” icon_color=”” icon_boxed_mode=”” icon_box_color=”” icon_alignment=”” toggle_hover_accent_color=”” hide_on_mobile=”small-visibility,medium-visibility,large-visibility” title=”Пример 1″ open=”no”] [/fusion_toggle][fusion_toggle title=”Пример 2″ open=”no”] [/fusion_toggle][fusion_toggle title=”Пример 3″ open=”no”] [/fusion_toggle][/fusion_accordion]
Сообщения от компилятора
#pragma
Указывает компилятору, что данный файл нужно подключить только один раз. Является более удобной и современной заменой конструкции вида
Такую конструкцию вы можете встретить в 99% библиотек, файлов ядра и вообще заголовочников с кодом.
Конструкция с #pragma pack и #pragma pop позволяет более рационально распределять структуры в памяти. Тема сложная, читайте на Хабре.
Макросы
У препроцессора есть несколько интересных макросов, которыми можно пользоваться в своём коде. Рассмотрим некоторые полезные из них, которые работают на Arduino (точнее, на компиляторе avr-gcc).
__func__ и __FUNCTION__
Макросы __func__ и __FUNCTION__ “возвращают” в виде символьного массива (строки) название функции, внутри которой они вызваны. Являются аналогом друг друга. Например:
__DATE__ и __TIME__
__DATE__ возвращает дату компиляции по системному времени в виде символьного массива (строки) в формате __TIME__ возвращает время компиляции по системному времени в виде символьного массива (строки) в формате ЧЧ:ММ:СС
Работать напрямую с этим макросом очень неудобно, это ведь просто набор символов. У меня есть библиотека buildTime, которая позволяет получать отдельно каждый параметр (день, месяц, год, часы, минуты, секунды). Скачать/почитать можно здесь.
__FILE__ и __BASE_FILE__
__FILE__ и __BASE_FILE__ возвращают полный путь к текущему файлу, опять же как строку. Являются аналогами друг друга.
__LINE__
__LINE__ возвращает номер строки в документе, в которой вызван этот макрос 
__COUNTER__
__COUNTER__ возвращает значение, начиная с 0. Значение __COUNTER__ увеличивается на единицу с каждым вызовом макроса в коде.
__COUNTER__ можно использовать для генерации уникальных имён переменных, но об этом мы поговорим когда нибудь в другой раз.
Директива #define Arduino IDE
Директива #define в Arduino IDE позволяет задавать имена значениям (константам), которые делают скетч более понятным. Т.е. можно в начале программы один раз определить имя константы или фрагмента кода, а затем использовать в скетче только это название. Рассмотрим на примерах с описанием правильные варианты использования функции #define в языке программирования Arduino IDE.
Arduino define описание директивы
Синтаксис директивы:
#define
При использовании директивы дефайн следует избегать использования имени другой переменной, константы или команды Ардуино, иначе оно оно будет заменено при компиляции. И обратите внимание, что в строчке не ставится точка с запятой и знак равенства, как это происходит при объявлении переменной, иначе компилятор выдаст ошибку. Рассмотрим использование #define на примере с подробным описанием.
Пример директивы: Arduino define pin
Это часть кода, от примера с мигающим трехцветным светодиодом. В данном примере мы присвоили имена для пинов 11, 12 и 13, к которым подключен светодиод. При написании кода нам удобнее использовать имена вместо номеров, чтобы каждый раз не вспоминать какой цвет к какому пину подключен. А программа автоматически будет заменять имена RED, GRN, BLU на соответствующие значения при компиляции.
Команды #ifdef, #ifndef и #endif в скетче
Инструкция #ifdef Arduino IDE проверят, было ли встречено в программе данное определение ранее, если было, то ставится блок кода с последующей строки и до #endif. В примере проверяется был ли ранее в #define определен признак отладки, если да, то код (вывод сообщения на монитор порта Arduino IDE) будет выполнен, если признак не определен, то сообщение на мониторе выводиться не будет.
Инструкции #ifndef проверят, было ли встречено в программе данное определение ранее и, если не было, то ставится блок кода с последующей строки и до #endif. В следующем простом примере мы объявляем новую константу, если только не объявляли ее в скетче ранее. Если дефайн с таким именем уже использовался, то программа проигнорирует строчки внутри конструкции #ifndef … #endif.
Замена функций с помощью define Arduino
Кроме использования дефайн в программе для объявления констант, можно заменять целые фрагменты кода с помощью директивы #define. Это более сложный, но интересный вариант использования define, который позволяет создать много разных упрощающих инструкций в скетче. Например, мы можем в первом примере заменить функцию pinMode() на конструкцию с дефайн с заданными параметрами.
В примере мы закодировали команду pinMode() одним словом «out». Теперь, где в скетче встретится слово «out», компилятор подставит строку pinMode(pin, OUTPUT) с заданным параметром pin. Таким же образом можно заменить команды digitalWrite() и delay(). Используя RGB светодиод или три обычных светодиода с Ардуино вы можете проверить работу следующего примера скетча с директивой дефайн.
Обратите внимание, что on(11, 500) и другие строчки не являются функциями, конструкция просто подставляет в код нужный текст. В более сложных программах есть риск создать самому ошибки, так как в скетче могут быть десятки подключаемых библиотек, где дефайн может что-то незаметно поменять. При этом будут возникать ошибки компиляции или ошибки во время исполнения программы.
Arduino define или const, что выбрать
Иногда бывает не удобно применять директиву #define для создания констант, в этом случае используют ключевое слово const. В отличие от глобальных переменных, значение const должно быть определено сразу при объявлении константы. Помните, что при использовании #define имена следует делать максимально уникальными, чтобы не было совпадений с командами из подключаемых библиотек.
Если использовать константу вместо дефайн в скетче из первого примера, то результат будет одинаковый – в коде вместо переменной RED будет подставляться цифра 11. На константы в программе действуют общие правила области видимости глобальных и локальных переменных. Кроме того, использованием #define или const не дает никаких преимуществ, с точки зрения экономии объема памяти микроконтроллера.
Типы данных, переменные

В ближайшее время нас будет интересовать только SRAM память, в которой хранятся переменные, именно о них дальше и пойдёт речь.
Двоичная система
В цифровом мире, к которому относится также микроконтроллер, информация хранится, преобразуется и передается в цифровом виде, то есть в виде нулей и единиц. Соответственно элементарная ячейка памяти, которая может запомнить 0 или 1, называется бит (bit). Таким образом мы плавно переходим к двоичной системе исчисления. Ну же, вспоминайте школьную информатику! Не вдаваясь в подробности “как это работает”, просто попробуем рассмотреть закономерность
| Двоичная | Десятичная |
| 0000 | 0 |
| 0001 | 1 |
| 0010 | 2 |
| 0011 | 3 |
| 0100 | 4 |
| 0101 | 5 |
| 0110 | 6 |
| 0111 | 7 |
| 1000 | 8 |
| 1001 | 9 |
| … | … |
| 10000 | 16 |
И так далее. Помимо закономерности увеличения разрядов и чисел есть ещё одна: приглядитесь к числам в двоичной системе со всеми нулями справа от единицы:
| 10 | 2 |
| 100 | 4 |
| 1000 | 8 |
| 10000 | 16 |
Именно, степень двойки! Именно на степенях двойки в цифровом мире завязано очень много. Чтобы получить количество десятичных чисел, которые могут быть закодированы заданным количеством бит, нужно возвести 2 в степень количества бит. Смотрим на таблицу выше и продолжаем:
И так далее. Сразу нужно запомнить, что в программировании счёт начинается с нуля, то есть 5ю битами мы можем закодировать десятичное число от 0 до 31, 8-ю битами – от 0 до 255, 10-ю битами – от 0 до 1023. Очень важно понять и запомнить это, дальше очень пригодится. Следующая по величине единица измерения в цифровом мире – байт (byte), состоит из 8 бит. Почему 8? Исторически сложилось, что шины первых микропроцессоров имели разрядность 8 бит, возможно поэтому это количество приняли за более старшую единицу памяти. Также 8 это 2 в степени 3, что очень символично и удобно. А ещё, для кодирования всех латинских букв, знаков препинания, математических знаков и просто символов (всех что на клавиатуре) раньше хватало 7-ми бит (128 символов), но потом их стало мало, и ввели дополнительный бит, восьмой. То есть 8 бит это также размер таблицы символов, которая называется ASCII. К ней мы вернёмся уже в этой главе. Так что вопрос почему в одном байте 8 бит четкого ответа не имеет, ведь бывает и 6-ти битный байт, и 9-ти битный… Но это исключения старых процессоров, в современных цифровых устройствах в одном байте обычно содержится 8 бит (на отличных от AVR архитектурах может быть иначе), что позволяет закодировать 256 десятичных чисел от 0 до 255 соответственно. Дальше вы уже точно знаете:
Двоичная система является родной для микроконтроллера, и для работы с отдельными битами существует целый ряд инструментов, о них мы поговорим в уроке о битовых операциях из раздела продвинутых уроков.
Другие системы исчисления
Данные в памяти микроконтроллера хранятся в двоичном представлении, но помимо него существуют и другие системы исчисления, в которых мы можем работать. Постарайтесь сразу запомнить и понять, что переводить числа из одной системы исчисления в другую не нужно, Ардуино абсолютно всё равно, в каком формате вы скармливаете значение переменной, они автоматически будут интерпретированы в двоичный вид. Разные системы исчисления введены в первую очередь для удобства программиста. Теперь по сути: ардуино поддерживает (да в целом другого и не нужно) четыре классических системы исчисления: двоичную, восьмеричную, десятичную и шестнадцатеричную. Да, и до неё добрались. Краткая напоминалка: 16-ричная система имеет 16 значений на один разряд, первые 10 как у десятичной, остальные – первые буквы латинского алфавита: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f. С десятичной системой всё просто, пишем числа так, как они выглядят. 10 это десять, 25 это двадцать пять, и так далее. Двоичная (Binary) имеет префикс 0b (ноль бэ) или B, то есть двоичное число 101 запишется как 0b101 ИЛИ B101. Восьмеричная (Octal) имеет префикс 0 (ноль), например 012. Шестнадцатеричная (hexadecimal) имеет префикс 0x (ноль икс), FF19 запишется как 0xFF19.
| Базис | Префикс | Пример | Особенности |
| 2 (двоичная) | B или 0b (ноль бэ) | B1101001 | цифры 0 и 1 |
| 8 (восьмеричная) | 0 (ноль) | 0175 | цифры 0 – 7 |
| 10 (десятичная) | нет | 100500 | цифры 0 – 9 |
| 16 (шестнадцатеричная) | 0x (ноль икс) | 0xFF21A | цифры 0-9, буквы A-F |
Основная фишка 16-ричной системы в том, что она позволяет записывать длинные десятиричные числа короче, например один байт (255) запишется как 0xFF, два байта (65 535) как 0xFFFF, а жуткие три байта (16 777 215) как 0xFFFFFF. Вы не представляете (или уже имеете представление), насколько удобно и понятно это позволяет работать с цветами и оттенками. Двоичная же система обычно используется для наглядного представления данных и низкоуровневых конфигураций различного железа. Например конфиг кодируется одним байтом, каждый бит в нём отвечает за отдельную настройку (вкл/выкл), и передав один байт вида 0b10110100 можно сразу кучу всего настроить, к этому мы вернёмся в уроке работа с регистрами из раздела продвинутых уроков. В документации по этому поводу пишут в стиле “первый бит отвечает за это, второй за то” и так далее.
Переменные
Переменная – это ячейка SRAM памяти, которая имеет своё уникальное название и хранит числа соответственно своему размеру. К переменной мы можем обратиться по её имени и получить значение, либо изменить его. Степень двойки преследует нас и дальше, ведь объём одной ячейки памяти в микроконтроллере тоже ей кратен:
Да, больше четырёх байт в ардуино (точнее в МК от AVR) уже не влезет, при использовании обычных типов данных. Для работы с разными диапазонами значений используются разные типы данных (переменных). По сути можно использовать 4 байта для хранения чего угодно, но это не оптимально. Это как знать, что вам нужно будет унести максимум 200 мл воды (меньше 1 байта), но вы всё равно берёте 19 литровую бутыль (2 байта). Или железнодорожную цистерну на 120 тонн (4 байта). Если хотите писать красивый и оптимальный код, используйте соответствующие типы данных. Кстати, вот они:
Типы данных
*Не встречал упоминания об этом в официальных источниках, но Ардуино (точнее компилятор) также поддерживает 64 битные числа, соответственно тип данных int64_t и uint64_t Максимальный размер всех типов данных хранится в константах, и его можно использовать в коде по надобности:
Есть ещё несколько нестандартных типов, которые иногда встречаются в чужом коде:
Объявление и инициализация переменных
Преобразование типов
Преобразование _cast (Pro)
Иногда можно встретить преобразование типов через оператор cast. Отличную статью можно глянуть на Хабре, а я кратко опишу 4 основных каста:
Как пользоваться: на примере предыдущего примера
Константы
Что такое константа понятно из её названия – что-то, значение чего мы можем только прочитать и не можем изменить. Задать (объявить) константу можно двумя способами:
Ещё пару слов о константах и переменных: если обычная переменная нигде не изменяется в процессе выполнения программы – компилятор может самостоятельно сделать её константой и она не займёт места в оперативной памяти, т.е. будет помещена во Flash.
Область видимости
Переменные, константы и другие типы данных (структуры и перечисления) имеют такое важное понятие, как область видимости. Она бывает
Глобальная
Глобальная переменная объявляется вне функций и доступна для чтения и записи в любом месте программы, в любой её функции.
Локальная
Локальная переменная живёт внутри функции или внутри любого блока кода, заключённого в < фигурные скобки >, доступна для чтения и записи только внутри него. При попытке обратиться к локальной переменной из другой функции (за пределами её < блока >) вы получите ошибку, потому что локальная переменная создаётся заново при выполнении содержащего её блока кода (или функции) и удаляется из памяти при завершении выполнения этого блока (или функции):
Важный момент: если имя локальной переменной совпадает с глобальной, то приоритет обращения по имени в функции отдаётся локальной переменной:
Формальная (параметр)
Формальная переменная, она же параметр, передаваемый в функцию, ведёт себя как обыкновенная локальная переменная, но появляется при немного других условиях: при вызове функции. Эту переменную можно читать и менять внутри её функции. Также читайте отдельный урок про функции.
Структуры (Pro)
Структура struct – очень интересный тип данных: это совокупность разнотипных переменных, объединённых одним именем. В некоторых случаях структуры позволяют очень сильно упростить написание кода, сделать его более логичным и легко модифицируемым. Тип данных структура объявляется вот по такой схеме:
Ярлык будет являться новым типом данных, и, используя этот ярлык, можно объявлять уже непосредственно саму структуру:
Также есть вариант объявления структуры без создания ярлыка, т.е. создаём структуру, не объявляя её как тип данных со своим именем.
Рассмотрим большой пример, где показано всё вышеописанное
Размер элемента структуры
Структуры позволяют делать одну очень интересную вещь для оптимизации памяти: указывать максимальный вес элемента в битах. Таким образом можно делать даже однобитные флаги (обычный bool / boolean занимает в памяти 8 бит). Делается это при помощи оператора двоеточие :
Вложенные структуры
Структуры также могут быть вложенными друг в друга, доступ к нужному элементу осуществляется так же при помощи оператора “точка”, смотрите простой пример:
Перечисления (Pro)
Перечисления ( enum – enumeration) – тип данных, представляющий собой набор именованных констант, нужен в первую очередь для удобства программиста. Сразу пример из опыта: допустим у нас есть переменная mode, отвечающая за номер режима работы устройства. Мы для себя запоминаем, какому значению переменной какой режим будет соответствовать, и где-нибудь себе записываем, например 0 – обычный режим, 1 – режим ожидания, 2 – режим настройки_1, 3 – режим настройки_2, 4 – калибровка, 5 – аварийный режим, ошибка. При написании или чтении программы часто придётся обращаться к этому списку, чтобы не запутаться. Можно сделать первый шаг по оптимизации: обозвать каждый режим при помощи дефайна:
Таким образом вместо цифры можно будет использовать понятные слова и ориентироваться в коде будет гораздо проще. Использование enum ещё немного упрощает эту конструкцию: перечисление позволяет создать переменную (по умолчанию типа int ), которая может принимать только те “названия”, которые для неё указаны. Это удобно тем, что в одной программе могут находиться разные хранители режимов с одинаковыми названиями, и в отличие от #define это не будет приводить к ошибкам. Объявление перечисления чем-то похоже на объявление структуры:
Таким образом мы объявили ярлык. Теперь, используя этот ярлык, можно объявить само перечисление:
Также как и у структур, можно объявить перечисление без создания ярлыка (зачем нам лишняя строчка?):
Созданное таким образом перечисление является переменной, которая может принимать указанные для неё имена, также с этими именами её можно сравнивать. Теперь самое главное: имена для программы являются числами, начиная с 0 и далее по порядку увеличиваясь на 1. В абстрактном примере выше имя1 равно 0, имя2 равно 1, имя3 равно 2, и так далее. Помимо указанных имён, перечислению можно приравнять и число напрямую, но как бы зачем. Рассмотрим пример!
Таким образом SET1 имеет значение 1, SET2 будет 2 и так далее по порядку.
Пользовательские типы (Pro)
В С++ (и на Ардуино) этого делать не нужно! Наоборот, typedef в этом применении может приводить к ошибкам. Например:
Пространство имён (Pro)
Пространство имён – очень удобная возможность языка, с её помощью можно разделить функции или переменные с одинаковыми именами друг от друга, то есть защитить свой набор данных инструментов от конфликтов имён с другими именами. “Именная область” определяется при помощи оператора namespace :
Чтобы использовать содержимое из пространства имён, нужно обратиться через его название и оператор разрешения области видимости ::
Более подробный пример:
И ниже по коду можно будет пользоваться содержимым пространства имён без обращения через имя::
Спецификаторы (Pro)
Помимо возможности сделать переменную константой при помощи спецификатора const у нас есть ещё несколько интересных инструментов по работе с переменной.
static
static – делает переменную (или константу) статичной. Что это значит? Статичная локальная Для начала вспомним, как работает обычная локальная переменная: при вызове функции локальная переменная создаётся заново и получает нулевое значение, если не указано иначе. Если локальная переменная объявлена как static – она будет хранить своё значение от вызова к вызову функции, то есть станет грубо говоря глобально-локальной. Пример:
Статическая локальная:
extern
extern – указывает компилятору, что переменная объявлена где-то в другом файле программы, и при компиляции он её найдёт и будет использовать. А если не найдёт – ошибки не будет. Например при помощи данного кода можно сбросить счётчик millis()
volatile
volatile – данный спецификатор указывает компилятору, что данную переменную не нужно оптимизировать и её значение может быть изменено откуда-то извне. Обычно переменные с таким спецификатором используются в обработчиках прерываний. Вычисления с такими переменными также не оптимизируются и занимают больше процессорного времени.





