Menu

Препроцессор C: #include, #define и всё, что работает до компиляции

Препроцессор правит текст исходника ещё до того, как его увидит компилятор: вставляет заголовки, подставляет макросы и включает или выключает куски кода. Здесь — всё семейство директив и способ посмотреть на результат его работы.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Каждый файл на C, который вы когда-либо компилировали, начинался со строки вроде #include <stdio.h>, и эта строка — не C. Это инструкция отдельной программе — препроцессору, — которая работает первой, переписывает текст вашего исходника и передаёт результат компилятору.

Понимание этого разделения на два этапа объясняет многое в поведении C: почему заголовки вставляются, а не импортируются; почему неудачный макрос порождает ошибку на строке, которая выглядит безупречно; и почему один и тот же исходник может компилироваться в разные программы на разных системах.

Что на самом деле происходит до компиляции

Компиляция файла .c — не один шаг. Грубо говоря, их четыре:

  1. Препроцессинг — выполнить все строки, начинающиеся с #, получив один большой раскрытый текст исходника (единицу трансляции).
  2. Компиляция — превратить этот текст в ассемблер, а затем в объектный файл.
  3. Ассемблирование — получить машинный код.
  4. Компоновка — соединить объектные файлы с библиотеками в исполняемый файл.

Препроцессор работает исключительно с текстом. Он не знает, что такое переменная, что такое тип и сходятся ли у вас фигурные скобки. Он видит символы и лексемы, заменяет одни другими и идёт дальше. Всё странное в макросах следует из одного этого факта.

К тому моменту, когда компилятор читает эту программу, никакого GREETING в ней нет. Препроцессор уже заменил его на литерал "Привет от макроса" — ровно так, как если бы вы набрали его руками.

Семейство директив

Любая директива препроцессора начинается с # как первого непробельного символа строки. Точки с запятой нет, а директива заканчивается концом строки, если только вы не продолжите её обратной косой чертой.

ДирективаЧто делает
#includeВставить содержимое другого файла
#defineОпределить макрос (текстовую подстановку)
#undefУбрать определение макроса
#ifdef, #ifndefСохранить следующий код, только если макрос (не) определён
#if, #elif, #else, #endifСохранить код по константному выражению
#errorОстановить компиляцию с сообщением
#pragmaИнструкция конкретному компилятору, например #pragma once
#lineИзменить сообщаемый номер строки (редко)

Трём из них посвящены отдельные страницы: макросы подробно разбирают #define, условная компиляция — семейство #if, а заголовочные файлы — то, как #include используется для структурирования программы из нескольких файлов.

#include: угловые скобки против кавычек

#include делает ровно одну вещь: заменяет собственную строку полным содержимым указанного файла. Включённый файл сам проходит препроцессинг, поэтому его собственные строки #include тоже раскрываются.

#include <stdio.h>     /* искать в системных каталогах включения */
#include "config.h"    /* искать сначала в каталоге этого файла */

Разница — в порядке поиска:

  • <угловые скобки> ищут в стандартных каталогах включения компилятора — /usr/include, собственные папки тулчейна плюс всё, что вы добавили через -I. Это для библиотечных заголовков.
  • "кавычки" ищут сначала в каталоге файла, выполняющего включение, а затем откатываются к тому же списку, что и угловые скобки. Это для написанных вами заголовков.

На большинстве компиляторов обе формы работают для заголовка любого вида, но за соглашением стоит смысл: угловые скобки говорят «это чужой заголовок», кавычки — «это мой». Путаница между ними — то, из-за чего проект начинает подключать устаревший системный заголовок вместо собственного.

Поскольку включение — это текстовая вставка, двойное подключение одного заголовка вставляет его дважды; именно поэтому заголовкам нужны стражи включения. С этого и начинает страница заголовочные файлы.

#define: подстановка текста и ничего больше

#define ИМЯ замена говорит препроцессору: начиная отсюда, везде, где встретится лексема ИМЯ, подставляй вместо неё замена.

Обратите внимание, чего здесь не происходит. У MAX_USERS нет типа. Он нигде не хранится. Его нельзя посмотреть в отладчике. Это правило «найти и заменить», и после препроцессинга программа буквально содержит printf("%s допускает %d пользователей\n", "Coddy", 100);.

Это значит ещё и то, что подстановка слепа. Вот такой код компилируется и делает неожиданное:

#define SIZE 5 + 1

int arr[SIZE];          /* нормально: int arr[5 + 1]; */
int total = SIZE * 2;   /* 5 + 1 * 2 == 7, а не 12 */

Решение — скобки вокруг всего — это центральное правило страницы макросы, там же разбираются макросы с аргументами.

#define без текста замены определяет имя как «присутствует, но пусто». Как подстановка это бесполезно, а как флаг — необходимо:

#define DEBUG          /* определён, раскрывается в ничто */

После этого код может спросить через #ifdef, существует ли DEBUG.

Как посмотреть вывод препроцессора

Лучший способ развить интуицию — взглянуть на то, что препроцессор на самом деле выдал. gcc -E останавливается после препроцессинга и печатает результат:

gcc -E hello.c

Для файла, подключающего <stdio.h>, это от 700 до 30 000 строк в зависимости от системы, и почти всё это — содержимое самого заголовка. Чтобы увидеть только свою часть, возьмите хвост:

gcc -E hello.c | tail -20

Попробуйте на файле вроде такого:

#define SQUARE(x) ((x) * (x))
#define LIMIT 10

int main(void) {
    int n = SQUARE(LIMIT);
    return n;
}

Хвост вывода показывает:

int main(void) {
    int n = ((10) * (10));
    return n;
}

Все макросы исчезли; остался только подставленный текст. Когда макрос ведёт себя странно, эта команда объясняет причину за секунды — и это лучше, чем гадать. Два полезных спутника: gcc -dM -E - < /dev/null перечисляет все макросы, предопределённые вашим компилятором, а gcc -E -P file.c убирает шум из строковых маркеров.

Почему ошибки указывают не на ту строку

Поскольку компилятор видит раскрытый текст, ошибка внутри макроса сообщается там, где макрос использован, а не там, где он написан:

#define HALF(x) (x / 2

int main(void) {
    int y = HALF(8);   /* об ошибке сообщается здесь */
    return 0;
}

Недостающая скобка находится в строке #define, но компилятор жалуется на строку с HALF(8) — часто с сообщением про неожиданную лексему, которое в этом контексте лишено смысла. Когда ошибка выглядит невозможной, раскройте файл через gcc -E и прочитайте настоящую строку.

Современные компиляторы помогают: GCC и clang печатают примечание «in expansion of macro», указывающее обратно на определение. Собирайте с -Wall -Wextra, чтобы действительно видеть такие примечания.

Предопределённые макросы

Часть макросов препроцессор поставляет сам. Для диагностики они по-настоящему полезны:

__FILE__ и __LINE__ раскрываются в имя текущего файла и номер строки — именно так макросы проверок и логирования сообщают, где что-то пошло не так. (__func__ слегка отличается: это настоящий идентификатор, предоставляемый компилятором, а не макрос препроцессора, но используется он так же.)

Компиляторы также предопределяют платформенные макросы вроде __linux__, _WIN32 и __APPLE__. Код, который должен отличаться от системы к системе, проверяет их через #ifdef — этому посвящена условная компиляция.

Что стоит вынести

Препроцессор мал и глуп, и то и другое сделано намеренно. Он даёт вам три возможности — подтянуть файл, подставить текст, включить или выключить код — и никакой проверки типов в придачу.

Из-за этого размена стандартный совет — сначала тянуться к средствам самого языка: для констант использовать const int или enum вместо #define, когда это возможно, и настоящую функцию вместо функциеподобного макроса. А там, где препроцессор действительно подходящий инструмент — заголовки, переключатели переносимости, конфигурация времени компиляции, — он незаменим.

Далее страница макросы берётся за #define всерьёз: аргументы, правила расстановки скобок и ловушки, возникающие при подстановке текста в код, который писали не вы.

Часто задаваемые вопросы

Что такое препроцессор в C?

Это этап обработки текста, который выполняется до компиляции. Он подчиняется строкам, начинающимся с #: вставляет содержимое заголовочных файлов вместо #include, подставляет макросы, объявленные через #define, и удаляет или сохраняет код по условиям #if/#ifdef. Компилятор видит только результат, но никогда не ваш исходный файл.

Чем отличаются #include <stdio.h> и #include "myfile.h"?

Угловые скобки ищут в системных каталогах включения компилятора — там, где лежат заголовки стандартной библиотеки. Кавычки ищут сначала в каталоге текущего файла, а затем откатываются к системным путям. Угловые скобки — для библиотечных заголовков, кавычки — для написанных вами.

Как посмотреть, что получилось после препроцессора?

Запустите gcc -E file.c: обработка остановится после препроцессинга и раскрытый исходник будет выведен на экран. Для файла с <stdio.h> это тысячи строк, поэтому направьте вывод дальше: gcc -E file.c | tail -30 покажет только ваш собственный код со всеми уже подставленными макросами.

#include — это оператор языка C?

Нет. Директивы не входят собственно в язык C: у них своя построчная грамматика, они не требуют точки с запятой и исчезают ещё до того, как компилятор начнёт разбирать программу. Именно поэтому ошибка в макросе всплывает как непонятное сообщение на строке, которая выглядит совершенно нормальной.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