2.9 – Знакомство с препроцессором
Трансляция и препроцессор
Когда вы компилируете свой код, вы можете ожидать, что компилятор компилирует код именно в том виде, как вы его написали. На самом деле это не так.
Перед компиляцией файл кода проходит этап, известный как трансляция. На этапе трансляции происходит много всего, чтобы подготовить ваш код к компиляции (если вам интересно, здесь вы можете найти список этапов трансляции). Файл кода с примененными к нему трансляциями называется единицей трансляции.
Самый примечательный из этапов трансляции связан с препроцессором. Препроцессор лучше всего рассматривать как отдельную программу, которая манипулирует текстом в каждом файле кода.
Когда препроцессор запускается, он просматривает файл кода (сверху вниз) в поисках директив препроцессора. Директивы препроцессора (часто называемые просто директивами) – это инструкции, которые начинаются с символа #
и заканчиваются символом новой строки (НЕ точкой с запятой). Эти директивы сообщают препроцессору, что нужно выполнить определенные задачи по обработке текста. Обратите внимание, что препроцессор не понимает синтаксис C++ – вместо этого директивы имеют свой собственный синтаксис (который иногда напоминает синтаксис C++, а иногда не очень).
Выходные данные препроцессора проходят еще несколько этапов трансляции, а затем компилируются. Обратите внимание, что препроцессор никоим образом не изменяет исходные файлы кода – скорее, все изменения текста, сделанные препроцессором, временно размещаются в памяти при каждой компиляции файла кода.
В этом уроке мы обсудим, что делают некоторые из наиболее распространенных директив препроцессора.
В качестве отступления...
Директивы using
(представленные в уроке «2.8 – Конфликты имен и пространства имен») не являются директивами препроцессора (и, следовательно, препроцессором не обрабатываются). Таким образом, хотя термин директива обычно означает директиву препроцессора, это не всегда так.
Включения
Вы уже видели в действии директиву #include
(обычно это было #include <iostream>
). Когда вы включаете файл с помощью #include
, препроцессор заменяет директиву #include
содержимым включенного файла. Включенное содержимое затем предварительно обрабатывается (вместе с остальной частью файла), а затем компилируется.
Рассмотрим следующую программу:
#include <iostream>
int main()
{
std::cout << "Hello, world!";
return 0;
}
Когда препроцессор запускается для этой программы, он заменяет #include <iostream>
предварительно обработанным содержимым файла с именем "iostream".
Поскольку #include
почти всегда используется для включения заголовочных файлов , мы обсудим эту директиву более подробно в следующем уроке (когда мы будем подробнее обсуждать заголовочные файлы).
Определения макросов
Директива #define
может использоваться для создания макросов. В C++ макрос – это правило, определяющее, как входной текст преобразуется в выходной текст с помощью замены.
Существует два основных типа макросов: макросы, подобные объектам, и макросы, подобные функциям.
Макросы, подобные функциям, действуют как функции и служат той же цели. Мы не будем здесь их обсуждать, потому что их использование обычно считается опасным, и почти всё, что они могут сделать, можно сделать и с помощью обычных функций.
Макросы, подобные объектам, можно определить одним из двух способов:
#define идентификатор
#define идентификатор подставляемый_текст
В первом определении нет подставляемого при замене текста, а во втором есть. Поскольку это директивы препроцессора (а не инструкции), обратите внимание, что ни одна из форм не заканчивается точкой с запятой.
Макросы, подобные объектам, с подставляемым текстом
Когда препроцессор встречает эту директиву, любое дальнейшее появление идентификатора заменяется подставляемым текстом. Идентификатор традиционно набирается заглавными буквами с использованием подчеркивания для обозначения пробелов.
Рассмотрим следующую программу:
#include <iostream>
#define MY_NAME "Alex"
int main()
{
std::cout << "My name is: " << MY_NAME;
return 0;
}
Препроцессор преобразует приведенный выше код в следующее:
// Сюда вставляется содержимое iostream
int main()
{
std::cout << "My name is: " << "Alex";
return 0;
}
Этот код при запуске печатает: My name is: Alex.
Объектоподобные макросы использовались как более дешевая альтернатива постоянным переменным. Те времена давно прошли, поскольку компиляторы стали умнее, а язык вырос. Теперь объектоподобные макросы можно увидеть только в устаревшем коде.
Мы рекомендуем вообще избегать таких макросов, так как существуют более эффективные способы сделать аналогичные вещи. Мы обсудим это более подробно в уроке «4.14 – const, constexpr и символьные константы».
Макросы, подобные объектам, без подставляемого текста
Макросы, подобные объектам, также могут быть определены без подставляемого при замене текста.
Например:
#define USE_YEN
Макросы этого типа работают так, как и следовало ожидать: любое дальнейшее появление идентификатора удаляется и ничем не заменяется!
Это может показаться довольно бесполезным, и, да, это бесполезно для замены текста. Однако эта форма директивы обычно используется для другого. Мы обсудим использование этой формы чуть позже.
В отличие от объектоподобных макросов с заменяющим текстом, макросы этой формы обычно считаются приемлемыми для использования.
Условная компиляция
Директивы препроцессора условной компиляции позволяют указать, при каких условиях что-то будет или не будет компилироваться. Существует довольно много разных директив условной компиляции, но здесь мы рассмотрим только три, которые используются чаще всего: #ifdef
, #ifndef
и #endif
.
Директива препроцессора #ifdef
позволяет препроцессору проверять, был ли идентификатор ранее определен с помощью #define
. Если это так, код между #ifdef
и соответствующим #endif
компилируется. В противном случае код игнорируется.
Рассмотрим следующую программу:
#include <iostream>
#define PRINT_JOE
int main()
{
#ifdef PRINT_JOE
std::cout << "Joe\n"; // если PRINT_JOE определен, компилируем этот код
#endif
#ifdef PRINT_BOB
std::cout << "Bob\n"; // если PRINT_BOB определен, компилируем этот код
#endif
return 0;
}
Поскольку PRINT_JOE
был определен с #define
, строка cout << "Joe\n"
будет скомпилирована. Поскольку PRINT_BOB
не был определен с #define
, строка cout << "Bob\n"
будет проигнорирована.
#ifndef
– это противоположность #ifdef
в том, что он позволяет вам проверить, НЕ был ли идентификатор еще определен с помощью #define
.
#include <iostream>
int main()
{
#ifndef PRINT_BOB
std::cout << "Bob\n";
#endif
return 0;
}
Эта программа печатает «Bob», потому что PRINT_BOB
никогда не был определен с #define
.
Вместо #ifdef PRINT_BOB
и #ifndef PRINT_BOB
вы также можете увидеть #if defined (PRINT_BOB)
и #if !defined(PRINT_BOB)
. Они делают то же самое, но используют синтаксис, немного более похожий на C++.
#if 0
Еще одно распространенное использование условной компиляции включает использование #if 0
для исключения блока кода из компиляции (как если бы он находился внутри блока комментариев):
#include <iostream>
int main()
{
std::cout << "Joe\n";
#if 0 // Ничего не компилировать, начиная с этого места
std::cout << "Bob\n";
std::cout << "Steve\n";
#endif // до этой точки
return 0;
}
Приведенный выше код печатает только «Joe», потому что «Bob» и «Steve» находились внутри блока #if 0
, который препроцессор исключил из компиляции.
Это обеспечивает удобный способ «закомментировать» код, содержащий многострочные комментарии.
Макросы, подобные объектам, не влияют на другие директивы препроцессора.
Теперь вам может быть это интересно:
#define PRINT_JOE
#ifdef PRINT_JOE
// ...
Поскольку мы определили PRINT_JOE
как ничто, почему препроцессор не заменил PRINT_JOE
в #ifdef PRINT_JOE
ничем?
Макросы вызывают замену текста только в обычном коде. Другие команды препроцессора игнорируются. Следовательно, PRINT_JOE
в #ifdef PRINT_JOE
не меняется.
Например:
#define FOO 9 // Вот макрос-подстановка
#ifdef FOO // Этот FOO не заменяется, потому что он является частью другой директивы препроцессора
std::cout << FOO; // Этот FOO заменяется на 9, потому что это часть обычного кода
#endif
На самом деле вывод препроцессора вообще не содержит директив – все они разрешаются/удаляются перед компиляцией, потому что компилятор не знает, что с ними делать.
Область видимости определений #define
Директивы разрешаются перед компиляцией сверху вниз для каждого файла.
Рассмотрим следующую программу:
#include <iostream>
void foo()
{
#define MY_NAME "Alex"
}
int main()
{
std::cout << "My name is: " << MY_NAME;
return 0;
}
Несмотря на то, что похоже, что #define MY_NAME "Alex"
определен внутри функции foo
, препроцессор этого не заметит, поскольку он не понимает таких понятий C++, как функции. Следовательно, эта программа ведет себя идентично программе, в которой #define MY_NAME "Alex"
был определен либо до, либо сразу после функции foo
. Для большей удобочитаемости идентификаторы определяются с помощью #define
обычно вне функций.
После завершения препроцессора все определенные в данном файле идентификаторы отбрасываются. Это означает, что директивы действительны только с точки определения до конца файла, в котором они определены. Директивы, определенные в одном файле кода, не влияют на другие файлы кода в том же проекте.
Рассмотрим следующий пример:
function.cpp:
#include <iostream>
void doSomething()
{
#ifdef PRINT
std::cout << "Printing!";
#endif
#ifndef PRINT
std::cout << "Not printing!";
#endif
}
main.cpp:
void doSomething(); // предварительное объявление для функции doSomething()
#define PRINT
int main()
{
doSomething();
return 0;
}
Приведенная выше программа напечатает:
Not printing!
Несмотря на то, что PRINT
был определен в main.cpp, это не повлияло на код в function.cpp (PRINT
определен только от точки определения до конца main.cpp). Это будет иметь значение, когда будем обсуждать защиту заголовков в будущем уроке.