Урок №22. Директивы препроцессора

  Юрий  | 

  |

  Обновл. 11 Сен 2021  | 

 175711

 ǀ   44 

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

Директива #include

Вы уже видели директиву #include в действии. Когда вы подключаете файл с помощью директивы #include, препроцессор копирует содержимое подключаемого файла в текущий файл сразу после строки с #include. Это очень полезно при использовании определенных данных (например, предварительных объявлений функций) сразу в нескольких местах.

Директива #include имеет две формы:

   #include <filename>, которая сообщает препроцессору искать файл в системных путях (в местах хранения системных библиотек языка С++). Чаще всего вы будете использовать эту форму при подключении заголовочных файлов из Стандартной библиотеки C++.

   #include "filename", которая сообщает препроцессору искать файл в текущей директории проекта. Если его там не окажется, то препроцессор начнет проверять системные пути и любые другие, которые вы указали в настройках вашей IDE. Эта форма используется для подключения пользовательских заголовочных файлов.

Директива #define


Директиву #define можно использовать для создания макросов. Макрос — это правило, которое определяет конвертацию идентификатора в указанные данные.

Есть два основных типа макросов: макросы-функции и макросы-объекты.

Макросы-функции ведут себя как функции и используются в тех же целях. Мы не будем сейчас их обсуждать, так как их использование, как правило, считается опасным, и почти всё, что они могут сделать, можно осуществить с помощью простой (линейной) функции.

Макросы-объекты можно определить одним из следующих двух способов:

#define идентификатор

Или:

#define идентификатор текст_замена

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

Макросы-объекты с текст_замена

Когда препроцессор встречает макросы-объекты с текст_замена, то любое дальнейшее появление идентификатор заменяется на текст_замена. идентификатор обычно пишется заглавными буквами с символами подчёркивания вместо пробелов.

Рассмотрим следующий фрагмент кода:

Препроцессор преобразует вышеприведенный код в:

Результат выполнения:

My favorite number is: 9

Мы обсудим это детально, и почему так не стоит делать, на следующих уроках.

Макросы-объекты без текст_замена


Макросы-объекты также могут быть определены без текст_замена, например:

Любое дальнейшее появление идентификатора USE_YEN удаляется и заменяется «ничем» (пустым местом)!

Это может показаться довольно бесполезным, однако, это не основное предназначение подобных директив. В отличие от макросов-объектов с текст_замена, эта форма макросов считается приемлемой для использования.

Условная компиляция

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

   #ifdef

   #ifndef

   #endif

Директива #ifdef (сокр. от «if defined» = «если определено») позволяет препроцессору проверить, было ли значение ранее определено с помощью директивы #define. Если да, то код между #ifdef и #endif скомпилируется. Если нет, то код будет проигнорирован. Например:

Поскольку PRINT_JOE уже был определен, то строка std::cout << "Joe" << std::endl; скомпилируется и выполнится. А поскольку PRINT_BOB не был определен, то строка std::cout << "Bob" << std::endl; не скомпилируется и, следовательно, не выполнится.

Директива #ifndef (сокр. от «if not defined» = «если не определено») — это полная противоположность к #ifdef, которая позволяет проверить, не было ли значение ранее определено. Например:

Результатом выполнения этого фрагмента кода будет Bob, так как PRINT_BOB ранее никогда не был определен. Условная компиляция очень часто используется в качестве header guards (о них мы поговорим на следующем уроке).

Область видимости директивы #define


Директивы выполняются перед компиляцией программы: сверху вниз, файл за файлом. Рассмотрим следующую программу:

Несмотря на то, что директива #define MY_NAME "Alex" определена внутри функции boo(), препроцессор этого не заметит, так как он не понимает такие понятия языка C++, как функции. Следовательно, выполнение этой программы будет идентично той, в которой бы #define MY_NAME "Alex" было определено ДО, либо сразу ПОСЛЕ функции boo(). Для лучше читабельности кода определяйте идентификаторы (с помощью #define) вне функций.

