osn_progr_final
.pdfcloseBase();
}
211
8 ОБ’ЄКТНО-ОРІЄНТОВАНЕ ПРОГРАМУВАННЯ ТА С++.
8.1 ЕЛЕМЕНТИ КОНЦЕПЦІЇ ООП
Об’єктно-орієнтована парадигма зумовлює специфічний підхід до розробки програмного забезпечення. Фундаментальна концепцiя об’єктно-орієнтованої парадигми полягає в передачi повiдомлень об'єктам. Для цього необхiдно, щоб процедури визначались в одному програмному об’єкті разом з даними, якими вони маніпулюють. Відмітимо, що в процедурному програмуванні спочатку визначаються структури даних, якi потiм передаються в процедури як параметри.
Iснує п'ять компонент об’єктно-орієнтованої парадигми (пара- дигма-концепція):
1 - об'єкт ;
2- повiдомлення ;
3- клас ;
4- наслiдування ;
5- метод .
Будь-яка об’єктно-орієнтована мова повинна мати властивостi абстракцiї, iнкапсуляцiї, наслiдування та полiморфiзму.
Об’єкт - це iнкапсульована абстракцiя , яка включає iнформацiю про стан та чітко визначену множину протоколу доступу (повiдомлення, яке обробляє об'єкт.)
Повідомлення - це спецiальний символ, iдентифiкатор чи ключове слово (може бути з параметрами чи без), яке представляє дiю , що виконується об'єктом.
Клас представляє собою певний тип об'єктiв i задається за допомогою опису
класу. Опис класу визначає змiннi стану та протокол доступу до об'єктiв даного класу. Класи органiзовуються iєрархiчно, причому похідні класи наслiдують властивостi класiв, що їх породжують. В деяких ОО-мовах класи є також об'єктами.
Метод існує для кожного повідомлення, визначеного для деякого класу. Він визначає реакцiю об'єкта на повiдомлення. Як правило, метод складається з ряду виразів і може використовувати протокол з іншого класу.
Таким чином, об’єкти належать до окремих класів. Об’єкти обробляють повідомлення у відповідності з методами, заданими в описі класу. Об’єкти мають змінні стану, визначені в описі класу. Змінні стану можуть мати однакові чи різні значення в різних об’єктів (екземплярів класу).
212
Розглянемо основні поняття об’єктно-орієнтованої парадигми по відношенні до С++.
Клас - це новий тип даних , який вводиться за допомогою ключового слова class.
Об’єкт - це змiнна типу ClassName, де ClassName - це ім’я ранiше визначеного класу.
Змінні стану - це змiннi, якi оголошенi в описi класу. Будемо називати їх полями даних .
Повідомлення , на якi може реагувати об'єкт, вказується за допомогою прототипiв функцiй в описi класу. Тобто, фактично, повідомлення - це набори фактичних значень, що відповідають сигнатурам фун- кцій-членів класу.
Метод - це визначення(реалізація) функцiї-члена класу. Абстрагування - це формування представлення про якостi чи властивостi предмету шляхом мисленного видiлення деяких його часткових рис.
Iнкапсуляція - це об'єднання , закриття в "капсулi" певних конструкцій.
Наслідування - це властивiсть похiдного класу наслiдувати властивостi базового класу. При цьому похiдний клас може мати якiсь свої специфiчнi властивостi. Ця властивiсть дає можливiсть утворювати iєрархiю класiв . Iснує поняття множинного наслiдування, коли похiдний клас може бути породжений кiлькома базовими.
Потрiбно розрiзняти наслiдування та контейнернi вiдношення. Якщо клас 1 має всi властивостi класу 0, i крiм того , якiсь свої додатковi, то в такому випадку оправдана iєрархiчна структура ( наслiдування):
class 1 <- class 0
Про контейнерне вiдношення говорять, якщо об'єкт класу class 0 мiстить об'єкт класу class 1 , тобто одне з полiв класу class0 має тип class 1.
Поліморфізм - цє здатність повiдомлення викликати рiзнi дiї (реакції методів) на етапi виконання програми . Тобто конкретна форма реакції на повiдомлення визначається i зв'язується з об'єктом пiд час виконання програми (пiзнє зв'язування). В С++ поліморфізм проявляється у підтримці наступних механізмів:
1.Перевантаження функцiй (всерединi опису класу можна перевантажити функцiї).
2.Перевантаження операцiй.
213
3. Вiртуальнi функцiї.
8.2 ВІД СТРУКТУР ANSI С ДО КЛАСІВ С++.
Синтаксичною основою реалізації концепції об’єктноорієнтованого програмування стало введення в С++ нових властивостей структур та класів. Розглянемо, як можна прийти до розуміння необхідності цих властивостей, виходячи з точки зору ідеології об’єктно-орієнтованого програмування, поняття об‘єкта. Нехай описана, наприклад, така структура:
struct Time { int year; int month; int day; int hour;
int minute; }; Time v;
Очевидно, що змінна v може використовуватись для збереження інформації про певний час (рік, місяць, день, година, хвилина). Безсумнівно, що нам може бути необхідною функція, яка виводить цю інформацію в певній формі на якийсь стандартний пристрій виводу (монітор, наприклад):
void Display (Time*p)
{printf ("year=%d month=%d day=%d hour=%d \ minute=%d \n", p->year, p->month, p->day,\ p-> hour ,p-> minute);}
Проте, функція Display семантично дуже пов’язана з структурою Time. Це наводить на думку пов’язати функцію з структурою синтаксично. В С++ (на відміну від ANSI C) є така можливість: функцію можна записувати як поле структури. Тоді наш приклад можемо переписати так:
struct Time { int year; int month; int day; int hour; int minute;
void Display (void)
{printf ("year=%d month=%d day=%d hour=%d \ minute=%d \n", p->year, p->month, p->day,\ p->hour ,p->minute);} };
214
Можна записати функцію-член структури , визначивши її за межами формального опису структури з використанням операції розширення області видимості:
struct Time {
int year; int month; int day; int hour; int minute;
void Display (void);}; void Time::Display (void)
{printf ("year=%d month=%d day=%d hour=%d \ minute=%d \n", p->year,p->month, p->day,\
p->hour ,p->minute);}
Що ж ми отримали ? Зробивши функцію членом структури, ми не внесли нічого нового з точки зору можливостей . Адже будь-яка зовнішня функція має вільний доступ до полів даних структури (через операцію “.”). Все те, що можна робити з даними за допомогою функції-члена структури можна робити і за допомогою зовнішніх функцій . Проте, в семантичному плані це безсумнівне досягнення. Об’єднання даних та функцій , що працюють з цими даними в одній структурі - це приклад інкапсуляції.
Функція-член структури стає ніби властивістю структури. А що ж це за структура, що має якісь властивості? Де її можна використовувати? Очевидно, що такі структури могли б бути корисними для моделювання реальних природних об’єктів. В нашому випадку, наприклад, структура Time з функцією-членом Display може служити моделлю годинника. Тобто об’єкта, при звертанні до якого ми отримуємо інформацію про час та дату. Але очевидно, що природні об’єкти мають дані, які є скритими від певних зовнішніх об’єктів.
Наприклад, спостерігаючи за літаком, можна визначити його швидкість, висоту, тип, але не можна ззовні визначити, скільки у нього залишилося пального та боєприпасів. Тоді приходимо до думки, що деякі поля даних повинні бути закритими. Тобто недопустимим синтаксично повинне бути звертання до цих полів за допомогою, наприклад, операції “ . ”. Так, виходячи з поняття інкапсуляції, приходимо до концепції закритих полів даних. В С++ існують спеціальні службові слова - специфікатори доступу: public, protected та private. Дані, розміщені в розділах protected та private стають доступними лише для функцій-членів і недоступними для зовнішніх функцій. Доступ до них може здійснюватись лише за допомогою функцій, описа-
215
них у відкритому розділі.
Оголосимо поля даних в нашому прикладі закритими: struct Time {
private:
int year; int month; int day; int hour; int minute;
public:
void Display (void);} tt1; void Time::Display (void)
{printf ("year=%d month=%d day=%d hour=%d \ minute=%d \n",p->year,p->month,p->day,\
p->hour ,p->minute);}
Тоді при звертанні виду tt1.day компілятор видасть повідомлення про помилку.
Концепція закритих даних є значним кроком вперед з точки зору теорії програмування. Адже в класичному процедурному програмуванні діє правило: дані формують код. Тобто спочатку визначаються структури даних, з якими доводиться працювати, а потім пишуться функції роботи з ними. При такому підході програма стає надзвичайно залежною від структур даних. Припустимо, наприклад, що ми написали величезну кількість функцій, що працюють з полями структу-
ри Time.
Але ж дату можна зберігати і в секундах, використовуючи змінну типу long. Уявімо собі, скільки змін потрібно внести в програму, щоб реалізувати цю ідею. Адже потрібно змінювати всі функції типу Display , всі оператори, які звертаються до полів. У випадку ж закритих даних змінити доведеться лише функції-члени структури, які працюють з закритими даними. Все інше може залишитися без змін ! Більше того, можна взагалі писати програму, визначаючи спочатку функціональний інтерфейс і лише на останньому кроці визначати внутрішню структуру закритих даних. Обмеженість впливу даних на код є однією з суттєвих особливостей ООП.
Відмітимо, що структури та класи в С++ мають лише незначні відмінності у властивостях. Іх можна розглядати як синтаксично допустимий спосіб утворення типів даних користувача. В С++ ім’я структури чи класу - це тип даних. Якщо, наприклад, в ANSI C хочемо оголосити змінну типу struct Time, то можемо написати оголошення виду: struct Time a; Тут обов’язковим є ключове слово struct. В С++
216
слово struct не обов’язкове. Тобто допускається еквівалентне оголошенння виду: Time а; Для можливості такого оголошення в ANSI C потрібно використовувати засіб typedef:
typedef struct Time {…};
Time a;
8.3 ОПИС ПРОТОКОЛУ КЛАСУ.
Розглянемо нові типи даних, що задаються за допомогою ключового слова class. Опис класу майже нічим не відрізняється від опису структури. У прикладі з структурою Time ми могли б описати аналогічний клас Time:
class
Схематично опис класу можемо задати наступним чином: class <Ім’я_Класу>
{ private:
<тип> |
<ідентифікатор>; |
. . . . . . . . . . . . . . . . . . . |
|
<тип> |
<ідентифікатор>; |
<тип> <Ім’я_Функції 11>(<сигнатура>);
. . . . . . . . . . . . . . . . . . .
<тип> <Ім’я_Функції 1N1>(<сигнатура>); protected:
<тип> <ідентифікатор>;
. . . . . . . . . . . . . . . . . . .
<тип> <ідентифікатор>;
<типрезультату> <Ім’я_Функції21>(<сигнатура>);
. . . . . . . . . . . . . . . . . . .
<типрезультату> <Ім’я_Функції2N2>(<сигнатура>); public:
<тип> <ідентифікатор>;
. . . . . . . . . . . . . . . . . . .
217
<тип> <ідентифікатор>;
<типрезультату> <Ім’я_Функції 31>(<сигнатура>);
. . . . . . . . . . . . . . . . . . .
<типрезультату> <Ім’я_Функції 3N3>(<сигнатура>);
<Ім’я_Класу>(<сигнатура А>); <Ім’я_Класу> (<сигнатура Б>)
{<тіло функції> }
~<Ім’я_Класу>(void);};
<типрезультату> <Ім’я_Класу>::< Ім’я_Функції 11>(<сигнатура>) { <тіло функції>}
<типрезультату> <Ім’я_Класу>:: <Ім’я_Функції 12>(<сигнатура>) {<тіло функції>}
. . . . . . . . . . . . . . . . . . . . . . . . . . .
<типрезультату> <Ім’я_Класу>:: <Ім’я_Функції 3N3>(<сигнатура>) {<тіло функції>}
<Ім’я_Класу> ::<Ім’я_Класу>(<сигнатура А>) {<тіло функції>}
<Ім’я_Класу> (<сигнатура Б>) { <тіло функції> }
<Ім’я_Класу> :~ <Ім’я_Класу>(void)
{ <тіло функції> }
Всі програмні компоненти, що містяться між відкриваючою та закриваючою фігурними дужками опису класу “{“ - ”};” утворюють
формальний опис класу. Під протоколом класу будемо розуміти об-
ласть, що складається з області формального опису класу та тіл фун- кцій-членів, визначених за межами формального опису класу при допомозі операції розширення області видимості.
Бачимо, що формальний опис класу ділиться на три роздiли , що задаються за допомогою ключових слів private, protected та public. Кожен з цих розділів може мiстити поля даних та функцiї-члени. Порядок слідування роздiлiв та їх кiлькiсть можуть бути довiльними. Кількість полів у кожному розділі та порядок їх розміщення також може бути довільним. Якщо ключове слово вiдсутнє , то поля даних i функцiї-члени в цiй частинi опису класу вважаються закритими (для структур -вiдкритими ).
Як бачимо з схеми опису класу, крім “звичайних “ функційчленів класи можуть мати функції, імена яких співпадають з іменем класу або утворюються по схемі: ~ <Ім’я_Класу>. Це так звані конструктори та деструктори.
Конструктор - це функцiя-член, iм'я якої спiвпадає з iменем класу. Конструктор може мати порожнiй список параметрiв. Допускаються параметри по замовчуванню, перевантаження. Ціль констру-
218
кторів - ініціалізація полів об’єкту (екземпляра класу). Присутність конструктора в протоколі класу не є обов’язковою.
Деструктор - це функція, ім’я якої ~ <Ім’я_Класу> . Деструктор повинен обов’язково мати порожнiй список параметрiв. В класі може бути не більше одного деструктора. Ціль деструктора - проведення корректних операцій при знищенні екземпляра класу (наприклад, звільнення пам’яті).
Конструктори i деструктори, так само як i iншi функцiї-члени можуть бути визначеними за межами формального опису класу. Якщо визначення функцiї включається в межі формального опису класу, то така функцiя розглядається компілятором як inline -функцiя. Діє правило: будь-яка змінна та функція є доступною в межах протоколу класу. Тобто в межах протоколу класу можна вільно звертатись до будь-якої змінної чи функції, описаної в цьому ж протоколі (так, ніби вони знаходяться в одному складеному операторі). Наприклад, в функції void Timer::Display (void) вільно використовуються поля month, hour та ін. без будь-яких додаткових описів. Що ж стосується доступу до полів ззовні протоколу (випадок, коли вже визначений певний об’єкт-екземпляр класу), то прямий доступ можливий лише до полів даних та функцій -членів відкритого розділу (аналогічно, як вже розглядалось стосовно структур). Доступ до полів закритого та захищеного розділів здійснюється лише через функції-члени, що містяться у відкритих розділах.
Як вже відмічалось, обмеженість доступу до полів даних класу має ряд переваг:
а) перший етап вiдладки програми , - локалiзацiя помилки -, виконується ще до запуску програми за рахунок її органiзацiї . Адже помилка, пов’язана з використанням закритих даних, може виникнути лише в функції ( описаній у відкритому розділі), що працює з цими даними.
б) для того, щоб працювати з об’єктами, користувач не обов'язково повинен знати структуру закритих даних. Достатньо лише знати функцiї, якi працюють з цими даними.
в) легко можна змiнювати закриту частину даних без змiни основної програми.
Специфіка захищеного розділу буде розглянута пізніше.
Для ілюстрації пункту б) розглянемо, наприклад , ввід-вивід в С++ (детально він буде розглянутий пізніше). В систему С++ включаються класи ostream та istream. Вивiд iнформацiї здiйснюється
219
шляхом передачi об'єкту cout класа ostream повiдомлення, яке виводиться. Наприклад, якщо хочемо надрукувати рядок " a string", в С++ можемо зробити це так:
cout << " a string\n";
Об'єкту cout можуть передаватись множиннi повiдомлення: int i;
cout<<"<<i<<"\n";
Допускається при цьому (в ранніх версіях) форматований вивiд за допомогою функцiї form , параметри якої аналогічні параметрам функ-
ції printf:
cout<< form( “ i=%d”,i);
Для вводу потрiбно здiйснити передачу об'єкту cin класу istream повiдомлення iз змiнною, яка приймає данi, що вводяться, в якостi параматра. Аналогічно здійснюється форматований ввiд:
cin>> form( “ i=%d”,&i);
Взагалі, система вводу-виводу С++ є дуже складною. Класи вводувиводу утворюють складну ієрархію, кожен клас ієрархії має свої функції та поля даних. Проте , користувач може взагалі не знати всієї цієї внутрішньої організації . Достатньо знати лише, яким об’єктам і як передаються відповідні повідомлення. Навіть з кількох наведених вище прикладів та короткої інформації ми формально можемо навчитись вводити та виводити дані в С++ (у найпростіших випадках). При цьому, поки що, для нас залишається незрозумілим, чому саме так і як все відбувається на внутрішньому рівні.
Синтаксично, визначення об’єкта (екземпляра класу) у найпростішому випадку нічим не відрізняється від визначення елемента ти-
пу struct:
class ClassName
{
public: int p;};
ClassName object;
Можемо визначити вказівник на клас ClassName:
ClassName * objectPointer;
Очевидно, що і доступ до відкритих полів екземпляра класу буде синтаксично реалізований аналогічно, як і для структур, за допомогою операцій “.” та ”->”:
cout<<ClassName .p; objectPointer = new ClassName; cout<<objectPointer->p;
З точки зору концепції ООП доступ до полів об’єкта означає переда-
220