Ключевым компонентом компилятора для языка программирования C является препроцессор, который подготавливает код для последующей компиляции. Он выполняет ряд задач, таких как включение содержимого одного файла в другой, замена имен констант на их значения, а также удаление символов конца строки, необходимых только для удобства прочтения программистом. Многие действия препроцессора выполняются автоматически, но некоторые программист может задать с помощью специальных директив в коде. Такие директивы начинаются с символа # и оканчиваются новой строкой. В отличие от стандартных выражений в языке C, точка с запятой в конце директив не требуется. Ниже приводятся общие сведения о часто используемых директивах препроцессора и их возможностях, хотя они на этом не ограничиваются.
Директива #include
Директива #include знакома нам по использованию заголовочных файлов стандартной библиотеки языка, содержащих прототипы функций. При обнаружении этой директивы препроцессор распознает ее как указание на имя файла и включает его содержимое в исходный код программы. Поэтому код вашей программы может значительно увеличиться после обработки его препроцессором.
Если имя файла после директивы #include
помещено в угловые скобки, поиск заголовочного файла осуществляется в специальном
системном каталоге. Однако в программе может также присутствовать запись:
В этом случае препроцессор сначала ищет заголовочный файл в текущем каталоге. Это дает программисту возможность задать собственные заголовочные файлы для своих проектов. Можно также указывать путь к заголовочному файлу:
Директива #define
Символические константы
Мы уже сталкивались с директивой препроцессора #define
, которая позволяет объявлять и определять так называемые символические константы.
Например:
При обработке кода препроцессором перед компиляцией символические константы, такие как N и HELLO, в исходном коде языка C будут заменены на их соответствующие числовые или строковые значения.
Символические константы можно определять в любом месте кода. Однако для их переопределения необходимо снять предыдущее определение. Если этого
не сделать, компилятор может выдать предупреждение или ошибку. Для отмены определения символической константы используется директива
#undef
:
Если в данном примере убрать строку #undef HELLO
, то при компиляции в GNU/Linux вы получите предупреждение: "HELLO" переопределен.
Принято писать символические константы заглавными буквами. Это не обязательное правило, но удобное соглашение для облегчения чтения кода.
Макросы как усложненные символьные константы
При помощи директивы #define
можно замещать не только числовые и строковые константы, но и практически любую часть кода:
В теле функции main()
PN будет заменено на printf("\n")
, а SUM на цикл for
препроцессором. Такие макросы
удобны, когда однотипный код часто повторяется в программе, но выделять его в отдельную функцию нет необходимости.
Макросы PN и SUM из примера являются макросами без аргументов. Однако препроцессор языка C позволяет создавать макросы с аргументами:
Вызов макроса DIF(67,90)
в коде приводит к тому, что при обработке программы препроцессором в это место подставляется
выражение (67) > (90) ? (67)-(90) : (90)-(67)
. Это выражение вычисляет разность двух чисел с использованием условного выражения
(см. урок 3). Скобки могут быть не нужны, но они помогают в сложных операциях, например, при умножении или делении.
Важно помнить, что в именах макросов не должно быть пробелов, как в DIF(a,b)
. Пробел после идентификатора обозначает окончание
символической константы и начало выражения для подстановки в код.
- Создайте программу с макросами: первый вычисляет сумму элементов массива, второй выводит элементы массива на экран.
- Напишите программу с макросами, которые вычисляют площади различных геометрических фигур (например, квадрата, прямоугольника, окружности).
Директивы условной компиляции
Условная компиляция дает возможность включать или исключать части кода в зависимости от наличия или значений символических констант.
В простейшем виде условное выражение для препроцессора выглядит следующим образом:
Код между #if
и #endif
выполняется, если выражение при #if
истинно. В этой зоне можно поместить как
директивы препроцессора, так и код на языке C.
Условное включение может быть дополнено ветвлениями #else
и #elif
.
Рассмотрим конкретные примеры.
Если константа N определена и отлична от нуля, цикл for
выполнится, заполняя массив arr нулями. В случае, если N равна нулю или не
определена, цикл не сработает:
Если необходимо выполнить определенный код при наличии символической константы, независимо от ее значения, используется следующая запись:
Она сокращенно выглядит так:
Когда неизвестно, определена ли ранее символическая константа, может использоваться следующий код:
Таким образом, мы зададим значение N, если она не была определена. Эта проверка может быть полезна в проектах с множеством файлов. Выражение
#if !defined(N)
может быть сокращено до:
Условная компиляция иногда используется для отладки кода, а также для адаптации программ под различные операционные системы.
Важно помнить, что препроцессор выполняет обработку кода перед компиляцией. В итоговом двоичном коде не содержится условных выражений для препроцессора, поэтому в нем не может быть переменных, для которых значения определяются на этапе выполнения программы.
Придумайте программу, которую можно скомпилировать различными способами в зависимости от того, определена ли в ней конкретная символическая константа.
Константы, определенные препроцессором
Препроцессор автоматически определяет пять констант, отличающихся от пользовательских наличием символов подчеркивания в начале и конце их имен.
- __DATE__ - дата компиляции;
- __FILE__ - название файла, который компилируется;
- __LINE__ - номер текущей строки исходного текста программы;
- __STDC__ - равно 1, если компилятор соответствует стандарту ANSI для языка C;
- __TIME__ - время компиляции.
Если эти константы встречаются в коде, они заменяются соответствующими строками или числами. Так как это происходит до компиляции, мы видим, например, дату компиляции, а не дату выполнения программы. Программа ниже выводит на экран значения предустановленных имен препроцессора:
Результат: