View Categories

Директивы препроцессора

Перед компиляцией исходного файла программы выполняется препроцессинг - изменение текста программы. Его выполняет препроцессор - отдельная утилита, которая имеет свой набор команд для управления: все команды начинаются с символа #. По сути возможности препроцессора делают его отдельным языком программирования, при помощи которого можно генерировать код программы на 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]
}

Очень удобная штука при разработке и отладке крупного проекта.

0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх