Лабораторна робота 2

Використання поліморфізму та шаблонів у C++

1 Завдання на лабораторну роботу

1.1 Ієрархія класів

Реалізувати класи "Людина", "Громадянин", "Студент", "Співробітник". В кожному класі визначити віртуальну функцію виведення даних про об'єкт на екран. Створити масив указівників на різні об'єкти ієрархії. В циклі для кожного об'єкта вивести на екран рядок даних про нього.

1.2 Використання поліморфізму

Створити клас для розв'язання завдання 1.2 шостої лабораторної роботи попереднього семестру. Клас повинен містити принаймні дві функції-члена – функцію, яка повертає значення відповідно до індивідуального завдання, а також суто віртуальну функцію, яка викликається з попередньої та визначає ліву частину рівняння або функцію для дослідження (відповідно до завдання).

Клас розташувати в окремому заголовному файлі. У відповідному файлі реалізації здійснити визначення однієї з двох функцій класу.

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

Примітка: Для обчислення першої (другої) похідної слід додати окремі функції-члени базового класу.

1.3 Узагальнений клас для представлення двовимірного масиву

Переробити клас, створений у завданні 1.2 попередньої лабораторної роботи, на шаблон класу. Реалізувати зовнішню узагальнену функцію знаходження мінімального елемента масиву. В функції main() створити масиви цілих, дійсних і простих дробів (раніше створений клас). Для цих трьох масивів здійснити перевірку роботи функції знаходження мінімального значення серед елементів масиву, здійснити тестування всіх можливостей класу з перехопленням можливих винятків, а також розв'язати індивідуальну задачу.

Примітка: для того, щоб можна було знаходити мінімальне значення у масиві дробів, у класі "Простий дріб" необхідно перевантажити операції порівняння.

2 Методичні вказівки

2.1 Успадкування

2.1.1 Синтаксис успадкування

Успадкування (inheritance) застосовують для створення класу, похідного від певного базового класу. Похідні класи успадковують елементи базових класів, а також можуть додавати нові елементи.

Синтаксично успадкування визначається списком базових класів після імені класу і двокрапки. Перед іменем базового класу вказується тип успадкування (public або інше):

class X            // Базовий клас
{
    . . .
};

class Y : public X // Похідний клас
{
    . . .
};

Мова С++ підтримує одиночне та множинне успадкування. Найбільш розповсюдженим є одиночне успадкування, коли клас походить від одного базового класу. У випадку використання множинного успадкування клас може походити від кількох базових класів. Наприклад,

class X            // Базовий клас
{
    . . .
};

class Z            // Базовий клас
{
    . . .
};

class Y : public X, public Z // Похідний клас
{
    . . .
};

Функції-елементи похідного класу мають доступ до відкритих (public) та захищених (protected) елементів базових класів, при чому захищені елементи призначені саме для використання у похідних класах. Закриті елементи базового класу недоступні з функцій похідних класів.

У С++ прийняті такі типи успадкування:

  • відкритий базовий клас (public): відкриті елементи базового класу залишаються відкритими у похідному класі, захищені елементи базового класу залишаються захищеними у похідному класі, закриті елементи базового класу є закритими у похідному класі;
  • захищений базовий клас (protected): відкриті та захищені елементи базового класу є захищеними у похідному класі, закриті елементи базового класу є закритими у похідному класі;
  • закритий базовий клас (private): відкриті та захищені елементи базового класу є закритими у похідному класі, закриті елементи базового класу є закритими у похідному класі; цей спосіб є прийнятим за умовчанням.

Похідні класи успадковують усі елементи базового класу, окрім конструкторів, деструкторів та перевантаженої операції присвоєння.

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

class X
{
public:
    X(int j) { ... }
    . . .
};

class Y : public X
{
public:
    Y(int k) : X(k) { ... }
    . . .
};

2.1.2 Особливості множинного успадкування

Множинне успадкування (multiple inheritance) застосовується в тому випадку, коли об'єкт являє собою поняття, що поєднує в собі кілька загальних понять, кожне з яких може бути представлено базовим класом.

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

Доступ до члена базового класу вважається неоднозначним, якщо вираз, що використовується для доступу, задає більше однієї функції, об'єкта, типу або елемента переліку. Перевірка на однозначність відбувається до перевірки можливості доступу.

До специфікації базового класу можна додати службове слово virtual. Окремий об'єкт віртуального базового класу V розділяється між всіма класами, які вказали V, визначаючи свої базові класи, наприклад:

class A : virtual public V 
{ 
    . . . 
};

class B : virtual public V 
{
    . . .
};

class C : public A, public B { . . . };

Тут об'єкт класу C матиме тільки один вкладений об'єкт класу V (ромбовидне успадкування). Конструктори віртуальних базових класів викликаються до будь-яких конструкторів невіртуальних базових класів.

У визначенні класів можна використовувати using-оголошення. Оголошення using дозволяють створювати набір перевантажених функцій з базових і похідних класів; using-оголошення у визначенні класу має відношення до членів базового класу. using-оголошення не може бути використано для отримання доступу до закритої інформації базового класу.

Оголошення функції-елемента в похідному класі не перевантажує, а приховує однойменні функції-елементи в базовому, навіть якщо їх списки параметрів різні. Для того, щоб зробити однойменні функції перевантаженими, також може використовуватися using-оголошення:

class Base
{
public:
    void setValue(int);
};

class Descendant : private Base
{
public:
    using Base::setValue;
    void setValue(string);
};

int main()
{
    Descendant d;
    d.setValue("a");
    d.setValue(2); // Без using-оголошення була б помилка 
    return 0;
}

Множинне успадкування класів незалежно від способів його реалізації є небезпечним з точки зору можливого конфлікту імен, його використання може призвести до виникнення суперечливих і дубльованих даних. Використання множинного успадкування слід уникати в більшості випадків. Єдиний сенс його застосування – коли один з базових класів є абстрактним і зовсім не містить даних. Абстрактні класи будуть розглянуті нижче.

2.1.3 Ієрархії класів-винятків

На відміну від багатьох інших мов програмування, С++ не вимагає, щоб усі класи винятків походили з одного базового класу. Проте іноді доцільно створювати власні ієрархії винятків:

class Error
{
    // ...
};

class MathError : public Error
{
    // ...
};

Оброблювач базового типу обробляє також винятки похідних типів:

...
catch (Error)
{
    // обробляє виняток типу Error та MathError
}

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

...
catch (MathError)
{
    // обробляє виняток типу MathError
}
catch (Error)
{
    // обробляє виняток типу Error
}
catch (...)
{
    // обробляє всі інші винятки
}

2.2 Поліморфізм

2.2.1 Концепція поліморфізму

Поліморфізм (polymorphism) – це механізм визначення серед функцій з однаковими іменами функції для виклику, заснований на типі параметрів (поліморфізм часу компіляції) або об'єкта, для якого викликається метод (поліморфізм часу виконання). Поліморфізм часу виконання у C++ використовує ієрархії успадкування.

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

Мова C++ пропонує альтернативний спосіб вирішення цієї проблеми. Цей спосіб базується на тому, що у С++ значення вказівників на похідні класи можна присвоювати змінним типу вказівників на базовий клас. Аналогічне правило є для посилань:

class X
{
    . . .
};

class Y : public X
{
    . . .
};

class Z : public Y
{
    . . .
};

X *px = new Y();
Z  z;
X &rx = z;

Для інших перетворень типів указівників та посилань на об'єкти однієї ієрархії вживають спеціальний оператор dynamic_cast<тип>(вираз).

Тепер можна створити масив указівників на базовий клас та здійснити ініціалізацію елементів указівниками на похідні класи:

X *a[3];
x[0] = new X();
x[1] = new Y();
x[2] = new Z();

Точні типи об'єктів у масиві вказівників, а також адреси функцій-елементів цих об'єктів не можуть бути визначені компілятором. Поліморфізм дозволяє процесору отримати адреси функцій через указівники на об'єкти.

2.2.2 Віртуальні функції

Віртуальна функція (virtual function) - це функція-елемент, адреса якої може визначатись динамічно під час виконання програми. Віртуальні функції - це методи з фіксованим інтерфейсом виклику та різними реалізаціями у різних похідних класах. У визначені класу для ідентифікації віртуальних функцій вживається слово virtual:

class SomeClass
{
    . . .
public:
    virtual void firstFunc();
    virtual int secondFunc(int x);
};

Слово virtual не вживається, якщо функція визначається за межами класу:

void SomeClass::firstFunc()
{
    . . . // Реалізація
}

Похідні класи можуть перекривати всі або частину віртуальних функцій базового класу. Функція, яка перекриває віртуальну функцію, повинна точно повторювати її заголовок. Слово virtual не є обов'язковим, але його застосування рекомендується. Розглянемо приклад:

class FirstDescendant public SomeClass
{
    . . .
public:
    virtual void firstFunc();
    virtual int secondFunc(int x);
};

class SecondDescendant : public SomeClass
{
    . . .
public:
    void firstFunc();
    virtual double thirdFunc(double x, double y);
};

class ThirdDescendant : public SecondDescendant
{
    . . .
public:
    virtual double thirdFunc(double x, double y);
};

Клас SecondDescendant не перекриває функції secondFunc(). Об'єкти цього класу використовують функцію secondFunc(), яка визначена у базовому класі. Клас ThirdDescendant використовує firstFunc() класу SecondDescendant. Він використовує функцію secondFunc() класу SomeClass та перекриває thirdFunc().

Є три варіанти виклику функцій-елементів: для об'єктів, для вказівників та для посилань. Виклик віртуальної функції для об'єкту визначається під час компіляції. В інших випадках адреса конкретної функції визначається під час виконання програми.

Об'єкт класу, у якому є віртуальні функції, має назву "поліморфний об'єкт", а відповідний клас – поліморфним класом (polymorphic class). Кожний поліморфний об'єкт містить спеціальне поле, у яке конструктор записує адресу так званої таблиці віртуальних методів (Virtual Method Table, VMT). Така таблиця створюється для усіх класів з віртуальними функціями. Таблиця віртуальних методів містить адреси віртуальних функцій класу. Ці адреси використовуються для виклику віртуальних функцій.

Деструктори також можуть бути віртуальними. Після оголошення деструктору віртуальним деструктори усіх похідних класів є віртуальними автоматично. Віртуальні деструктори дозволяють точно визначити обсяг пам'яті, яку необхідно звільнити, коли знищується об'єкт. Це особливо важливо для об'єктів, які створені у динамічній пам'яті та знищуються за допомогою операції delete.

Віртуальні функції не можуть бути описані з модифікатором static. Віртуальні функції можуть бути inline, але цей специфікатор має ефект лише для виклику функції для об'єкту (не для вказівника або посилання).

2.2.3 Абстрактні класи

Іноді у базовому класі не доцільно здійснювати реалізацію певної віртуальної функції, оскільки клас створено для представлення базової абстракції. В такому випадку функцію можна описати як суто віртуальну (pure virtual function). Для цього використовують спеціальний синтаксис:

virtual void f() = 0;

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

class AbstractBaseClass
{
    . . .
public:
    virtual void f() = 0;
    virtual int secondFunc(int x);
};

Похідні класи повинні перевантажити суто віртуальні функції. В іншому випадку такі класи залишаються абстрактними.

Не можна створити об'єкт абстрактного класу, але можна описати вказівник або посилання на абстрактний клас та проініціалізувати їх конкретними похідними класами.

