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

Обновлено: 06.07.2024

Препроцессор входит в любой компилятор программ на Си++ и любую среду разработки, рассчитаную на этот язык. Препроцессор обрабатывает исходный код программ до их компиляции. Препроцессорные команды, или директивы, управляют работой препроцессора.

Так например, в программе

Переменная а примет значение 5.

Спецификация пути это имя файла, которому может предшествовать имя каталога. Это должно быть имя существующего файла. Синтаксис спецификации файла зависит от операционной системы, в которой компилируется программа.

При поиске файлов препроцессор использует концепцию «стандартного» каталога. Расположение стандартных каталогов для файлов зависит от реализации и операционной системы. Определение стандартного каталога можно найти в руководстве по компилятору.

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

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

Если спецификация файла заключена в угловые скобки ( < > ), то препроцессор не проводит поиска в текущем рабочем каталоге. Поиск файла начинается в каталогах, заданных в командной строке компилятора, а затем в стандартном каталоге.

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

Изменяет внутренний номер строки и имя файла компилятора. Если имя файла опущено, оно остается прежним. Cинтаксис директивы:

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

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

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

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

Подключение файлов.
Условная компиляция

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Можно, конечно оставить и так, но есть решение получше, называется — библиотека 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_ и количества требуемых повторений. Это несколько упрощенное объяснение, в реальности там чуть сложнее, но основной принцип такой.

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

Перед компиляцией файл кода проходит этап, известный как трансляция. На этапе трансляции происходит много всего, чтобы подготовить ваш код к компиляции (если вам интересно, здесь вы можете найти список этапов трансляции). Файл кода с примененными к нему трансляциями называется единицей трансляции.

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

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

В этом уроке мы обсудим, что делают некоторые из наиболее распространенных директив препроцессора.

В качестве отступления.

Директивы using (представленные в уроке «2.8 – Конфликты имен и пространства имен») не являются директивами препроцессора (и, следовательно, препроцессором не обрабатываются). Таким образом, хотя термин директива обычно означает директиву препроцессора, это не всегда так.

Включения

Рассмотрим следующую программу:

Определения макросов

Существует два основных типа макросов: макросы, подобные объектам, и макросы, подобные функциям.

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

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

В первом определении нет подставляемого при замене текста, а во втором есть. Поскольку это директивы препроцессора (а не инструкции), обратите внимание, что ни одна из форм не заканчивается точкой с запятой.

Макросы, подобные объектам, с подставляемым текстом

Когда препроцессор встречает эту директиву, любое дальнейшее появление идентификатора заменяется подставляемым текстом. Идентификатор традиционно набирается заглавными буквами с использованием подчеркивания для обозначения пробелов.

Рассмотрим следующую программу:

Препроцессор преобразует приведенный выше код в следующее:

Этот код при запуске печатает: My name is: Alex .

Объектоподобные макросы использовались как более дешевая альтернатива постоянным переменным. Те времена давно прошли, поскольку компиляторы стали умнее, а язык вырос. Теперь объектоподобные макросы можно увидеть только в устаревшем коде.

Мы рекомендуем вообще избегать таких макросов, так как существуют более эффективные способы сделать аналогичные вещи. Мы обсудим это более подробно в уроке «4.14 – const, constexpr и символьные константы».

Макросы, подобные объектам, без подставляемого текста

Макросы, подобные объектам, также могут быть определены без подставляемого при замене текста.

Макросы этого типа работают так, как и следовало ожидать: любое дальнейшее появление идентификатора удаляется и ничем не заменяется!

Это может показаться довольно бесполезным, и, да, это бесполезно для замены текста. Однако эта форма директивы обычно используется для другого. Мы обсудим использование этой формы чуть позже.

В отличие от объектоподобных макросов с заменяющим текстом, макросы этой формы обычно считаются приемлемыми для использования.

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

Рассмотрим следующую программу:

Это обеспечивает удобный способ «закомментировать» код, содержащий многострочные комментарии.

Макросы, подобные объектам, не влияют на другие директивы препроцессора.

Теперь вам может быть это интересно:

На самом деле вывод препроцессора вообще не содержит директив – все они разрешаются/удаляются перед компиляцией, потому что компилятор не знает, что с ними делать.

Директивы разрешаются перед компиляцией сверху вниз для каждого файла.

Рассмотрим следующую программу:

После завершения препроцессора все определенные в данном файле идентификаторы отбрасываются. Это означает, что директивы действительны только с точки определения до конца файла, в котором они определены. Директивы, определенные в одном файле кода, не влияют на другие файлы кода в том же проекте.


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

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

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

Проблемы

