Препроцессор лучше всего рассматривать как отдельную программу, которая выполняется перед компиляцией. При запуске программы, препроцессор просматривает код сверху вниз, файл за файлом, в поиске директив. Директивы — это специальные команды, которые начинаются с символа #
и НЕ заканчиваются точкой с запятой. Есть несколько типов директив, которые мы рассмотрим ниже.
Директива #include
Вы уже видели директиву #include в действии. Когда вы подключаете файл с помощью директивы #include, препроцессор копирует содержимое подключаемого файла в текущий файл сразу после строки с #include. Это очень полезно при использовании определенных данных (например, предварительных объявлений функций) сразу в нескольких местах.
Директива #include имеет две формы:
#include <filename>
, которая сообщает препроцессору искать файл в системных путях (в местах хранения системных библиотек языка С++). Чаще всего вы будете использовать эту форму при подключении заголовочных файлов из Стандартной библиотеки C++.
#include "filename"
, которая сообщает препроцессору искать файл в текущей директории проекта. Если его там не окажется, то препроцессор начнет проверять системные пути и любые другие, которые вы указали в настройках вашей IDE. Эта форма используется для подключения пользовательских заголовочных файлов.
Директива #define
Директиву #define можно использовать для создания макросов. Макрос — это правило, которое определяет конвертацию идентификатора в указанные данные.
Есть два основных типа макросов: макросы-функции и макросы-объекты.
Макросы-функции ведут себя как функции и используются в тех же целях. Мы не будем сейчас их обсуждать, так как их использование, как правило, считается опасным, и почти всё, что они могут сделать, можно осуществить с помощью простой (линейной) функции.
Макросы-объекты можно определить одним из следующих двух способов:
#define идентификатор
Или:
#define идентификатор текст_замена
Верхнее определение не имеет никакого текст_замена
, в то время как нижнее — имеет. Поскольку это директивы препроцессора (а не простые стейтменты), то ни одна из форм не заканчивается точкой с запятой.
Макросы-объекты с текст_замена
Когда препроцессор встречает макросы-объекты с текст_замена
, то любое дальнейшее появление идентификатор
заменяется на текст_замена
. идентификатор
обычно пишется заглавными буквами с символами подчёркивания вместо пробелов.
Рассмотрим следующий фрагмент кода:
1 2 3 |
#define MY_FAVORITE_NUMBER 9 std::cout << "My favorite number is: " << MY_FAVORITE_NUMBER << std::endl; |
Препроцессор преобразует вышеприведенный код в:
1 |
std::cout << "My favorite number is: " << 9 << std::endl; |
Результат выполнения:
My favorite number is: 9
Мы обсудим это детально, и почему так не стоит делать, на следующих уроках.
Макросы-объекты без текст_замена
Макросы-объекты также могут быть определены без текст_замена
, например:
1 |
#define USE_YEN |
Любое дальнейшее появление идентификатора USE_YEN
удаляется и заменяется «ничем» (пустым местом)!
Это может показаться довольно бесполезным, однако, это не основное предназначение подобных директив. В отличие от макросов-объектов с текст_замена
, эта форма макросов считается приемлемой для использования.
Условная компиляция
Директивы препроцессора условной компиляции позволяют определить, при каких условиях код будет компилироваться, а при каких — нет. На этом уроке мы рассмотрим только три директивы условной компиляции:
#ifdef
#ifndef
#endif
Директива #ifdef (сокр. от «if defined» = «если определено») позволяет препроцессору проверить, было ли значение ранее определено с помощью директивы #define. Если да, то код между #ifdef и #endif скомпилируется. Если нет, то код будет проигнорирован. Например:
1 2 3 4 5 6 7 8 9 |
#define PRINT_JOE #ifdef PRINT_JOE std::cout << "Joe" << std::endl; #endif #ifdef PRINT_BOB std::cout << "Bob" << std::endl; #endif |
Поскольку PRINT_JOE
уже был определен, то строка std::cout << "Joe" << std::endl;
скомпилируется и выполнится. А поскольку PRINT_BOB
не был определен, то строка std::cout << "Bob" << std::endl;
не скомпилируется и, следовательно, не выполнится.
Директива #ifndef (сокр. от «if not defined» = «если не определено») — это полная противоположность к #ifdef, которая позволяет проверить, не было ли значение ранее определено. Например:
1 2 3 |
#ifndef PRINT_BOB std::cout << "Bob" << std::endl; #endif |
Результатом выполнения этого фрагмента кода будет Bob
, так как PRINT_BOB
ранее никогда не был определен. Условная компиляция очень часто используется в качестве header guards (о них мы поговорим на следующем уроке).
Область видимости директивы #define
Директивы выполняются перед компиляцией программы: сверху вниз, файл за файлом. Рассмотрим следующую программу:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <iostream> void boo() { #define MY_NAME "Alex" } int main() { std::cout << "My name is: " << MY_NAME; return 0; } |
Несмотря на то, что директива #define MY_NAME "Alex"
определена внутри функции boo(), препроцессор этого не заметит, так как он не понимает такие понятия языка C++, как функции. Следовательно, выполнение этой программы будет идентично той, в которой бы #define MY_NAME "Alex"
было определено ДО, либо сразу ПОСЛЕ функции boo(). Для лучше читабельности кода определяйте идентификаторы (с помощью #define) вне функций.
После того, как препроцессор завершит свое выполнение, все идентификаторы (определенные с помощью #define) из этого файла — отбрасываются. Это означает, что директивы действительны только с точки определения и до конца файла, в котором они определены. Директивы, определенные в одном файле кода, не влияют на директивы, определенные внутри других файлов этого же проекта.
Рассмотрим следующий пример:
function.cpp:
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> void doSomething() { #ifdef PRINT std::cout << "Printing!"; #endif #ifndef PRINT std::cout << "Not printing!"; #endif } |
main.cpp:
1 2 3 4 5 6 7 8 9 10 |
void doSomething(); // предварительное объявление функции doSomething() int main() { #define PRINT doSomething(); return 0; } |
Результат выполнения программы:
Not printing!
Несмотря на то, что мы объявили PRINT
в main.cpp (#define PRINT
), это все равно не имеет никакого влияния на что-либо в function.cpp. Поэтому, при выполнении функции doSomething(), у нас выводится Not printing!
, так как в файле function.cpp мы не объявляли идентификатор PRINT
(с помощью директивы #define). Это связано с header guards.
Не понятно как работает пример с function.cpp и main.cpp. Как это компилируется? Содержимое function.cpp добавляется в конец main.cpp?
" #include "filename", которая сообщает препроцессору искать файл в текущей директории проекта."
Включая вложенные папки или нет?
Из двух файлов последнего примера сделан оидн. Он перед Вами, и он после компиляции все равно выдает
"Not printing!"
Почему? Ведь теперь #define PRINT объявлен внутри этого единого файла?
Andrew, уберите строку 12 или сделайте её строкой 2
1. Andrew, уберите строку 12.
2. 16-ю строку, сделайте строкой 2 (cut & paste) ибо ->
-> только глобальное объявление переменной или функции,
даст возможность видеть её всем функциям.
#define PRINT появляется уже после проверки, поэтому так
Ничего не меняется.
Доходим до функции int main()
в ней объявлена директива #define PRINT
и уже ПОСЛЕ объявления этой директивы вызывается функция doSomething();
Почему она выводит "Not printing!"? Ведь директива #define PRINT УЖЕ объявлена.
Извините мою непонятливость.
Директива #define PRINT определена после директив #ifdef/#ifndef
Препроцессор читает код построчно, неперепрыгивая на другие строки. Поэтому сначала читается #ifdef PRINT, потом #ifndef PRINT, потом #define PRINT.
Andrew!
Как сказано в этом уроке "Директивы выполняются перед компиляцией программы: сверху вниз, файл за файлом". У вас директива препроцессора #define PRINT стоит после группы директив препроцессора условной компиляции, поэтому не оказывает влияния ни на что. То, что внутри тела функции int main() объявлена директива #define PRINT и уже ПОСЛЕ объявления этой директивы вызывается функция doSomething() не имеет значения, так как на эту вызываемую функцию вообще не влияет содержимое тела функции main (УРОК 12). Эта функция вызывается и выполняется полностью изолированно от кода, который находится в теле main. Если переставить #define PRINT например на 2 строчку, то результат будет "Printing".
А дефайны из заголовочных можно перетягивать на другие файлы исходных кодов?
Спасибо огромное за ваш труд!
Как я понял шаги компиляции (согласно нашим знаниям, полученным до этого урока) следующие:
1) препроцессор прочесывает весь код в поисках директив: вставляет все объявления, определяет все макрося;
2) далее прочесываются другие команды;
Тогда вопрос: Вот в такой программе
происходит ошибка компиляции. Получается, что хоть препроцессор и прочесал код в поисках директив, мой компилятор не знает о существовании MY_NUMBER.
Мне искать дальше или я что-то пропустил из предыдущих уроков?
Нужно MY_NUMBER объявить до её вызова.
Получается, что да. Думал, что сначала препроцессор проверяет все дерективы и делает себе что-то вроде словоря. Оказывается не совсем
После того, как препроцессор завершит свое выполнение, все идентификаторы (определенные с помощью #define) из этого файла — отбрасываются. Это означает, что директивы действительны только с точки определения и до конца файла, в котором они определены.
В уроке это описывается
Спасибо за эти уроки. Пока что ничего даже близкого по качеству о C++ я не видел. Даже на Pluralsight всё намного хуже.
Пожалуйста))
Менее половины дня потребовалось дойти до этого урока, всё настолько просто и доходчиво объяснено, что вопросов не остаётся.
Вам учебники нужно писать для образовательных учреждений.
Мне вот бы тоже хотелось, чтобы такая подача была и в образовательных учреждениях. Но этому вряд ли быть))
Годнее контента я ещё не видел…. Автору прям большой плюс и спасибо за столь хорошее объяснение. Столько книг уже по С++ в 21 веке и не в одной так доходчиво и на примере, не разъяснено как тут. Не понимаю зачем книги по С++, если есть эта божественная статья.
Эти статьи напроч отбивают вопросы которые бы возникли где нибудь ещё. Если по С++ ты читаешь книгу, то там куча вопросов возникает еще в начале первой странице. А тут все по порядку и красиво сделано
Спасибо, очень приятно 🙂
Читаю, не могу остановится!
Че так годно то?
Ну шо есть, то есть)
В предпоследнем примере вы утверждаете, что препроцессор не знает что такое функция и внутри неё работать не будет, а в конце статьи мы видим обратное. Что же верно?
Верно то, что написано в уроке. Пройдите ещё с 10-ок уроков и возвратитесь к этому уроку и перечитайте его. Возможно, станет понятнее.
Я понял так что препроцессор не знает что такое функция т.е. для него это пустое место, следовательно
работать будет.
Просто для большей читабельности и возможно еще чего-нибудь, надо ставить его вверху.
Дело в том, что две функции находятся в разных файлах — Директива PRINT сначала определяется в функции main(), потом в этой функции идёт обращение к функции doSomething(), которая находиться в другом файле — function.cpp, поэтому, не закончив с main.cpp, выполнение переходит в function.cpp, а поскольку main.cpp не закончился, то и определение PRINT ещё существует, и, как раз, поэтому проверка наличия определения(#ifdef PRINT) является истинной, и, соответственно, выводится слово Printing!
Если бы обе эти функции находились в одном файле, и реализация doSomething() была бы выше main(), то, поскольку во время проверки наличия определения его не существует- он ниже, выводится Not printing!, но если реализация находиться под main(), то выводиться Printing!, потому что в этом случае определение уже существует к тому моменту, когда мы проверяем его наличие.
Насколько я понимаю, суть может быть вот в чем (очень приближенно). Вот у вас программа пишется для 64 бит системы и для 32. Но в сам код это вставлять не хочется. Пускай препроцессор сам выбирает тип компиляции в зависимости от системы. То есть на одном этапе надо так скомпилировать программу, а на другом эдак. Но получилось так, что мы можем использовать это как переменные)))
Компилируется только без " std::cout << "Not printing!"; "
Как только пишу эту строчку компилятор выдает ошибку " error C2065: cout: необъявленный идентификатор ". В чем же проблема?
" — Это символ, который нужно тебе убрать, для чего ты его туда вообще поставил? Он лишний, т.к. сам по себе ты окончил стейтмент точкой с запятой, но зачем то ты ставишь ".
Пиши так:
В общем понятно. Препроцессор берет составленную нами прогу и заменяет в ней все директивы на что-то свое. В результате получаем прогу, которая все еще написана на С++, но директив со значками # там уже нет. Т.е. это предварительное "пережовывание" программы перед тем, как она компилируется.
Подтверждаю, эта глава написана не очень доходчиво для новичка. Может, проблема со стилем? Читатель (я) постоянно задает вопрос: а зачем эти директивы нужны? Нет мотивации схватить материал за рога. Буду перечитывать еще раз. С предыдущими главами такого не было.
Директивы, определенные в одном файле кода, не влияют на код других файлов того же проекта. Не согласен, а если в файле function.cpp у нас будет написано #define PRINT, а в файле main.cpp напишем #include "function.cpp" — PRINT будет влиять на код в обеих файлах. Все зависит от порядка подключения. А определения передаваемые в аргументы компилятора видны, вообще, всем файлам проекта.
Вячеслав, когда вы пишите #include "function.cpp", вы просто Copy-Paste-ите код.
Нашел хорошую иллюстрацию процесса. Но! Решил попробовать.
$ g++ main.cpp -o hello
Ну вот так получилось.
Здравствуйте. Можно ли определить с помощью команды к примеру #define MY_DEFINE в файле main.cpp в файле add.cpp?
В файле main.cpp в файле add.cpp? Я что-то не понял, что вы имеете в виду. MY_DEFINE можно определить только один раз и в одном файле проекта.
а мы будем проходить работу с графикой в программировании?
В этих уроках конкретно с графикой материала не будет. Здесь дается необходимый фундамент знаний, исходя из которого вы уже сами можете углубляться в любую отрасль программирования.
ПОНЯТНО,спасибо
Да уж, тема сложновата… Придется посидеть…
Да, мне сперва тоже так показалось, но после повторного прочтения всё стало на свои места.
В предыдущем уроке оставил несколько вопросов в комментариях, но благодаря этому уроку ответов на них уже не требуется — до всего дошел сам.
Несмотря на то, что PRINT был определен в main.cpp, он все равно не имеет никакого влияния на что-либо в function.cpp.
PRINT был объявлен же, а не определён.
Иди я что-то путаю
Исправил. В этом случае директива #defined используется как в качестве объявления, так и определения. Т.е. это одно и то же (в этом случае).