Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Не підтверджено.doc
Скачиваний:
0
Добавлен:
01.04.2025
Размер:
3.08 Mб
Скачать

Int mainO

{

map<string, string> dictionary;

dictionary.insert(pair<string, string>("fliM", "Місце мешкання.")); dictionary.insert(pair<string, string>("iuiaBiaTypa", "Пристрій введення даних.")); dictionary.insert(pair<string, з(гіпд>("програмування", "Процес розроблення програми.")); dictionary.insert(pair<string, string>("STL", "Standard Template Library."));

string s;

cout«"Введіть слово:"; сіп » s;

map<string, string>::iteratorp;

p = dictionary .find(s); if(p != dictionary.endO)

cout«"Визначення:"« p->second « endl;

else

cout«"Такого слова у словнику немає." « endl; getchO; return 0;

}

Внаслідок виконання ця програма відображає на екрані такі результати:

Введіть слово: дім Визначення: Місце мешкання.

Введіть слово: хата Такого слова у словнику немає.

Введіть слово: програмування Визначення: Процес розроблення програми.

Введіть слово: STL

Визначення: Standard Template Library.

Розділ 13. ОСОБЛИВОСТІ РОБОТИ ПРЕПРОЦЕСОРА C++

Цей розділ навчального посібника присвячено вивченню особливостей робо­ти препроцесора C++. Препроцесор мови програмування C++ - це частина компі­лятора, яка піддає Вашу програму різним текстовим перетворенням до реальної трансляції початкового коду програми в об'єктний. Програміст може давати преп- роцесору команди, що задаються директивами препроцесора (preprocessor directi­ves), які, не будучи формальною частиною мови програмування C++, здатні роз­ширити область дії його середовища програмування.

Директиви препроцесора C++

#define

#error

#include

#if

#else

#el if

#endif

#ifdef

#ifndef

#undef

#line

#pragma


Як бачите, всі директиви препроцесора починаються з символу "#". Тепер розглянемо кожну з них окремо.

Нео! хгдноапам' ятати! Препроцесор мови програмування C++ є прямим на­щадком препроцесора мови С, і деякі його засоби виявилися надлишковими після введення у мові програмування C++ нових елементів. Проте він, як і раніше, є важливою частиною C++-середовища програмування.

  1. Поняття про директиви препроцесора C++

Директива #define. Цю директиву використовують для визначення ідентифі­катора та імені символьної послідовності, яка буде підставлена замість ідентифі­катора скрізь, де він траплятиметься у початковому коді програми. Цей ідентифі­катор називають макроіменем, а процес заміни - макропідстановкою (реалізацією макророзширення). Загальний формат використання цієї директиви має такий виг­ляд:

#define макроім'я поспідовність_символів

Зверніть увагу на те, що у цьому записі немає крапки з комою. Задана поспі- довність_символів завершується тільки символом кінця рядка. Між елементами ім'я_макроса і послідовність_символів може бути будь-яка кількість пропусків.

Директива #define визначає ім'я макросу.

Отже, після внесення цієї директиви кожне входження текстового фрагмента, що є визначеним як макроім'я, замінюють заданим елементом послідовність_символів. Наприклад, якщо виникає бажання використовувати слово UP як значення 1 і сло­во DOWN як значення 0, оголосіть такі директиви #define:

#define UP 1 #define DOWN 0

Дані директиви змусять компілятор підставляти 1 або 0 кожного разу, коли у файлі початкового коду програми трапиться слово UP або DOWN відповідно. Нап­риклад, у процесі виконання такої настанови cout« UP «"" « DOWN «"" « UP + UP; на екран буде виведено таке:

102

Після визначення імені макросу його можна використовувати як частину виз­начення інших макроімен. Наприклад, такий програмний код визначає імена ONE, TWO і THREE і відповідні їм значення:

#define ONE 1 #define TWO ONE+ONE #define THREE ONE+TWO

Важливо розуміти, що макропідстановка - це просто заміна ідентифікатора відповідним рядком. Отже, якщо виникає потреба визначити стандартне повідом­лення, то використовують програмний код, подібний до цього:

#define GETFILE "Введіть ім'я файлу"

  1. .

Препроцесор замінить рядком "Введіть ім'я файлу" кожне входження ідентифі­катора GETFILE. Для компілятора ця cout-настанова cout« GETFILE; насправді виглядає так:

cout«"Введіть ім'я файлу";

Ніякої текстової заміни не відбудеться, якщо ідентифікатор знаходиться в рядку, поміщеному в лапки. Наприклад, у процесі виконання такого коду програ­ми

#define GETFILE "Введіть ім'я файлу"

  1. .

cout«"GETFILE - це макроім'я"« endl;

на екрані буде відображена ця інформація GETFILE - це макроім'я а не така:

Введіть ім'я файлу - це макроім'я

Якщо текстова послідовність не поміщається в рядку, то її можна продовжи­ти в наступному, поставивши зворотну косу риску у кінці рядка, як це показано у такому прикладі:

#define LONG_STRING "Це дуже довга послідовність, \ яка використовується як приклад."

Серед С++-програмістів прийнято використовувати для макроімен прописні букви. Ця домовленість дає змогу з першого погляду зрозуміти, що тут викорис­товується макропідстановка. Окрім цього, найкраще помістити всі директиви #de- fine у початок файлу або включити в окремий файл, щоб не шукати їх потім у всій програмі.

Макропідстановки часто використовують для визначення "магічних чисел" програми. Наприклад, у Вас є програма, яка визначає певний масив, і ряд функцій, які отримують доступ до нього. Замість "жорсткого" кодування розміру масиву за допомогою константи краще визначити ім'я, яке б представляло розмір, а потім використовувати це ім'я скрізь, де повинен знаходитися розмір масиву. Тоді, якщо цей розмір доведеться змінити, Вам достатньо буде внести тільки одну зміну, а потім перекомпілювати програму. Розглянемо такий приклад:

#define Max_size 100

II...

float balance[Max_size]; double index[Max_size]; int num_emp[Max_sizej;

Нео! хідноапам'ятати'.аУ мові програмування C++ передбачено ще один спосіб визначення констант, який полягає у використанні специфікатора const. Проте багато програмістів "прийшли" у мову програмування C++ з С- середовища, де для цих потреб зазвичай використовувалася директива #define. Тому Вам ще часто доведеться з нею мати справу у С++-коді програми.

Макровизначення, що діють як функції. Директива #define має ще одне призначення: макроім'я може використовуватися з аргументами. При кожному входженні макроімені пов'язані з ним аргументи замінюються реальними аргу­ментами, вказаними у коді програми. Такі макровизначення діють подібно до фу­нкцій. Розглянемо такий приклад.

Код програми 13.1. Демонстрація механізму використання "функціональних" макровизначень #include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

#define MIN(a, b) (((a)<(b))? a: b) int mainO {

int x = 10; у = 20;

cout«"Мінімум дорівнює:"« MIN(x, у);

getchO; return 0;

}

У процесі компілювання цієї програми вираз, визначений ідентифікатором MIN(a, b), буде замінено, але х і у розглядатимуться як операнди. Це означає, що з out-настанова після компілювання виглядатиме так: cout«"Мінімум дорівнює: "«(((х)<(у))?х:у);

По суті, таке макровизначення є способом визначити функцію, яка замість викли­ку дає змогу розкрити свій код у рядку.

Макровизначення, що діють як функції, - це макровизначення, які прийма­ють аргументи. Круглі дужки, що здаються надлишковими, в які поміщено макро­визначення MIN, необхідні для того, щоби гарантувати правильне сприйняття компілятором замінюваного виразу. Насправді додаткові круглі дужки повинні застосовуватися практично до всіх макровизначень, що діють подібно до цієї фун­кцій. Потрібно завжди дуже уважно ставитися до визначення таких макросів; ін­акше можливе отримання несподіваних результатів. Розглянемо, наприклад, цю навчальну програму, яка використовує макрос для визначення парності значення.

Код програми 13.2. Демонстрація неправильної роботи коду програми #include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

#define EVEN (а) а%2==0? 1 :0 int mainO {

if(EVEN(9 +1)) cout «"парне число";

else cout«"непарне число";

getchO; return 0;

}

