что такое директива препроцессора

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

что такое директива препроцессора. Смотреть фото что такое директива препроцессора. Смотреть картинку что такое директива препроцессора. Картинка про что такое директива препроцессора. Фото что такое директива препроцессора

Препроцессор

Процесс компиляции прошивки очень непростой и имеет несколько этапов, один из первых – работа препроцессора. Препроцессору можно давать команды, которые он выполнит перед компиляцией кода прошивки: это может быть подключение файлов, замена текста, условные конструкции и некоторые другие вещи. Также у препроцессора есть макросы, которые позволяют добавлять в код некоторые интересные вещи.

#include – подключить файл

Также можно указать путь к файлу, который нужно подключить. Например у нас в папке со скетчем есть папка libs, а в ней – файл mylib.h. Чтобы подключить такой файл, пишем:

Компилятор будет искать его в папке со скетчем, в подпапке libs.

#define / undef

Или быстрого и удобного отключения отладки в коде:

Или даже задефайнить целый кусок кода, используя переносы и обратный слэш

Если DEBUG задефайнен, то DEBUG_PRINT – это макро-функция, которая выводит значение в порт. А если не задефайнен – все вызовы DEBUG_PRINT просто убираются из кода и экономят память!

Если DEBUG_ENABLE задефайнен – все вызовы DEBUG() в коде будут заменены на вывод в порт. Если не задефайнен – они будут заменены НИЧЕМ, то есть просто “вырежутся” из кода! Также по DEBUG_ENABLE можно запустить сериал и получить полный контроль над отладкой: если она не нужна – убрали DEBUG_ENABLE и из кода убрался запуск порта и все выводы, что резко сокращает объём занимаемой памяти:

Проблемы

что такое директива препроцессора. Смотреть фото что такое директива препроцессора. Смотреть картинку что такое директива препроцессора. Картинка про что такое директива препроцессора. Фото что такое директива препроцессора На этом сложности не заканчиваются: #define из одной библиотеки может пролезть в другую библиотеку, которая подключена после первой! Вернёмся к тому же примеру с DarkMagenta – если в моей библиотеке я задефайню это слово и подключу библиотеку до подключения FastLED – я получу ошибку компиляции! Если поменять подключение местами – ошибки не будет. Но, если я захочу использовать DarkMagenta в своём скетче, я буду неприятно удивлён =) что такое директива препроцессора. Смотреть фото что такое директива препроцессора. Смотреть картинку что такое директива препроцессора. Картинка про что такое директива препроцессора. Фото что такое директива препроцессора Что я хочу сказать в итоге: #define – гораздо более мощный инструмент, чем может показаться на первый взгляд. Использование define с невнимательным отношением к именам может привести к ошибке, которую будет непросто отловить. Это палка о двух концах: с одной стороны хочется использовать в своей библиотеке define, чтобы никто другой случайно не пролез со своими дефайнами. В то же время, своя библиотека может начать конфликтовать с другими библиотеками. Какой тут выход? Очень простой! Делать имена дефайнов максимально уникальными: если это библиотека – оставлять префикс библиотеки, если это скетч – делать префикс с именем скетча. Также можно отказаться от define в пользу констант или enum, enum кстати удобнее define в плане создания набора констант, а места занимает совсем немного!

#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__ можно использовать для генерации уникальных имён переменных, но об этом мы поговорим когда нибудь в другой раз.

Источник

Урок №22. Директивы препроцессора

Обновл. 11 Сен 2021 |

Препроцессор лучше всего рассматривать как отдельную программу, которая выполняется перед компиляцией. При запуске программы, препроцессор просматривает код сверху вниз, файл за файлом, в поиске директив. Директивы — это специальные команды, которые начинаются с символа # и НЕ заканчиваются точкой с запятой. Есть несколько типов директив, которые мы рассмотрим ниже.

Директива #include

Вы уже видели директиву #include в действии. Когда вы подключаете файл с помощью директивы #include, препроцессор копирует содержимое подключаемого файла в текущий файл сразу после строки с #include. Это очень полезно при использовании определенных данных (например, предварительных объявлений функций) сразу в нескольких местах.

Директива #include имеет две формы:

Директива #define

Директиву #define можно использовать для создания макросов. Макрос — это правило, которое определяет конвертацию идентификатора в указанные данные.

