Перед компиляцией исходного файла программы выполняется препроцессинг - изменение текста программы. Его выполняет препроцессор - отдельная утилита, которая имеет свой набор команд для управления: все команды начинаются с символа #
. По сути возможности препроцессора делают его отдельным языком программирования, при помощи которого можно генерировать код программы на C/C++. На макросах делаются очень сложные и интересные конструкции, заменяющие много строк кода, либо какие-то хитрые алгоритмы для организации программы.
include #
Вставляет содержимое заголовочного файла, указанного в:
"двойных кавычках"
- искать файл сначала в текущей папке с исходником, затем в папке с библиотеками (пользовательскими и системными)<угловых скобках>
- искать файл в папке с библиотеками
#include "mylib.h" // подключить mylib.h, сначала поискать в папке с проектом
#include <mylib.h> // подключить mylib.h из папки с библиотеками
Более подробно о путях к файлам рассказано в отдельном уроке.
define #
Команда #define
позволяет буквально заменить в тексте программы указанный набор символов на другой набор символов. Синтаксис очень простой: #define A B
- заменить A
на B
:
- Точка с запятой не ставится, т.к. это не инструкция и не относится к синтаксису языка
- Между
A
иB
должен быть как минимум один пробел, то есть больше - можно B
может быть не указана - в этом случаеA
просто вырежется из кода, но останется доступной для условной компиляции
Это очень мощный, полезный, но в то же время довольно опасный инструмент - если заменяемый текст присутствует в программе в тех местах, где этого не ожидается - будет ошибка компиляции, причина которой может быть очень разной. Например:
#define my_var 3
int my_var = 3;
// эта строка превратится в 3 = 3, что приведёт к ошибке компиляции
// "попытка изменить константу"
define-константы имеют глобальную область видимости - препроцессор заменяет указанный набор символов во всём исходном файле программы
Поэтому дефайн-константы принято называть в СТИЛЕ_КРИЧАЩЕЙ_ЗМЕИ
и использовать префиксы модулей, например в своей библиотеке:
#define MY_LIB_CONST1 10
#define MY_LIB_ADDRESS 0x85
#define MY_LIB_MODE0 0
#define MY_LIB_MODE1 1
В общем стараться максимально уменьшить вероятность пересечения имён дефайн-констант с другими именами в программе.
Также "дефайнить" имена можно при помощи флагов компилятора, многие системные константы задефайнены где-то в файлах ядра платформы. Например, почти на всех МК работает константа F_CPU
, значение которой показывает частоту процессора в Гц
undef #
Задефайненный символ можно "раздефайнить" при помощи #undef
:
#define MY_DEF 123
#undef MY_DEF
// здесь MY_DEF уже не существует
#define SOME_SETTING
#undef SOME_SETTING
// здесь SOME_SETTING уже не существует
Макросы #
При помощи директивы #define
можно создавать не только константы, но и функции. Такие функции называются макро-функциями или просто макросами (macro). Работает оно так же, как создание констант - просто заменяет один указанный текст другим, по сути позволяет менять код программы при помощи препроцессора. Нужно понимать, что макрос просто вставит код в программу и кода станет больше, программа будет занимать кратно больше места, чем это была бы обычная функция. С другой стороны, вызов обычной функции выполняется какое-то время и в некоторых ситуациях макрос может ускорить вычисление. Макросы - очень мощный и опасный инструмент, нужно использовать их с умом:
- Создаётся точно так же, как константа, но на конце добавляются круглые скобки:
#define MACRO() инструкция
MACRO(); // развернётся в инструкция;
- Может принимать параметры, как функция, но у параметров не указываются типы - оно просто вставится в код:
#define SUM(a, b) a + b
int sum = SUM(3, 4); // развернётся в 3 + 4
- Не может быть перегружен: макрос с существующим именем будет просто заменён:
#define SUM(a, b) a + b
#define SUM(a, b, c) a + b + c
// SUM(a, b) уже не существует, его заменил SUM(a, b, c)
- Параметры макроса рекомендуется заключать в скобки, как и всё тело макроса. Иначе могут быть ошибки:
// сложить
#define SUM1(a, b) a + b
#define SUM2(a, b) (a) + (b)
#define SUM3(a, b) ((a) + (b))
// ожидаемое поведение
// SUM(1 + 2, 3) * 4; // ((1 + 2) + 3) * 4 = 24
SUM1(1 + 2, 3) * 4; // развернётся в 1 + 2 + 3 * 4 = 15
SUM2(1 + 2, 3) * 4; // развернётся в (1 + 2) + (3) * 4 = 15
SUM3(1 + 2, 3) * 4; // развернётся в ((1 + 2) + (3)) * 4 = 24 - верно
// умножить
#define MUL1(a, b) a * b
#define MUL2(a, b) (a) * (b)
#define MUL3(a, b) ((a) * (b))
// ожидаемое поведение
// MUL(1 + 2, 3); // (1 + 2) * 3 = 9
MUL1(1 + 2, 3); // развернётся в 1 + 2 * 3 = 7
MUL2(1 + 2, 3); // развернётся в (1 + 2) * (3) = 9 - верно
MUL3(1 + 2, 3); // развернётся в ((1 + 2) * (3)) = 9 - верно
- Для создания многострочного макроса можно использовать перенос строки обратным слэшем, для последней строки перенос не ставится:
#define FOO1() \
инструкция1; \
инструкция2; \
инструкция3;
- Многострочные конструкции принято заключать в
do-while(0)
для корректной работы блока кода:
#define FOO2() do { \
инструкция1; \
инструкция2; \
инструкция3; } while (0)
Посмотрим, во что развернутся предыдущие два примера:
// вызовем вот так
if (true) FOO();
// FOO1 - некорректно!
if (true) инструкция1;
инструкция2;
инструкция3;
// FOO2 - корректно
if (true) do {
инструкция1;
инструкция2;
инструкция3;
} while (0);
- Пустой макрос позволяет вырезать код из программы, т.к. заменится "ничем":
#define TEST(x)
TEST(123);
TEST(a = b); // ничего не произойдёт
Условная компиляция #
Препроцессор даёт ещё несколько инструментов для более глобального изменения текста программы: можно написать условие, по которому часть кода будет скрыта или наоборот показана:
#ifdef
- если определено через define#ifndef
- если не определено через define#if
- аналог if в условной конструкции#elif
- аналог else if в условной конструкции#else
- аналог else в условной конструкцииdefined(x)
- возвращаетtrue
, еслиx
определено через define, иfalse
- если нет#endif
- завершает условную конструкцию- Операторы сравнения и логики:
>
,<
,>=
,<=
,==
,!=
,&&
,||
Внутри условных конструкций работают остальные директивы - можно дефайнить, подключать файлы и так далее
// #define USE_PARAM
#ifdef USE_PARAM
// код, если задан USE_PARAM
#else
// код, если не задан
#endif
// #define USE_PARAM_1
// #define USE_PARAM_2
#if defined(USE_PARAM_1) && defined(USE_PARAM_2)
// код, если заданы USE_PARAM_1 и USE_PARAM_2
#else
// код, если не задан
#endif
// #define MY_PARAM 3
#if (MY_PARAM == 3)
// код, если MY_PARAM == 3
#elif (MY_PARAM == 4)
// код, если MY_PARAM == 4
#else
// код во всех остальных случаях
// даже если MY_PARAM не задан
#endif
// можно просто скрывать куски кода, меняя 1 на 0
#if 0
#endif
Условная компиляция - крайне мощный инструмент: позволяет писать универсальный код, состав которого можно менять при помощи задефайненных констант. Очень популярный пример - реализация в зависимости от платформы:
#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
// код для ATmega1280 и ATmega2560
#elif defined(__AVR_ATmega32U4__)
// код для ATmega32U4
#elif defined(__AVR_ATmega1284__)
// код для ATmega1284
#else
// код для остальных МК
#endif
error #
Можно вызвать ошибку компиляции при помощи #error "сообщение"
- сообщение выведется в консоль. Например в условной конструкции из предыдущей главы:
#if (SOME_SETTING < 5)
#error "Ошибка! SOME_SETTING должна иметь значение меньше 5"
#endif
pragma #
#pragma
это набор директив с разными возможностями, могут поддерживаться не всеми компиляторами.
message #
#pragma message "текст"
просто выводит текст в консоль:
#pragma message "Используем библиотеку v1.0"
once #
#pragma once
является удобным include guard - указывает, что данный файл нужно подключить только один раз. Добавляется в начало заголовочного файла:
// === lib.h
#pragma once
pack/pop #
Позволяет запаковать структуру, игнорируя выравнивание - переменные займут место в памяти ровно друг за другом с учётом размера своего типа:
#pragma pack(push, 1)
struct Foo {
char a;
long b;
char c;
};
#pragma pack(pop)
// Foo занимает 6 байт
region #
В некоторых редакторах, например VS Code, выводит указанные #pragma region ИМЯ
метки в навигаторе документа:
Операторы #
Стрингификация #
Оператор #
превращает следующее за ним слово в строку, т.е. оборачивает в "двойные кавычки". Может вызываться внутри макроса:
#define MAKE_STR(x) #x
const char* str = MAKE_STR(HELLO); // == "HELLO"
char arr[] = MAKE_STR(stroka_text); // == "stroka_text"
Конкатенация #
Оператор ##
"склеивает" переданные названия в одно, может вызываться внутри макроса:
#define CONCAT(x, y) x##y
int CONCAT(my_, val); // равносильно int my_val;
Константы #
__FUNCTION__ #
__func__
и __FUNCTION__
разворачиваются в строки с именем функции, внутри которой они вызваны:
void myFunc() {
const char* str = __func__; // == "myFunc"
}
__DATE__ #
__DATE__
разворачивается в строку с датой компиляции по системному времени в формате <первые три буквы месяца> <число> <год>
:
const char* date = __DATE__; // == "Dec 1 2024"
__TIME__ #
__TIME__
разворачивается в строку со временем компиляции по системному времени в формате ЧЧ:ММ:СС
:
const char* time = __TIME__; // == "04:03:25"
__FILE__ #
__FILE__
и __BASE_FILE__
разворачиваются в строки с полным путём к текущему файлу:
const char* file = __FILE__; // == "C:\Users\Alex\Desktop\test\test.ino"
__LINE__ #
__LINE__
возвращает номер строки в документе, на которой находится:
int L1 = __LINE__; // == 3
int L2 = __LINE__; // == 5
__COUNTER__ #
__COUNTER__
разворачивается в число, которое увеличивается на 1
с каждым следующим вызовом:
int arr[] = {__COUNTER__, __COUNTER__, __COUNTER__}; // == {4, 5, 6}
Можно использовать для генерации уникальных имён переменных, например:
#define _CONCAT(a, b) a##b
#define CONCAT_INNER(a, b) _CONCAT(a, b)
#define MAKE_VAR(name) CONCAT_INNER(name, __COUNTER__)
int MAKE_VAR(var) = 123; // создана переменная var20
int MAKE_VAR(var) = 456; // создана переменная var21
Пример отладчика #
Пример конструкции для Arduino, которая при определении константы USE_LOG
как объекта Stream
заменяет все вызовы LOG(x)
на печать в консоль самого x
и информации о файле и строке. Пока константа не определена - все вызовы вырезаются из кода программы и не замнимают места:
#ifdef USE_LOG
#define LOG(x) \
do { \
USE_LOG.print(F("> ")); \
USE_LOG.print(x); \
USE_LOG.print(F(" in ")); \
USE_LOG.print(__FUNCTION__); \
USE_LOG.print(F("() [" __FILE__ " : ")); \
USE_LOG.print(__LINE__); \
USE_LOG.println(']'); \
} while (0);
#else
#define LOG(x)
#endif
#define USE_LOG Serial
void setup() {
LOG("hello");
// > hello in setup() [/main.cpp : 17]
}
Очень удобная штука при разработке и отладке крупного проекта.