Ця програма не працюватиме коректно, оскільки не забезпечена правильна підстановка значень. У процесі компілювання вираз EVEN(9 + 1) буде замінений та­ким чином:

9+1 %2==0 ? 1 :0

Нагадаю, що оператор ділення за модулем "%" має вищий пріоритет, ніж опе­ратор додавання "+". Це означає, що спочатку виконається операція ділення за мо­дулем (%) для числа 1, а потім її результат буде складний з числом 9, що (зви­чайно ж) не дорівнює 0. Щоб виправити помилку, достатньо помістити у круглі дужки аргумент, а в макровизначенні EVEN, як це показано в наступній (виправле­ній) версії тієї ж самої програми.

Код програми 13.3. Демонстрація коректної роботи коду програми #include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

#define EVEN (а) (а)%2==0? 1 :0 int mainO {

if(EVEN(9 +1)) cout «"парне число";

else cout«"непарне число";

getchO; return 0;

}

Тепер сума 9+1 обчислюється до виконання операції ділення за модулем. У загальному випадку краще завжди брати параметри макровизначення у круглі ду­жки, щоб уникнути непередбачених результатів, подібних описаному вище.

Використання макровизначень замість справжніх функцій має одну істотну перевагу: оскільки програмний код макровизначення розширюється в рядку, і не­має ніяких витрат системних ресурсів на виклик функції, то швидкість роботи Ва­шої програми буде вищою порівняно із застосуванням звичайної функції. Але під­вищення швидкості є платою за збільшення розміру програми (через дублювання коду функції).

