Menu

Условная компиляция в C: #ifdef, #ifndef, #if и -D

Как собирать разный код для разных сборок с помощью #ifdef, #ifndef, #if, #elif и #else: отладочные переключатели, ветки под платформы, определение макросов из командной строки через -D и отключение кода через #if 0.

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

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

Вот чем это отличается от обычного if. Оба выбирают между путями; только один из них выбирает до того, как программа появилась на свет.

#ifdef и #ifndef

Самый простой вопрос — определён ли макрос вообще:

Удалите строку #define DEBUG, и каждый отладочный вывод исчезнет из сборки — не отключится, а исчезнет. Запустите программу как есть, затем уберите эту строку и запустите снова.

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

Каждый #ifdef и #ifndef должен закрываться #endif. Подпись у закрывающей директивы в длинных файлах — #endif /* DEBUG */ — экономит потом уйму времени.

#if, #elif, #else

#if принимает константное целочисленное выражение и сохраняет блок, когда оно ненулевое. Это делает возможными сравнения версий и уровней:

Поменяйте LOG_LEVEL на 0 или 3 и запустите снова, чтобы увидеть, как сама программа меняет форму.

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

#if FEATURE_X        /* FEATURE_X нигде не определён -> 0 -> блок выброшен */

defined(ИМЯ) превращает вопрос #ifdef в нечто пригодное для использования внутри #if, так что проверки можно комбинировать:

#if defined(DEBUG) && !defined(NDEBUG)
    /* отладочная сборка, и проверки assert включены */
#endif

#if defined(LINUX) || defined(BSD)
    /* любая unix-подобная система */
#endif

#if defined(X) и #ifdef X означают одно и то же; берите первое, когда нужны &&, || или !.

Определение макросов из командной строки

Настоящая сила этих переключателей в том, что исходник вообще не обязан меняться. gcc -D определяет макрос для всей компиляции:

gcc -DDEBUG program.c -o program        # DEBUG определён как 1
gcc -DLOG_LEVEL=3 program.c -o program  # конкретное значение
gcc -DDEBUG -DBUFFER_SIZE=512 a.c b.c -o app

-DИМЯ без значения равносильно #define ИМЯ 1. -U ИМЯ отменяет определение — это важно, когда заголовочный файл определяет что-то, что вам нужно выключить.

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

/* config.h */
#ifndef LOG_LEVEL
#define LOG_LEVEL 1        /* по умолчанию тихо */
#endif

#ifndef MAX_CONNECTIONS
#define MAX_CONNECTIONS 64
#endif
# сборка для разработки
gcc -DDEBUG -DLOG_LEVEL=3 -Wall -Wextra -g src/*.c -o app-dev

# релизная сборка
gcc -DNDEBUG -O2 src/*.c -o app

NDEBUG стандартизован: его определение отключает каждый assert() в программе, потому что сам <assert.h> написан с использованием условной компиляции. Это тот же приём в миниатюре — заголовочный файл, который компилируется в разное в зависимости от того, что определила сборка.

Переключатели платформы

Компиляторы предопределяют макросы, идентифицирующие целевую систему, поэтому один файл исходника может вызывать на каждой из них свой API:

#if defined(_WIN32)
    #include <windows.h>
    #define CLEAR_SCREEN "cls"
#elif defined(__APPLE__)
    #include <unistd.h>
    #define CLEAR_SCREEN "clear"
#elif defined(__linux__)
    #include <unistd.h>
    #define CLEAR_SCREEN "clear"
#else
    #error "Unsupported platform"
#endif

Самые ходовые: _WIN32 (определён и на 32-, и на 64-битной Windows), _WIN64, __linux__, __APPLE__, __unix__, __ANDROID__. У самого компилятора свой набор — __GNUC__, __clang__, _MSC_VER, — и у архитектуры тоже: __x86_64__, __aarch64__.

Запускаемая версия, которая сообщает, на чём её собрали:

#error стоит знать отдельно: он останавливает компиляцию с вашим сообщением. Завершение цепочки платформ через #else / #error "Unsupported platform" превращает молчаливую неправильную сборку во внятный сбой в самом верху журнала сборки.

Чтобы вывести все макросы, которые предопределяет ваш компилятор:

gcc -dM -E - < /dev/null

Этот вывод — авторитетный ответ на вопрос «что вообще можно проверять на этой машине».

#if 0 для отключения кода

Закомментирование блока через /* ... */ ломается в тот момент, когда внутри блока оказывается собственный комментарий, потому что комментарии C не вкладываются: первый же */ внутри завершает внешний комментарий, и всё после него становится бесхозным кодом. У #if 0 такой проблемы нет:

#if 0
    /* Весь этот участок удаляется, вместе с комментариями. */
    legacy_init();
    int n = old_calculation(42);  /* и этот комментарий не помеха */
    report(n);
#endif

Замените на #if 1, чтобы вернуть код. Редакторы продолжают подсвечивать его как C, а #if 0 можно вложить внутрь другого #if.

Это инструмент отладки, а не хранилище. Код, лежащий в #if 0, никогда не компилируется, поэтому тихо гниёт: к тому моменту, когда кто-нибудь щёлкнет переключателем, он уже не соберётся. Пользуйтесь этим, пока локализуете проблему, а потом удаляйте блок и доверьте память системе контроля версий.

Где условная компиляция даёт сбой

Основную часть боли объясняют три сценария.

Код, который никогда не компилируется, никогда и не проверяется. Опечатка внутри неактивной ветки #ifdef невидима, пока кто-нибудь не соберёт эту конфигурацию — возможно, через несколько месяцев, возможно, в CI на платформе, которой у вас нет. Если в проекте есть нетривиальные ветки под платформы, собирайте их все регулярно.

Переплетение #ifdef с потоком управления очень быстро становится нечитаемым. О такой конструкции трудно рассуждать:

if (ready) {
#ifdef FAST_PATH
    fast_send(buf);
} else {
#endif
    slow_send(buf);
}

Скобки, открытые в одной ветке и закрытые в другой, законны и ужасны. Лучше оборачивать условиями функции целиком и выбирать между ними:

#ifdef FAST_PATH
static void send_data(const char *buf) { fast_send(buf); }
#else
static void send_data(const char *buf) { slow_send(buf); }
#endif

Обычный if времени выполнения часто лучше. Если обе ветки компилируются везде, обычный if (debug_enabled) сохраняет оба пути проверенными по типам, тестируемыми и переключаемыми без пересборки. Оставьте препроцессору то, что умеет только он: код, который действительно не может скомпилироваться на другой платформе.

Приём, объединяющий всё это, — заголовочный файл со значениями по умолчанию, -D для конкретной сборки и ветки под платформы внутри защиты от повторного включения — смотрите в заголовочных файлах; про сам #define — в макросах.

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

Что такое условная компиляция в C?

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

В чём разница между #ifdef и #if?

#ifdef ИМЯ спрашивает только о том, существует ли макрос, независимо от его значения. #if выражение вычисляет константное целочисленное выражение, поэтому умеет сравнивать значения: #if VERSION >= 3. Используйте #ifdef для флагов, которые просто есть или которых нет, и #if, когда важно значение. #if defined(ИМЯ) совмещает оба варианта и сочетается с && и ||.

Как определить макрос из командной строки в gcc?

Через -D: gcc -DDEBUG program.c определяет DEBUG так, будто файл начинался с #define DEBUG 1, а -DMAX=50 задаёт конкретное значение. Именно так системы сборки включают возможности, не правя исходник; -U ИМЯ отменяет определение.

Почему стоит использовать #if 0 вместо комментирования кода?

Потому что комментарии /* ... */ не вкладываются: первый же */ внутри участка досрочно завершает комментарий, и остаток превращается в сломанный код. #if 0 ... #endif убирает любой объём кода с любым количеством комментариев внутри, сохраняет подсветку синтаксиса и элементарно возвращается обратно заменой на #if 1.

Coddy programming languages illustration

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

НАЧАТЬ