Каждый файл на C, который вы когда-либо компилировали, начинался со строки вроде #include <stdio.h>, и эта строка — не C. Это инструкция отдельной программе — препроцессору, — которая работает первой, переписывает текст вашего исходника и передаёт результат компилятору.
Понимание этого разделения на два этапа объясняет многое в поведении C: почему заголовки вставляются, а не импортируются; почему неудачный макрос порождает ошибку на строке, которая выглядит безупречно; и почему один и тот же исходник может компилироваться в разные программы на разных системах.
Что на самом деле происходит до компиляции
Компиляция файла .c — не один шаг. Грубо говоря, их четыре:
- Препроцессинг — выполнить все строки, начинающиеся с
#, получив один большой раскрытый текст исходника (единицу трансляции). - Компиляция — превратить этот текст в ассемблер, а затем в объектный файл.
- Ассемблирование — получить машинный код.
- Компоновка — соединить объектные файлы с библиотеками в исполняемый файл.
Препроцессор работает исключительно с текстом. Он не знает, что такое переменная, что такое тип и сходятся ли у вас фигурные скобки. Он видит символы и лексемы, заменяет одни другими и идёт дальше. Всё странное в макросах следует из одного этого факта.
К тому моменту, когда компилятор читает эту программу, никакого 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: у них своя построчная грамматика, они не требуют точки с запятой и исчезают ещё до того, как компилятор начнёт разбирать программу. Именно поэтому ошибка в макросе всплывает как непонятное сообщение на строке, которая выглядит совершенно нормальной.