Препроцессор языка С. Директивы и макросы
В компилятор языка программирования C входит препроцессор, который осуществляет подготовку программы к компиляции. Среди прочего он, например, включает содержимое одних файлов в другие, заменяет в тексте исходного кода имена констант на их значения, удаляет символы конца строки (которые нужны только программисту, чтобы код можно было легко читать, но не нужны компилятору). Что-то препроцессор делает по-умолчанию, а какие-то его действия программируются с помощью специальных директив в исходном коде. Директивы препроцессора начинаются со знака #
и заканчиваются переходом на новую строку. В отличие от законченного выражения на Си, в конце директив не надо ставить точку с запятой. Ниже рассматриваются наиболее распространенные директивы препроцессора и некоторые его свойства, но это далеко не все, что может делать препроцессор.
Директива #include
С этой директивой мы уже не раз встречались, подключая заголовочные файлы стандартной библиотеки языка, содержащие объявления (прототипы) функций. Когда препроцессор встречает такую директиву, то понимает, что после нее идет имя файла, и включает все содержимое указанного файла в исходный код программы. Поэтому объем кода вашей программы после обработки ее препроцессором может сильно увеличиться.
Если имя файла после директивы #include
заключено в угловые скобки (например, <stdio.h>
), то поиск заголовочного файла производится в стандартном (специально оговоренном системой) каталоге. Однако в тексте программы может встречаться и такая запись:
#include "ext.h"
В таком случае заголовочный файл в первую очередь будет искаться в текущем каталоге. Таким образом, программист сам может определять заголовочные файлы для своих проектов. Кроме того, можно указывать адрес заголовочного файла:
#include "/home/iam/project10/const.h"
Директива #define
Символические константы
С директивой препроцессора #define
мы также уже знакомы. С ее помощью объявляются и определяются так называемые символические константы. Например:
#define N 100 #define HELLO "Hello. Answer the questions."
Когда перед компиляцией исходный код будет обработан препроцессором, то все символьные константы (в примере это N и HELLO) в тексте исходного кода на языке C будут заменены на соответствующие им числовые или строковые литералы.
Символические константы можно определять в любом месте исходного кода. Однако чтобы переопределить их (изменить значение), следует отменить предыдущее определение. Иначе возникнет предупреждение (но не ошибка). Для удаления символической константы используют директиву #undef
:
#include <stdio.h> #define HELLO "Hello. Answer the questions.\n" int main () { printf(HELLO); #undef HELLO #define HELLO "Good day. Tell us about.\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<N; i++) sum += 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)); }
Вызов макроса DIV(67, 90)
в теле main
приводит к тому, что при обработке программы препроцессором туда подставляется выражение (67)>(90)?(67)-(90):(90)-(67)
. В этом выражении вычисляется разница между двумя числами с помощью условного выражения (см. урок 4). В данном случае скобки не нужны. Однако при таком разворачивании (876-x)>(90+y)?(876-x)-(90+y):(90+y)-(876-x)
скобки подчеркивают порядок операций. Если бы вместо сложения и вычитания фигурировали операции умножения или деления, то наличие скобок было бы принципиальным.
Обратите внимание, что после имени идентификатора не должно быть пробела: DIF(a, b)
. Иначе, он бы означал конец символической константы и начало выражения для подстановки.
- Напишите программу, содержащую пару макросов: один вычисляет сумму элементов массива, другой выводит элементы массива на экран.
- Напишите программу, содержащую макросы с аргументами, вычисляющие площади различных геометрических фигур (например, квадрата, прямоугольника, окружности).
Директивы условной компиляции
Так называемая условная компиляция позволяет компилировать или не компилировать части кода в зависимости от наличия символьных констант или их значения.
Условное выражение для препроцессора выглядит в сокращенном варианте так:
#if … … #endif
То, что находится между #if
и #endif
выполняется, если выражение при #if
возвращает истину. Находится там могут как директивы препроцессора так и исходный код на языке C.
Условное включение может быть расширено за счет веток #else
и #elif
.
Рассмотрим несколько примеров.
Если в программе константа N не равна 0, то цикл for
выполнится, и массив arr заполнится нулями. Если N определена и равна 0, или не определена вообще, то цикл выполняться не будет:
#include <stdio.h> #define N 10 int main() { int i, arr[100]; #if N for (i = 0; i < N; i++) { arr[i] = 0; printf("%d ", arr[i]); } #endif printf("\n"); }
Если нужно выполнить какой-то код в зависимости от наличия символьной константы, а не ее значения, то директива #if
будет выглядеть так:
#if defined(N)
Или сокращенно (что тоже самое):
#ifdef N
Когда нет уверенности, была ли определена ранее символьная константа, то можно использовать такой код:
#if !defined(N) #define N 100 #endif
Таким образом мы определим константу N, если она не была определена ранее. Такие проверки могут встречаться в многофайловых проектах. Выражение препроцессора #if !defined(N)
может быть сокращено так:
#ifndef N
Условную компиляцию иногда используют при отладке программного кода, а также с ее помощью компилируют программы под конкретные операционные системы.
Препроцессор обрабатывает программу до компиляции. В двоичном коде уже отсутствуют какие-либо условные выражения для препроцессора. Поэтому в логическом выражении "препроцессорного if" не должно содержаться переменных, значение которых определяется в момент выполнения программы.
Константы, определенные препроцессором
Препроцессор самостоятельно определяет пять констант. От обычных (определенных программистом) они отличаются наличием пары символов подчеркивания в начале и конце их имени.
- __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; }
Результат:
Dec 17 2023 7 e5_const.c 21:48:52 1
Курс с решением задач:
pdf-версия