Условная компиляция является весьма мощным инструментом, при помощи которого можно вмешиваться в компиляцию кода и делать его очень универсальным как для пользователя, так и для железа. Рассмотрим директивы условной компиляции:

Указывает компилятору, что данный файл нужно подключить только один раз. Является более удобной и современной заменой конструкции вида

Такую конструкцию вы можете встретить в 99% библиотек, файлов ядра и вообще заголовочников с кодом.

Макросы

У препроцессора есть несколько интересных макросов, которыми можно пользоваться в своём коде. Рассмотрим некоторые полезные из них, которые работают на Arduino (точнее, на компиляторе avr-gcc).

__func__ и __FUNCTION__

__DATE__ и __TIME__

__DATE__ возвращает дату компиляции по системному времени в виде символьного массива (строки) в формате <первые три буквы месяца> <число> <год> __TIME__ возвращает время компиляции по системному времени в виде символьного массива (строки) в формате ЧЧ:ММ:СС

__FILE__ и __BASE_FILE__

__FILE__ и __BASE_FILE__ возвращают полный путь к текущему файлу, опять же как строку. Являются аналогами друг друга.

__LINE__


__LINE__ возвращает номер строки в документе, в которой вызван этот макрос

__COUNTER__

__COUNTER__ возвращает значение, начиная с 0. Значение __COUNTER__ увеличивается на единицу с каждым вызовом макроса в коде.

__COUNTER__ можно использовать для генерации уникальных имён переменных, но об этом мы поговорим когда нибудь в другой раз.

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

Перед компиляцией файл кода проходит этап, известный как трансляция. На этапе трансляции происходит много всего, чтобы подготовить ваш код к компиляции (если вам интересно, здесь вы можете найти список этапов трансляции). Файл кода с примененными к нему трансляциями называется единицей трансляции.

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

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

В этом уроке мы обсудим, что делают некоторые из наиболее распространенных директив препроцессора.

В качестве отступления.

Директивы using (представленные в уроке «2.8 – Конфликты имен и пространства имен») не являются директивами препроцессора (и, следовательно, препроцессором не обрабатываются). Таким образом, хотя термин директива обычно означает директиву препроцессора, это не всегда так.

Включения

Рассмотрим следующую программу:

Определения макросов

Существует два основных типа макросов: макросы, подобные объектам, и макросы, подобные функциям.

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

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

В первом определении нет подставляемого при замене текста, а во втором есть. Поскольку это директивы препроцессора (а не инструкции), обратите внимание, что ни одна из форм не заканчивается точкой с запятой.

Макросы, подобные объектам, с подставляемым текстом

Когда препроцессор встречает эту директиву, любое дальнейшее появление идентификатора заменяется подставляемым текстом. Идентификатор традиционно набирается заглавными буквами с использованием подчеркивания для обозначения пробелов.

Рассмотрим следующую программу:

Препроцессор преобразует приведенный выше код в следующее:

Этот код при запуске печатает: My name is: Alex .

Объектоподобные макросы использовались как более дешевая альтернатива постоянным переменным. Те времена давно прошли, поскольку компиляторы стали умнее, а язык вырос. Теперь объектоподобные макросы можно увидеть только в устаревшем коде.

Мы рекомендуем вообще избегать таких макросов, так как существуют более эффективные способы сделать аналогичные вещи. Мы обсудим это более подробно в уроке «4.14 – const, constexpr и символьные константы».

Макросы, подобные объектам, без подставляемого текста

Макросы, подобные объектам, также могут быть определены без подставляемого при замене текста.

Макросы этого типа работают так, как и следовало ожидать: любое дальнейшее появление идентификатора удаляется и ничем не заменяется!

Это может показаться довольно бесполезным, и, да, это бесполезно для замены текста. Однако эта форма директивы обычно используется для другого. Мы обсудим использование этой формы чуть позже.

В отличие от объектоподобных макросов с заменяющим текстом, макросы этой формы обычно считаются приемлемыми для использования.

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

Рассмотрим следующую программу:

Это обеспечивает удобный способ «закомментировать» код, содержащий многострочные комментарии.

Макросы, подобные объектам, не влияют на другие директивы препроцессора.

Теперь вам может быть это интересно:

На самом деле вывод препроцессора вообще не содержит директив – все они разрешаются/удаляются перед компиляцией, потому что компилятор не знает, что с ними делать.

Директивы разрешаются перед компиляцией сверху вниз для каждого файла.

Рассмотрим следующую программу:

После завершения препроцессора все определенные в данном файле идентификаторы отбрасываются. Это означает, что директивы действительны только с точки определения до конца файла, в котором они определены. Директивы, определенные в одном файле кода, не влияют на другие файлы кода в том же проекте.

Читайте также: