stringtranslate.com

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

Препроцессор C — это макропрепроцессор для нескольких языков программирования , таких как C , Objective-C , C++ и различных языков Fortran . Препроцессор обеспечивает включение заголовочных файлов , макрорасширений , условной компиляции и управления строками.

Язык директив препроцессора слабо связан с грамматикой языка C, поэтому иногда используется для обработки других видов текстовых файлов . [1]

История

Препроцессор был представлен в C около 1973 года по настоянию Алана Снайдера, а также в знак признания полезности механизмов включения файлов, доступных в BCPL и PL/I . Его первоначальная версия предлагала только включение файлов и простую замену строк с использованием #includeи #defineдля макросов без параметров соответственно. Вскоре после этого он был расширен сначала Майком Леском , а затем Джоном Райзером для включения макросов с аргументами и условной компиляции. [2]

Препроцессор C был частью давней традиции макроязыков в Bell Labs, которая была начата Дугласом Иствудом и Дугласом Макилроем в 1959 году. [3]

Фазы

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

  1. Замена триграфа: препроцессор заменяет последовательности триграфа на символы, которые они представляют. Эта фаза будет удалена в C23 после шагов C++17 .
  2. Объединение строк: физические исходные строки, которые продолжаются экранированными последовательностями новой строки, объединяются в логические строки.
  3. Токенизация: Препроцессор разбивает результат на токены предварительной обработки и пробелы . Он заменяет комментарии пробелами.
  4. Расширение макросов и обработка директив: выполняются строки директив предварительной обработки, включая включение файлов и условную компиляцию. Препроцессор одновременно расширяет макросы и, начиная с версии стандарта C 1999 года, обрабатывает _Pragmaоператоры.

Включая файлы

Одним из наиболее распространенных применений препроцессора является включение другого исходного файла:

#include <stdio.h> int main ( void ) { printf ( "Привет, мир! \n " ); return 0 ; }    

Препроцессор заменяет строку #include <stdio.h>текстовым содержимым файла «stdio.h», в котором, помимо прочего, объявляется printf() функция .

Это также можно записать с использованием двойных кавычек, например #include "stdio.h". Если имя файла заключено в угловые скобки, файл ищется в стандартных путях включения компилятора. Если имя файла заключено в двойные кавычки, путь поиска расширяется, чтобы включить текущий каталог исходных файлов. Компиляторы C и среды программирования имеют возможность, позволяющую программисту определять, где можно найти файлы включения. Это можно ввести с помощью флага командной строки, который можно параметризовать с помощью makefile , так что другой набор файлов включения можно заменить для разных операционных систем, например.

По соглашению, включаемые файлы именуются с расширением .hили .hpp. Однако нет необходимости соблюдать это. Файлы с .defрасширением могут обозначать файлы, предназначенные для многократного включения, каждый раз расширяя одно и то же повторяющееся содержимое; #include "icon.xbm"скорее всего, это будет относиться к файлу образа XBM (который в то же время является исходным файлом C).

#includeчасто вынуждает использовать #includeзащитные приспособления или #pragma onceпредотвращать двойное включение.

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

Директивы if–else , #if, #ifdef, #ifndef, #elseи #elifмогут #endifиспользоваться для условной компиляции . #ifdefи #ifndefявляются простыми сокращениями для #if defined(...)и #if !defined(...).

#if VERBOSE >= 2 printf ( "сообщение трассировки" ); #endif 

Большинство компиляторов, ориентированных на Microsoft Windows, неявно определяют _WIN32. [4] Это позволяет коду, включая команды препроцессора, компилироваться только при ориентировании на системы Windows. Несколько компиляторов определяют WIN32вместо этого. Для таких компиляторов, которые неявно определяют _WIN32макрос, его можно указать в командной строке компилятора, используя -D_WIN32.

#ifdef __unix__ /* __unix__ обычно определяется компиляторами, ориентированными на системы Unix */ # include <unistd.h> #elif defined _WIN32 /* _WIN32 обычно определяется компиляторами, ориентированными на 32- или 64-разрядные системы Windows */ # include <windows.h> #endif    

