2.9 – Знакомство с препроцессором

Добавлено 12 апреля 2021 в 11:59

Трансляция и препроцессор

Когда вы компилируете свой код, вы можете ожидать, что компилятор компилирует код именно в том виде, как вы его написали. На самом деле это не так.

Перед компиляцией файл кода проходит этап, известный как трансляция. На этапе трансляции происходит много всего, чтобы подготовить ваш код к компиляции (если вам интересно, здесь вы можете найти список этапов трансляции). Файл кода с примененными к нему трансляциями называется единицей трансляции.

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

Когда препроцессор запускается, он просматривает файл кода (сверху вниз) в поисках директив препроцессора. Директивы препроцессора (часто называемые просто директивами) – это инструкции, которые начинаются с символа # и заканчиваются символом новой строки (НЕ точкой с запятой). Эти директивы сообщают препроцессору, что нужно выполнить определенные задачи по обработке текста. Обратите внимание, что препроцессор не понимает синтаксис 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). Это будет иметь значение, когда будем обсуждать защиту заголовков в будущем уроке.

Теги

C++ / CppLearnCppДля начинающихОбучениеПрепроцессорПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.