Примітка: суто абстрактний класи (без даних і жодної неабстрактної функції) є практично безпечним з точки зору множинного успадкування. Такий клас використовують як один з базових, імітуючи таким чином відсутній в C++ механізм реалізації інтерфейсів.

2.3 Використання шаблонів

2.3.1 Опис та використання шаблонних функцій

Шаблонні функції і шаблони типів є основними елементами узагальненого програмування у C++.

Шаблонні функції (template functions) призначені для запису узагальнених функцій, що можуть працювати з даними різних типів.

Визначення й оголошення шаблону функції починається службовим словом template. За ним йде у кутових дужках ("<" і ">") список формальних параметрів шаблона, розділених комами. Список формальних параметрів шаблона не може бути порожнім. Кожен формальний параметр, що визначає тип, складається зі службового слова class, (або typename, що більше відповідає сучасному стандарту) за яким йде ідентифікатор. Ім'я формального параметра в списку повинне бути унікальним.

Формальні параметри шаблонів можуть використовуватися для визначення типу результату і формальних параметрів шаблонної функції. У тілі шаблонної функції також можуть використовуватися формальні параметри шаблона.

Як приклад шаблона приведемо функцію підсумовування елементів масиву довільного типу. Головне, щоб для елементів масиву були визначені операції присвоювання, у тому числі присвоювання константи "нуль", і "+=".

template <class SomeType>
SomeType sumOfArray(SomeType *a, const int size)
{
    SomeType sum = 0;
    for (int i = 0; i < size; i++)
    {
        sum += a[i];
    }
    return sum;
}

Крім того, можна визначити шаблон функції виведення в стандартний потік елементів масиву:

template <class SomeType>
void printArray(SomeType *a, const int size)
{
    for (int i = 0; i < size; i++)
    {
        cout << a[i] << ' ';
    }
    cout << endl;
}

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

int main()
{
    int a[] = {1, 2, 3};
    printArray(a, 3);
    cout << sumOfArray(a, 3) << endl;
    double b[] = {1.1, 2.2, 3.3, 4.5};
    printArray(b, 4);
    cout << sumOfArray(b, 4) << endl;
    cin.get();
    return 0;
}

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

2.3.2 Порядок звернення до шаблонних функцій

Бувають випадки, коли для якихось конкретних типів потрібно дати особливе визначення шаблонної функції. У цьому випадку програміст повинен задати свій спеціальний варіант функції. Наприклад, шаблон функції min() працює для типів, для яких визначена операція "<":

template <class Type> 
Type min(Type a, Type b)
{
    return a < b ? a : b;
} 

Цей шаблон не підходить для варіанта порівняння рядків. Для них визначається спеціальний варіант функції:

char* min(char* s1, char* s2)
{
    return strcmp(s1, s2) < 0 ? s1 : s2;
}

Порядок виклику функцій буде таким.

  1. Досліджуються всі нешаблонні варіанти функції.
  2. Досліджуються всі шаблонні варіанти функції.
  3. Повторно досліджуються всі нешаблонні варіанти функції, з застосуванням перетворення типів.

Для того, щоб можна було конкретизувати шаблон, компілятор повинен бачити не тільки оголошення, але і визначення функції. Тому визначення шаблонних функцій можна і треба поміщати в заголовні файли.

Під час виклику функції фактичний параметр шаблона можна вказати явно, наприклад:

int i = min<int>(2, 3);

2.3.3 Шаблони класів

Шаблон класу (class template) можна використовувати для створення сімейства класів, які відрізняються типами або константними значеннями всередині опису.

Попереднє оголошення і визначення шаблону класу починається зі службового слова template. За ним іде список формальних параметрів шаблона типу. Цей список не може бути порожнім. Наприклад:

template <class T> class X 
{ 
    T t; 
public:
    void set(T t1) { t = t1; }
};

Шаблон класу не є класом. Інстанціювання шаблону (template instantiation) – це створення певних типів з шаблону. Такі класи мають назву екземплярів шаблону (template instances):