Нео! хідноа пам'ятати!а Незважаючи на те, що макровизначення все ще трапляється у С++-коді програми, макроси, що діють подібно до функцій, можна замінити специфікатором ІПІІПЄ, який справляється з тією ж функцією краще і безпечніше. (Пригадайте: специфікатор ІПІІПЄ забезпечує замість вик­лику функції розширення її тіла в рядку). Окрім цього, ІПІІпе-функцй не вима­гають додаткових круглих дужок, без яких не можуть обійтися макровизна­чення. Проте макроси, що діють подібно до функцій, все ще залишаються час­тиною С++-програм, оскільки багато хто з C/C++-програмістів продовжує використовувати їх за звичкою.

Директива #еггог. Ця директива дає вказівку компіляторові зупинити компі­лювання. Вона використовується здебільшого для відлагодження. Загальний фор­мат її запису є таким:

#еггог повідомлення

Зверніть увагу на те, що елемент повідомлення не поміщений у подвійні лапки. При потраплянні на директиву #еггог відображається задане повідомлення та інша ін­формація (вона залежить від конкретної реалізації робочого середовища), після чого компілювання припиняється. Щоб дізнатися, яку інформацію відображає у цьому випадку компілятор, достатньо провести експеримент.

Директива #ЄГГОГ відображає повідомлення про помилку.

Директива препроцесора #include. Ця директива зобов'язує компілятор вклю­чити або стандартний заголовок, або інший початковий файл, ім'я якого вказане у директиві #include. Ім'я стандартних заголовків береться у кутові дужки, як це по­казано у прикладах, наведених у цьому навчальному посібнику. Наприклад, ця директива

#include <vector> // Для роботи з контейнерним класом "Вектор"

містить стандартний заголовок для векторів.

Директива #include містить заголовок або інший початковий файл.

Під час включення іншого початкового файлу його ім'я може бути вказане у подвійних лапках або кутових дужках. Наприклад, наступні дві директиви зо­бов'язують компілятор C++ прочитати і скомпілювати файл з іменем sample.h:

#include <sample.h>

#include "sample.h"

Якщо ім'я файлу поміщене у кутові дужки, то пошук файлу здійснюватиметь­ся в одному або декількох спеціальних каталогах, визначених конкретною реалі­зацією.