Пример кода проверяет, определен ли макрос __unix__. Если да, то файл <unistd.h>включается. В противном случае он проверяет, _WIN32определен ли макрос. Если да, <windows.h>то файл включается.

Более сложный #ifпример может использовать операторы, например:

#if !(defined __LP64__ || defined __LLP64__) || defined _WIN32 && !defined _WIN64 // мы компилируем для 32-битной системы #else // мы компилируем для 64-битной системы #endif

Перевод также можно остановить, используя #errorдирективу:

#if RUBY_VERSION == 190 #error 1.9.0 не поддерживается #endif

Макроопределение и расширение

Существует два типа макросов: объектно-подобные и функционально-подобные . Объектно-подобные макросы не принимают параметры; функционально-подобные макросы принимают (хотя список параметров может быть пустым). Общий синтаксис для объявления идентификатора как макроса каждого типа, соответственно, следующий:

#define <идентификатор> <список токенов замены> // макрос, подобный объекту #define <идентификатор>(<список параметров>) <список токенов замены> // макрос, подобный функции, обратите внимание на параметры

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

Определение макроса можно удалить с помощью #undef:

#undef <идентификатор> // удалить макрос

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

Макросы, подобные объектам, традиционно использовались в рамках хорошей практики программирования для создания символических имен констант, например:

#определить ПИ 3.14159

вместо жесткого кодирования чисел по всему коду. Альтернативой как в C, так и в C++, особенно в ситуациях, когда требуется указатель на число, является применение constквалификатора к глобальной переменной. Это приводит к сохранению значения в памяти, а не его замене препроцессором. Однако в современном коде C++ вместо этого используется constexprключевое слово, введенное в C++11 :

constexpr double PI = 3.14159 ;    

Использование переменных, объявленных как constexpr, как макросов, подобных объектам, может быть заменено их значением во время компиляции. [5]

Пример макроса, похожего на функцию:

#define RADTODEG(x) ((x) * 57.29578)

Это определяет преобразование радиан в градусы, которое может быть вставлено в код, где это необходимо; например, RADTODEG(34). Это расширяется на месте, так что повторное умножение на константу не отображается по всему коду. Макрос здесь написан заглавными буквами, чтобы подчеркнуть, что это макрос, а не скомпилированная функция.

Второе xзаключено в собственную пару скобок, чтобы избежать возможности неправильного порядка операций , когда это выражение, а не одно значение. Например, выражение правильно расширяется как ; без скобок дает приоритет умножению.RADTODEG(r + 1)((r + 1) * 57.29578)(r + 1 * 57.29578)

Аналогично, внешняя пара скобок сохраняет правильный порядок операций. Например, расширяется до ; без скобок дает приоритет делению.1 / RADTODEG(r)1 / ((r) * 57.29578)1 / (r) * 57.29578

Порядок расширения

Функционально-подобное макрорасширение происходит в следующие этапы:

  1. Операции стрингификации заменяются текстовым представлением списка замен их аргументов (без выполнения расширения).
  2. Параметры заменяются списком их замены (без выполнения расширения).
  3. Операции конкатенации заменяются результатом конкатенации двух операндов (без расширения результирующего токена).
  4. Токены, происходящие от параметров, расширяются.
  5. Полученные токены расширяются как обычно.

Это может привести к неожиданным результатам:

#define HE HI #define LLO _THERE #define HELLO "HI THERE" #define CAT(a,b) a##b #define XCAT(a,b) CAT(a,b) #define CALL(fn) fn(HE,LLO) CAT ( HE , LLO ) // "HI THERE", потому что конкатенация происходит до обычного расширения XCAT ( HE , LLO ) // HI_THERE, потому что токены, происходящие из параметров ("HE" и "LLO"), расширяются первыми CALL ( CAT ) // "HI THERE", потому что это вычисляется как CAT(a,b)     

Специальные макросы и директивы

Определенные символы должны быть определены реализацией во время предварительной обработки. К ним относятся __FILE__и __LINE__, предопределенные самим препроцессором, которые расширяются в текущий файл и номер строки. Например, следующее:

// отладочные макросы, чтобы можно было сразу определить источник сообщения // плохо #define WHERESTR "[файл %s, строка %d]: " #define WHEREARG __FILE__, __LINE__ #define DEBUGPRINT2(...) fprintf(stderr, __VA_ARGS__) #define DEBUGPRINT(_fmt, ...) DEBUGPRINT2(WHERESTR _fmt, WHEREARG, __VA_ARGS__) // ИЛИ // хорошо #define DEBUGPRINT(_fmt, ...) fprintf(stderr, "[файл %s, строка %d]: " _fmt, __FILE__, __LINE__, __VA_ARGS__) DEBUGPRINT ( "эй, x=%d \n " , x ); 

печатает значение x, которому предшествует файл и номер строки в поток ошибок, что позволяет быстро получить доступ к строке, на которой было создано сообщение. Обратите внимание, что WHERESTRаргумент объединен со строкой, следующей за ним. Значения __FILE__и __LINE__можно изменять с помощью #lineдирективы. #lineДиректива определяет номер строки и имя файла для строки ниже. Например:

#строка 314 "pi.c" printf ( "строка=%d файл=%s \n " , __LINE__ , __FILE__ );  

генерирует printfфункцию:

printf ( "строка=%d файл=%s \n " , 314 , "pi.c" );  

Отладчики исходного кода также ссылаются на исходную позицию, определенную с помощью __FILE__и __LINE__. Это позволяет отлаживать исходный код, когда C используется в качестве целевого языка компилятора, для совершенно другого языка. Первый стандарт C указал, что макрос __STDC__должен быть определен как 1, если реализация соответствует стандарту ISO, и 0 в противном случае, и макрос __STDC_VERSION__был определен как числовой литерал, указывающий версию стандарта, поддерживаемую реализацией. Стандартные компиляторы C++ поддерживают __cplusplusмакрос. Компиляторы, работающие в нестандартном режиме, не должны устанавливать эти макросы или должны определять другие, чтобы сигнализировать о различиях.

Другие стандартные макросы включают в себя __DATE__текущую дату и __TIME__текущее время.

Во втором издании стандарта языка Си, C99 , была добавлена ​​поддержка __func__, которая содержит имя определения функции, в которой она содержится, но поскольку препроцессор не зависит от грамматики языка Си, это необходимо сделать в самом компиляторе с использованием локальной для функции переменной.

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

Один малоизвестный шаблон использования препроцессора C известен как X-Macros . [6] [7] [8] X-Macro — это заголовочный файл . Обычно они используют расширение .defвместо традиционного .h. Этот файл содержит список похожих вызовов макросов, которые можно назвать «макросами компонентов». Затем на включаемый файл ссылаются неоднократно.

Многие компиляторы определяют дополнительные, нестандартные макросы, хотя они часто плохо документированы. Распространенной ссылкой на эти макросы является проект Pre-defined C/C++ Compiler Macros, в котором перечислены «различные предопределенные макросы компилятора, которые могут использоваться для идентификации стандартов, компиляторов, операционных систем, аппаратных архитектур и даже базовых библиотек времени выполнения во время компиляции».

Строковизация токенов

Оператор #(известный как оператор стрингификации или оператор стрингования ) преобразует токен в строковый литерал C , соответствующим образом экранируя любые кавычки или обратные косые черты.

Пример:

#определить стр(ы) #sstr ( p = "foo \n " ;) // выводит "p = \"foo\\n\";" str ( \ n ) // выводит "\n"    

Если требуется стрингификация расширения аргумента макроса, необходимо использовать два уровня макросов:

#определить xstr(s) str(s) #определить str(s) #s #определить foo 4str ( foo ) // выводит "foo" xstr ( foo ) // выводит "4"    

Аргумент макроса нельзя объединить с дополнительным текстом и затем преобразовать в строку. Однако можно записать ряд смежных строковых констант и преобразованных в строку аргументов: компилятор C затем объединит все смежные строковые константы в одну длинную строку.

Конкатенация токенов

Оператор ##(известный как «оператор вставки токена») объединяет два токена в один токен.

Пример:

#define DECLARE_STRUCT_TYPE(имя) typedef имя структуры##_s имя##_tDECLARE_STRUCT_TYPE ( g_object ); // Выходные данные: typedef struct g_object_s g_object_t; 

