Директива препроцессора — это команда, которая начинается с символа # и выполняется до компиляции: препроцессор подставляет содержимое файлов, разворачивает макросы и включает или выключает участки кода, а компилятор уже получает готовый текст. В C и C++ это отдельный этап сборки, и работает он с текстом, а не с типами и переменными.
Содержание
Ниже — полный разбор: что делает препроцессор, как записываются директивы, справочная таблица основных директив и рабочие примеры с прогнанным выводом. Отдельно разберем условную компиляцию (#if, #ifdef, #ifndef, #elif, #else, #endif) и типичную ошибку с макросами. Все примеры на C собраны компилятором и запущены, вывод в тексте — реальный.
Что делает препроцессор
Препроцессор — это первый проход сборки. Он не понимает синтаксис языка: он работает с текстом по своим правилам и выдает так называемую единицу трансляции, которую дальше разбирает компилятор.
Три основные операции препроцессора:
- подключение файлов (
#include) — вставка текста другого файла на место директивы; - макроподстановка (
#define) — замена одного фрагмента текста другим; - условная компиляция (
#ifи родственные) — включение или исключение кусков кода до компиляции.
Важно различать этапы. Препроцессор правит текст, а проверку типов, вычисление выражений и генерацию машинного кода делает уже компилятор. Поэтому ошибку в макросе часто видно не на строке #define, а там, где макрос развернулся.
Синтаксис директив
Директива занимает отдельную логическую строку и устроена так:
- символ
#(перед ним допускаются пробелы и табуляция); - имя директивы — одно из ключевых слов препроцессора (
define,include,if,ifdefи других); - параметры, зависящие от директивы.
Точки с запятой в конце нет — это не оператор языка, а команда препроцессора. Если после # стоит только пустая строка (так называемая null-директива), она допустима и ничего не делает. А вот неизвестное имя после # — ошибка: сборка прерывается сообщением вида invalid preprocessing directive.
Основные директивы: справочная таблица
| Директива | Что делает | Пример |
|---|---|---|
#include |
вставляет текст другого файла | #include <stdio.h> |
#define |
задает макрос или символическую константу | #define PI 3.14159 |
#undef |
отменяет ранее заданный #define |
#undef PI |
#if / #elif / #else / #endif |
условная компиляция по значению выражения | #if VERSION == 2 |
#ifdef / #ifndef |
ветка по факту определен ли макрос | #ifndef MYMATH_H |
#error |
прервать сборку с заданным сообщением | #error "нужен C11" |
#pragma |
команда конкретному компилятору | #pragma once |
#line |
переопределить номер строки и имя файла в сообщениях | #line 100 "gen.c" |
Дальше — подробнее о самых частых директивах, с кодом.
#include: подключение файлов
#include вставляет на свое место содержимое указанного файла. Есть две формы записи, и они различаются порядком поиска файла:
#include <файл>— угловые скобки. Поиск идет в системных каталогах компилятора. Так подключают стандартную библиотеку:<stdio.h>,<stdlib.h>, в C++ —<iostream>,<vector>.#include "файл"— двойные кавычки. Поиск начинается с каталога текущего файла (и дополнительных путей проекта), а затем — в системных. Так подключают собственные заголовки:#include "mymath.h".
Практическое правило простое: свои заголовки — в прямых двойных кавычках "...", библиотечные — в угловых скобках.
#define: константы и макросы
#define вводит макрос — правило текстовой замены. Есть две разновидности.
Объектный макрос (символическая константа) заменяет имя на текст:
#define PI 3.14159
Функциональный макрос принимает параметры:
#define SQUARE(x) ((x) * (x))
Соберем минимальный рабочий пример, где есть и константа, и функциональный макрос, и флаг условной компиляции. Его можно скопировать и запустить целиком:
#include <stdio.h>
#define PI 3.14159 // символическая константа
#define SQUARE(x) ((x) * (x)) // макрос с параметром
#define DEBUG 1 // флаг условной компиляции
int main(void) {
double r = 2.0;
printf("area = %.5f\n", PI * SQUARE(r));
#if DEBUG
printf("[debug] radius = %.1f\n", r);
#endif
return 0;
}
Вывод программы:
area = 12.56636
[debug] radius = 2.0
Здесь SQUARE(r) разворачивается в ((r) * (r)), а PI — в число. Строка с [debug] попала в сборку только потому, что DEBUG равен 1: поставьте 0 — и препроцессор ее вырежет еще до компиляции.
Частая ошибка: макрос без скобок
Функциональный макрос — это подстановка текста, а не вызов функции. Если забыть скобки вокруг параметров, приоритет операций сломается. Покажем ошибку тройкой: неверный макрос, его фактический результат, исправление.
#include <stdio.h>
#define BAD(x) x * x // без скобок вокруг x
#define GOOD(x) ((x) * (x)) // со скобками
int main(void) {
printf("BAD(1 + 2) = %d\n", BAD(1 + 2)); // развернется в 1 + 2 * 1 + 2
printf("GOOD(1 + 2) = %d\n", GOOD(1 + 2)); // развернется в ((1 + 2) * (1 + 2))
return 0;
}
Вывод программы:
BAD(1 + 2) = 5
GOOD(1 + 2) = 9
BAD(1 + 2) превращается в 1 + 2 * 1 + 2, а из-за приоритета умножения это 1 + 2 + 2 = 5, а не квадрат тройки. GOOD со скобками дает ожидаемые 9. Отсюда правило: в функциональном макросе оборачивать в скобки и каждый параметр, и все выражение целиком.
Граница применимости: там, где важна типобезопасность и предсказуемость, в C++ вместо макросов обычно берут const/constexpr и inline-функции или шаблоны. Макросы остаются нужны для условной компиляции и подключения файлов.
#undef: отмена определения
#undef убирает ранее заданный макрос — после него имя снова считается неопределенным. Это полезно, когда имя нужно переопределить или ограничить область действия макроса:
#include <stdio.h>
#define MAX 100
int a = MAX; // 100
#undef MAX
#define MAX 200
int b = MAX; // 200
int main(void) {
printf("a = %d, b = %d\n", a, b);
return 0;
}
Вывод программы:
a = 100, b = 200
Условная компиляция
Условная компиляция управляет тем, какие куски кода вообще попадут в сборку. Ее ведут директивы #if, #elif, #else, #endif, а также #ifdef и #ifndef. Решение принимается на этапе препроцессора, поэтому исключенный код не компилируется совсем — его как будто нет.
#if выражение— включить блок, если константное выражение истинно (не ноль);#elif выражение— «иначе если», проверить следующее условие;#else— ветка по умолчанию;#endif— конец условного блока (обязателен);#ifdef ИМЯ— включить блок, если макросИМЯопределен;#ifndef ИМЯ— включить блок, если макросИМЯНЕ определен;defined(ИМЯ)— оператор внутри#if, дающий 1, если макрос определен.
Разберем #if/#elif/#else на выборе варианта сборки по числовому флагу:
#include <stdio.h>
#define LEVEL 2
int main(void) {
#if LEVEL == 1
puts("сборка: базовая");
#elif LEVEL == 2
puts("сборка: расширенная");
#else
puts("сборка: неизвестная");
#endif
#ifdef LEVEL
printf("LEVEL определен, значение %d\n", LEVEL);
#endif
return 0;
}
Вывод программы:
сборка: расширенная
LEVEL определен, значение 2
В сборку вошла только ветка LEVEL == 2; две другие препроцессор отбросил. Смените LEVEL на 1 или 3 — и в бинарник попадет другая строка.
Различать: #if, #ifdef и defined
Эти три конструкции легко перепутать, разведем их прямо:
#ifdef MACROпроверяет только сам факт: макрос определен или нет, значение не важно.#if MACROвычисляет значение макроса как число. Если макрос не определен, в#ifон считается нулем.#if defined(MACRO)— то же, что#ifdef, но внутри#if, поэтому его можно комбинировать:#if defined(A) && !defined(B).
Защита заголовков (include guard)
Самое частое применение #ifndef — защита заголовочного файла от повторного включения. Если один заголовок подключить дважды (напрямую и через другой заголовок), без защиты определения продублируются и сборка упадет. Классический шаблон:
// файл mymath.h
#ifndef MYMATH_H // если макрос еще не определен -
#define MYMATH_H // определяем его, тело подключится один раз
int add(int a, int b);
#endif // MYMATH_H
При первом включении MYMATH_H еще не определен — тело подключается и макрос выставляется. При повторном включении макрос уже есть, и весь блок до #endif пропускается. Тот же эффект дает нестандартная, но повсеместно поддерживаемая директива #pragma once — одна строка вместо трех, но #ifndef-вариант переносим на любой компилятор.
#error и #pragma
#error прерывает сборку и печатает заданный текст. Ее ставят там, где сборка при неверных условиях бессмысленна:
#if __STDC_VERSION__ < 201112L
#error "нужен компилятор со стандартом C11 или новее"
#endif
#pragma — это канал команд конкретному компилятору. Ее поведение зависит от компилятора: неизвестную #pragma компилятор обязан молча проигнорировать. Самая переносимая на практике — #pragma once (защита заголовка). Прочие специфичны для компилятора, и их стоит сверять с его документацией.
Директивы, специфичные для компилятора
В старом тексте статьи как «обычные» директивы фигурировали #import и #using. Обе они не входят в стандарт C и C++, а являются расширениями компилятора Microsoft (MSVC).
#importв MSVC подключает библиотеку типов COM (файлы.tlb/.olb).#usingподключает сборку .NET и работает только в режиме C++/CLI (ключ/clr).
На GCC и Clang в обычном проекте этих директив нет, поэтому в переносимом коде на них полагаться нельзя. Не путайте #import MSVC и одноименную директиву в Objective-C — это разные вещи из разных языков.
Отдельно про C#: у него тоже есть директивы препроцессора (#define, #if, #region), но работают они иначе. В C# #define только объявляет символ-флаг для условной компиляции — макросов с подстановкой текста и #include там нет. Поэтому «единый препроцессор на все Си-семейство» — упрощение: у C# своя, урезанная модель.
Выводы
- Препроцессор — первый проход сборки: он работает с текстом (подключение файлов, макроподстановка, условная компиляция), а типы, выражения и машинный код — это уже компилятор.
- Директива начинается с
#, занимает отдельную логическую строку и не завершается точкой с запятой; неизвестное имя после#прерывает сборку. - Функциональный макрос — текстовая замена, а не вызов функции: без скобок вокруг параметров и всего выражения ломается приоритет операций (
BAD(1 + 2)дает 5 вместо 9). - Условная компиляция (
#if,#ifdef,#ifndef,#elif,#else,#endif) решается до компиляции, поэтому исключенный код не попадает в бинарник; частный случай — защита заголовка через#ifndef-guard или#pragma once. #importи#using— расширения MSVC, а не стандартные директивы; у C# свой урезанный препроцессор без макросов-подстановок и#include.
Где применяется и что учить дальше
Директивы препроцессора — повседневный инструмент C и C++. Защита заголовков через #ifndef/#pragma once стоит почти в каждом проекте; условная компиляция включает отладочные проверки в debug-сборке и убирает их в релизе, а также разводит код под разные платформы; макросы задают константы и короткие подстановки. Понимание того, что препроцессор работает до компилятора, помогает читать чужой код и не ловить странные ошибки в развернутых макросах.
Освойте тему на практике
Если хочется разобрать C++ системно — от синтаксиса и сборки до указателей, классов и стандартной библиотеки, с практикой на реальных задачах, посмотрите курс C++ Basic. Оценить формат и уровень до старта помогают бесплатные открытые уроки Otus — живые занятия с преподавателями.
FAQ
Чем макрос отличается от константы const? Макрос #define — это текстовая замена на этапе препроцессора: у него нет типа и его не видит отладчик. const/constexpr — это переменная с типом, ее проверяет компилятор. В C++ для констант обычно предпочитают constexpr, а макросы держат для условной компиляции.
Зачем нужен #pragma once, если есть #ifndef? Обе конструкции защищают заголовок от повторного включения. #pragma once короче (одна строка) и не требует придумывать уникальное имя, но это нестандартное расширение; #ifndef-guard работает на любом компиляторе. Часто их даже используют вместе.
Можно ли внутри #if проверять значение обычной переменной программы? Нет. Препроцессор работает до компиляции и не знает про переменные и типы. В #if допустимы только константные выражения из чисел и макросов; проверка значения переменной — это уже обычный if в коде.



