Препроцессор Си

Материал из Википедии — свободной энциклопедии
Перейти к: навигация, поиск

Препроцессор С/С++ — программный инструмент, изменяющий код программы для последующей компиляции и сборки, используемый в языках программирования Си и его потомка - C++. Этот препроцессор обеспечивает использование стандартного набора возможностей:

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

Препроцессор языка Си — низкоуровневый, лексический препроцессор, потому что он требует только лексического анализа, то есть он обрабатывает только исходный текст перед парсингом, выполняя простую замену лексем и специальных символов заданными последовательностями символов, в соответствии с правилами, установленными пользователями.

Препроцессор си не является тьюринг-полным хотя бы потому, что он не может зависнуть в принципе. См. Рекурсивная функция (теория вычислимости)

Директивы[править | править вики-текст]

Директивой препроцессора (или командной строкой препроцессора[1]) называется строка в исходном коде, которая начинается с символа # и следующего за ним ключевого слова препроцессора. Есть чётко определённый список ключевых слов:

  • define — задаёт макроопределение (макрос) или символическую константу
  • undef — отменяет предыдущее определение
  • include — вставляет текст из указанного файла
  • if — осуществляет условную компиляцию при истинности константного выражения
  • ifdef — осуществляет условную компиляцию при определённости символической константы
  • ifndef — осуществляет условную компиляцию при неопределённости символической константы
  • else — ветка условной компиляции при ложности выражения
  • elif — ветка условной компиляции, образуемая слиянием else и if
  • endif — конец ветки условной компиляции
  • line — препроцессор изменяет номер текущей строки и имя компилируемого файла
  • error — вынуждает препроцессор выдать отчет об ошибке при компиляции
  • warning — вынуждает препроцессор выдать предупреждение при компиляции
  • pragma — действие, зависящее от конкретной реализации компилятора
  • пустое слово - пустое действие.

Функции[править | править вики-текст]

Включение[править | править вики-текст]

Препроцессор Си, встречая следующие директивы:

#include "..."

или

#include <...>

полностью копирует содержимое указанного файла в файл, в котором указана эта директива, в месте вызова директивы. Эти файлы обычно содержат определение интерфейса для различных функций библиотек и типов данных, которые должны быть подключены перед их использованием; таким образом, директива #include обычно указывается в начале (заголовке) файла. По этой причине подключаемые файлы и называются заголовочными. Некоторые содержат примеры из стандартной библиотеки Си (<math.h> и <stdio.h>), обеспечивая математические функции и функции ввода-вывода соответственно.

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

Начиная с 1970-х среди программистов все большее распространение и известность получают альтернативные способы переиспользования подключения файлов, применяемых в большинстве языков программирования: Java и Common Lisp используют пакеты, Паскаль использует юниты (единицы), Modula, OCaml, Haskell и Python используют модули, а D, разработанный как замена языков Си и C++, использует импорт.

Макросы[править | править вики-текст]

Макросы в языке Си преимущественно используются для определения небольших фрагментов кода. Во время обработки кода препроцессором, каждый макрос заменяется соответствующим ему определением. Если макрос имеет параметры, то они указываются в теле макроса; таким образом, макросы языка Си могут походить на Си-функции. Распространенная причина использования — избежание накладных расходов при вызове функции в простейших случаях, когда небольшого кода, вызываемого функцией, достаточно для ощутимого снижения производительности.

Например,

#define max(a,b) ((a) > (b) ? (a) : (b))

определяет макрос max, использующий два аргумента a и b. Этот макрос можно вызывать как любую Си-функцию, используя схожий синтаксис. То есть, после обработки препроцессором,

z = max(x,y);

становится

z = ((x) > (y) ? (x) : (y));

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

Например, если f и g — две функции, вызов

z = max(f(), g());

не вычислит один раз f()и один раз g(), и поместит наибольшее значение в z, как этого можно было ожидать. Вместо этого одна из функций будет вычислена дважды. Если функция имеет побочные эффекты, то вероятно, что её поведение будет отличаться от ожидаемого.

