Ключевым компонентом компилятора для языка программирования C является препроцессор, который подготавливает код для последующей компиляции. Он выполняет ряд задач, таких как включение содержимого одного файла в другой, замена имен констант на их значения, а также удаление символов конца строки, необходимых только для удобства прочтения программистом. Многие действия препроцессора выполняются автоматически, но некоторые программист может задать с помощью специальных директив в коде. Такие директивы начинаются с символа # и оканчиваются новой строкой. В отличие от стандартных выражений в языке C, точка с запятой в конце директив не требуется. Ниже приводятся общие сведения о часто используемых директивах препроцессора и их возможностях, хотя они на этом не ограничиваются.
Директива #include
Директива #include знакома нам по использованию заголовочных файлов стандартной библиотеки языка, содержащих прототипы функций. При обнаружении этой директивы препроцессор распознает ее как указание на имя файла и включает его содержимое в исходный код программы. Поэтому код вашей программы может значительно увеличиться после обработки его препроцессором.
Если имя файла после директивы #include
помещено в угловые скобки, поиск заголовочного файла осуществляется в специальном
системном каталоге. Однако в программе может также присутствовать запись:
#include "ext.h"
В этом случае препроцессор сначала ищет заголовочный файл в текущем каталоге. Это дает программисту возможность задать собственные заголовочные файлы для своих проектов. Можно также указывать путь к заголовочному файлу:
#include "/home/iam/project10/const.h"
Директива #define
Символические константы
Мы уже сталкивались с директивой препроцессора #define
, которая позволяет объявлять и определять так называемые символические константы.
Например:
#define N 100
#define HELLO "Hello. Answer the next questions, please."
При обработке кода препроцессором перед компиляцией символические константы, такие как N и HELLO, в исходном коде языка C будут заменены на их соответствующие числовые или строковые значения.
Символические константы можно определять в любом месте кода. Однако для их переопределения необходимо снять предыдущее определение. Если этого
не сделать, компилятор может выдать предупреждение или ошибку. Для отмены определения символической константы используется директива
#undef
:
#include <stdio.h>
#define HELLO "Hello. Answer the next questions, please.\n"
int main () {
printf(HELLO);
#undef HELLO
#define HELLO "Good day. Tell us about you.\n"
printf(HELLO);
}
Если в данном примере убрать строку #undef HELLO
, то при компиляции в GNU/Linux вы получите предупреждение: "HELLO" переопределен.
Принято писать символические константы заглавными буквами. Это не обязательное правило, но удобное соглашение для облегчения чтения кода.
Макросы как усложненные символьные константы
При помощи директивы #define
можно замещать не только числовые и строковые константы, но и практически любую часть кода:
#include <stdio.h>
#define N 100
#define PN printf("\n")
#define SUM for(i=0; i
int main () {
int i, sum = 0;
SUM;
printf("%d", sum);
PN;
}
В теле функции main()
PN будет заменено на printf("\n")
, а SUM на цикл for
препроцессором. Такие макросы
удобны, когда однотипный код часто повторяется в программе, но выделять его в отдельную функцию нет необходимости.
Макросы PN и SUM из примера являются макросами без аргументов. Однако препроцессор языка C позволяет создавать макросы с аргументами:
#include <stdio.h>
#define DIF(a,b) (a) > (b) ? (a)-(b) : (b)-(a)
int main () {
int x = 10, y = 30;
printf("%d\n", DIF(67,90));
printf("%d\n", DIF(876-x,90+y));
}
Вызов макроса DIF(67,90)
в коде приводит к тому, что при обработке программы препроцессором в это место подставляется
выражение (67) > (90) ? (67)-(90) : (90)-(67)
. Это выражение вычисляет разность двух чисел с использованием условного выражения
(см. урок 3). Скобки могут быть не нужны, но они помогают в сложных операциях, например, при умножении или делении.
Важно помнить, что в именах макросов не должно быть пробелов, как в DIF(a,b)
. Пробел после идентификатора обозначает окончание
символической константы и начало выражения для подстановки в код.
- Создайте программу с макросами: первый вычисляет сумму элементов массива, второй выводит элементы массива на экран.
- Напишите программу с макросами, которые вычисляют площади различных геометрических фигур (например, квадрата, прямоугольника, окружности).
Директивы условной компиляции
Условная компиляция дает возможность включать или исключать части кода в зависимости от наличия или значений символических констант.
В простейшем виде условное выражение для препроцессора выглядит следующим образом:
#if …
…
#endif
Код между #if
и #endif
выполняется, если выражение при #if
истинно. В этой зоне можно поместить как
директивы препроцессора, так и код на языке C.
Условное включение может быть дополнено ветвлениями #else
и #elif
.
Рассмотрим конкретные примеры.
Если константа N определена и отлична от нуля, цикл for
выполнится, заполняя массив arr нулями. В случае, если N равна нулю или не
определена, цикл не сработает:
#include <stdio.h>
#define N 10
int main() {
int i, arr[100];
#if N
for(i=0; i; i++) {
arr[i] = 0;
printf("%d ", arr[i]);
}
#endif
printf("\n");
}
Если необходимо выполнить определенный код при наличии символической константы, независимо от ее значения, используется следующая запись:
#if defined(N)
Она сокращенно выглядит так:
#ifdef N
Когда неизвестно, определена ли ранее символическая константа, может использоваться следующий код:
#if !defined(N)
#define N 100
#endif
Таким образом, мы зададим значение N, если она не была определена. Эта проверка может быть полезна в проектах с множеством файлов. Выражение
#if !defined(N)
может быть сокращено до:
#ifndef N
Условная компиляция иногда используется для отладки кода, а также для адаптации программ под различные операционные системы.
Важно помнить, что препроцессор выполняет обработку кода перед компиляцией. В итоговом двоичном коде не содержится условных выражений для препроцессора, поэтому в нем не может быть переменных, для которых значения определяются на этапе выполнения программы.
Придумайте программу, которую можно скомпилировать различными способами в зависимости от того, определена ли в ней конкретная символическая константа.
Константы, определенные препроцессором
Препроцессор автоматически определяет пять констант, отличающихся от пользовательских наличием символов подчеркивания в начале и конце их имен.
- __DATE__ - дата компиляции;
- __FILE__ - название файла, который компилируется;
- __LINE__ - номер текущей строки исходного текста программы;
- __STDC__ - равно 1, если компилятор соответствует стандарту ANSI для языка C;
- __TIME__ - время компиляции.
Если эти константы встречаются в коде, они заменяются соответствующими строками или числами. Так как это происходит до компиляции, мы видим, например, дату компиляции, а не дату выполнения программы. Программа ниже выводит на экран значения предустановленных имен препроцессора:
#include <stdio.h>
#define NL printf("\n")
int main () {
printf(__DATE__); NL;
printf("%d",__LINE__); NL;
printf(__FILE__); NL;
printf(__TIME__); NL;
printf("%d",__STDC__); NL;
}
Результат:
Sep 2 2018
7
main.c
09:59:43
1