Чужой скетч ночника из интернета начинался с восьми строк #define, свои правки я сделал через const, а в комментариях кто-то советовал enum — и три стиля уживались в одном файле, как три эпохи. Досадно было не разнообразие, а то, что я не мог объяснить, чем они отличаются: компилятор ел всё молча. Пока однажды макрос без скобок не подсунул мне 5 вместо 9 — и я полез разбираться, что происходит на каждом этапе пути от текста до прошивки.
Разница там настоящая, и она не про вкус. #define обрабатывает препроцессор: это замена текста до компиляции, без типов и без проверок. const — полноценная типизированная константа, которую компилятор видит, проверяет и обычно даже не кладёт в память. enum — список целых с автоматической нумерацией, нарочитый для состояний. Дальше — по каждому на живом скетче, с граблями и правилом выбора за пять секунд.
Про сами переменные напоминать не буду — базовый урок о переменных и типах это закрыл. Здесь только именованные константы: кто их подставляет, где они живут и что меняется для вас как читателя кода.
#define: препроцессор заменяет текст
Работает #define до компилятора. Препроцессор проходит по файлу и заменяет каждое вхождение имени на текст после него: #define POROG 400 — и все POROG ещё до первого этапа сборки превращаются в 400. Ни типа, ни проверки, ни ячейки памяти: в самой прошивке #define не существует. Потому им и удобно задавать пины — CAPS-имя читается как плата, а цена — ноль байтов.
Бесплатность — и корень грабель. Точка с запятой после #define уезжает в подстановку: #define POROG 400; ломает каждую строку с POROG, причём далеко от места определения, и ругань компилятора указывает не на виновника, а на жертву. Макрос с параметрами без скобок — вторая классика: #define KVADRAT(x) x*x при вызове KVADRAT(1+2) разворачивается в 1+2*1+2, то есть в 5. Лечится дисциплиной: каждый параметр и всё выражение — в скобках, ((x)*(x)).
Третье отличие — нет области видимости. #define работает от строки определения до конца файла, сквозь функции и блоки, даже там, где по смыслу не должен. В проекте из нескольких файлов имя вроде PIN_LED легко встречается дважды в разных библиотеках — и препроцессор молча подставит последнее определение. Отсюда и конвенция: имена макросов пишут капсом как метку «осторожно, текстовая замена».
В заголовочных файлах макросы живут ещё и сторожами: пара #ifndef / #define с именем файла и #endif в конце не даёт подключить заголовок дважды и получить двойное определение. Это самая массовая конструкция с #define в мире C++ — почти каждая чужая .h начинается с неё. Граница с моим советом простая: сторожа и #ifdef — законная территория макросов; числа, даже пины, в большом проекте надёжнее отдавать const.
Есть у макросов и полезные ограничения, которые стоит знать. Препроцессор не трогает то, что в кавычках: Serial.print("POROG") напечатает слово POROG, а не 400 — поэтому отладочные печати выглядят парой строк «текст, затем значение». Скоростью макросы с параметрами тоже больше не выигрывают: avr-gcc с настройками по умолчанию инлайнит маленькие функции сам, а макрос лишает вас проверки типов. Честное применение у препроцессора одно — текстовая работа: #ifdef для сборки с отладкой и без, включение-выключение кусков кода одной строкой.
const: константа, которую проверяет компилятор
const int POROG = 400; — переменная, которую нельзя менять после инициализации. У неё есть тип, и компилятор проверяет каждое касание: передадите порог в функцию, ждущую float, — получите внятную диагностику вместо тихой подстановки. У неё есть область видимости: const внутри функции существует только там, глобальная — во всём файле после объявления. Для читающего человека это уже язык, а не заклинание.
Миф, который держит #define на плаву: «const ест память». На AVR оптимизатор avr-gcc обычно держит const int в регистрах или вшивает значение прямо в команды — ячейка ОЗУ не выделяется, пока программу не берут её адрес через указатель или ссылку. Масштаб для спокойствия: у Uno 2 КБ ОЗУ, и одна константа не изменит расклад — массивы и строки съедают память на порядок быстрее. Единственный платный случай я назвал честно: взяли адрес — компилятор обязан выделить ячейку.
Тип выручает и на границах: Serial.print(POROG) знает, что это int, и печатает соответственно. С макросом компилятор видит голую 400 и обрабатывает её по общим правилам, без намёка на замысел автора. Для одиночных чисел я выбираю const всегда — кроме коротких примеров, где CAPS-имя пина читается как плата, и условной компиляции #ifdef, где без препроцессора не обойтись.
Отдельное удовольствие — локальная const. Объявленная внутри loop(), она существует только там, и компилятор вшивает её без памяти: в симуляторе ниже const int porog рождается каждый проход и умирает на закрывающей скобке. Глобальная const заявляет «число для всего файла», локальная — «число для этого витка». Такой градации у #define не бывает вообще: макрос одинакован везде, где виден текст.
// Этот вариант — для настоящей платы: движок симулятора enum не парсит.#define PIN_LED 12 enum Rezhim { REZH_DEN = 0, REZH_NOCH, REZH_SEZON }; // 0, 1, 2 само Rezhim rezhim = REZH_NOCH;const int POROG_DEN = 620;const int POROG_NOCH = 350; void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(9600);} void loop() { int svet = analogRead(A0); switch (rezhim) { case REZH_NOCH: digitalWrite(PIN_LED, svet < POROG_NOCH ? HIGH : LOW); break; case REZH_DEN: digitalWrite(PIN_LED, svet < POROG_DEN ? HIGH : LOW); break; default: digitalWrite(PIN_LED, LOW); break; }}enum: список состояний с бесплатной нумерацией
enum Rezhim { REZH_DEN, REZH_NOCH, REZH_SEZON }; создаёт три именованных целых: REZH_DEN равен 0, REZH_NOCH — 1, REZH_SEZON — 2, нумерация автоматическая и всегда по порядку; стартовое значение можно задать явно, а базу — например uint8_t, чтобы ужать до байта. Главное отличие от трёх const подряд — смысловая связка: это не «три числа», а состояния одной сущности, и switch по такому типу читается как список режимов.
Движок симулятора enum не парсит, поэтому полный автомат — в код-блоке выше, для платы. На словах всё просто: внутри switch перечисляете case REZH_DEN, case REZH_NOCH — и добавление четвёртого режима не требует перепроверять числа по всему скетчу. Каркасы у нас уже есть: конечный автомат на светофоре и пульт на switch по Serial — enum ложится в них без единой правки чисел.
Значения enum можно задать и явно, тогда список превращается в протокол: enum Cmd { CMD_ON = 0x31, CMD_OFF = 0x32, CMD_STAT = 0x33 }; — и switch по принятому с компьютера байту читается как перечень команд, а не как шестнадцатеричное гадание. Имена констант работают как комментарии, которые невозможно забыть обновить: сменили код команды — сменилось одно место в enum, а не десяток рассыпанных 0x31 по файлу.
| Свойство | #define | const | enum |
|---|---|---|---|
| Кто подставляет значение | препроцессор, замена текста | компилятор, как переменную | компилятор, как целое |
| Тип и проверка | нет — подставит что угодно | есть: int, float, byte | только целые, по порядку |
| Область видимости | от строки до конца файла | как у обычной переменной | весь файл или класс |
| Естественное место | пины, условная сборка #ifdef | пороги, номиналы, адреса | режимы и состояния автомата |
В макетке два стиля работают в одном скетче и по-разному видны на деле: GISTEREZ разворачивается ещё до компиляции — движок симулятора честно повторяет подстановку, — а POROG_DEN и POROG_NOCH живут как настоящие типизированные значения, которые можно напечатать и сравнить. Гистерезис здесь не украшение: ночник на фоторезисторе без него дрожит на границе, зажигаясь и гася каждые полсекунды.
Порядок выбора я бы записал так: одиночное число — const; набор взаимоисключающих состояний — enum; пин или порог в примере на одну страницу — не грех и #define ради капса; настоящая текстовая замена вроде #ifdef или скобочного макроса — территория препроцессора. Сомневаетесь — задайте себе вопрос: это число, список чисел или состояние? Три ответа — три инструмента, и в меню на двух кнопках все три вполне могут ужиться в одном файле.
Чужие библиотеки стоит читать с этой картой в голове: в заголовках популярных библиотек #define держит версии и пины, const — калибровочные константы, enum — статусы и коды. Это не неряшливость, а слои истории C++: макросы пришли из чистого Си, const и enum — уже полноценные жители языка. Как только различие становится привычкой, чужой заголовочный файл читается за минуту, а не за вечер.
По глазам всё это различается за секунду: капс — макрос, camelCase с типом — const, перечисление через запятую в фигурных скобках — enum. Я третий стиль не выдумываю и чужой не ломаю: в чужом файле пишу в его стиле, в своём — по правилу выбора выше. Единообразие внутри файла — половина читаемости; спор «как правильнее» между файлами — уже не техника, а вкусовщина, на которую тратить вечер жалко.
- 1. Что препроцессор делает со строкой #define POROG 400?
- 2. Чем const int POROG = 400; надёжнее #define POROG 400?
- 3. enum Cvet { KRASNYI, ZHOLTYI, ZELENYI }; чему равно ZELENYI?
- 4. #define KVADRAT(x) x*x — что вычислит KVADRAT(1+2)?
- 5. Когда const int реально потратит ячейку ОЗУ на AVR?
- #define — замена текста до компилятора: без типа, без области видимости, без ячейки памяти; имя пишут капсом.
- const — типизированная константа: компилятор проверяет, ОЗУ не тратит, пока программа не берёт её адрес.
- enum строит список целых с нумерацией 0, 1, 2 — язык для состояний автомата и switch.
- Грабли #define: точка с запятой в подстановке и макрос без скобок — KVADRAT(1+2) даёт 5 вместо 9.
- Правило выбора: число — const, состояния — enum, настоящая текстовая замена и #ifdef — макрос.
Частые вопросы по теме урока
- Что препроцессор делает со строкой #define POROG 400?
- #define — текстовая замена до компилятора. Ни типа, ни проверки, ни ячейки памяти: в прошивке макроса не существует. заменяет каждое POROG на 400 ещё до компиляции
- Чем const int POROG = 400; надёжнее #define POROG 400?
- const — типизированная константа: компилятор проверяет касания, а видимость ограничена блоком. Память она тратит только при взятии адреса. имеет тип и область видимости
- enum Cvet { KRASNYI, ZHOLTYI, ZELENYI }; чему равно ZELENYI?
- Нумерация в enum автоматическая и с нуля: KRASNYI — 0, ZHOLTYI — 1, ZELENYI — 2. Старт можно задать явно, если нужен другой. 2
- #define KVADRAT(x) x*x — что вычислит KVADRAT(1+2)?
- Подстановка текстовая: 1+2*1+2 = 5. Скобки вокруг параметра и всего выражения — ((x)*(x)) — дают честные 9. 5
- Когда const int реально потратит ячейку ОЗУ на AVR?
- Без взятия адреса avr-gcc вшивает значение в команды и ОЗУ не тратит. Адрес потребовал — компилятор обязан выделить ячейку. если взять её адрес указателем или ссылкой