Ошибки компиляции, определяемые пользователем

Директива #errorвыводит сообщение через поток ошибок.

#error "сообщение об ошибке"

Включение двоичных ресурсов

C23 введет #embedдирективу для включения двоичных ресурсов. [9] Это позволяет включать двоичные файлы (например, изображения) в программу, не будучи допустимыми исходными файлами C (например, XBM), без необходимости обработки внешними инструментами, такими как xxd -iи без использования строковых литералов , которые имеют ограничение по длине в MSVC . Аналогично xxd -iдиректива заменяется списком целых чисел, разделенных запятыми, соответствующих данным указанного ресурса. Точнее, если массив типа unsigned charинициализируется с помощью #embedдирективы, результат будет таким же, как если бы ресурс был записан в массив с помощью fread(если только параметр не изменяет ширину элемента встраивания на что-то иное, чем CHAR_BIT). Помимо удобства, #embedкомпиляторам также проще обрабатывать, поскольку им разрешено пропускать расширение директивы до ее полной формы из-за правила as-if .

Файл для внедрения может быть указан идентично #include, то есть либо между шевронами , либо между кавычками. Директива также позволяет передавать ей определенные параметры для настройки ее поведения, которые следуют за именем файла. Стандарт C определяет следующие параметры, а реализации могут определять свои собственные. Параметр limitиспользуется для ограничения ширины включенных данных. Он в основном предназначен для использования с «бесконечными» файлами, такими как urandom . Параметры prefixи suffixпозволяют программисту указывать префикс и суффикс для внедренных данных, которые используются только в том случае, если внедренный ресурс не пуст. Наконец, параметр if_emptyзаменяет всю директиву, если ресурс пуст (что происходит, если файл пуст или указан предел 0). Все стандартные параметры также могут быть окружены двойными подчеркиваниями, как и стандартные атрибуты в C23, например, __prefix__взаимозаменяемы с prefix. Параметры, определяемые реализацией, используют форму, похожую на синтаксис атрибутов (например, vendor::attr), но без квадратных скобок. Хотя всем стандартным параметрам требуется передача аргумента (например, limit требует ширину), это, как правило, необязательно, и даже скобки можно опустить, если аргумент не требуется, что может иметь место для некоторых параметров, определяемых реализацией.

Реализации

Все реализации C, C++ и Objective-C предоставляют препроцессор, поскольку предварительная обработка является обязательным шагом для этих языков, а ее поведение описывается официальными стандартами для этих языков, такими как стандарт ISO C.

Реализации могут предоставлять собственные расширения и отклонения и различаться по степени соответствия письменным стандартам. Их точное поведение может зависеть от флагов командной строки, указанных при вызове. Например, препроцессор GNU C можно сделать более соответствующим стандартам, указав определенные флаги. [10]

Возможности препроцессора, специфичные для компилятора

Директива #pragmaявляется директивой, специфичной для компилятора , которую поставщики компиляторов могут использовать в своих целях. Например, a #pragmaчасто используется для подавления определенных сообщений об ошибках, управления отладкой кучи и стека и т. д. Компилятор с поддержкой библиотеки параллелизации OpenMP может автоматически распараллеливать forцикл с помощью #pragma omp parallel for.

C99 ввел несколько стандартных #pragmaдиректив, принимая форму #pragma STDC ..., которые используются для управления реализацией с плавающей точкой. Альтернативная, макроподобная форма _Pragma(...)также была добавлена.

Возможности препроцессора, специфичные для конкретного языка

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

Другие применения

Поскольку препроцессор C может быть вызван отдельно от компилятора, с которым он поставляется, его можно использовать отдельно, на разных языках. Известные примеры включают его использование в ныне устаревшей системе imake и для предварительной обработки Fortran . Однако такое использование в качестве препроцессора общего назначения ограничено: входной язык должен быть достаточно C-подобным. [10] Компилятор GNU Fortran автоматически вызывает «традиционный режим» (см. ниже) cpp перед компиляцией кода Fortran, если используются определенные расширения файлов. [17] Intel предлагает препроцессор Fortran, fpp, для использования с компилятором ifort , который имеет схожие возможности. [18]

CPP также приемлемо работает с большинством языков ассемблера и языков типа Algol. Это требует, чтобы синтаксис языка не конфликтовал с синтаксисом CPP, что означает отсутствие строк, начинающихся с , #и чтобы двойные кавычки, которые cpp интерпретирует как строковые литералы и, таким образом, игнорирует, не имели синтаксического значения, отличного от этого. «Традиционный режим» (действующий как препроцессор C до ISO) обычно более разрешителен и лучше подходит для такого использования. [19]

Препроцессор C не является полным по Тьюрингу , но он очень близок к этому: рекурсивные вычисления могут быть определены, но с фиксированной верхней границей объема выполненной рекурсии. [20] Однако препроцессор C не предназначен для того, чтобы быть языком программирования общего назначения, и он не работает хорошо как язык программирования общего назначения. Поскольку препроцессор C не имеет функций некоторых других препроцессоров, таких как рекурсивные макросы, выборочное расширение в соответствии с кавычками и оценка строк в условных операторах, он очень ограничен по сравнению с более общим макропроцессором, таким как m4 .

Смотрите также

Ссылки

  1. ^ Универсальная предварительная обработка текста с помощью препроцессора C. С использованием JavaScript
  2. ^ Ричи (1993)
  3. ^ "Bell SAP – SAP с условными и рекурсивными макросами". HOPL: Онлайновая историческая энциклопедия языков программирования .
  4. ^ Список предопределенных макросов реализации ANSI C и Microsoft C++.
  5. ^ Габриэль Дос Рейс; Бьярне Страуструп (22 марта 2010 г.). "General Constant Expressions for System Programming Languages, Proceedings SAC '10" (PDF) . Архивировано (PDF) из оригинала 13 июня 2018 г. . Получено 8 июля 2024 г. .
  6. ^ Вирзениус, Ларс. C "Препроцессорный трюк для реализации похожих типов данных". Получено 9 января 2011 г.
  7. ^ Мейерс, Рэнди (май 2001 г.). "Новые макросы C: X". Журнал доктора Добба . Получено 1 мая 2008 г.
  8. ^ Beal, Stephan (август 2004). "Supermacros" . Получено 27 октября 2008 . {{cite journal}}: Цитировать журнал требует |journal=( помощь )
  9. ^ "WG14-N3017: #embed – сканируемый, удобный для инструментов механизм включения двоичных ресурсов". open-std.org . 27 июня 2022 г. Архивировано из оригинала 24 декабря 2022 г.
  10. ^ ab "The C Preprocessor: Overview" . Получено 17 июля 2016 г. .
  11. ^ "WG14-N3096: Проект ISO/IEC 9899:2023" (PDF) . open-std.org . 1 апреля 2023 г. Архивировано (PDF) из оригинала 2 апреля 2023 г.
  12. ^ «Рабочий проект, Стандарт языка программирования C++» (PDF) . 22 марта 2023 г.
  13. ^ Устаревшие функции GCC
  14. ^ «Заголовки-оболочки (препроцессор C)».
  15. ^ "N4720: Рабочий проект, Расширения C++ для модулей" (PDF) . Архивировано (PDF) из оригинала 30 апреля 2019 г.
  16. ^ «P1857R1 – Обнаружение зависимостей модулей».
  17. ^ "1.3 Препроцессинг и условная компиляция". Проект GNU.
  18. ^ "Использование препроцессора fpp". Intel . Получено 14 октября 2015 г.
  19. ^ "Обзор (препроцессор C)". gcc.gnu.org . Сказав это, вы часто можете обойтись без использования cpp в вещах, которые не являются C. Другие языки программирования Algol-ish часто безопасны (Ada и т. д.). Так же как и ассемблер, с осторожностью. Режим -traditional-cpp сохраняет больше пробелов и в остальном более разрешителен. Многих проблем можно избежать, если писать комментарии в стиле C или C++ вместо комментариев на родном языке и делать макросы простыми.
  20. ^ "Является ли препроцессор C99 Turing полным?". Архивировано из оригинала 24 апреля 2016 г.

Источники

Внешние ссылки