Директивы препроцессора в C и C++: список, синтаксис и примеры

Директивы препроцессора в C и C++: список, синтаксис и примеры Полезное

Директива препроцессора — это команда, которая начинается с символа # и выполняется до компиляции: препроцессор подставляет содержимое файлов, разворачивает макросы и включает или выключает участки кода, а компилятор уже получает готовый текст. В 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 в коде.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)