Якщо ж ім'я файлу поміщене в лапки, пошук файлу здійснюється, як прави­ло, у поточному каталозі (що також визначено конкретною реалізацією). У ба­гатьох випадках це означає пошук поточного робочого каталога. Якщо заданий файл не знайдено, то пошук повторюється з використанням першого способу (не­начебто ім'я файлу було поміщено у кутові дужки). Щоб ознайомитися з подроби­цями, пов'язаними з різною обробкою директиви #include у разі використання ку­тових дужок і подвійних лапок, зверніться до настанови користувача, яка до­дається до Вашого компілятора. Настанови #include можуть бути вкладеними в ін­ші поміщені файли.

  1. Директиви умовного компілювання

Існують директиви, які дають змогу вибірково компілювати частини початко­вого коду програми. Цей процес, названий умовною компіляцією, широко вико­ристовують комерційні фірми з розроблення програмного забезпечення, які ство­рюють і підтримують багато різних версій однієї програми.

Директиви #if, #else, #elif і #endif. Головна ідея полягає у тому, що коли вираз, який знаходиться після директиви #if виявляється істинним, то буде скомпільова­ний програмний код, розташований між нею і директивою #endif; інакше цей прог­рамний код буде опущений. Директиву #endif використовують для позначення кін­ця блоку #if.

Директиви #if, #ifdef, #ifndef, #else, #elif і #endif — це директиви умовного компілювання.

Загальна форма запису директиви #if має такий вигляд:

#if константний_вираз

послідовність настанов #endif

Якщо константний_вираз є істинним, то програмний код, розташований безпо­середньо за цією директивою, буде скомпільований. Розглянемо такий приклад.

Код програми 13.4. Демонстрація механізму використання директиви #if #include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

#define МАХ 100

int mainO

{

#if МАХ>10

cout«"Потрібна додаткова память" « endl;

#endif

II...

getchO; return 0;

}

Внаслідок виконання ця програма відображає на екрані повідомлення Потрібна додаткова пам'ять оскільки, як визначено у програмі, значення константи МАХ більше 10. Цей прик­лад ілюструє важливий момент: вираз, який знаходиться після директиви #if, об­числюється при компілюванні. Отже, воно повинно містити тільки ідентифікато­ри, які були заздалегідь визначені, або константи. Використання ж змінних тут виключене.

Поведінка директиви #else багато в чому подібна до поведінки настанови else, яка є частиною мови програмування C++: вона визначає альтернативу на випадок невиконання директиви #if. Щоб показати, як працює директива #else, скорис­таємося попереднім прикладом, дещо його розширивши.

Код програми 13.5. Демонстрація механізму використання директив #if / #else #include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

#define МАХ 6

int mainO

{

#if МАХ>10

cout«"Потрібна додаткова пам'ять" « endl;);

#else

cout«"Достатньо наявної пам'яті"« endl;

#endif

II...

getchO; return 0;

}

У цьому коді програми для імені МАХ визначено значення, яке менше 10, то­му #іТ-гілка коду програми не відкомпілюється, проте відкомпілюєгься альтерна­тивна #еІзе-гілка. Як наслідок, відобразиться повідомлення:

Достатньо наявної пам'яті.

Зверніть увагу на те, що директиву #else використовують для індикації одно­часно як кінця #іТ-блоку, так і початку #еІзе-блоку. У цьому є логічна необхідність, оскільки тільки одна директива #endif може бути пов'язана з директивою #if.

Директива #elif еквівалентна зв'язці настанов else-if і використовують для фор­мування багатоланкової схеми if-else-if, яка представляє декілька варіантів компі­лювання. Після директиви #elif повинен знаходитися константний вираз. Якщо цей вираз істинний, то наступний блок коду програми відкомпілюєгься, і ніякі ін­ші #elif-BHpa3H не тестуватимуться або не компілюватимуться. Інакше буде переві­рений наступний по черзі #elif-BHpa3. Ось як виглядає загальний формат викорис­тання директиви #elif:

#if вираз

послідовність настанов #elif вираз 1

послідовність настанов #elif вираз 2

послідовність настанов #elif вираз З

послідовність настанов

її...

#elif вираз N

послідовність настанов #endif

Наприклад, наведений нижче фрагмент коду програми використовує іденти­фікатор COMPILED_BY, який дає змогу визначити, хто компілює програму:

#define JOHN 0 #define BOB 1 #define TOM 2

#define COMPILED_BY JOHN

#if COMPILED_BY == JOHN char Show[] = "John";

#elif COMPILED_BY == BOB char ShowQ = "Bob";

#else

char ShowQ = "Tom";

#endif

Директиви #if і #elif можуть бути вкладеними. У цьому випадку директива #endif, #else або #elif зв'язується з найближчою директивою #if або #elif. Наприклад, такий фрагмент коду програми є абсолютно допустимим:

#if COMPILED_BY == BOB #if DEBUG == FULL int port = 198;

#elif DEBUG == PARTIAL int port = 200;

#endif

#else

cout«"Борис повинен скомпілювати код"

«"для відлагодження виведення даних" « endl;

#endif

Директиви #ifdef і #ifndef. Ці директиви пропонують ще два варіанти умовно­го компілювання, які можна виразити як "якщо визначено" і "якщо не визначено" відповідно. Загальний формат використання директиви #ifdef такий:

#ifdef макроім'я

послідовність настанов #endif

Якщо макроім'я заздалегідь визначено за допомогою якої-небудь директиви #define, то послідовність настанов, розташована між директивами #ifdef і #endif, буде ском­пільована.

Загальний формат використання директиви #ifndef такий:

#ifndef макроім'я

послідовність настанов #endif

Якщо макроім'я не визначене за допомогою якої-небудь директиви #define, то послі­довність настанов, розташована між директивами #ifdef і #endif, буде скомпільована.

Як директива #ifdef, так і директива #ifndef може мати директиву #else або #elif. Розглянемо такий приклад.

#include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

#define ТОМ

int mainO

{

#ifdef ТОМ

cout«"Програміст Том" « endl;

#else

cout«"Програміст невідомий" « endl;

#endif

#ifndef RALPH

cout«"Ім'я RALPH не визначене" « endl;

#endif getchO; return 0;

}

Внаслідок виконання ця програма відображає на екрані такі результати: Програміст Том.

Ім'я RALPH не визначене.

Але якби ідентифікатор ТОМ не було визначено, то результат виконання цієї програми виглядав би так:

Програміст невідомий.

Ім'я RALPH не визначене.

Bapmoa нати! Директиви ttifdef і #ifndef можна вкладати так само, як і ди­рективи т

Директива #undef. Цю директиву використовують для видалення поперед­нього визначення певного макроімені. Її загальний формат є таким:

#undef макроім'я Розглянемо такий приклад:

#define TIMEOUT 100 #define WAIT 0

II...

#undef TIMEOUT #undefWAIT

У цих записах імена TIMEOUT і WAIT визначені доти, доки не виконається директи­ва #undef.

Основне призначення директиви #undef - дати змогу локалізувати макроімена для тих частин коду програми, в яких вони потрібні.

Використання оператора defined. Окрім директиви #ifdef, існує ще один спо­сіб з'ясувати, чи визначене у програмі певне макроім'я. Для цього можна викорис­товувати директиву #if у поєднанні з оператором часу компілювання defined. Нап­риклад, щоб дізнатися, чи визначено макроім'я MYFILE, можна використовувати одну з таких команд препроцесорного оброблення:

#if defined MYFILE

або

#ifdef MYFILE

У разі потреби, щоб реверсувати умову перевірки, операторові defined може передувати символ Наприклад, такий фрагмент коду програми відкомпі- люється тільки у тому випадку, якщо макроім'я DEBUG не визначене:

#if Idefined DEBUG

cout«"Остаточна версія!" « endl;

#endif

Про значення препроцесора. Як ми уже зазначали раніше, препроцесор мо­ви програмування C++ - прямий спадкоємець препроцесора мови С, до того ж без жодних удосконалень. Проте його вагомість у мові програмування C++ набагато менша, ніж препроцесора у мові С. Йдеться про те, що багато завдань, які виконує препроцесор у мові С, реалізовані мовою C++ у вигляді елементів мови. Страус- труп тим самим виразив свій намір зробити функції препроцесора непотрібними, щоб врешті-решт від нього можна було зовсім звільнити мову.

На цьому етапі препроцесор вже частково надлишковий. Наприклад, дві най­більш використовувані властивості директиви #define було замінено настановами мови C++. Зокрема, її здатність створювати константне значення і визначати мак- ровизначення, що діє подібно до функцій, тепер абсолютно надлишкова. У мові програмування C++ є ефективніші засоби для виконання цих завдань. Для ство­рення константи достатньо визначити const-змінну. А із створенням вбудованої (що підставляється) функції легко справляється специфікатор inline. Обидва ці за­соби краще працюють, ніж відповідні механізми директиви #define.

Наведемо ще один приклад заміни елементів препроцесора елементами мови. Він пов'язаний з використанням однорядкового коментарю. Одна з причин його створення - дозволити "перетворення" коду програми у коментар. Як уже зазнача­лося вище, коментар, що використовує /*...*/-стиль, не може бути вкладеним. Це означає, що фрагменти коду програми, що містять /*...*/-коментарі, одним махом "перетворити на коментар" не можна. Але це можна зробити з //-коментарями, оточивши їх /*...*/-символами коментарю. Можливість "перетворення" коду прог­рами у коментар робить використання таких директив умовного компілювання, як #ifdef, частково надлишковим.

Директива #1іпе. Цю директиву використовують для зміни вмісту псевдоз-

мінних . LINE і FILE_ _, які є зарезервованими ідентифікаторами (макроімена-

ми). Псевдозмінна LINE містить номер скомпільованого рядка, а псевдозмінна

FILE _ - ім'я компільованого файлу. Базова форма запису цієї команди має такий

вигляд:

#Ііпе номер "ім'я_файлу"

У цьому записі номер - це будь-яке позитивне ціле число, а ім'я_файлу - будь- який допустимий ідентифікатор файлу. Значення елемента номер стає номером по­точного початкового рядка, а значення елемента ім'я_файлу - іменем початкового файлу. Ім'я файлу - елемент необов'язковий. Директива #1іпе використовується, в основному, з метою відлагодження і у спеціальних додатках.

Директива #ІІпе змінює вміст псевдозмінних LINE__ і FILE .

Наприклад, наведений нижче код програми зобов'язує починати рахунок ряд­ків з позиції 200. Настанова cout відображає номер 202, оскільки це третій рядок у програмі після директивної настанови #1іпе 200.

#include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

#Ііпе 200 // Встановлюємо лічильник рядків, що дорівнює 200.

int mainO // Цей рядок зараз має номер 200.

{ //Номер цього рядка дорівнює 201. cout« LINE ; //Тут виводиться номер 202.

getchO; return 0;

}

Директива #pragma. Робота цієї директиви залежить від конкретної реалізації компілятора. Вона дає змогу видавати компіляторові різні настанови, передбачені творцем компілятора. Загальний формат його використання такий:

#pragma ім'я

У цьому записі елемент ім'я представляє ім'я бажаної #ргадта-настанови. Якщо вказане ім'я не розпізнається компілятором, директива #pragma просто ігнорується без повідомлення про помилку.

Директива #pragma залежить від конкретної реалізації компілятора.

Нео! хідноапам ятати'.аДля отримання детальної інформації про можливі варіанти використання директиви #pragma варто звернутися до системної до­кументації з використовуваного Вами компілятора. Ви можете знайти для себе дуже корисну інформацію. Звичайно #ргадта-настанови дають змогу виз­начити, які застережні повідомлення видає компілятор, як генерується прог­рамний код і які бібліотеки компонуються з Вашими програмами.

  1. Оператори препроцесора і "##"

У мові програмування C++ передбачено підтримку двох операторів препро­цесора: "#" і "##". Ці оператори використовують спільно з директивою #define. Оператор "#" перетворить наступний за ним аргумент у рядок, поміщений у лап­ки. Розглянемо, наприклад, такий код програми:

#include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

#define mkstr(s) # s