Есть два основных типа макросов: макросы-функции и макросы-объекты.

Макросы-функции ведут себя как функции и используются в тех же целях. Мы не будем сейчас их обсуждать, так как их использование, как правило, считается опасным, и почти всё, что они могут сделать, можно осуществить с помощью простой (линейной) функции.

Макросы-объекты можно определить одним из следующих двух способов:

#define идентификатор текст_замена

Макросы-объекты с текст_замена

Источник

Препроцессор C

что такое директива препроцессора. Смотреть фото что такое директива препроцессора. Смотреть картинку что такое директива препроцессора. Картинка про что такое директива препроцессора. Фото что такое директива препроцессора

Си препроцессор представляет собой макро язык, который используется для преобразования программы до того как она будет скомпилирована. Причем сама программа может быть не обязательно на Си, она может быть на С++, Objective-C или даже на ассемблере. В общем препроцессор представляет собой примитивный как-бы функциональный язык, с помощью которого можно делать вполне интересные вещи.

Как работает препроцессор.

Для понимания работы препроцессора важно осознавать уровень абстракций с которыми он работает. Основным понятием в препроцессоре является токен (token) — это, грубо говоря последовательность символов, отделённая разделителями, похоже на идентификатор в Си, но значительно шире. В общем в препроцессоре есть только директивы, токены, строковые и числовые литералы и выражения, еще он понимает комментарии (просто их игнорирует). Упрощенно говоря, препроцессор работает с текстовыми строчками, умеет их склеивать, превращать в строковый литерал, выполнять макроподстановку, и подключать файлы.

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

Директивы — это специальные команды, которые препроцессор распознаёт и выполняет. Все директивы начинаются со знака #. Если первый непробельный символ в строке это — #, то препроцессор будет пытаться распознать в ней свою директиву.
Существуют следующие директивы:
— Подключение файлов: #include, #include_next.
— Условная компиляция: #if, #ifdef, #ifndef, #else, #elif and #endif.
— Диагностика: #error, #warning, #line.
— Дополнительная информация компилятору: #pragma
— Макроопределения: #define

Подключение файлов.

Первая директива, которая всем встречается при изучении языка Си — это #include. Записывается так:

Встретив в исходнике эту директиву, препроцессор заменяет её на содержимое файла, имя которого указанно в параметре. Различие между первой и второй формой записи состоит в том, где в первую очередь, препроцессор будет искать указанный файл. В первом случае он сначала будет искать в каталогах с системными заголовками. Во втором — в том-же каталоге, где находится компилируемый исходник. Грубо говоря, при подключении системных/стандартных заголовков нужно имя файла писать в угловых скобках, а для своих — в кавычках.
Мало кто знает, но есть ещё одна директива для включения файлов — #include_next. Записывается она также как и обычный #include, но ее поведение несколько отличается. Дело в том, что препроцессор ищет подключаемые заголовки по многим путям, и бывает, что искомый файл есть сразу в нескольких каталогах. В случае применения директивы #include, он подключает первый попавшийся файл с совпавшим именем. В случае #include_next — будет подключен первый файл с совпавшим именем, который еще не включался в эту единицу трансляции, то есть следующий еще не подключенный. Причем применять #include_next можно только в этих самых заголовках с совпадающими именами, применённая в.с файле эта директива ведёт себя как обычный #include. Таким образом, если в каждом из заголовков с одинаковыми именами применить #include_next, то конечном итоге, они все будут подключены.
Ещё одна интересная особенность директивы #include то, что в ней тоже выполняется макроподстановка. То есть, параметра в ней можно использовать любой макрос, который развернётся в имя файла в одной из двух допустимых форм(в кавычках или в угловых скобках). Например:

Условная компиляция

Применяется, когда в зависимости от значения различных макросов, нужно компилировать, или нет, тот или иной кусок кода, или установить другие макросы.

Где условие — это выражение препроцессора. Это может быть любая комбинация макросов, условий и целочисленных литералов, которая в результате макроподстановки превратится в выражение состоящее только из целочисленных литералов, арифметических операций и логических операторов. Так-же здесь ещё можно использовать единственный «макрооператор» — defined — он превращается в 1, если его операнд определён, и 0 — если нет.

__AVR__ и __ICCAVR__ — это специальные предопределённый макросы, позволяющие определить используемый компилятор. Соответственно для каждого компилятора существует предопределённый макрос, который позволяет его однозначно идентифицировать.
Как уже говорилось, препроцессор работает на уровне отдельных токенов — текстовых строчек, их значение препроцессору безразлично, и он ничего не знает о правилах и грамматике целевого языка. Поэтому в директивах условной компиляции нельзя использовать никакие конструкции языка Си. Например:

В обоих приведённых примерах условия будут всегда ложны и содержимое #if блоков не выполнится. Препроцессор не знает ничего, ни о структурах и их размере, ни о переменных — они-ж не макросы. По этому в первом случае нужно использовать static_assert, реализованный средствами самого Си. А вот во втором случае можно извернутся так:

Условия могут быть сложными и содержать в себе макросы, которые будут полностью развёрнуты перед вычислением условия:

В данном примере блок #if выполнится если макрос BUFFER_SIZE имеет значение кратное степени двойки и если определен макрос OPTIMIZE_FOR_POWER_OF_2. Конструкция IS_POWER_OF_2(BUFFER_SIZE) после макроподстановки развернется в выражение ((16) & (16)-1 == 0), которое препроцессор легко вычислит.
Для конструкции типа #if defined есть сокращенная форма: #ifdef. Она во всём эквивалентна полной форме, за исключением того, что в сокращенной форме нельзя комбинировать несколько условий.
Также директивы условной компиляции часто используются для предотвращения повторного включения заголовочных файлов (include guard):

Эта конструкция гарантирует, что все определения из заголовка будут включены только один раз в единицу трансляции.

Диагностика.

В предыдущих примерах мы уже встретились с одной диагностической директивой — #error. Назначение её предельно просто — остановить компиляцию с сообщением об ошибке, указанном после директивы. Её можно использовать совместно с директивами условной компиляции для того, чтоб убедиться установлен ли какой-то важный макрос и, что он имеет правильное значение.
Также существует директива #warning, аналогична #error, но не прерывает компиляцию, а выдаёт предупреждение.
Директива #line служит для задания номеров строк и имени файла, показываемых в сообщениях об ошибках и возвращаемые специальными макросами __LINE__ и __FILE__. Например:

При этом в сообщениях об ошибках мы увидим следущее:

Надо учитывать, что такая конструкция собьёт столку любую IDE (и человека тоже) и найти место ошибки будет очень не просто. Однако этот трюк можно использовать, чтоб указать на ошибку, возникшую где-то далеко от места, где мы ее проверяем, например на какой-то важный макрос, определённый в другом файле и имеющий не правильное значение. Надо только точно знать где он расположен.

#pragma
Макроопределения

Теперь переходим к интересному, собственно к макросам. существуют два типа макросов: макрос-объект(object-like macro) и макрос-функция(function-like macro), оба типа объявляются с помощью директивы #define. Рассмотрим сначала макросы-объекты. Объявляются они как:
#define ИМЯ_МАКРОСА [замещающий текст]
Всё, что идёт после имя макроса до конца строки является замещающим текстом.

При дальнейшей обработке файла, если препроцессор находит имя макроса, он заменяет его на соответствующий замещающий текст — это называется макроподстановка. Если в замещающем тексте макроса встречаются имена других макросов, препроцессор выполнит макроподстановку для каждого из них, и так далее, пока не будут развёрнуты все известные на данный момент макросы.

Когда препроцессор будет обрaбатывать строчку:
char buffer[DOUBLE_BUFFER];
Сначала будет выполнена первая макроподстановка и токен DOUBLE_BUFFER будет заменен на EXTRA_BUFFER * 2. Тут-же будет выполнена вторая макроподстановка и токен EXTRA_BUFFER заменется на (BUFFER_SIZE +10), потом BUFFER_SIZE заменется на 32. В результате вся строчка после препроцессинга будет выглядеть так:

Здесь становится понятно, зачем были нужны скобки в макросе EXTRA_BUFFER, без них результирующее выражение получилось бы таким:

