Препроцесор краще розглядати як окрему програму, яка виконується перед компіляцією. При запуску програми препроцесор переглядає код зверху вниз, файл за файлом, в пошуку директив. Директиви — це спеціальні команди, які починаються з символу #
і НЕ закінчуються крапкою з комою. Є кілька типів директив, які ми розглянемо нижче.
Директива #include
Ви вже бачили директиву #include в дії. Коли ви підключаєте файл за допомогою директиви #include, препроцесор копіює вміст файлу, який підключається, в поточний файл відразу після рядка з #include. Це дуже корисно при використанні певних даних (наприклад, попередніх оголошень функцій) відразу в декількох місцях.
Директива #include має дві форми:
#include <filename>
, яка повідомляє препроцесору шукати файл в системних директоріях. Частіше всього ви будете використовувати цю форму при підключенні заголовкових файлів зі Стандартної бібліотеки С++.
#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.