Препроцессор языка С. Директивы и макросы

В компилятор языка программирования 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). Иначе, он бы означал конец символической константы и начало выражения для подстановки.

  1. Напишите программу, содержащую пару макросов: один вычисляет сумму элементов массива, другой выводит элементы массива на экран.
  2. Напишите программу, содержащую макросы с аргументами, вычисляющие площади различных геометрических фигур (например, квадрата, прямоугольника, окружности).

Директивы условной компиляции

Так называемая условная компиляция позволяет компилировать или не компилировать части кода в зависимости от наличия символьных констант или их значения.

Условное выражение для препроцессора выглядит в сокращенном варианте так:

#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" не должно содержаться переменных, значение которых определяется в момент выполнения программы.

Константы, определенные препроцессором

Препроцессор самостоятельно определяет пять констант. От обычных (определенных программистом) они отличаются наличием пары символов подчеркивания в начале и конце их имени.

Если эти константы встречаются в тексте программы, то заменяются на соответствующие строки или числа. Т.к. это происходит до компиляции, то, например, мы видим дату компиляции, а не дату запуска программы на выполнение. Программа ниже выводит значение предопределенных препроцессором имен на экран:

#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-версия


Основы языка C. Курс




Все разделы сайта