А это явно не то, что мы хотели получить. Отсюда правило:
Если макрос содержит какое-то выражение, то оно обязательно должно быть заключено в скобки, иначе могут происходить всякие неочевидные вещи.
Также важно понимать, что препроцессор сам ничего не вычисляет (кроме как в условных директивах #if), он просто склеивает текстовые строчки.
А что будет, если какой-то макрос будет ссылаться сам на себя, непосредственно, и косвенно через другие макросы? Ничего не будет, рекурсии не получится, как только препроцессор просечет рекурсию, он прекратит макроподстановку макроса её вызвавшего и оставит его как есть. Например:

При этом будет определён и символ препроцессора flags и переменная flags. такую особенность часто используют для того, чтобы иметь возможность проверить наличие переменной(или любого другого идентификатора) с помощью директив условной компиляции #ifdef/#else/#endif:
// если флаги определены

Хотя я бы не рекомендовал использовать такой приём без крайней необходимости, так как он сильно затрудняет понимание программы и чреват ошибками, поскольку мы по сути пишем две версии программы в одном наборе исходников со всеми вытекающими последствиями. Ведь есть-же системы контроля версий!

Предопределённые макросы

У каждого компилятора есть множество предопределённых макросов, есть стандартные — общие для всех: gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html#Standard-Predefined-Macros
Есть специфичные для каждого отдельного компилятора, например у gcc:
gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html#Common-Predefined-Macros
И даже для каждой поддерживаемой платформы, например для avr-gcc, список всех предопределённых макросов(кроме контекстно зависимых, таких как __LINE__ и т.д) можно получить набрав в командной строке:

Соответственно, вместо atmega16 писать интересующий контроллер.
В других компиляторах предопределённые макросы ищутся в соответствующей документации.
Все эти макросы могут использоваться для определения платформы, для которой компилируется программа, используемого языка (Си, Си++ или ассемблер) и различных особенностей целевой архитектуры.
Также есть макросы предназначенные в основном для отладки: __FILE__, __LINE__ и __FUNCTION__. __FILE__ разворачивается в строковый литерал, содержащий имя обрабатываемого файла. __LINE__ — целочисленный литерал означающий номер текущей строки. __FUNCTION__ — имя текущей функции. Надо заметить, что макрос __FUNCTION__ разворачивается всё-таки не препроцессором а компилятором — препроцессор ничего не знает о функциях в языке Си. Также надо учитывать, что значения __LINE__ и __FILE__ могут изменяться с помощью директивы #line.
Типичное использование макросов __LINE__, __FILE__ и __FUNCTION__:

При этом вызов функции MyError превратится во что-то такое:

Макросы-функции

Второй вид макросов — это макро-функции (function-like macros). Определяются они с помощью той-же директивы #define, после которой (сразу без пробелов) в круглых скобках идёт список разделённых запятыми аргументов:

Макрос SQR предназначен вычислять квадрат переданного ему выражения, в приведённом примере SQR(b) развернётся в (b * b). Вроде-бы нормально, но если этому макросу передать более сложное выражение
,
то он развернётся совсем не в то, что нужно:

Очевидно, что умножение выполнится первым и это у нас уже далеко не квадрат.
Поэтому все аргументы макросов используемые в математических и не только выражениях надо обязательно заключать в скобки:

Однако и этот вариант не свободен от недостатков, например:

Переменная b будет инкрементирована два раза. И у этого недостатка есть решения гибкие и не очень, стандартные и нет, но о них говорить не будем. В данном примере гораздо лучше применить встраиваемую (inline) функцию, она свободна от недостатков макросов:

У макросов-функций есть интересная особенность — макроподстановка в них выполняется два раза. Первый раз — для каждого из параметров до того, как они будут подставлены в тело макроса. Второй раз — для всего тела макроса после подстановки в него параметров. В большинстве случаев это не имеет особого значения. Подробнее об этом можно прочитать здесь:
gcc.gnu.org/onlinedocs/cpp/Argument-Prescan.html#Argument-Prescan

В макро-функциях можно использовать два специальных макро-оператора: # и ##. Первый превращает свой операнд в строковый литерал:

Вызов PRINT_VAR в данном случае превратится в

При этом будет напечатана строка: my_var = 10. Здесь для склеивания форматной строки использован тот факт, что две строки разделённые лишь пробельными символами компилятор считает одной строкой: «%s = %» «d».
Макро-оператор ## склеивает два токена в один токен, для которого после будет выполнена макроподстановка:

Применять эти макро-операторы можно только к параметрам макросов. Причем для параметров к которым они применены макроподстановка будет применена только один раз — для полученного результата. То есть параметр PORT_LETTER не будет отдельно сканироваться на наличие в нем макросов. Почему макрос SET_PIN состоит из двух уровней объясняется ниже.
Теперь, допустим, нам нужен макрос, который склеивает идентификатор из двух кусков:

Если параметра этого макроса непосредственно, те токены, что нам нужно склеить, как в примере выше, то всё сработает как надо. Если-же это макросы, которые сначала нужно раскрыть, то придется вводить еще один вспомогательный макрос, который сначала развернёт параметры и передаст их следующему макросу:

Из-за того, что для параметров, для которых применена конкатенация, не производится макроподстановка, в препроцессорном метапрограммировании часто приходится применять такие двухуровневые макросы: один — для развёртывания параметров, второй — делает непосредственную работу.

Макро-функции можно передать имя другой макро-функции в качестве параметра и, соответственно, вызвать её:

Практический пример препроцессорного метапрограммирования

В качестве примера рассмотрим генерацию таблицы для вычисления контрольной суммы CRC16. Функция для вычисления CRC16 для каждого байта выглядит так:

Где newchar — очередной байт сообщения для которого вычисляем CRC,
crcval — предыдущее значение CRC.
сrcTable — таблица из 256 значений, которую нам надо сгенерировать.
Функция возвращает новое значение контрольной суммы.

Первоначальная идея была и вовсе вычислять CRC16 от строкового литерала с помощью препроцессора, чтобы можно было реализоват «switch» по CRC16 от строки, с удобочитаемыми метками. Но только на препроцессоре это сделать не получилось из-за степенной сложности генерируемых выражений — компилятору банально не хватает памяти, чтоб посчитать таким образом CRC16 для двух символов. На шаблонах С++ это можно сделать без проблем.

Елементы таблицы сrcTable можно вычислить с помощью такой функции:

Где v — индекс в таблице,
polynom — полином контрольной суммы, в данном примере будем использовать значение 0x8408, соответствующее стандарту CRC-CCITT.

Теперь нужно этот алгоритм реализовать с помощью препроцессора. Как быть с циклом? В препроцессоре нет ни циклов ни рекурсии. Прийдётся цикл развернуть вручную:

Теперь, вызывая макрос CRC_TABLE_8 мы получаем константное выражение для одного элемента таблицы. Выражение это, кстати, очень длинное порядка 200-400 тысяч символов! Это происходит потому, что каждый(кроме первого) макрос CRC_TABLE_x вызывает 3 макроса более нижнего уровня, а ведь препроцессор сам выражения не вычисляет, оставляя это компилятору. И получается в результате длинна такого выражения порядка 3 в восьмой степени помножить на длинны выражения низшего уровня. Но ничего, это компилятор еще прожевывает. Теперь нужно сгенерировать саму таблицу:
#define CRC_POLYNOM 0x8408

Можно, конечно оставить и так, но есть решение получше, называется — библиотека Boost preprocessor. В ней имеется много всяких полезняшек, в частности есть макрос BOOST_PP_REPEAT, который повторяет заданное количество раз макрос, переданный ему в качестве параметра. С использованием BOOST_PP_REPEAT геерацию таблицы можно написать так:

Выглядит уже вполне неплохо. Макрос, который будет повторяться в BOOST_PP_REPEAT, должен иметь три параметра. Первый уровень вложенности повторения, если мы будем использовать вложенные повторения, мы его не используем. Второй — счётчик, текущая итерация — индекс в нашей таблице. Третий — дополнительный параметр, мы в нем передаём полином контрольной суммы.

Как-же работает BOOST_PP_REPEAT, если в перпроцессоре нет ни циклов, ни рекурсии. Очень просто — определено 256 макросов с именами типа BOOST_PP_REPEAT_x, где х — номер итерации, которые вызывают друг друга по цепочке. В макросе BOOST_PP_REPEAT склеивается имя макроса этой цепочки из токена BOOST_PP_REPEAT_ и количества требуемых повторений. Это несколько упрощенное объяснение, в реальности там чуть сложнее, но основной принцип такой.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *