Заметки по языку с и с++

Препроцессорные директивы C/C++: #define, #include, #if, #pragma

обновлено 687 просмотров

Введение

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

Как работают препроцессорные директивы

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

Результат работы препроцессора — один большой текстовый файл. В нём уже нет директив и макросов. Именно этот текст и получает компилятор. Поэтому ошибка в макросе часто выглядит странно: компилятор показывает уже раскрытый код.

Посмотреть результат можно самому. В GCC и Clang для этого есть флаг -E, в MSVC — флаг /P. Это хороший способ понять, во что превращается сложный макрос.

#define — объявление макроса

Директива #define заменяет идентификатор на заданную последовательность символов везде, где он встречается дальше в файле:

#define BUFFER_SIZE 1024

char buffer[BUFFER_SIZE];   // после препроцессора: char buffer[1024];

Макросы могут принимать параметры:

#define MAX(x, y) ((x) > (y) ? (x) : (y))

int value = MAX(1, 4);
// после препроцессора:
int value = ((1) > (4) ? (1) : (4));

Скобки вокруг параметров и всего выражения обязательны. Без них приоритет операций может дать неожиданный результат. Длинный макрос можно перенести на следующую строку. Для этого в конце строки ставят обратную косую черту \:

#define LOG_ERROR(msg) \
    qCritical() << __FILE__ << __LINE__ << msg

В современном C++ вместо макросов-констант лучше использовать constexpr, а вместо макросов-функций — шаблоны или inline-функции. Подробнее о макросах и их подводных камнях читайте в статье Препроцессор: #define, макросы, условная компиляция.

Операторы # и ##

Внутри макроса оператор # превращает параметр в строку, а ## склеивает два токена:

#define TO_STRING(x) #x
#define MAKE_VAR(name, n) name##n

const char *s = TO_STRING(hello);   // "hello"
int MAKE_VAR(value, 1) = 10;        // int value1 = 10;

#undef — отмена макроса

Директива #undef отменяет ранее объявленный макрос. После неё идентификатор можно объявить заново с другим значением:

#define BUFFER_SIZE 1024
// ...
#undef BUFFER_SIZE
#define BUFFER_SIZE 4096

Если переопределить макрос без #undef, компилятор выдаст предупреждение. Никогда не переопределяйте стандартные макросы вроде NULL: это ломает чужой код и библиотеки.

#include — подключение файлов

Директива #include вставляет текст другого файла в место вызова. Так подключаются объявления функций и классов:

#include <QDebug>       // системные и библиотечные заголовки
#include "mainwindow.h" // заголовки вашего проекта

Угловые скобки означают, что файл ищется в системных каталогах. Кавычки — что поиск начнётся с каталога текущего файла. Чтобы заголовок не подключался дважды, используют include guards или #pragma once. Подробнее об этом — в статье Заголовочные файлы и include guards.

Условная компиляция: #if, #ifdef, #ifndef, #elif, #else, #endif

Условные директивы решают, какие участки кода попадут в компиляцию. Так пишут код для разных платформ и отладочный вывод:

#define DEBUG_LEVEL 2

#if DEBUG_LEVEL >= 2
    qDebug() << "Подробный вывод";
#elif DEBUG_LEVEL == 1
    qDebug() << "Краткий вывод";
#else
    // вывод отключён
#endif

Директивы #ifdef и #ifndef проверяют, объявлен ли макрос, а оператор defined() позволяет комбинировать проверки:

#ifdef Q_OS_WIN
#include <windows.h>
#endif

#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
    // код только для настольного Linux
#endif

В C++17 появилась проверка __has_include, которая позволяет узнать, доступен ли заголовок:

#if __has_include(<optional>)
#include <optional>
#endif

#error и #warning — сообщения компилятора

Директива #error останавливает компиляцию с указанным сообщением. Она полезна для проверки обязательных настроек:

#if !defined(Q_OS_WIN)
#error "Этот модуль поддерживается только в Windows"
#endif

#if __cplusplus < 201703L
#error "Требуется C++17 или новее"
#endif

Директива #warning выводит предупреждение, но не прерывает сборку. Она давно поддерживается GCC и Clang и вошла в стандарт C++23.

#pragma — указания компилятору

Директива #pragma передаёт компилятору специальные инструкции. Их набор зависит от компилятора. Самые распространённые:

  • #pragma once — подключать заголовок только один раз;
  • #pragma pack(push, 1) / #pragma pack(pop) — выравнивание полей структур;
  • #pragma warning(disable : 4996) — отключить предупреждение в MSVC;
  • #pragma message("текст") — вывести сообщение при компиляции.

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

Компилятор автоматически объявляет ряд полезных макросов:

  • __FILE__ — имя текущего файла;
  • __LINE__ — номер текущей строки;
  • __DATE__ и __TIME__ — дата и время компиляции;
  • __cplusplus — версия стандарта C++ (например, 201703L для C++17);
  • _MSC_VER, __GNUC__, __clang__ — признаки конкретного компилятора.
qDebug() << "Ошибка в" << __FILE__ << "строка" << __LINE__;
qDebug() << "Собрано:" << __DATE__ << __TIME__;

Заключение

Препроцессорные директивы управляют тем, какой код увидит компилятор. #include подключает заголовки. #define и #undef объявляют и отменяют макросы. Условные директивы выбирают нужные участки кода. #error и #pragma управляют самой сборкой. Для констант и функций лучше использовать средства языка C++.