Препроцессор C
В этой статье есть несколько проблем. Пожалуйста, помогите улучшить его или обсудите эти проблемы на странице обсуждения . ( Узнайте, как и когда удалять эти шаблонные сообщения )
|
Препроцессор C — это препроцессор макросов для нескольких языков программирования , таких как C , Objective-C , C++ и различных языков Fortran . Препроцессор обеспечивает включение заголовочных файлов , расширение макросов , условную компиляцию и управление строками.
Язык директив препроцессора лишь слабо связан с грамматикой C и поэтому иногда используется для обработки других типов текстовых файлов . [1]
История
[ редактировать ]Препроцессор был представлен в C примерно в 1973 году по настоянию Алана Снайдера , а также в знак признания полезности механизмов включения файлов, доступных в BCPL и PL/I . Его первоначальная версия предлагала только включение файлов и простую замену строк с помощью #include
и #define
для макросов без параметров соответственно. Вскоре после этого он был расширен сначала Майком Леском , а затем Джоном Райзером, чтобы включить макросы с аргументами и условную компиляцию. [2]
Препроцессор C был частью давней традиции макроязыков в Bell Labs, начатой Дугласом Иствудом и Дугласом Макилроем в 1959 году. [3]
Фазы
[ редактировать ]Предварительная обработка определяется первыми четырьмя (из восьми) фазами трансляции, указанными в стандарте C.
- Замена триграфа: препроцессор заменяет последовательности триграфов символами, которые они представляют. Эта фаза будет удалена в C23, следуя инструкциям C++17 .
- Объединение строк: строки физического источника, которые продолжаются экранированными новой строки последовательностями , объединяются в логические строки.
- Токенизация: препроцессор разбивает результат на токены предварительной обработки и пробелы . Он заменяет комментарии пробелами.
- Расширение макросов и обработка директив: выполняются строки директив предварительной обработки, включая включение файлов и условную компиляцию. Препроцессор одновременно расширяет макросы и, начиная с версии стандарта C 1999 года, обрабатывает
_Pragma
операторы.
Включая файлы
[ редактировать ]Одним из наиболее распространенных вариантов использования препроцессора является включение другого исходного файла:
#include <stdio.h>
int main(void)
{
printf("Hello, World!\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("trace message");
#endif
Большинство компиляторов, предназначенных для Microsoft Windows, неявно определяют _WIN32
. [4] Это позволяет компилировать код, включая команды препроцессора, только для систем Windows. Некоторые компиляторы определяют WIN32
вместо. Для таких компиляторов, которые неявно не определяют _WIN32
макрос, его можно указать в командной строке компилятора, используя -D_WIN32
.
#ifdef __unix__ /* __unix__ is usually defined by compilers targeting Unix systems */
# include <unistd.h>
#elif defined _WIN32 /* _WIN32 is usually defined by compilers targeting 32 or 64 bit Windows systems */
# include <windows.h>
#endif
В примере кода проверяется, есть ли макрос __unix__
определяется. Если это так, файл <unistd.h>
затем включается. В противном случае он проверяет, был ли макрос _WIN32
вместо этого определяется. Если это так, файл <windows.h>
затем включается.
Более сложный #if
пример может использовать операторы; например:
#if !(defined __LP64__ || defined __LLP64__) || defined _WIN32 && !defined _WIN64
// we are compiling for a 32-bit system
#else
// we are compiling for a 64-bit system
#endif
Трансляцию также можно вызвать сбоем, используя команду #error
директива:
#if RUBY_VERSION == 190
#error 1.9.0 not supported
#endif
Определение и расширение макроса
[ редактировать ]Существует два типа макросов: объектные и функциональные . Объектноподобные макросы не принимают параметров; макросы, подобные функциям (хотя список параметров может быть пустым). Общий синтаксис объявления идентификатора как макроса каждого типа соответственно следующий:
#define <identifier> <replacement token list> // object-like macro
#define <identifier>(<parameter list>) <replacement token list> // function-like macro, note parameters
В объявлении функционального макроса не должно быть пробелов между идентификатором и первой открывающей круглой скобкой. Если присутствуют пробелы, макрос будет интерпретироваться как объектный, и в список токенов будет добавлено все, начиная с первой скобки.
Определение макроса можно удалить с помощью #undef
:
#undef <identifier> // delete the macro
Всякий раз, когда идентификатор появляется в исходном коде, он заменяется списком токенов замены, который может быть пустым. Идентификатор, объявленный как макрос, подобный функции, заменяется только в том случае, если следующий токен также является левой скобкой, которая начинает список аргументов вызова макроса. Точная процедура расширения функциональных макросов с аргументами невелика.
Объектноподобные макросы традиционно использовались как часть хорошей практики программирования для создания символических имен констант; например:
#define PI 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
отдает предпочтение разделению.
Порядок расширения
[ редактировать ]Функциональное расширение макроса происходит в следующие этапы:
- Операции строкообразования заменяются текстовым представлением списка замены их аргументов (без выполнения расширения).
- Параметры заменяются списком их замены (без выполнения расширения).
- Операции конкатенации заменяются объединенным результатом двух операндов (без расширения результирующего токена).
- Токены, происходящие из параметров, расширяются.
- Полученные токены расширяются как обычно.
Это может привести к удивительным результатам:
#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", because concatenation occurs before normal expansion
XCAT(HE, LLO) // HI_THERE, because the tokens originating from parameters ("HE" and "LLO") are expanded first
CALL(CAT) // "HI THERE", because this evaluates to CAT(a,b)
Специальные макросы и директивы
[ редактировать ]Определенные символы должны быть определены реализацией во время предварительной обработки. К ним относятся __FILE__
и __LINE__
, предопределенный самим препроцессором, который расширяется до текущего файла и номера строки. Например, следующее:
// debugging macros so we can pin down message origin at a glance
// is bad
#define WHERESTR "[file %s, line %d]: "
#define WHEREARG __FILE__, __LINE__
#define DEBUGPRINT2(...) fprintf(stderr, __VA_ARGS__)
#define DEBUGPRINT(_fmt, ...) DEBUGPRINT2(WHERESTR _fmt, WHEREARG, __VA_ARGS__)
// OR
// is good
#define DEBUGPRINT(_fmt, ...) fprintf(stderr, "[file %s, line %d]: " _fmt, __FILE__, __LINE__, __VA_ARGS__)
DEBUGPRINT("hey, x=%d\n", x);
печатает значение x
, которому предшествует номер файла и номер строки потока ошибок, что позволяет быстро узнать, на какой строке было создано сообщение. Обратите внимание, что WHERESTR
Аргумент объединяется со следующей за ним строкой. Значения __FILE__
и __LINE__
можно манипулировать с помощью #line
директива. #line
Директива определяет номер строки и имя файла строки ниже. Например:
#line 314 "pi.c"
printf("line=%d file=%s\n", __LINE__, __FILE__);
генерирует printf
функция:
printf("line=%d file=%s\n", 314, "pi.c");
исходного кода Отладчики также обращаются к позиции исходного кода, определенной с помощью __FILE__
и __LINE__
.
Это позволяет отлаживать исходный код, когда C используется в качестве целевого языка компилятора, для совершенно другого языка. Первый стандарт C указывал, что макрос __STDC__
быть определено как 1, если реализация соответствует стандарту ISO, и 0 в противном случае, а макрос __STDC_VERSION__
определяется как числовой литерал, определяющий версию Стандарта, поддерживаемую реализацией. Стандартные компиляторы C++ поддерживают __cplusplus
макрос. Компиляторы, работающие в нестандартном режиме, не должны устанавливать эти макросы или должны определять другие, чтобы сигнализировать о различиях.
Другие стандартные макросы включают в себя __DATE__
, текущая дата и __TIME__
, текущее время.
Во втором издании стандарта C, C99 , добавлена поддержка __func__
, который содержит имя определения функции, в которой оно содержится, но поскольку препроцессор не зависит от грамматики C, это должно быть сделано в самом компиляторе с использованием локальной для функции переменной.
Макросы, которые могут принимать различное количество аргументов ( вариативные макросы ), не разрешены в C89, но были введены рядом компиляторов и стандартизированы в C99 . Макросы с переменным числом параметров особенно полезны при написании оболочек для функций, принимающих переменное количество параметров, например: printf
, например, при регистрации предупреждений и ошибок.
Один малоизвестный шаблон использования препроцессора C известен как X-Macros . [6] [7] [8] X-Macro — это заголовочный файл . Обычно они используют расширение .def
вместо традиционного .h
. Этот файл содержит список похожих вызовов макросов, которые можно назвать «макросами компонентов». Затем на включаемый файл ссылаются неоднократно.
Многие компиляторы определяют дополнительные нестандартные макросы, хотя они зачастую плохо документированы. Общей ссылкой на эти макросы является проект «Предварительно определенные макросы компилятора C/C++» , в котором перечислены «различные предварительно определенные макросы компилятора, которые можно использовать для идентификации стандартов, компиляторов, операционных систем, аппаратных архитектур и даже базовых библиотек времени выполнения». во время компиляции».
Стрингификация токена
[ редактировать ]The #
Оператор (известный как оператор строкирования или оператор строкообразования ) преобразует токен в строковый литерал C , соответствующим образом экранируя любые кавычки или обратную косую черту.
Пример:
#define str(s) #s
str(p = "foo\n";) // outputs "p = \"foo\\n\";"
str(\n) // outputs "\n"
Если требуется строковое расширение аргумента макроса, необходимо использовать два уровня макросов:
#define xstr(s) str(s)
#define str(s) #s
#define foo 4
str (foo) // outputs "foo"
xstr (foo) // outputs "4"
Аргумент макроса нельзя объединить с дополнительным текстом и затем преобразовать в строку. Однако можно записать ряд констант соседних строк и строковых аргументов: компилятор C затем объединит все константы соседних строк в одну длинную строку.
Конкатенация токенов
[ редактировать ]The ##
Оператор (известный как «Оператор вставки токена») объединяет два токена в один токен.
Пример:
#define DECLARE_STRUCT_TYPE(name) typedef struct name##_s name##_t
DECLARE_STRUCT_TYPE(g_object); // Outputs: typedef struct g_object_s g_object_t;
Пользовательские ошибки компиляции
[ редактировать ]The #error
Директива выводит сообщение через поток ошибок.
#error "error message"
Включение двоичных ресурсов
[ редактировать ]C23 представит #embed
директива для включения двоичных ресурсов. [9] Это позволяет включать двоичные файлы (например, изображения) в программу, не являясь действительными исходными файлами C (например, XBM), без необходимости обработки внешними инструментами, такими как xxd -i
и без использования строковых литералов , длина которых ограничена в MSVC . Аналогично xxd -i
директива заменяется списком целых чисел, разделенных запятыми, соответствующих данным указанного ресурса. Точнее, если массив типа unsigned char
инициализируется с помощью #embed
директиве, результат будет таким же, как если бы ресурс был записан в массив с помощью fread
(если параметр не изменяет ширину элемента внедрения на что-то отличное от CHAR_BIT
). Помимо удобства, #embed
также проще обрабатывать компиляторам, поскольку им разрешено пропускать раскрытие директивы до ее полной формы из-за правила «как если бы» .
Файл для встраивания можно указать так же, как и #include
, то есть либо между шевронами , либо между кавычками. Директива также позволяет передавать ей определенные параметры для настройки ее поведения, которые следуют за именем файла. Стандарт C определяет следующие параметры, а реализации могут определять свои собственные. limit
Параметр используется для ограничения ширины включаемых данных. В основном он предназначен для использования с «бесконечными» файлами, такими как urandom . prefix
и suffix
параметры позволяют программисту указать префикс и суффикс для внедренных данных, которые используются тогда и только тогда, когда внедренный ресурс не пуст. Наконец, if_empty
Параметр заменяет всю директиву, если ресурс пуст (что происходит, если файл пуст или указано ограничение 0). Все стандартные параметры также могут быть окружены двойным подчеркиванием, как, например, стандартные атрибуты в C23. __prefix__
взаимозаменяем с prefix
. Параметры, определенные реализацией, используют форму, аналогичную синтаксису атрибута (например, vendor::attr
), но без квадратных скобок. Хотя все стандартные параметры требуют передачи аргумента (например, для ограничения требуется ширина), это обычно необязательно, и даже набор круглых скобок можно опустить, если аргумент не требуется, что может иметь место в некоторых реализациях. определенные параметры.
Реализации
[ редактировать ]Все реализации C, C++ и Objective-C предоставляют препроцессор, поскольку предварительная обработка является обязательным шагом для этих языков, а ее поведение описывается официальными стандартами для этих языков, такими как стандарт ISO C.
Реализации могут иметь собственные расширения и отклонения и различаться по степени соответствия письменным стандартам. Их точное поведение может зависеть от флагов командной строки, предоставляемых при вызове. Например, препроцессор GNU C можно сделать более совместимым со стандартами, предоставив определенные флаги. [10]
Функции препроцессора, специфичные для компилятора
[ редактировать ]The #pragma
Директива — это директива, специфичная для компилятора , которую поставщики компиляторов могут использовать в своих целях. Например, #pragma
часто используется для подавления определенных сообщений об ошибках, управления отладкой кучи и стека и т. д. Компилятор с поддержкой библиотеки распараллеливания OpenMP может автоматически распараллеливать for
петля с #pragma omp parallel for
.
C99 представил несколько стандартных #pragma
директивы, принимая форму #pragma STDC ...
, которые используются для управления реализацией чисел с плавающей запятой. Альтернативная макроподобная форма _Pragma(...)
также был добавлен.
- Многие реализации не поддерживают триграфы или не заменяют их по умолчанию.
- Многие реализации (например, компиляторы C от GNU, Intel, Microsoft и IBM) предоставляют нестандартную директиву для вывода предупреждающего сообщения на выходе, но не для остановки процесса компиляции ( C23 [11] и С++23 [12] добавит
#warning
стандарту для этой цели). Типичное использование — предупреждение об использовании некоторого старого кода, который сейчас устарел и включен только из соображений совместимости; например:// GNU, Intel and IBM #warning "Do not use ABC, which is deprecated. Use XYZ instead."
// Microsoft #pragma message("Do not use ABC, which is deprecated. Use XYZ instead.")
- Некоторые препроцессоры Unix традиционно предоставляют «утверждения», которые мало похожи на утверждения, используемые в программировании. [13]
- GCC предоставляет
#include_next
для объединения заголовков с одинаковым именем. [14]
Функции препроцессора, зависящие от языка
[ редактировать ]Существуют некоторые директивы препроцессора, которые были добавлены в препроцессор C спецификациями некоторых языков и специфичны для этих языков.
- Objective-C имеют Препроцессоры
#import
, что похоже#include
но включает файл только один раз. Распространенная прагма поставщика с аналогичной функциональностью в C:#pragma once
. - В C++, начиная с C++20, есть директивы import и mod для модулей . [15] [16] Эти директивы — единственные, которые не начинаются с
#
характер; вместо этого они начинаются сimport
иmodule
соответственно, необязательно, которому предшествуетexport
.
Другое использование
[ редактировать ]Поскольку препроцессор C можно вызывать отдельно от компилятора, с которым он поставляется, его можно использовать отдельно на разных языках. Яркие примеры включают его использование в ныне устаревшей системе imake и для предварительной обработки Fortran . Однако такое использование в качестве препроцессора общего назначения ограничено: язык ввода должен быть в достаточной степени C-подобным. [10] Компилятор GNU Fortran автоматически вызывает «традиционный режим» (см. ниже) cpp перед компиляцией кода Fortran, если используются определенные расширения файлов. [17] Intel предлагает препроцессор Fortran, fpp, для использования с компилятором ifort , который имеет аналогичные возможности. [18]
CPP также приемлемо работает с большинством языков ассемблера и языков, подобных Алголу. Для этого необходимо, чтобы синтаксис языка не конфликтовал с синтаксисом CPP, что означает отсутствие строк, начинающихся с #
и эти двойные кавычки, которые cpp интерпретирует как строковые литералы и, следовательно, игнорирует, не имеют другого синтаксического значения. «Традиционный режим» (действующий как препроцессор до ISO C), как правило, более разрешителен и лучше подходит для такого использования. [19]
Препроцессор C не является полным по Тьюрингу , но он очень близок к этому: можно указать рекурсивные вычисления, но с фиксированной верхней границей объема выполняемой рекурсии. [20] Однако препроцессор C не предназначен для использования в качестве языка программирования общего назначения и не работает в качестве языка программирования общего назначения. Поскольку препроцессор C не имеет функций некоторых других препроцессоров, таких как рекурсивные макросы, выборочное расширение в соответствии с кавычками и вычисление строк в условных выражениях, он очень ограничен по сравнению с более общим макропроцессором, таким как m4 .
См. также
[ редактировать ]Ссылки
[ редактировать ]- ^ Предварительная обработка текста общего назначения с помощью препроцессора C. Использование JavaScript
- ^ Ричи (1993)
- ^ «Bell SAP – SAP с условными и рекурсивными макросами» . HOPL: Интернет-историческая энциклопедия языков программирования .
- ^ Список предопределенных макросов реализации ANSI C и Microsoft C++.
- ^ Габриэль Дос Рейс; Бьерн Страуструп (22 марта 2010 г.). «Общие константные выражения для языков системного программирования, Труды SAC '10» (PDF) . Архивировано (PDF) из оригинала 13 июня 2018 года . Проверено 8 июля 2024 г.
- ^ Вирзениус, Ларс. C «Прием препроцессора для реализации подобных типов данных». Проверено 9 января 2011 г.
- ^ Мейерс, Рэнди (май 2001 г.). «Новые макросы C:X» . Журнал доктора Добба . Проверено 1 мая 2008 г.
- ^ Бил, Стефан (август 2004 г.). «Супермакросы» . Проверено 27 октября 2008 г.
{{cite journal}}
: Для цитирования журнала требуется|journal=
( помощь ) - ^ «WG14-N3017: #embed — сканируемый, удобный для инструментов механизм включения двоичных ресурсов» . open-std.org . 27 июня 2022 года. Архивировано из оригинала 24 декабря 2022 года.
- ^ Перейти обратно: а б «Препроцессор C: обзор» . Проверено 17 июля 2016 г.
- ^ «WG14-N3096: Проект ISO/IEC 9899:2023» (PDF) . open-std.org . 1 апреля 2023 г. Архивировано (PDF) из оригинала 2 апреля 2023 г.
- ^ «Рабочий проект стандарта языка программирования C++» (PDF) . 22 марта 2023 г.
- ^ Устаревшие функции GCC
- ^ «Заголовки оберток (препроцессор C)» .
- ^ «N4720: Рабочий проект, расширения C++ для модулей» (PDF) . Архивировано (PDF) из оригинала 30 апреля 2019 г.
- ^ «P1857R1 — Обнаружение зависимостей модулей» .
- ^ «1.3 Предварительная обработка и условная компиляция» . Проект ГНУ.
- ^ «Использование препроцессора fpp» . Интел . Проверено 14 октября 2015 г.
- ^ «Обзор (препроцессор C)» . gcc.gnu.org .
Сказав это, вам часто может сойти с рук использование cpp для вещей, которые не являются C. Другие языки программирования, подобные Алголу, часто безопасны (Ada и т. д.). Как и ассемблер, но с осторожностью. Режим -traditional-cpp сохраняет больше пробелов и в остальном является более разрешительным. Многих проблем можно избежать, написав комментарии в стиле C или C++ вместо комментариев на родном языке и сохранив простоту макросов.
- ^ «Является ли препроцессор Тьюринга C99 завершенным?» . Архивировано из оригинала 24 апреля 2016 года.
Источники
[ редактировать ]- Ричи, Деннис М. (март 1993 г.). «Развитие языка Си» . Уведомления ACM SIGPLAN . 28 (3). АКМ: 201–208. дои : 10.1145/155360.155580 .
- Ричи, Деннис М. (1993). «Развитие языка Си» . Вторая конференция ACM SIGPLAN по истории языков программирования (HOPL-II) . АКМ . стр. 201–208. дои : 10.1145/154766.155580 . ISBN 0-89791-570-4 . Проверено 4 ноября 2014 г.
Внешние ссылки
[ редактировать ]- ИСО/МЭК 9899 . Последняя общедоступная версия стандарта C11 представляет собой окончательный проект .
- Онлайн-руководство по GNU CPP
- Справочник по препроцессору Visual Studio .NET
- Проект предварительно определенных макросов компилятора C/C++ : перечисляет «различные предварительно определенные макросы компилятора, которые можно использовать для идентификации стандартов, компиляторов, операционных систем, аппаратных архитектур и даже базовых библиотек времени выполнения во время компиляции».