X<int> xi;
X<double> xd;

Примітка: шаблони класів іноді умовно називають узагальненими класами (generic classes).

Параметрами шаблонів можуть бути параметри-типи, параметри звичайних типів і параметри-шаблони. У шаблона може бути кілька параметрів. Цілі аргументи використовуються найчастіше для завдання розмірів і границь масивів. Цілий аргумент шаблона повинен бути константою. Можливе завдання усталених параметрів шаблона:

template <class T, int size = 64> class Y 
{ 
    . . . 
};

Фактичне значення цілого параметру повинно бути константним виразом:

const int N = 128;
int i = 256;

Y<int, 2*N> b1;// OK
Y<float, i> b2;// Помилка: i не константа

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

Функція-елемент шаблонного класу вважається неявною шаблонною функцією, а параметри шаблона типу для її класу – параметрами її шаблона. Для деяких типів стандартні функції-елементи не підходять. У таких випадках можна явно задавати реалізацію функції, розрахованої на конкретний тип. Перед реалізацією таких функцій потрібно спеціальне оголошення template<> без параметрів. Крім того, можна дати особливе визначення шаблонного класу, розраховане на конкретний тип. Функція-друг для шаблона типу не є неявною шаблонною функцією.

Шаблон класу може мати статичні елементи. Кожен клас, згенерований по шаблону, має свою копію статичних елементів.

2.3.4 Шаблони класів і відповідність типів

Створені за одним шаблоном типи будуть різними і між ними неможливе відношення спадкування крім єдиного випадку, коли в цих типів ідентичні параметри шаблона. Наприклад:

template<class T> class X
{
    /* ... */
}

X<int>   x1;
X<short> x2;
X<int>   x3;

Тут x1 і x3 одного типу, а x2 – зовсім іншого. Автоматичне приведення типів не здійснюється:

x2 = x3; // помилка

3 Приклади програм

3.1 Ієрархія об'єктів реального світу

Припустимо, необхідно розробити ієрархію класів "Регіон" – "Населений район" – "Країна". Окремі класи цієї ієрархії можуть стати базовими для інших класів (наприклад "Незаселений острів", "Національний парк", "Адміністративний район", "Автономна республіка" і т.д.). Ієрархію класів можна доповнити класами "Місто" і "Острів". Доцільно в кожен клас додати конструктор, який ініціалізує усі поля. Можна також створити масив указівників на різні об'єкти ієрархії і для кожного об'єкта вивести на екран рядок даних про нього.

Можна запропонувати таку ієрархію класів, в яких для більшої наочності всі функції реалізовані всередині класів:

#pragma warning(disable:4996)


#include <cstring>
#include <iostream>

using std::strcpy;
using std::cout;
using std::endl;

class Region
{
private:
    char name[30];
    double area;
public:
    Region(const char *name, double area) { strcpy(this->name, name); this->area = area; }
    char* getName() { return name; }
    double getArea() const { return area; }
    virtual void show() { cout << endl << "Назва: " << name << ".\tПлоща: " << area << " кв.км."; }
    virtual ~Region() { }
};

class PopulatedRegion : public Region
{
private:
    int population;
public:
    PopulatedRegion(const char* name, double area, int population) 
        : Region(name, area) { this->population = population; }
    int getPopulation() const { return population; }
    double density() const { return population / getArea(); }
    virtual void show() { Region::show(); 
        cout << "\tНаселення:" << population << " чол.\tЩiльнiсть: " << density() << " чол/кв.км."; }
};

class Country : public PopulatedRegion
{
private:
    char capital[20];
public: 
    Country(const char* name, double area, int population, char* capital) 
        : PopulatedRegion(name, area, population) { strcpy(this->capital, capital); }
public:
    char* getCapital() { return capital; }
    virtual void show() { PopulatedRegion::show();
        cout << "\tСтолиця " << capital; }
};