После того, как препроцессор завершит свое выполнение, все идентификаторы (определенные с помощью #define) из этого файла — отбрасываются. Это означает, что директивы действительны только с точки определения и до конца файла, в котором они определены. Директивы, определенные в одном файле кода, не влияют на директивы, определенные внутри других файлов этого же проекта.

Рассмотрим следующий пример:

function.cpp:

main.cpp:

Результат выполнения программы:

Not printing!

Несмотря на то, что мы объявили PRINT в main.cpp (#define PRINT), это все равно не имеет никакого влияния на что-либо в function.cpp. Поэтому, при выполнении функции doSomething(), у нас выводится Not printing!, так как в файле function.cpp мы не объявляли идентификатор PRINT (с помощью директивы #define). Это связано с header guards.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (1 020 оценок, среднее: 4,74 из 5)
Загрузка...

Комментариев: 44

  1. Серёга:

    Не понятно как работает пример с function.cpp и main.cpp. Как это компилируется? Содержимое function.cpp добавляется в конец main.cpp?

  2. Denis:

    " #include "filename", которая сообщает препроцессору искать файл в текущей директории проекта."

    Включая вложенные папки или нет?

  3. Andrew:

    Из двух файлов последнего примера сделан оидн. Он перед Вами, и он после компиляции все равно выдает

    "Not printing!"

    Почему? Ведь теперь #define PRINT объявлен внутри этого единого файла?

    1. Юлий:

      Andrew, уберите строку 12 или сделайте её строкой 2

      1. Oleg:

        1. Andrew, уберите строку 12.
        2. 16-ю строку, сделайте строкой 2 (cut & paste) ибо ->
        -> только глобальное объявление переменной или функции,
        даст возможность видеть её всем функциям.

    2. Юлий:

      #define PRINT появляется уже после проверки, поэтому так

      1. Andrew:

        Ничего не меняется.
        Доходим до функции int main()
        в ней объявлена директива #define PRINT
        и уже ПОСЛЕ объявления этой директивы вызывается функция doSomething();
        Почему она выводит "Not printing!"? Ведь директива #define PRINT УЖЕ объявлена.
        Извините мою непонятливость.

        1. Денис:

          Директива #define PRINT определена после директив #ifdef/#ifndef
          Препроцессор читает код построчно, неперепрыгивая на другие строки. Поэтому сначала читается #ifdef PRINT, потом #ifndef PRINT, потом #define PRINT.

        2. ОЛЕГ:

          Andrew!
          Как сказано в этом уроке "Директивы выполняются перед компиляцией программы: сверху вниз, файл за файлом". У вас директива препроцессора #define PRINT стоит после группы директив препроцессора условной компиляции, поэтому не оказывает влияния ни на что. То, что внутри тела функции int main() объявлена директива #define PRINT и уже ПОСЛЕ объявления этой директивы вызывается функция doSomething() не имеет значения, так как на эту вызываемую функцию вообще не влияет содержимое тела функции main (УРОК 12). Эта функция вызывается и выполняется полностью изолированно от кода, который находится в теле main. Если переставить #define PRINT например на 2 строчку, то результат будет "Printing".

  4. Евгений:

    А дефайны из заголовочных можно перетягивать на другие файлы исходных кодов?

  5. Салех:

    Спасибо огромное за ваш труд!
    Как я понял шаги компиляции (согласно нашим знаниям, полученным до этого урока) следующие:
    1) препроцессор прочесывает весь код в поисках директив: вставляет все объявления, определяет все макрося;
    2) далее прочесываются другие команды;

    Тогда вопрос: Вот в такой программе

    происходит ошибка компиляции. Получается, что хоть препроцессор и прочесал код в поисках директив, мой компилятор не знает о существовании MY_NUMBER.

    Мне искать дальше или я что-то пропустил из предыдущих уроков?

    1. Руслан:

      Нужно MY_NUMBER объявить до её вызова.

      1. Салех:

        Получается, что да. Думал, что сначала препроцессор проверяет все дерективы и делает себе что-то вроде словоря. Оказывается не совсем

    2. Akr1d0v:

      После того, как препроцессор завершит свое выполнение, все идентификаторы (определенные с помощью #define) из этого файла — отбрасываются. Это означает, что директивы действительны только с точки определения и до конца файла, в котором они определены.

      В уроке это описывается

  6. AndreyOlegovich.ru:

    Спасибо за эти уроки. Пока что ничего даже близкого по качеству о C++ я не видел. Даже на Pluralsight всё намного хуже.

    1. Фото аватара Юрий:

      Пожалуйста))

  7. Павел:

    Менее половины дня потребовалось дойти до этого урока, всё настолько просто и доходчиво объяснено, что вопросов не остаётся.
    Вам учебники нужно писать для образовательных учреждений.

    1. Фото аватара Юрий:

      Мне вот бы тоже хотелось, чтобы такая подача была и в образовательных учреждениях. Но этому вряд ли быть))

  8. Георгий:

    Годнее контента я ещё не видел…. Автору прям большой плюс и спасибо за столь хорошее объяснение. Столько книг уже по С++ в 21 веке и не в одной так доходчиво и на примере, не разъяснено как тут. Не понимаю зачем книги по С++, если есть эта божественная статья.
    Эти статьи напроч отбивают вопросы которые бы возникли где нибудь ещё. Если по С++ ты читаешь книгу, то там куча вопросов возникает еще в начале первой странице. А тут все по порядку и красиво сделано

    1. Фото аватара Юрий:

      Спасибо, очень приятно 🙂

    2. Рустам:

      Читаю, не могу остановится!

  9. Никита:

    Че так годно то?

    1. Фото аватара Юрий:

      Ну шо есть, то есть)

  10. Алексей:

    В предпоследнем примере вы утверждаете, что препроцессор не знает что такое функция и внутри неё работать не будет, а в конце статьи мы видим обратное. Что же верно?

    1. Фото аватара Юрий:

      Верно то, что написано в уроке. Пройдите ещё с 10-ок уроков и возвратитесь к этому уроку и перечитайте его. Возможно, станет понятнее.

    2. Олег:

      Я понял так что препроцессор не знает что такое функция т.е. для него это пустое место, следовательно

      работать будет.
      Просто для большей читабельности и возможно еще чего-нибудь, надо ставить его вверху.

    3. Александр:

      Дело в том, что две функции находятся в разных файлах — Директива PRINT сначала определяется в функции main(), потом в этой функции идёт обращение к функции doSomething(), которая находиться в другом файле — function.cpp, поэтому, не закончив с main.cpp, выполнение переходит в function.cpp, а поскольку main.cpp не закончился, то и определение PRINT ещё существует, и, как раз, поэтому проверка наличия определения(#ifdef PRINT) является истинной, и, соответственно, выводится слово Printing!
      Если бы обе эти функции находились в одном файле, и реализация doSomething() была бы выше main(), то, поскольку во время проверки наличия определения его не существует- он ниже, выводится Not printing!, но если реализация находиться под main(), то выводиться Printing!, потому что в этом случае определение уже существует к тому моменту, когда мы проверяем его наличие.

  11. Vl:

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

  12. Сергей:

    Компилируется только без " std::cout << "Not printing!"; "
    Как только пишу эту строчку компилятор выдает ошибку " error C2065: cout: необъявленный идентификатор ". В чем же проблема?

    1. Steve Dekart:

      " — Это символ, который нужно тебе убрать, для чего ты его туда вообще поставил? Он лишний, т.к. сам по себе ты окончил стейтмент точкой с запятой, но зачем то ты ставишь ".
      Пиши так:

  13. Oleksiy:

    В общем понятно. Препроцессор берет составленную нами прогу и заменяет в ней все директивы на что-то свое. В результате получаем прогу, которая все еще написана на С++, но директив со значками # там уже нет. Т.е. это предварительное "пережовывание" программы перед тем, как она компилируется.

  14. Oleksiy:

    Подтверждаю, эта глава написана не очень доходчиво для новичка. Может, проблема со стилем? Читатель (я) постоянно задает вопрос: а зачем эти директивы нужны? Нет мотивации схватить материал за рога. Буду перечитывать еще раз. С предыдущими главами такого не было.

  15. Вячеслав:

    Директивы, определенные в одном файле кода, не влияют на код других файлов того же проекта. Не согласен, а если в файле function.cpp у нас будет написано #define PRINT, а в файле main.cpp напишем #include "function.cpp" — PRINT будет влиять на код в обеих файлах. Все зависит от порядка подключения. А определения передаваемые в аргументы компилятора видны, вообще, всем файлам проекта.

    1. Александр:

      Вячеслав, когда вы пишите #include "function.cpp", вы просто Copy-Paste-ите код.

      1. prihoxxanin:

        Нашел хорошую иллюстрацию процесса. Но! Решил попробовать.

        $ g++  main.cpp  -o  hello

        Ну вот так получилось.

  16. Алексей:

    Здравствуйте. Можно ли определить с помощью команды к примеру #define MY_DEFINE в файле main.cpp в файле add.cpp?

    1. Фото аватара Юрий:

      В файле main.cpp в файле add.cpp? Я что-то не понял, что вы имеете в виду. MY_DEFINE можно определить только один раз и в одном файле проекта.

  17. илья:

    а мы будем проходить работу с графикой в программировании?

    1. Фото аватара Юрий:

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

      1. илья:

        ПОНЯТНО,спасибо

  18. Максим():

    Да уж, тема сложновата… Придется посидеть…

    1. painkiller:

      Да, мне сперва тоже так показалось, но после повторного прочтения всё стало на свои места.
      В предыдущем уроке оставил несколько вопросов в комментариях, но благодаря этому уроку ответов на них уже не требуется — до всего дошел сам.

  19. Vlados:

    Несмотря на то, что PRINT был определен в main.cpp, он все равно не имеет никакого влияния на что-либо в function.cpp.

    PRINT был объявлен же, а не определён.
    Иди я что-то путаю

    1. Фото аватара Юрий:

      Исправил. В этом случае директива #defined используется как в качестве объявления, так и определения. Т.е. это одно и то же (в этом случае).

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *