Препроцессор C — это макропрепроцессор для нескольких языков программирования , таких как C , Objective-C , C++ и различных языков Fortran . Препроцессор обеспечивает включение заголовочных файлов , макрорасширений , условной компиляции и управления строками.
Язык директив препроцессора слабо связан с грамматикой языка C, поэтому иногда используется для обработки других видов текстовых файлов . [1]
Препроцессор был представлен в C около 1973 года по настоянию Алана Снайдера, а также в знак признания полезности механизмов включения файлов, доступных в BCPL и PL/I . Его первоначальная версия предлагала только включение файлов и простую замену строк с использованием #include
и #define
для макросов без параметров соответственно. Вскоре после этого он был расширен сначала Майком Леском , а затем Джоном Райзером для включения макросов с аргументами и условной компиляции. [2]
Препроцессор C был частью давней традиции макроязыков в Bell Labs, которая была начата Дугласом Иствудом и Дугласом Макилроем в 1959 году. [3]
Предварительная обработка определяется первыми четырьмя (из восьми) фазами перевода, указанными в стандарте языка Си.
_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
Функционально-подобное макрорасширение происходит в следующие этапы:
Это может привести к неожиданным результатам:
#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(...)
также была добавлена.
#warning
к стандарту для этой цели). Типичное использование — предупреждение об использовании некоторого старого кода, который теперь устарел и включен только из соображений совместимости; например:// GNU, Intel и IBM #предупреждение "Не используйте ABC, он устарел. Вместо этого используйте XYZ."
// Microsoft #pragma message("Не используйте ABC, это устаревший формат. Вместо этого используйте XYZ.")
#include_next
цепочку заголовков с одинаковыми именами. [14]Существуют некоторые директивы препроцессора, которые были добавлены в препроцессор C спецификациями некоторых языков и являются специфичными для этих языков.
#import
, что похоже #include
, но включает файл только один раз. Распространенная прагма поставщика с похожей функциональностью в C — #pragma once
.#
символа; вместо этого они начинаются с import
и module
соответственно, возможно, с предшествующим export
.Поскольку препроцессор 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 .
{{cite journal}}
: Цитировать журнал требует |journal=
( помощь )Сказав это, вы часто можете обойтись без использования cpp в вещах, которые не являются C. Другие языки программирования Algol-ish часто безопасны (Ada и т. д.). Так же как и ассемблер, с осторожностью. Режим -traditional-cpp сохраняет больше пробелов и в остальном более разрешителен. Многих проблем можно избежать, если писать комментарии в стиле C или C++ вместо комментариев на родном языке и делать макросы простыми.