class City : public PopulatedRegion
{
private:
    int boroughs;
public:
    City(const char* name, double area, int population, int boroughs)
        : PopulatedRegion(name, area, population) { this->boroughs = boroughs; }
public:
    int getBoroughs() const { return boroughs; }
    virtual void show() { PopulatedRegion::show();
        cout << "\tРайонiв: " << boroughs; }
};

class Island : public PopulatedRegion
{
private:
    char sea[30];
public:
    Island(const char* name, double area, int population, char* sea)
        : PopulatedRegion(name, area, population) { strcpy(this->sea, sea); }
    char* getSea() { return sea; }
    virtual void show() { PopulatedRegion::show();
        cout << "\tМоре: " << sea;
    }
};

void main()
{
    setlocale(LC_ALL, "UKRAINIAN");
    const int N = 4;
    Region *regions[N] = { 
        new City("Київ", 839, 2679000, 10),
        new Country("Україна", 603700, 46294000, "Київ"),
        new City("Харкiв", 310, 1461000, 9),
        new Island("Змiїний", 0.2, 30, "Чорне")
    };
    for (int i = 0; i < N; i++)
    {
        regions[i]->show();
    }
    for (int i = 0; i < N; i++)
    {
        delete regions[i];
    }
}

Примітка: підключення нестандартної директиви #pragma warning(disable:4996) у MS Visual Studio дозволяє уникнути повідомлень про помилку, пов'язаних з використанням "небезпечної" функції strcpy().

У функції main() ми використовуємо масив указівників замість масиву об'єктів, оскільки тільки в цьому випадку працює механізм поліморфізму. В протилежному випадку функція show() викликалася б тільки з базового класу.

3.2 Клас для розв'язання рівняння методом ділення відрізку навпіл

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

Створюємо новий порожній проект, до якого додаємо новий клас, наприклад AbstractDichotomy. Це можна зробити за допомогою функції головного меню Project | Add Class.... В майстрі створення класів обираємо C++ Class і натискуємо Add. У наступному вікні (Generic C++ Class Wizard) вводимо ім'я класу (AbstractDichotomy) та натискуємо Finish. Для класу автоматично генерується пара файлів - заголовний файл і файл реалізації. Файл AbstractDichotomy.h містить такий код:

#pragma once
class AbstractDichotomy
{
public:
    AbstractDichotomy();
    ~AbstractDichotomy();
};

Файл AbstractDichotomy.cpp містить такий код:

#include "AbstractDichotomy.h"

AbstractDichotomy::AbstractDichotomy()
{
}

AbstractDichotomy::~AbstractDichotomy()
{
}

Згенерований текст містить нестандартну директиву препроцесору #pragma once. Застосування цієї директиви замість стандартних стражів включення унеможливить компіляцію коду в інших середовищах, окрім MS Visual Studio. Замість цієї директиви до заголовного файлу слід додати стражів включення. Для кожного класу, який автоматично генерується, слід виправляти цей недолік.

В нашому випадку клас не міститиме конструкторів та деструкторів. Замість них оголошуємо функцію знаходження кореня root() і суто віртуальну функцію f():

#ifndef AbstractDichotomy_h
#define AbstractDichotomy_h


class AbstractDichotomy
{
public:
    double root(double a, double b, double eps = 0.001);
    virtual double f(double x) = 0;
};

#endif

Файл реалізації містить визначення функції root():

#include <cmath>
#include "AbstractDichotomy.h"

double AbstractDichotomy::root(double a, double b, double eps)
{
    double x;
    do
    {
        x = (a + b) / 2;
        if (f(a) * f(x) > 0)
        {
            a = x;
        }
        else
        {
            b = x;
        }
    } 
    while (std::fabs(b - a) > eps);
    return x;
}

Тепер у програмі, яка потребує розв'язання рівняння, можна підключити заголовний файл і створити новий похідний клас, в якому буде визначено певну функцію – ліву частину рівняння:

#include <iostream>
#include "AbstractDichotomy.h"

