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

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

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

Условная компиляция дает возможность включать или исключать части кода в зависимости от наличия или значений символических констант.

В простейшем виде условное выражение для препроцессора выглядит следующим образом:

#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

Вопросы для самопроверки:

  1. Какие задачи выполняет препроцессор в языке программирования C?
  2. Как можно задать действия препроцессора вручную в коде?
  3. Каковы различия в применении угловых скобок и кавычек в директиве #include?
  4. Зачем принято писать символические константы заглавными буквами?
  5. Какова структура макроса с аргументами? Приведи пример его использования
  6. Для чего используется условная компиляция и как она основывается на значении символических констант?
  7. Какую роль играют встроенные константы, такие как __DATE__ и __TIME__, в программе на языке C?

Программа курса:

  1. Описание курса
  2. Введение в язык программирования C
  3. Типы данных в C и форматированный вывод
  4. Символьные типы и управляющие символы в C
  5. Операторы ветвления и switch в C
  6. Циклы и операторы в языке C
  7. Битовые операции в языке C
  8. Посимвольный ввод и вывод в C - буферизация
  9. Переменные, адреса и указатели в C
  10. Передача аргументов по ссылке и значению в C
  11. Форматированный ввод данных с использованием scanf
  12. Генерация псевдослучайных чисел на C
  13. Адресная арифметика в массивах C
  14. Передача массивов в функции и указатели
  15. Строки в языке C - особенности и функции работы
  16. Функции работы со строками в C
  17. Работа со структурами в C - создание и применение
  18. Динамические структуры данных в C
  19. Ввод и вывод данных из файлов в языке C
  20. Передача аргументов в C и работа с файлами
  21. Препроцессор в языке C - директивы и макросы
  22. Создание и компиляция многофайловых программ в C
  23. Использование статических и динамических библиотек в C