Препроцессор C — это препроцессор макросов для нескольких языков программирования , таких как C , Objective-C , C++ и различных языков Fortran . Препроцессор обеспечивает включение заголовочных файлов , расширение макросов , условную компиляцию и управление строками.
Язык директив препроцессора лишь слабо связан с грамматикой C и поэтому иногда используется для обработки других типов текстовых файлов . [1]
Препроцессор был представлен в C примерно в 1973 году по настоянию Алана Снайдера, а также в знак признания полезности механизмов включения файлов, доступных в BCPL и PL/I . Его первоначальная версия предлагала только включение файлов и простую замену строк с использованием #include
и #define
для макросов без параметров соответственно. Вскоре после этого он был расширен сначала Майком Леском , а затем Джоном Райзером, чтобы включить макросы с аргументами и условную компиляцию. [2]
Препроцессор C был частью давней традиции макроязыков в Bell Labs, начатой Дугласом Иствудом и Дугласом Макилроем в 1959 году . [3]
Предварительная обработка определяется первыми четырьмя (из восьми) фазами трансляции, указанными в стандарте C.
_Pragma
операторы.Одним из наиболее распространенных вариантов использования препроцессора является включение другого исходного файла:
#include <stdio.h> int main ( void ) { printf ( «Привет, мир! \n » ); вернуть 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 define _WIN32 /* _WIN32 обычно определяется компиляторами, ориентированными на 32 или 64-битные системы Windows */ # include <windows.h > #эндиф
__unix__
В примере кода проверяется, определен ли макрос . Если да, <unistd.h>
то файл включается. В противном случае он проверяет _WIN32
, определен ли вместо этого макрос . Если да, <windows.h>
то файл включается.
В более сложном #if
примере можно использовать операторы; например:
#if !(определено __LP64__ || определено __LLP64__) || определено _WIN32 && !defined _WIN64 // компилируем для 32-битной системы #else // компилируем для 64-битной системы #endif
Трансляцию также можно вызвать сбоем с помощью #error
директивы:
#if RUBY_VERSION == 190 #ошибка 1.9.0 не поддерживается #endif
Существует два типа макросов: объектные и функциональные . Объектноподобные макросы не принимают параметров; макросы, подобные функциям (хотя список параметров может быть пустым). Общий синтаксис объявления идентификатора как макроса каждого типа соответственно следующий:
#define <идентификатор> <список токенов замены> // макрос типа объекта #define <идентификатор>(<список параметров>) <список токенов замены> // макрос типа функции, параметры примечания
В объявлении функционального макроса не должно быть пробелов между идентификатором и первой открывающей круглой скобкой. Если присутствуют пробелы, макрос будет интерпретироваться как объектный, и в список токенов будет добавлено все, начиная с первой скобки.
Определение макроса можно удалить с помощью #undef
:
#undef <идентификатор> // удаляем макрос
Всякий раз, когда идентификатор появляется в исходном коде, он заменяется списком токенов замены, который может быть пустым. Идентификатор, объявленный как макрос, подобный функции, заменяется только в том случае, если следующий токен также является левой круглой скобкой, которая начинает список аргументов вызова макроса. Точная процедура расширения функциональных макросов с аргументами невелика.
Объектноподобные макросы традиционно использовались как часть хорошей практики программирования для создания символических имен констант; например:
#define PI 3.14159
вместо жесткого кодирования чисел по всему коду. Альтернативой как в C, так и в C++, особенно в ситуациях, когда требуется указатель на число, является применение квалификатора const
к глобальной переменной. Это приводит к тому, что значение сохраняется в памяти, а не заменяется препроцессором.
Пример функционального макроса:
#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 ) // «Привет», потому что это равно CAT(a,b)
Определенные символы должны быть определены реализацией во время предварительной обработки. К ним относятся __FILE__
и __LINE__
, предопределенные самим препроцессором, которые расширяются до текущего файла и номера строки. Например, следующее:
// отладка макросов, чтобы мы могли сразу определить происхождение сообщения // это плохо #define WHERESTR "[file %s, строка %d]: " #define WHEEARRG __FILE__, __LINE__ #define DEBUGPRINT2(...) fprintf(stderr , __VA_ARGS__) #define DEBUGPRINT(_fmt, ...) DEBUGPRINT2(WHERESTR _fmt, WHEEARRG, __VA_ARGS__) // ИЛИ // хорошо #define DEBUGPRINT(_fmt, ...) fprintf(stderr, "[file %s, line %d]: " _fmt, __FILE__, __LINE__, __VA_ARGS__) DEBUGPRINT ( "эй, 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 . [5] [6] [7] X-макрос — это файл заголовка . Обычно они используют расширение .def
вместо традиционного .h
. Этот файл содержит список похожих вызовов макросов, которые можно назвать «макросами компонентов». Затем на включаемый файл ссылаются неоднократно.
Многие компиляторы определяют дополнительные нестандартные макросы, хотя они зачастую плохо документированы. Общей ссылкой на эти макросы является проект «Предварительно определенные макросы компилятора C/C++», в котором перечислены «различные предварительно определенные макросы компилятора, которые можно использовать для идентификации стандартов, компиляторов, операционных систем, аппаратных архитектур и даже базовых библиотек времени выполнения». во время компиляции».
Оператор #
(известный как оператор строкообразования или оператор строкообразования ) преобразует токен в строковый литерал C , соответствующим образом экранируя любые кавычки или обратную косую черту.
Пример:
#define str(s) #sstr ( p = "foo \n " ;) // выводит "p = \"foo\\n\";" str ( \ n ) // выводит "\n"
Если требуется строковое расширение аргумента макроса, необходимо использовать два уровня макросов:
#define xstr(s) str(s) #define str(s) #s #define foo 4str ( foo ) // выводит "foo" xstr ( foo ) // выводит "4"
Аргумент макроса нельзя объединить с дополнительным текстом и затем преобразовать в строку. Однако можно записать ряд констант соседних строк и строковых аргументов: компилятор C затем объединит все константы соседних строк в одну длинную строку.
Оператор ##
(известный как «Оператор вставки токена») объединяет два токена в один токен.
Пример:
#define DECLARE_STRUCT_TYPE(name) typedef имя структуры##_s name##_tDECLARE_STRUCT_TYPE ( g_object ); // Выходы: typedef struct g_object_s g_object_t;
Директива #error
выводит сообщение через поток ошибок.
#error "сообщение об ошибке"
C23 представит #embed
директиву для включения двоичных ресурсов. [8] Это позволяет включать в программу двоичные файлы (например, изображения), не являющиеся действительными исходными файлами 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 можно сделать более совместимым со стандартами, предоставив определенные флаги. [9]
Эта #pragma
директива является директивой, специфичной для компилятора , которую поставщики компиляторов могут использовать в своих целях. Например, a #pragma
часто используется для подавления определенных сообщений об ошибках, управления отладкой кучи и стека и т. д. Компилятор с поддержкой библиотеки распараллеливания OpenMP может автоматически распараллеливать for
цикл с помощью #pragma omp parallel for
.
В C99 появилось несколько стандартных #pragma
директив вида #pragma STDC ...
, которые используются для управления реализацией чисел с плавающей запятой. _Pragma(...)
Также была добавлена альтернативная макроподобная форма .
#warning
к стандарту для этой цели). Типичное использование — предупреждение об использовании некоторого старого кода, который сейчас устарел и включен только из соображений совместимости; например:// GNU, Intel и IBM #предупреждение «Не используйте ABC, который устарел. Вместо этого используйте XYZ».
// Сообщение Microsoft #pragma("Не используйте ABC, он устарел. Вместо этого используйте XYZ.")
#include_next
объединение заголовков с одинаковыми именами. [13]Существуют некоторые директивы препроцессора, которые были добавлены в препроцессор C спецификациями некоторых языков и специфичны для этих языков.
#import
имеют , что похоже #include
, но включает файл только один раз. Распространенной прагмой поставщиков с аналогичной функциональностью в C является #pragma once
.#
символа; вместо этого они начинаются с import
и module
соответственно, которым может предшествовать export
.Поскольку препроцессор C можно вызывать отдельно от компилятора, с которым он поставляется, его можно использовать отдельно на разных языках. Яркие примеры включают его использование в ныне устаревшей системе imake и для предварительной обработки Fortran . Однако такое использование в качестве препроцессора общего назначения ограничено: язык ввода должен быть в достаточной степени C-подобным. [9] Компилятор GNU Fortran автоматически вызывает «традиционный режим» (см. ниже) cpp перед компиляцией кода Fortran, если используются определенные расширения файлов. [16] Intel предлагает препроцессор Fortran, fpp, для использования с компилятором ifort , который имеет аналогичные возможности. [17]
CPP также приемлемо работает с большинством языков ассемблера и языков, подобных Алголу. Для этого необходимо, чтобы синтаксис языка не конфликтовал с синтаксисом CPP, что означает отсутствие строк, начинающихся с, #
и чтобы двойные кавычки, которые cpp интерпретирует как строковые литералы и, следовательно, игнорирует, не имеют другого синтаксического значения. «Традиционный режим» (действующий как препроцессор до ISO C), как правило, более разрешителен и лучше подходит для такого использования. [18]
Препроцессор C не является полным по Тьюрингу , но он очень близок к этому: можно указать рекурсивные вычисления, но с фиксированной верхней границей объема выполняемой рекурсии. [19] Однако препроцессор C не предназначен для использования в качестве языка программирования общего назначения и не работает в качестве языка программирования общего назначения. Поскольку препроцессор C не имеет функций некоторых других препроцессоров, таких как рекурсивные макросы, выборочное расширение в соответствии с кавычками и вычисление строк в условных выражениях, он очень ограничен по сравнению с более общим макропроцессором, таким как m4 .
{{cite journal}}
: Требуется цитировать журнал |journal=
( помощь )Сказав это, вам часто может сойти с рук использование cpp для вещей, которые не являются C. Другие языки программирования, подобные Алголу, часто безопасны (Ada и т. д.). Как и ассемблер, но с осторожностью. Режим -traditional-cpp сохраняет больше пробелов и в остальном является более разрешительным. Многих проблем можно избежать, написав комментарии в стиле C или C++ вместо комментариев на родном языке и сохранив простоту макросов.