using std::cout;
using std::endl;

class MyDichotomy : public AbstractDichotomy
{
    virtual double f(double x) { return x * x - 2; }
};

void main()
{
    MyDichotomy d;
    cout << d.root(0, 6) << endl;           // 1.41431
    cout << d.root(0, 6, 0.00001) << endl;  // 1.41421
}

Нові класи слід створювати для кожної конкретної функції.

3.3 Узагальнений клас для представлення одновимірного масиву

Клас, створений у попередній лабораторній роботі, можна перетворити на шаблон. Тоді можна буде створювати масиви об'єктів різних типів. Клас можна розташувати в окремому заголовному файлі Array.h. Узагальнена функція getSum(), яка знаходиться в цьому ж файлі, дозволяє знайти суму елементів масиву. Текст файлу Array.h буде таким:

#ifndef Array_h
#define Array_h

#include <iostream>

using std::istream;
using std::ostream;

template <typename T> class Array
{
    friend ostream& operator<<(ostream& out, const Array& a)
    {
        for (int i = 0; i < a.size; i++)
            out << a.pa[i] << ' ';
        return out;
    }
    friend istream& operator >> (istream& in, Array& a)
    {
        for (int i = 0; i < a.size; i++)
            in >> a.pa[i];
        return in;
    }
private:
    T *pa;
    int  size;
public:
    class OutOfBounds
    {
        int index;
    public:
        OutOfBounds(int i) : index(i) { }
        int getIndex() const { return index; }
    };
    Array() { pa = 0; size = 0; }
    Array(int n);
    Array(Array& arr);
    ~Array() { if (pa) delete[] pa; }
    void addElem(T elem);
    T& operator[](int index);
    const Array& operator=(const Array& a);
    bool operator==(const Array& a) const;
    int getSize() const { return size; }
};

template <typename T> Array<T>::Array(int n)
{
    pa = new T[size = n];
}

template <typename T> Array<T>::Array(Array& arr)
{
    size = arr.size;
    pa = new T[size];
    for (int i = 0; i < size; i++)
        pa[i] = arr.pa[i];
}

template <typename T> void Array<T>::addElem(T elem)
{
    T *temp = new T[size + 1];
    if (pa)
    {
        for (int i = 0; i < size; i++)
            temp[i] = pa[i];
        delete[] pa;
    }
    pa = temp;
    pa[size] = elem;
    size++;
}

template <typename T> T& Array<T>::operator[](int index)
{
    if (index < 0 || index >= size)
        throw OutOfBounds(index);
    return pa[index];
}

template <typename T> const Array<T>& Array<T>::operator=(const Array<T>& a)
{
    if (&a != this)
    {
        if (pa)
            delete[] pa;
        size = a.size;
        pa = new T[size];
        for (int i = 0; i < size; i++)
            pa[i] = a.pa[i];
    }
    return *this;
}

template <typename T> bool Array<T>::operator==(const Array<T>& a) const
{
    if (&a == this)
        return true;
    if (size != a.size)
        return false;
    for (int i = 0; i < size; i++)
        if (pa[i] != a.pa[i])
            return false;
    return true;
}


template <typename T> T getSum(Array<T>& a)
{
    T sum = T();
    for (int i = 0; i < a.getSize(); i++)
    {
        sum = sum + a[i];
    }
    return sum;
}


#endif

Для того, щоб здійснити тестування класу для різних типів, можна створити заголовний файл Vector.h і перенести в нього вихідний код класу Vector з прикладу попередньої лабораторної роботи:

#ifndef Vector_h
#define Vector_h

#include <iostream>

using std::istream;
using std::ostream;

class Vector {
    friend istream& operator >> (istream& in, Vector& v) { return in >> v.x >> v.y; }
    friend ostream& operator<<(ostream& out, const Vector& v) { return out << "x=" << v.x << " y=" << v.y; }
    friend Vector operator+(Vector v1, Vector v2) { return Vector(v1.x + v2.x, v1.y + v2.y); }
    friend Vector operator*(double k, Vector v) { return Vector(v.x * k, v.y * k); }
    friend Vector operator*(Vector v, double k) { return operator*(k, v); }
    friend double operator*(Vector v1, Vector v2) { return v1.x * v2.x + v1.y * v2.y; }
private:
    double x, y;
public:
    Vector() { x = y = 0; }
    Vector(double x, double y) { this->x = x; this->y = y; }
    double getX() { return x; }
    void setX(double x) { this->x = x; }
    double getY() { return y; }
    void setY(double y) { this->y = y; }
};

#endif

Тепер можна скористатися створеним класом Array для зберігання як цілих чисел, так і векторів:

#include <iostream>
#include "Array.h"
#include "Vector.h"

using std::cout;
using std::endl;

void main()
{
    Array<int> intArray;
    intArray.addElem(11);
    intArray.addElem(12);
    cout << intArray << endl;
    try
    {
        intArray[1] = 4;
        intArray[10] = 35;
    }
    catch (Array<int>::OutOfBounds e)
    {
        cout << "Bad index: " << e.getIndex() << endl;
    }
    cout << getSum(intArray) << endl;
    Array<Vector> vectorArray;
    vectorArray.addElem(Vector(1, 2));
    vectorArray.addElem(Vector(3, 4));
    vectorArray.addElem(Vector(5, 6));
    cout << vectorArray << endl;
    cout << getSum(vectorArray) << endl; // x=9 y=12
}

Можна також зберігати дані інших типів, наприклад, вказівники на ціле:

int m = 1, n = 2;
// Створюємо масив вказівників на int
Array<int*> pointerArray;
pointerArray.addElem(&m);
pointerArray.addElem(&n);
// Виводимо розіменовані значення:
cout << *pointerArray[0] << " " << *pointerArray[1] << endl; // 1 2

Але спроба знайти суму елементів призведе до помилки компіляції, оскільки не можна знаходити суму вказівників:

// Не можна знаходити суму вказівників:
cout << getSum(pointerArray) << endl; // Помилка компіляції (cannot add two pointers)

4 Вправи для контролю

  1. Створити ієрархію класів "Будинок" – "Навчальний корпус". Створити масив указівників і вивести в циклі дані про об'єкти різних типів.
  2. Створити ієрархію класів "Цифровий пристрій" – "Мобильний телефон". Створити масив указівників і вивести в циклі дані про об'єкти різних типів.
  3. Створити шаблонну функцію для пошуку в масиві елементів, які знаходяться в певному діапазоні. Перевірити роботу для різних типів елементів.
  4. Створити шаблон класу для зберігання пари чисел різних типів.

5 Контрольні запитання

  1. У чому полягає концепція успадкування?
  2. Чим відрізняється відкрите, захищене і закрите успадкування?
  3. Які переваги й недоліки множинного успадкування?
  4. Для чого визначають віртуальний базовий клас?
  5. Для чого створюють ієрархії винятків?
  6. У чому полягає концепція поліморфізму?
  7. Чим відрізняється поліморфізм часу компіляції від поліморфізму часу виконання?
  8. Що таке віртуальна функція?
  9. Які класи вважаються поліморфними?
  10. Чому в поліморфних класах доцільно визначати віртуальні деструктори?
  11. Що таке таблиця віртуальних методів?
  12. Що таке суто віртуальна функція?
  13. Що таке абстрактний клас?
  14. Для чого використовують шаблонні функції?
  15. Чи можна шаблонну функцію окремо реалізувати для певного типу?
  16. Навіщо параметр шаблону в шаблонній функції вказувати явно?
  17. Як створити шаблон класу?
  18. Якими можуть бути параметри шаблону?
  19. Для чого використовують цілі параметри шаблону?
  20. Що таке інстанціювання шаблону?
  21. Як здійснюється перетворення об'єктів інстанційованих типів?

 

up