Скетч зарядника я писал на макросах: #define TOK1 a + b — сумма показаний двух замеров. Через месяц добавил сравнение «двойной ток против порога»: if (2 * TOK1 > POROG) — и зарядник стал считать двойной ток неправильно: 2 * a + b. Час ушёл на поиск, потому что в строке с if всё выглядит прилично; сломана не строка использования, а само определение макроса без скобок.
#define — не константа и не переменная: команда препроцессору, который до компиляции заменяет каждое вхождение имени на записанный текст. Компилятору достаётся уже раскрытый файл. const int — настоящая константа с типом и областью видимости, и на Uno она стоит ровно столько же: ноль байтов ОЗУ.
Наборы значений через enum уже разобраны в уроке про define, const и enum, типы переменных — в базовом уроке о переменных. Здесь — цена выбора: что именно происходит с макросом до компиляции, почему скобки обязательны и какой стиль нужен, чтобы оба варианта не утонули: имена, комментарии, формат.
Стиль — не украшение. В том заряднике имя TOK1 без единицы измерения и без типа стоило мне часа отладки; переименованный в TOK_MA (миллиамперы) и переписанный на const int скетч читается сам, без раскопок в истории правок.
#define: робот-копипастер до компилятора
Препроцессор проходит по файлу и заменяет каждое вхождение имени на записанный текст — буквально, без разбора смысла. #define TOK1 a + b превращает 2 * TOK1 в 2 * a + b, потому что умножение по таблице приоритетов сильнее сложения. Типа нет, проверок нет, области видимости нет: имя видно от строки define до конца файла, в том числе внутри всего, что подключено ниже.
Лечение известно: скобки вокруг всего тела и вокруг каждого параметра. #define SUMMA (a + b) разворачивается в 2 * (a + b) и считается как задумано; #define KVADRAT(x) ((x) * (x)) при вызове KVADRAT(2 + 1) даёт ((2 + 1) * (2 + 1)) = 9, а без внутренних скобок — 2 + 1 * 2 + 1 = 5. Каждая потерянная пара скобок — вычисление, которое «почти работает» и ломается на других значениях.
Вторая грабля — точка с запятой: #define PERIOD 300; вставляет точку с запятой в каждое использование, и if (millis() - last > PERIOD) разваливается на два оператора. Диагностика компилятора при этом указывает на строку с if, а причина выше — в define. Отсюда правило: в #define точка с запятой не ставится никогда, даже если «так собирается».
Третье свойство макросов проявляется на подросших скетчах: имя из define видно всему, что ниже, включая библиотеки, подключённые вчера. Два одинаковых имени — ваше и библиотечное — дают предупреждение о переопределении, а собирается проект со значением того, кто определён позже. Все макросы скетча я держу вверху файла и приставляю к именам префиксы: вместо общего PERIOD — SHAG_MS, вместо LED — PIN_LED.
const int PERIOD = 300; — полноценная константа: с типом, проверкой присваиваний и областью видимости. Попытка записать её в byte при значении 300 — предупреждение; перепутали unsigned long с int в сравнении с millis() — предупреждение. У макроса все эти ситуации проходят молча: компилятор видит только подставленный текст и не знает, что там было задумано.
Область видимости работает на вас: константа, объявленная внутри функции, наружу не вылезет, и в двух частях проекта спокойно живут одноимённые пороги с разными значениями. Макрос с тем же именем переопределит соседа — с предупреждением или без, в зависимости от флагов сборки, и искать это развлечение лучше заранее.
Цена — ноль: на ATmega328P оптимизатор вшивает const в код так же, как макрос, ОЗУ она не занимает — проверяется строкой о глобальных переменных при сборке. Миф «const ест память» остался от старых компиляторов; в современном avr-gcc причин писать #define для числовых констант у меня нет. Живут макросы там, где const тесно: F()-строки и PROGMEM — разбор F() и PROGMEM, а вместо многострочных макросов-функций честнее обычные функции из урока о параметрах и возврате.
Константу можно считать из другой константы: const byte SHAG = 30; и предел от него — выражение компилятор посчитает ещё при сборке, без единого такта на плате. Макросы умели то же, но у const промежуточные значения не теряют тип, и промах в единицах измерения виден в той же строке, где объявлена константа, а не в случайном месте использования.
const byte PIN_LED = 9; // ШИМ-пин: на Uno это 3, 5, 6, 9, 10, 11const byte PIN_KN = 2; // кнопка «ярче»const unsigned long SHAG_MS = 250UL; // шаг нарастания; UL — против ловушки int // тело и каждый параметр — в скобках: вызов OGRAN(250 + 30, 0, 255) безопасен#define OGRAN(v, lo, hi) ((v) < (lo) ? (lo) : ((v) > (hi) ? (hi) : (v))) byte yarkost = 40; void setup() { pinMode(PIN_LED, OUTPUT); pinMode(PIN_KN, INPUT_PULLUP); Serial.begin(9600);} void loop() { if (digitalRead(PIN_KN) == LOW) { yarkost = OGRAN(yarkost + 30, 0, 255); // без скобок тело развалилось бы Serial.print("yarkost="); Serial.println(yarkost); delay(SHAG_MS); } analogWrite(PIN_LED, yarkost);}Имена, комментарии, формат: система, которая ищет за вас
Разбор по стилю: PIN_LED и PIN_KN — константы в CAPS с префиксом PIN_; SHAG_MS несёт суффикс единицы измерения — через полгода не гадаете, секунды там или миллисекунды; yarkost — переменная в camelCase. Тип unsigned long и суффикс 300UL у константы — страховка от переполнения в арифметике с millis(); вся ловушка разобрана в уроке о типах и переполнении.
Макрос OGRAN здесь уместен: одна строка, одно применение, поведение видно на месте вызова. Как только тело перерастает строку или появляется второй вызов — выношу в функцию: компилятор встроит её сам, а при промахе диагностика укажет на осмысленную строку, а не на кашу из скобок внутри развёрнутого макроса.
Правила имён у меня короткие: константы — CAPS (PIN_LED, SHAG_MS, POROG_TOKA), переменные и функции — camelCase (yarkost, chisloVspyshek, readAverage), пины — с префиксом PIN_. Глаз отличает «вшитое» от переменного мгновенно: строка analogWrite(PIN_LED, yarkost) читается без оглавления файла. Имя byte a стоит ноль символов и вечность понимания.
Комментарии — про «почему», а не «что». Строка // увеличиваем яркость над yarkost = OGRAN(...) — перевод строки на русский, мусор. Золото — объяснение решения: // шаг 30: от 40 до 255 за семь нажатий, ниже — не видно, выше — слепит. Через полгода комментарий должен отвечать на вопрос «почему так», а не пересказывать синтаксис.
Отдельный класс — имена-обманки: byte b2, int tmp1, data2. Они работают ровно до первого возврата к скетчу: через месяц вы ищете, где лежит порог, и находите три похожих tmp. Имя должно отвечать «что здесь лежит», функция — «что она делает»; правило выбора глагола для функций разобрано в уроке о параметрах и возврате.
Мои имена — латинская транслитерация (yarkost, nagrev, pereklyuchatel), и я не выдаю это за стандарт: главное — один алфавит на весь файл. Смешивать латиницу с кириллицей в идентификаторах не стоит: кодировка файла на другой машине или в другой среде может отличаться, и сохранение покажет кракозябры вместо половины имён. Латиница в идентификаторах, русский — в комментариях.
Ctrl+T (Auto Format) в Arduino IDE выравнивает отступы за секунду — жму перед каждой отправкой на плату. Компилятору отступы безразличны: он прожуёт файл и в одну строку. Формат — для глаз: виток loop виден слева, тело if — глубже, вложенный for — ещё глубже; на кривых отступах я дважды ловил пропущенный else, потому что его уровень совпал с чужим.
Выровненный файл ловит не только пропущенный else. Классика: if (digitalRead(PIN_KN) == LOW); — точка с запятой сразу после условия. Компилятор принимает её, тело становится пустым, а следующая строка выполняется всегда. На неотформатированном коде эта точка с запятой прячется в общей куче, на коде после Ctrl+T пустое тело выпирает уровнем отступа. В своих старых скетчах я находил такую конструкцию дважды — оба раза после неудачной вставки чужого куска.
Спорить, где ставить скобку — на той же строке или на новой, не собираюсь: на работу скетча это не влияет ни на такт. Единственное правило, которое у меня жёсткое, — одинаковость внутри файла: смешанные стили в одном скетче читаются хуже, чем любой «неправильный», но единый.
Смысл всего стиля — поиск: константы в CAPS находятся глазами за секунду, осмысленные имена заменяют половину комментариев, форматирование делает структуру видимой. Стиль — не красота, а скорость, с которой вы через полгода находите в собственном скетче нужную строку; на константах команд порта это работает в полную силу — как в уроке про пульт на Serial и switch.
| Свойство | #define | const |
|---|---|---|
| Что происходит | текстовая подстановка до компиляции | полноценная константа с типом |
| Проверки компилятора | нет: любой подставленный текст проходит молча | тип ловит перепутанные присваивания |
| Область видимости | от строки define до конца файла | от объявления до конца блока |
| Выражение в теле | скобки вокруг тела и параметров обязательны | скобки не нужны: значение считается заранее |
| Естественное место | F()-строки, PROGMEM, короткие вставки | пины, пороги, периоды — всё остальное |
- 1. Во что разворачивается 2 * SUMMA при #define SUMMA a + b?
- 2. Сколько ОЗУ занимает const int POROG = 740 на Uno?
- 3. Чем плох #define KVADRAT(x) x * x по сравнению с ((x) * (x))?
- 4. Где #define уместен, а не const?
- 5. Что делает Ctrl+T в Arduino IDE?
- #define — текстовая подстановка до компиляции: типа, проверок и области видимости нет, компилятор видит уже раскрытый текст.
- Скобки обязательны вокруг тела и каждого параметра макроса: 2 * SUMMA при #define SUMMA a + b даёт 2 * a + b.
- const int на Uno стоит ноль байтов ОЗУ, зато даёт тип, проверки и область видимости — дефолт для пинов, порогов и периодов.
- Имена: константы — CAPS с единицей измерения (SHAG_MS), переменные и функции — camelCase, пины — с префиксом PIN_.
- Комментарии отвечают на «почему», а не пересказывают строку; Ctrl+T перед каждой прошивкой делает структуру видимой.
Частые вопросы по теме урока
- Во что разворачивается 2 * SUMMA при #define SUMMA a + b?
- Препроцессор подставляет текст буквально, а умножение сильнее сложения: получается 2 * a + b. Скобки в теле макроса (#define SUMMA (a + b)) фиксируют это. 2 * a + b
- Сколько ОЗУ занимает const int POROG = 740 на Uno?
- Оптимизатор avr-gcc вшивает const в машинный код так же, как макрос. Миф «const ест ОЗУ» остался от старых компиляторов. ноль: значение вшивается в код
- Чем плох #define KVADRAT(x) x * x по сравнению с ((x) * (x))?
- Подстановка без скобок даёт 2 + 1 * 2 + 1 = 5. Скобки вокруг каждого параметра обязательны — иначе макрос «почти работает». без скобок KVADRAT(2 + 1) даёт 5 вместо 9
- Где #define уместен, а не const?
- Там, где нужен именно препроцессор: F()-строки и PROGMEM оставляют текст во флеше. Числовые константы — территория const int. для F()-строк и PROGMEM-текстов
- Что делает Ctrl+T в Arduino IDE?
- Auto Format выравнивает отступы за секунду. Компилятору формат безразличен — выравнивание нужно глазам: уровни вложенности видны слева. выравнивает отступы по стандарту