Урок №22. Директивы препроцессора
Обновл. 11 Сен 2021 |
Препроцессор лучше всего рассматривать как отдельную программу, которая выполняется перед компиляцией. При запуске программы, препроцессор просматривает код сверху вниз, файл за файлом, в поиске директив. Директивы — это специальные команды, которые начинаются с символа # и НЕ заканчиваются точкой с запятой. Есть несколько типов директив, которые мы рассмотрим ниже.
Директива #include
Вы уже видели директиву #include в действии. Когда вы подключаете файл с помощью директивы #include, препроцессор копирует содержимое подключаемого файла в текущий файл сразу после строки с #include. Это очень полезно при использовании определенных данных (например, предварительных объявлений функций) сразу в нескольких местах.
Директива #include имеет две формы:
Директива #define
Директиву #define можно использовать для создания макросов. Макрос — это правило, которое определяет конвертацию идентификатора в указанные данные.
Есть два основных типа макросов: макросы-функции и макросы-объекты.
Макросы-функции ведут себя как функции и используются в тех же целях. Мы не будем сейчас их обсуждать, так как их использование, как правило, считается опасным, и почти всё, что они могут сделать, можно осуществить с помощью простой (линейной) функции.
Макросы-объекты можно определить одним из следующих двух способов:
#define идентификатор текст_замена
Макросы-объекты с текст_замена
2.9 – Знакомство с препроцессором
Трансляция и препроцессор
Когда вы компилируете свой код, вы можете ожидать, что компилятор компилирует код именно в том виде, как вы его написали. На самом деле это не так.
Перед компиляцией файл кода проходит этап, известный как трансляция. На этапе трансляции происходит много всего, чтобы подготовить ваш код к компиляции (если вам интересно, здесь вы можете найти список этапов трансляции). Файл кода с примененными к нему трансляциями называется единицей трансляции.
Самый примечательный из этапов трансляции связан с препроцессором. Препроцессор лучше всего рассматривать как отдельную программу, которая манипулирует текстом в каждом файле кода.
Когда препроцессор запускается, он просматривает файл кода (сверху вниз) в поисках директив препроцессора. Директивы препроцессора (часто называемые просто директивами) – это инструкции, которые начинаются с символа # и заканчиваются символом новой строки (НЕ точкой с запятой). Эти директивы сообщают препроцессору, что нужно выполнить определенные задачи по обработке текста. Обратите внимание, что препроцессор не понимает синтаксис C++ – вместо этого директивы имеют свой собственный синтаксис (который иногда напоминает синтаксис C++, а иногда не очень).
Выходные данные препроцессора проходят еще несколько этапов трансляции, а затем компилируются. Обратите внимание, что препроцессор никоим образом не изменяет исходные файлы кода – скорее, все изменения текста, сделанные препроцессором, временно размещаются в памяти при каждой компиляции файла кода.
В этом уроке мы обсудим, что делают некоторые из наиболее распространенных директив препроцессора.
В качестве отступления.
Директивы using (представленные в уроке «2.8 – Конфликты имен и пространства имен») не являются директивами препроцессора (и, следовательно, препроцессором не обрабатываются). Таким образом, хотя термин директива обычно означает директиву препроцессора, это не всегда так.
Включения
Рассмотрим следующую программу:
Когда препроцессор запускается для этой программы, он заменяет #include предварительно обработанным содержимым файла с именем » iostream «.
Определения макросов
Директива #define может использоваться для создания макросов. В C++ макрос – это правило, определяющее, как входной текст преобразуется в выходной текст с помощью замены.
Существует два основных типа макросов: макросы, подобные объектам, и макросы, подобные функциям.
Макросы, подобные функциям, действуют как функции и служат той же цели. Мы не будем здесь их обсуждать, потому что их использование обычно считается опасным, и почти всё, что они могут сделать, можно сделать и с помощью обычных функций.
Макросы, подобные объектам, можно определить одним из двух способов:
В первом определении нет подставляемого при замене текста, а во втором есть. Поскольку это директивы препроцессора (а не инструкции), обратите внимание, что ни одна из форм не заканчивается точкой с запятой.
Макросы, подобные объектам, с подставляемым текстом
Когда препроцессор встречает эту директиву, любое дальнейшее появление идентификатора заменяется подставляемым текстом. Идентификатор традиционно набирается заглавными буквами с использованием подчеркивания для обозначения пробелов.
Рассмотрим следующую программу:
Препроцессор преобразует приведенный выше код в следующее:
Объектоподобные макросы использовались как более дешевая альтернатива постоянным переменным. Те времена давно прошли, поскольку компиляторы стали умнее, а язык вырос. Теперь объектоподобные макросы можно увидеть только в устаревшем коде.
Мы рекомендуем вообще избегать таких макросов, так как существуют более эффективные способы сделать аналогичные вещи. Мы обсудим это более подробно в уроке «4.14 – const, constexpr и символьные константы».
Макросы, подобные объектам, без подставляемого текста
Макросы, подобные объектам, также могут быть определены без подставляемого при замене текста.
Макросы этого типа работают так, как и следовало ожидать: любое дальнейшее появление идентификатора удаляется и ничем не заменяется!
Это может показаться довольно бесполезным, и, да, это бесполезно для замены текста. Однако эта форма директивы обычно используется для другого. Мы обсудим использование этой формы чуть позже.
В отличие от объектоподобных макросов с заменяющим текстом, макросы этой формы обычно считаются приемлемыми для использования.
Условная компиляция
Рассмотрим следующую программу:
Еще одно распространенное использование условной компиляции включает использование #if 0 для исключения блока кода из компиляции (как если бы он находился внутри блока комментариев):
Это обеспечивает удобный способ «закомментировать» код, содержащий многострочные комментарии.
Макросы, подобные объектам, не влияют на другие директивы препроцессора.
Теперь вам может быть это интересно:
Поскольку мы определили PRINT_JOE как ничто, почему препроцессор не заменил PRINT_JOE в #ifdef PRINT_JOE ничем?
Макросы вызывают замену текста только в обычном коде. Другие команды препроцессора игнорируются. Следовательно, PRINT_JOE в #ifdef PRINT_JOE не меняется.
На самом деле вывод препроцессора вообще не содержит директив – все они разрешаются/удаляются перед компиляцией, потому что компилятор не знает, что с ними делать.
Область видимости определений #define
Директивы разрешаются перед компиляцией сверху вниз для каждого файла.
Рассмотрим следующую программу:
После завершения препроцессора все определенные в данном файле идентификаторы отбрасываются. Это означает, что директивы действительны только с точки определения до конца файла, в котором они определены. Директивы, определенные в одном файле кода, не влияют на другие файлы кода в том же проекте.
Препроцессор языка С
Теоретическая часть
Препроцессор языка С выполняет макроподстановку, условную компиляцию и включение именованных файлов. Строки, начинающиеся со знака # (перед которыми разрешены символы пустого пространства), задают препроцессору инструкции-директивы. Их синтаксис не зависит от остальной части языка; они могут фигурировать где угодно и оказывать влияние (независимо от области действия) вплоть до конца единицы трансляции. Границы строк принимаются во внимание: каждая строка анализируется отдельно (но есть возможность и сцеплять строки). Лексемами для препроцессора являются все лексемы языка и последовательность символов, задающие имена файлов. Кроме того, любой символ, не определенный каким-либо другим способом, также воспринимается как лексема [19.2]. Влияние символов пустого пространства, отличающихся от пробелов и горизонтальных табуляций, внутри строк препроцессора не определено.
Имеются следующие директивы препроцессора:
| #define | #endif | # ifdef | #line |
| #elif | #error | #ifndef | # pragma |
| #else | #if | #include | #undef |
Каждая директива препроцессора должна занимать отдельную строку. Например, строка
Си++/Препроцессорные директивы
Препроцессор входит в любой компилятор программ на Си++ и любую среду разработки, рассчитаную на этот язык. Препроцессор обрабатывает исходный код программ до их компиляции. Препроцессорные команды, или директивы, управляют работой препроцессора.
Таких команд немного, они все начинаются со знака решётки ( # ) и должны быть в начале строки исходного кода:
#define эта директива предусматривает определение макросов или препроцессорных идентификаторов, простейшее применение это замены в тексте программы. #include позволяет включать текст других файлов в текст вашей программы. #undef отменяет действие директивы #define #if организация условной обработки директив. #ifdef организация условной обработки директивю #else организация условной обработки директив. #endif организация условной обработки директив. #elif организация условной обработки директив. #line управление нумерацией строк в тексте программы. #error задаёт текст диагностического сообщения, выводящиеся при наличии ошибок. #pragma зависит от среды разработки. # нулевая, или пустая, директива, бездейственно пропускается.
Директива #define [ править ]
Директива #define служит для замены часто использующихся констант, ключевых слов, операторов или выражений некоторыми идентификаторами. Идентификаторы, заменяющие текстовые или числовые константы, называют именованными константами. Идентификаторы, заменяющие фрагменты программ, называют макроопределениями, причём макроопределения могут иметь аргументы.
Основная форма синтаксиса директивы #define :
Так например, в программе
Переменная а примет значение 5.
Директива #include [ править ]
Директива #include добавляет содержимое заданного файла в другой файл. Можно организовать определения констант и макро в отдельном файле, а затем вставить его директивой #include в любой другой файл. Вставка файлов также очень удобна для объединения объявлений внешних переменных и сложных типов данных. Нужно определить и задать имена этих типов только один раз в созданный для этих целей файл.
Директива #include информирует препроцессор о том, что содержание файла с заданным именем следует обрабатывать так, как будто оно присутствует в исходной программе в месте расположения этой директивы. Новый текст также может содержать директивы препроцессора. Препроцессор выполняет директивы в новом тексте, а затем продолжает обработку текста исходного файла.
Спецификация пути это имя файла, которому может предшествовать имя каталога. Это должно быть имя существующего файла. Синтаксис спецификации файла зависит от операционной системы, в которой компилируется программа.
При поиске файлов препроцессор использует концепцию «стандартного» каталога. Расположение стандартных каталогов для файлов зависит от реализации и операционной системы. Определение стандартного каталога можно найти в руководстве по компилятору.
Препроцессор останавливает поиск сразу же после обнаружения файла с заданным именем. Если задать полную спецификацию файла, заключенную в двойные кавычки ( » » ), то препроцессор использует её для поиска и игнорирует стандартный каталог.
Если заключенная в двойные кавычки спецификация файла является неполной, то препроцессор сначала ищет каталог «родительского» файла. Родительский файл это файл, содержащий директиву #include. Например, если файл f2 вставляется в файл f1, то f1 будет родительским файлом.
Вставка файлов может быть вложенной. Т. е. директива #include может появляться в файле, который сам вставляется директивой #include. Файл f2 может вызывать файл f3. В этом случае f1 все еще будет родительским для f2, но «дедушкой» для f3.
При вложенной вставке файлов поиск каталогов начинается с родительского файла, затем проходит по дедушкиным файлам. Следовательно, поиск начинается в каталоге, который содержит обрабатываемый исходный файл. Если файл не найден, то поиск продолжается в каталогах, заданных в командной строке компилятора. И, наконец, производится поиск в стандартном каталоге.
Если спецификация файла заключена в угловые скобки ( ), то препроцессор не проводит поиска в текущем рабочем каталоге. Поиск файла начинается в каталогах, заданных в командной строке компилятора, а затем в стандартном каталоге.
Допускается вложение вставки файлов до 10 уровней. При обработке вложенных #include препроцессор всегда будет осуществлять вставку в первоначальный исходный файл.
Директива #undef [ править ]
Директива #undef удаляет текущее определение идентификатора. Поэтому все встречающиеся появления идентификатора будут игнорироваться предпроцессором. Для удаления определения макро с использованием #undef, нужно задать только идентификатор макро, не задавая список параметров.
Можно применить директиву #undef к идентификатору, у которого нет определения. Тем самым пользователь получает дополнительную гарантию того, что данный идентификатор не определён.
Директива #undef обычно используется в паре с директивой #define для задания области исходной программы, в которой идентификатор имеет специальное значение. Например, некоторая функция исходной программы может иметь объявленные константы, которые задают значения среды работы, которые не влияют на остальную часть программы. Директива #undef также работает с директивой #if для управления условной компиляцией исходной программы.
Условные директивы #if, #ifdef, #else, #endif, #elif [ править ]
Эти директивы позволяют подавить компиляцию части исходного файла, проверяя постоянное выражение или идентификатор. Результат проверки определяет, какие блоки текста будут переданы в компилятор и какие блоки текста будут удалены из исходного файла при предпроцессорной обработке.
Условная компиляция [ править ]
Директива #line [ править ]
Изменяет внутренний номер строки и имя файла компилятора. Если имя файла опущено, оно остается прежним. Cинтаксис директивы:
к примеру #line 1000 «file.сpp» устанавливается имя исходного файла file.сpp и текущий номер строки 1000. Текущий номер строки и имя файла доступны через константы препроцессора __LINE__ и __FILE__.
Директива #error [ править ]
Директива #error создаёт заданное пользователем сообщение об ошибке во время компиляции, а затем завершает компиляцию.
Примечание: Сообщение об ошибке, создаваемое этой директивой, содержит параметр token-string. Параметр token-string не подлежит расширению макроса. Эта директива наиболее полезна в ходе предварительной обработки и позволяет уведомлять разработчика о противоречиях в программе или о нарушении ограничений. В следующем примере демонстрируется обработка ошибки во время предварительной обработки.
Директива #pragma [ править ]
#pragma это инструкция компилятору, которая определяется реализацией. Конструкция #pragma в языке Си/Си++ используется для задания дополнительных указаний компилятору. С помощью этих конструкций можно указать как осуществлять выравнивание данных в структурах, запретить выдавать определённые предупреждения и так далее.
Что такое директива препроцессора
Директивы предпроцессора это инструкции предпроцессору С. Предпроцессор С это текстовый процессор, который манипулирует текстом исходного файла на первой фазе компиляции. Хотя компилятор вызывает предпроцессор на своей первой стадии, его можно вызвать и отдельно для обработки текста без компилирования.
Директивы предпроцессора обычно используются для облегчения внесения изменений в исходные программы и для облегчения их компилирования в разных средах выполнения. Расположенные в исходном файле директивы заставляют предпроцессор выполнять конкретные действия. Например, предпроцессор может заменить лексемы в тексте, вставить содержимое других файлов в исходный файл или подавить компиляцию части файла, удаляя сегменты текста.
Предпроцессор С распознает следующие директивы:
Директивы предпроцессора могут появляться в произвольном месте исходного файла, но они будут воздействовать только на оставшуюся часть исходного файла, в котором они появились.
«Оператор предпроцессора» это оператор, который признается оператором только в контексте директив предпроцессора. Есть только три специфические оператора предпроцессора: «строковый» (#), «вставки лексем» (##) и defined. Первые два оператора будут рассмотрены в данной Главе позднее в контексте директивы #define. Оператор defined также будет позже рассмотрен в данной Главе в Разделе «Директивы #if, #elif, #else и #endif».
«Прагма» это «прагматическая», или практическая, инструкция компилятору С. Прагмы в исходном файле С обычно используются для управления действиями компилятора над конкретной частью программы без воздействия на программу в целом. (Синтаксис прагм рассмотрен в Разделе 8.6.) Однако, доступность и предназначение отдельных прагм определяется конкретной реализацией компилятора. Информацию об использовании и действии конкретных прагм можно найти в Вашем Руководстве по компилятору.
Директива #define обычно используется для организации связи содержательных идентификаторов с константами, ключевыми словами и часто используемыми операторами и выражениями. Представляющие константы идентификаторы называются «объявленными константами». Представляющие операторы или выражения константы называются «макросами».
После определения идентификатора его нельзя переопределить для другого значения, если не удалить первоначальное определение. Однако, можно переопределить идентификатор тем же самым определением. Следовательно, одно определение может появиться в программе несколько раз.
Директива #undef удаляет определение идентификатора. После удаления определения идентификатор можно переопределить другим значением. Директивы #define и #undef рассматриваются соответственно в Разделах 8.2.2 и 8.2.3.
Практически можно выделить два типа макросов. «Объектные» макросы не принимают аргументов, а «функциональные» определены таким образом, что принимают аргументы, и выглядят и действуют подобно вызовам функций. Макросы не генерируют действительные вызовы функций, поэтому программа будет работать быстрее, если заменить вызовы функций макросами. Однако, макросы могут создать свои проблемы, если не подойти к их определению и использованию со всей тщательностью. В определении макросов с аргументами возможно придется воспользоваться скобками для установления надлежащего порядка проведения вычислений в выражениях. Кроме того, макросы могут некорректно обработать выражения с побочными эффектами. Дополнительную информацию можно увидеть в примерах Раздела 8.2.2, «Директива #define».
Операторы предпроцессора
Есть три специфических оператора предпроцессора: один представлен знаком номера (#), другой удвоенным знаком номера (##), а третий словом defined. «Строковый» оператор (#), который предшествует имени формального параметра макро, заставляет предпроцессор заключить соответствующий действительный аргумент в строковые цитатные скобки. Оператор «вставки лексем» (##) позволяет осуществить слияние лексем, заданных в качестве действительных аргументов, в форму другой лексемы. Эти два оператора используются в контексте директивы #define и описаны в Разделах 8.2.2.1 и 8.2.2.2.
И, наконец, оператор defined упрощает написание составных выражений в некоторых директивах макро. Он используется при условной компиляции и поэтому рассмотрен в Разделе 8.4.1 «Директивы #if, #elif, #else и #endif».
Директива #define
Если после идентификатора следует список параметров, то директива #define заменит каждое появление идентификатор(список-параметров) на версию аргумента текст-замены в которой формальные параметры заменены на действительные аргументы.
Аргумент «текст-замены» состоит из ряда лексем, таких как ключевые слова, константы или полные операторы. Один или несколько разделительных символов должны отделять текст-замены от идентификатора (или от закрывающей скобки списка параметров). Эти разделительные символы не считаются частью текста-замены, также как и любые разделительные символы, которые следуют за последней лексемой текста. Текст, занимающий более одной строки, может быть продолжен на следующей строке, если до символа перехода на новую строку поместить знак обратного деления (\).
Аргумент текст-замены может быть пустым. Выбор этой опции удаляет все появления идентификатора из исходного файла. Однако, идентификатор все еще считается определенным и дает значение 1 при его проверке директивой #if (рассматривается в Разделе 8.4.1).
Необязательный список параметров состоит из одного или нескольких имен формальных параметров, разделенных запятыми. Каждое имя списка должно быть уникальным и весь список должен быть заключен в скобки. Нельзя ставить пробел между идентификатором и открывающей скобкой. Сфера действия формального параметра распространяется до той строки, которая заканчивает текст-замены.
Имена формальных параметров появляются в тексте-замены для того, чтобы пометить места, куда будут вставлены действительные значения. Имя формального параметра может появляться в тексте замены несколько раз и имена могут следовать в произвольном порядке.
Действительные аргументы, которые следуют за появлениями идентификатора в исходном файле совпадают с соответствующими формальными параметрами списка параметров. Каждый формальный параметр в тексте замены, которому не предшествует оператор # или ##, или за которым следует оператор ##, будет заменен соответствующим действительным аргументом. Все макросы действительного аргумента будут раскрыты до замены ими формального параметра. (Операторы # и ## описаны в Разделах 8.2.2.1 и 8.2.2.2.) Список действительных аргументов должен иметь столько аргументов, сколько их в списке параметров.
Если имя определенного макро появляется в тексте замены (даже в результате раскрытия другого макро), то оно не раскрывается.
Аргументы с побочными эффектами иногда вынуждают макро выдавать неожиданные результаты. Заданный формальный параметр может появиться в тексте замены несколько раз. Если формальный параметр заменяется выражением с побочным эффектом, то выражение, с его побочным эффектом, будет вычисляться несколько раз. (См. Пример 4 в Разделе 8.2.2.2, «Оператор вставки лексем».
В результате предпроцессорной обработки получим следующий код:
Оператор вставки лексем (##)
Удвоенный знак номера или оператор «вставки лексем» (##) используется и в объектный и в функциональных макро. Он позволяет объединять отдельные лексемы в единую лексему и следовательно не может быть первой или последней лексемой в определении макро.
Если до или после формального параметра в определении макро стоит оператор вставки лексем, то формальный параметр немедленно заменяется на нераскрытый действительный аргумент. Раскрытие макро не производится над аргументом до его замены. Затем все появления оператора с тексте замены удаляются и предшествующие и последующие им лексемы объединяются. Результирующая лексема должна быть корректной. Если это так, то лексема просматривается на возможную замену, если она содержит имя макро. Пример 7 показывает, как лексемы могут быть вставлены вместе с использованием оператора вставки лексем.
В данном примере определяется идентификатор WIDTH как целая константа 80, в терминах WIDTH определяется LENGTH и целая константа 10. Каждое появление LENGTH заменяется на (WIDTH + 10). В свою очередь, каждое появление WIDTH + 10 заменяется выражением (80+10).
В данном примере определяется идентификатор FILEMESSAGE. Его определение расширяется на вторую строку использованием символа обратного деления, за которым следует символ новой строки.
В данном примере определяются три идентификатора: REG1, REG2 и REG3. REG1 и REG2 определяются как ключевое слово register. Определение REG3 пусто, поэтому из исходного файла будет удалены все появления REG3. Эти директивы можно использовать для того, чтобы убедиться в том, что наиболее важные переменные программы (объявленные с REG1 и REG2) заданы с классом хранения register. (Расширенную версию данного примера можно найти при рассмотрении директивы #if в Разделе 8.4.1.)
В данном примере определяется макро с именем MAX. После этого определения все появления идентификатора MAX в исходном файле будут заменены на выражение
Макро легче читается, чем соответствующее выражение, поэтому легче понять исходный текст программы.
Обратите внимание на то, что аргументы с побочными эффектами могут дать неожиданные результаты при выполнении макро. Например, появление MAX(i, s[i++]) будет заменено на ((i)>(s[i++]))?(i): (s[i++]). Выражение (s[i++]) может быть вычислено дважды, поэтому после вычисления тернарного оператора i может быть увеличено единожды или дважды, в зависимости от результатов сравнения.
В данном примере определяется макро MULT. После определения этого макро появление выражений подобных MULT(3,5) будет заменены на (3)*(5). Скобки вокруг параметров важны, т.к. управление интерпретацией сложных выражений формирует аргументы макро. Например, появление MULT(3+4,5+6) будет заменено на (3+4)*(5+6), что даст в результате 77. Без скобок получаем 3+4*5+6, или 29, т.к. оператор умножения (*) имеет приоритет выше, чем оператор сложения (+).
В данном примере определяются два макро. Объектное макро раскрывается в строковый литерал Hello,Word!, а другое функциональное макро вызывает show, которое берет один аргумент. Однако, определение второго макро включает строковый оператор (#), который непосредственно предшествует формальному параметру x. При передаче аргумента в макро show формальный параметр будет заменен действительным аргументом, заключенным в двойные цитатные скобки.
Данный пример показывает использование строкового оператора и оператора вставки лексем для организации вывода программы.



