
- •8.2.1. Реалізація класів
- •8.2.2. Породження об’єктів
- •8.2.3. Виклик операцій
- •8.2.4. Використання успадкування
- •8.2.5. Реалізація залежностей
- •8.2.6. Шаблони, шаблони-класи
- •Параметри шаблонів, що опускаються
- •Приклади, які використовують вектори символів
- •8.2.8. Основні компоненти stl
- •8.2.8.1. Контейнери
- •Контейнери послідовностей
- •Впорядковані асоціативні контейнери
- •8.2.8.2. Параметричні (родові) алгоритми
- •8.2.8.3. Ітератори
- •8.2.8.4. Об’єкти-функції
- •8.2.8.5. Адаптери
- •8.2.8.6. Алокатори
- •8.2.9. Stl та об’єктно-орієнтоване програмування
8.2.5. Реалізація залежностей
Залежності в мові С++ реалізуються за допомогою покажчиків чи за допомогою спеціальних об’єктів. Наприклад, залежність «багато-до-одного» між класами Item та Group реалізована через покажчики:
class Item{
public:
virtual void cut ();
virtual void move (Length deltax, Length deltay)=0;
virtual Boolean pick (Length px; Length py)=0;
virtual void ungroup ()=0;
private:
Group* group;
friend group::add_item (Item*);
friend group::remove_item (Item*);
public:
Group* get_group () {return group;};
};
class group: public Item
{
public:
void cut ();
void move (Length deltax, Length deltay);
Boolean pick (Length px; Length py)=0;
void ungroup (){ };
private:
ItemSet* іtems;
public:
void add_item (Item*);
void remove_item (Item*);
ItemSet* get_іtems() {return items;}
};
Щоразу, коли до залежності додається (чи з неї вилучається) зв’язок, два показники повинні бути змінені:
void Group::add_item (Item*item);
{
item->group = this;
items->add (item);
}
void Group::remove_item (Item*item);
{
item->group =0;
items->remove (item);
}
Методи Group:: add … item та Group:: remove … item можуть змінювати приватні (private) атрибути класу Item, хоч вони визначені у його підкласі Group, оскільки вони визначені як дружні (friends)для класу Item.
У розглянутому прикладі опущені перевірки:
чи не є графічний об’єкт, який влючається в групу, членом цієї групи (у цьому разі його не слід ще раз включати в групу);
чи не є зазначений графічний об’єкт членом якоїсь іншої групи (у такому разі його не можна включати в групу й необхідно видати на екран відповідне повідомлення).
Іноді зв’язані між собою (залежні) об’єкти включають у так звані колективні класи. Як приклад такого класу розглянемо клас ItemSet (набір об’єктів):
Class ItemSet
{
public:
ItemSet(); //скласти порожній набір об’єктів
~ItemSet(); //знищити набір об’єктів
void add(Item*); //включити об’єкт у набір
void remove(Item*); //виключити об’єкт з набору
Boolean includes(Item*); //перевірити наявність об’єкта у наборі
Int size(Item*); //визначити кількість об’єктів у наборі
}
Колективні класи часто використовуються в бібліотеках класів. При роботі з колективними класами зручно використовувати ітератори, тобто об’єкти, за допомогою яких можна «розглянути» колектив.
Залежності між класами можна реалізувати і за допомогою спеціальних класів (кожний об’єкт такого класу описує зв’язок між об’єктами відповідних класів). У цьому разі атрибути класу відповідають атрибутам описуваної ним залежності.
8.2.6. Шаблони, шаблони-класи
У мові С++ можливе і параметричне програмування (програмування з використанням родових компонентів). Родові (параметризовані) компоненти мають властивість адаптуватися до конкретної ситуації, в якій такий компонент використовується. Це дозволяє розробляти достатньо універсальні, водночас високоефективні компоненти програм (зокрема, об’єкти).
Параметричне програмування у мові С++ реалізоване за допомогою шаблонів (template). У С++ визначено два види шаблонів: шаблони-класи та шаблони-функції.
Шаблони-класи можуть використовуватись різними способами, але найбільш очевидним є їх використання в якості адаптивних об’єктів пам’яті. Щоб навести найпростіший приклад, припустимо, що потрібно визначити об’єкти, які можуть зберігати по два значення, одне ціле, а інше символьне. Для цього ми могли б визначити
class pair … int … char {
public:
int first;
char second;
pair … int … char(int x, char y):first(x), second(y) {}
};
Після цього ми могли б написати, наприклад,
pair … int … char pair1(13,`a’);
cout<< pair1.first<< end1;
cout<< pair1.second<< end1;
Якщо, крім цього, нам потрібні об’єкти, які можуть зберігати, скажімо, бульове значення та число подвійної точності, ми могли б визначити
class pair … bool … double {
public:
bool first;
double second;
pair … bool … double(bool x, double y): first(x), second(y) {}
};
і написати, наприклад:
pair … bool … double pair1 (true, 0.1);
cout<< pair1.first<< end1;
cout<< pair1.second<< end1;
Те саме можна було б здійснити для нескінченного числа пар типів, але шаблон-клас дозволяє охопити всі ці випадки єдиним визначенням:
template <class T1, class T2>
class pair {
public:
T1 first;
T2 second;
pair (T1 x, T2 y):first (x), second (y) {}
};
Останнє визначення класу написане з використанням двох імен довільних типів Т1 та Т2, які є типами-параметрами. Ці імена вводяться визначенням типів-параметрів за допомогою конструкції template <class T1, class T2>, яка означає, що можна подавати фактичні імена типів замість Т1 та Т2, отримує контрастний екземпляр шаблона-класу pair. Наприклад, конструкції
pair <int, char> pair3 (13, ‘a’);
pair <bool, double> part4 (true, 0.1);
оголошують об’єкти pair3 та pair4, які структурно тотожні об’єктам pair1 та pair2 відповідно.
Але тепер можна використати pair у незчисленній множині інших випадків з іншими комбінаціями фактичних типів, як наприклад
pair <double, long > pair5 (3.1415, 999);
pair <bool, bool > pair6 (false, true);
Як фактичні типи можуть використовуватися типи, визначені через інші класи; наприклад
pair <pair … int … char, float > pair7 (pair1, 1.23);
чи, що еквівалентно,
pair <pair<int, char>, float > pair8(pair3, 1.23);
Як показано тут, можна використовувати типи, які одержані як примірники (представники) шаблонів-класів, всюди, де можна використовувати звичайні типи.
Визначення шаблона-класу pair є дуже простим прикладом параметричного контейнерного класу; проте його можна адаптувати до багатьох різних застосувань. Звичайне перетворення звичайного класу на параметризований (оскільки воно знаходить застосування в найширшому класі прикладних програм) більш корисне, ніж просто додавання ключового слова template та списку параметрів типів. Можуть бути корисними деякі модифікації, щоб бути впевненим у тім, що результуючі примірники об’єктів так само ефективні, як і об’єкти, визначені звичайним способом, незалежно від того, які типи фактично будуть підставлені. У разі, коли визначення шаблона-класу таке просте, як pair, то можна зробити поліпшення. Визначення шаблона-класу було дуже простим:
pair (T1 x, T2 y);
Проблема в тому, що х та у передаються за значенням. Це означає, що якщо ці об’єкти великі, то при виклику конструктора будуть робитися їх додаткові копії, а це дорого коштує. Щоб уникнути цього, потрібно передавати х та у як константні посилальні параметри, щоб передавалися лише адреси. Саме таким чином визначається pair в STL:
template <class T1, class T2>
class pair {
public:
T1 first;
T2 second;
pair (T1& x, T2& y):first (x), second (y) {}
};
При визначенні складніших класів, як правило, буде потрібно обчислити набагато більше параметрів, щоб забезпечити їх адаптованість та ефективність.
Шаблони-функції можуть використовуватися для визначення параметризованих алгоритмів. Простим прикладом є функція для визначення максимального із двох цілих:
int intmax (int x, int y)
{
return x<y?y:x;
}
Вона може визначити максимальне тільки із двох цілих (int), але її легко узагальнити, перетворивши на шаблон-функцію:
template <class T>
T max(T x, T y)
{
return x < y ? y : x;
}
Основна відмінність шаблона-функції від шаблона-класу в тому, що не потрібно повідомляти компілятор, до яких типів параметрів застосовується функція, він сам може визначити це за типами її формальних параметрів.
int u = 3, v = 4;
double d = 4.7;
cout << max(u, v) << endl; //мається на увазі тип int
cout << max(d, 9.3) << endl; //мається на увазі тип double
cout << max(u, d) << endl; //помилка: невідповідність типів
Компілятор потребує, щоб через х та у передавалися значення однакових типів, оскільки в шаблоні для них було вказано один і той самий тип Т. Потрібно також, щоб було визначення операції < у вигляді списку її параметрів (Т, Т). Наприклад, повертаючись до використання шаблона-класу pair, визначеного в попередньому пунктi,
pair <double, long> pair 5(3.1415, 999);
компілятор видає помилку при компіляції оператора:
max (pair 5, pair 5);
через відсутність визначення операції operator < для об’єктів типу pair<double, long>. Але він буде компілювати, якщо попередньо визначити зміст операції operator < для об’єктів цього типу, наприклад:
pair <double, long> operator< (const pair <double, long)& x, const pair <double, long>& y)
//порівняти х та у за їх першими членами:
{
return x.first <y.first;
}
У цьому випадку компілятор може вияснити типи, які має на думці, та порівняти застосування операції чи виклик функції з правильними перевантаженими визначеннями, що робить шаблони-функції найбільш зручним засобом параметричного програмування.
У висновку визначимо, що наведене визначення шаблона-функції max може бути поліпшене так само, як це було зроблено для конструктора шаблона-класу pair, тобто введенням константних посилальних параметрів:
template <class T>
T max(const T& x, const T& y)
{
return x < y ? y : x;
}
Саме таке визначення використовується в STL.
Шаблони-функції як функції-члени
У визначенні звичайних класів чи шаблонів-класів функції-члени можуть мати шаблони в якості параметрів (у доповнення до шаблонів, які є параметрами класів). Ця нова властивість мови С++ використовується в шаблонах-класах при визначенні уміщуючих класів (контейнерів) STL. Наприклад, кожний уміщуючий клас має функцію-член insert, у якій є шаблон-параметр для визначення типу використовуваного ітератора:
template <class T>
сlass vector {
...
template <class Iterator >
insert(Iterator first, Iterator last);
...
};
Як буде показано далі, ця функція-член insert може використовуватися для того, щоб вставити елементи, які скопіювали з деякої іншої структури (наприклад, списку), використовуючи зв’язані з цією структурою ітератори. Визначення такої функції-члена як шаблона-функції, котрий має тип ітератора як параметра шаблона, робить її більш корисною, ніж коли б тип ітератора був у неї фіксований.
На жаль, надалі велика частина компіляторів С++ не підтримує функції-члени, які є шаблонами, що обмежує можливості STL.