Макросы Си могут походить на функции, создавая новый синтаксис в некоторых пределах, а также могут быть дополнены произвольным текстом (хотя компилятор Си требует, чтобы текст был без ошибок написанным Си-кодом или оформлен как комментарий), но у них есть некоторые ограничения как у программных конструкций. Макросы, схожие с функциями, например, могут быть вызваны как «настоящие» функции, но макрос не может быть передан другой функции при помощи указателя, по той причине, что макрос сам по себе не имеет адреса.

Некоторые современные языки обычно не используют такой способ метапрограммирования с использованием макросов как дополнений строк символов, в расчете или на автоматическое или на ручное подключение функций и методов, а вместо этого другие способы абстракции, такие как шаблоны, общие функции или параметрический полиморфизм. В частности, встраиваемые функции позволяют избежать одного из главных недостатков макросов в современных версия Си и C++, так как встроенная функция обеспечивает преимущество макросов в снижении накладных расходов при вызове функции, но её адрес можно передавать в указателе для косвенных вызовов или использовать в качестве параметра. Аналогично, проблема множественных вычислений, упомянутая выше в макросе max, для встроенных функций неактуальна.

Операторы # и ##[править | править вики-текст]

Эти операторы используются при создании макросов. Оператор # перед параметром макроса обрамляет его в двойные кавычки, например:

#define make_str(bar) # bar
printf(make_str(42));

препроцессор преобразует в:

printf("42");

Оператор ## в макросах конкатенирует две лексемы, например:

#define MakePosition(x) x##X,x##Y,x##Width,x##Height
int MakePosition(Object);

препроцессор преобразует в:

int ObjectX,ObjectY,ObjectWidth,ObjectHeight;

Формальное описание макроподстановок[править | править вики-текст]

1) Управляющая строка следующего вида заставляет препроцессор заменять идентификатор на последовательность лексем везде далее по тексту программы:

	#define идентификатор последовательность-лексем

При этом символы пустого пространства в начале и в конце последовательности лексем выбрасываются.
Повторная строка #define с тем же идентификатором считается ошибкой, если последовательности лексем не идентичны (несовпадения в символах пустого пространства не играют роли).

2) Строка следующего вида, где между первым идентификатором и открывающей круглой скобкой не должно быть символов пустого пространства, представляет собой макроопределение с параметрами, задаваемыми списком-идентификаторов.

	#define идентификатор(список-идентификаторов) последовательность-лексем

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

Управляющая строка следующего вида приказывает препроцессору "забыть" определение, данное идентификатору:

	#undef идентификатор

Применение директивы #undef к не определенному ранее идентификатору не считается ошибкой.

{

  • Если макроопределение было задано во второй форме, то любая следующая далее в тексте программы цепочка символов, состоящая из идентификатора макроса (возможно, с последующими символами пустого пространства), открывающей скобки, списка лексем, разделенных запятыми, и закрывающей скобки, представляет собой вызов макроса.
  • Аргументами вызова макроса являются лексемы, разделенные запятыми, причем запятые, взятые в кавычки или вложенные круглые скобки, в разделении аргументов не участвуют.
  •  !!Во время группировки аргументов раскрытие макросов в них не выполняется.
  • Количество аргументов в вызове макроса должно соответствовать количеству параметров макроопределения.
  • После выделения аргументов из текста символы пустого пространства, окружающие их, отбрасываются.
  • Затем в замещающей последовательности лексем макроса каждый идентификатор-параметр, не взятый в кавычки, заменяется на соответствующий ему фактический аргумент из текста.
  •  !!Если в замещающей последовательности перед параметром не стоит знак #, если и ни перед ним, ни после него нет знака ##, то лексемы аргумента проверяются на наличие в них макровызовов; если таковые есть, то до подстановки аргумента в нем выполняется раскрытие соответствующих макросов.

На процесс подстановки влияют два специальных знака операций.

  • Во-первых, если перед параметром в замещающей строке лексем вплотную стоит знак #, то вокруг соответствующего аргумента ставятся строковые кавычки ("), а потом идентификатор параметра вместе со знаком # заменяется получившимся строковым литералом.
    • Перед каждым символом " или \, встречающимся вокруг или внутри строковой или символьной константы, автоматически вставляется обратная косая черта.
  • Во-вторых, если последовательность лексем в макроопределении любого вида содержит знак ##, то сразу после подстановки параметров он вместе с окружающими его символами пустого пространства отбрасывается, благодаря чему сцепляются соседние лексемы, образуя тем самым новую лексему.
    • Результат не определен при генерировании таким образом недопустимых лексем языка или в случае, когда получающийся текст зависит от порядка применения операции ##.
    • Кроме того, знак ## не может стоять ни в начале, ни в конце замещающей последовательности лексем.

}

!!В макросах обоих видов замещающая последовательность лексем повторно просматривается в поиске новых define-идентификаторов.
!!Однако если какой-либо идентификатор уже был заменен в текущем процессе раскрытия, повторное появление такого идентификатора не вызовет ею замены; он останется нетронутым. !!Даже если развернутая строка макровызова начинается со знака #, она не будет воспринята как директива препроцессора. Двумя восклицательными знаками (!!) отмечены правила, отвечающие за рекурсивные вызов и определения.


Вот пример макроопределения:

#define cat(x,y)  х ## у

Вызов cat(var, 123) сгенерирует результат var123.
Однако вызов cat(cat(1,2) ,3) не даст желаемого результата, поскольку операция ## помешает правильному раскрытию аргументов внешнего вызова cat.
В результате получится следующая цепочка лексем:

cat ( 1 , 2 )3

где ) 3 — результат сцепления последней лексемы первого аргумента с первой лексемой второго аргумента — сам не является допустимой лексемой. Но можно задать второй уровень макроопределения в таком виде:

	#define xcat(x,y) cat(x,y)

Теперь все пройдет благополучно, и вызов xcat (xcat (1, 2) , 3) действительно даст результат 123, потому что в раскрытии самого макроса xcat не участвует знак ##.

Недостаток - снижают качество статического анализа[2].

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

Кроме пользовательских макросов препроцессор имеет встроенные макросы которые не требуют объявления:

  • __LINE__ - заменяется на номер строки, может быть переопределен директивой #line, чаще всего используется для создания отладочной информации
  • __FILE__ - заменяется на имя файла, так же может быть переопределено с помощью директивы #line
  • __DATE__ - заменяется на текущую дату (на момент компиляции, а точнее обработки препроцессором)
  • __TIME__ - заменяется на текущее время
  • __STDC__ - равен 1, говорит о том что компиляция происходит в соответствии со стандартом C
  • __STDC_HOSTED__ - определен в C99 и выше, равен 1 если выполнение происходит под управлением операционной системы
  • __STDC_VERSION__ - определен в C99 и выше, равен 199901, в C11 равен 201112
  • __STDC_IEC_559__ - в C99 и выше определен, если поддерживаются операции с числами с плавающей запятой по стандарту IEC 60559
  • __STDC_IEC_559_COMPLEX__ - в C99 и выше определен, если поддерживаются операции с комплексными числами по стандарту IEC 60559 (в любом случае стандарт С99 обязывает поддерживать операции с комплексными числами)
  • __STDC_NO_COMPLEX__ - в C11 равен единице, если не поддерживаются операции с комплексными числами
  • __STDC_NO_VLA__ - в C11 равен единице, если не поддерживаются массивы переменной длины (в С99 обязательно должны поддерживаться)
  • __VA_ARGS__ - в C99 может использоваться для создания макросов с переменным количеством аргументов

Условная компиляция[править | править вики-текст]

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

В общем случае, программисту необходимо использовать конструкцию типа:

# ifndef FOO_H
# define FOO_H
...(код заголовочного файла)...
# endif

Такая «защита макросов» предотвращает двойное подключение заголовочного файла путем проверки существования этого макроса, который имеет то же самое имя, что и заголовочный файл. Определение макроса FOO_H происходит, когда заголовочный файл впервые обрабатывается препроцессором. Затем, если этот заголовочный файл вновь подключается, FOO_H уже определен, в результате чего препроцессор пропускает полностью текст этого заголовочного файла.

То же самое можно сделать, включив в заголовочный файл директиву:

# pragma once

Условия препроцессора можно задавать несколькими способами, например:

# ifdef x
...
# else
...
# endif

или

# if x
...
# else
...
# endif

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

Большинство современных языков программирования не используют такие возможности, больше полагаясь на традиционные операторы условия if...then...else..., оставляя компилятору задачу извлечения бесполезного кода из компилируемой программы.

Примечания[править | править вики-текст]

Ссылки[править | править вики-текст]