Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

osn_progr_final

.pdf
Скачиваний:
37
Добавлен:
12.02.2016
Размер:
3.27 Mб
Скачать

Статична функція-член виконує дії, що стосуються всіх об’єктів даного класу.

Цікаві ситуації виникають , якщо розглядати локальнi та глобальнi об'єкти. Розглянемо такий приклад:

Timer t1(5); void f() {

static Timer t2(200);} main ()

{f();}

Вприкладі конструктор, очевидно, викличеться двiчі: спочатку для глобального об’єкту а потім при виклиці функції f(). А якщо в функції main() не викликається функція f()? Чи викликається конструктор для локального статичного об'єкту, якщо функцiя, де вiн опи-

саний, не викликається? Відповідь на таке запитання залежить від версії компілятора, що використовується (наприклад, для Visual c++ 6.0 конструктор не викликається) .

Для глобальних статичних об'єктiв конструктори викликаються в тому порядку, в якому вони описанi в програмi. Деструктори - в зворотньому порядку. Причому параметри конструкторiв для статичних об'єктiв повиннi бути константними виразами. Ситуація в прикладі, описаному нижче, допускається в останніх версіях компіляторів C++

фірми Microsoft:

void g(int a) {static Timer t(a);}

ЗАВДАННЯ

1.Описати клас Літак (див. попередні завдання) , у якому інформація про простір є статичним масивом (двочи трьохвимірним).

2.Описати клас Океан (див. попаередні завдання) , у якому інформація про простір є статичним масивом (двочи трьохвимірним).

3.Модифікувати клас Літак так, щоб він мав деякий “локатор”, тобто міг аналізувати простір навколо себе деякого радіусу.

4.Протестувати клас Point , доповнивши його функцією Draw().

5.Написати та протестувати приклад , коли статична функція працює з статичними полями даних класу.

6.Описати однозв’язний список об’єктів, що містять статичне поле -

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

251

8.9 НАСЛІДУВАННЯ

Наслідування є потужним апаратом ООП. Як вже відмічалось вище, наслідування полягає у здатності похiдного класу наслiдувати властивостi базового класу, набуваючи при цьому якiсь свої специфiчнi властивостi. За допомогою наслідування можна утворювати нові класи, використовуючи вже існуючі. Замість того, щоб писати код з чистої сторінки, на базі існуючої ієрархії класів можна побудувати нові, змінюючи властивості існуючих класів. А це дає величезну перевагу в часі, робить надзвичайно гнучким програмне забезпечення.

8.9.1 Синтаксична реалізація наслідування

Синтаксично наслідування реалізується наступним чином: <ключове слово > <ім’я похідного класу> : <базовий список>

{<поля даних> <функції-члени>}

<ключове слово > - це class, struct чи union;

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

Розглянемо схематичний приклад:

class Base{ private:

type data1; type function1;

protected: type data2;

type function2; public:

type data3; type function3;}

class PublicPohid: public Base { private:

type data4; type function4; protected:

type data5; type function; public:

type data6; type function6;}

class PrivatePohid: public Base { private:

type data7;

252

type function7; protected:

type data8; type function8;

public: type data9;

type function9;} class ProtectedPohid: public Base

{ private:

type data10; type function10;

protected: type data11;

type function11; public:

type data12; type function12; }

main()

{Base aBase; PublicPohid aPub;

PrivatePohid bPub;

ProtectedPohid aPro;}

8.9.2 Правила доступу до полів даних

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

Правила доступу до полів екземпляра похідного класу:

1.Члени відкритого базового класу в похідному класі зберігають свою специфікацію доступу.

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

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

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

Правила доступу в протоколі:

253

1.В протоколі класу є вільний доступ до всіх компонент цього протоколу; тобто будь-яка функція-член в будь-якому розділі опису класу має вільний доступ до будь-яких компонент протоколу класу.

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

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

наведеній вище схемі класи PublicPohid PrivatePohid та ProtectedPohid наслідують data1 та function1 з базового класу, але не мають прямого доступу до них навіть у протоколі. Доступ можна забезпечити через функцію, визначену в захищеній чи відкритій частинах класу Base.

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

PublicDerived, PrivatePohid та ProtectedPohid мають прямий доступ до data2 та function2. В той же час об’єкти aPriv та aPub не мають прямого доступу до data2 , function2 та своїх закритих та захищених розділів.

Відмітимо, що похідні класи самі можуть стати базовими. І тут необхідно враховувати зміни статусу полів. Розглянемо наступний приклад:

class Base{ protected: int x;

public: int y;// . . . }; class Tderived: private Base { public:

void f(void) { x=3;// . . .

y=4;};

В тілі функції f маємо прямий доступ до змінних x та y , але, оскільки Base є закритим базовим класом, статус цих змінних змінився на

254

private. Нехай визначений ще один клас: class Tdec:public Tderived { public:

void g(void){}// . . .}

Тут вже функція g() немає доступу до змінних x та y .

Зауваження.

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

І ще одна обставина. Припустимо, що оголошуючи клас B private-похідним від А, хочемо залишити відкритими окремі відкриті члени класу А. В такому випадку можна використати операцію розширення області видимості:

class A

{

public: int i;

}

class B: private A {// . . .

public:

A::i;};

Тоді змінна i буде мати відкритий статус в класі В (так, ніби вона визначена у відкритому розділі класу В).

8.9.3 Конструктори та деструктори в похідних класах

Як вже згадувалось , в похiдному класi, як правило, є конструктор , якщо вiн визначений i в базовому класi. Конструктор в похідному класі повинен бути хоча б для того, щоб викликати конструктор базового класу з відповідним параметром. Синтаксично допускається відсутність конструктора в похідному класі, якщо конструктор базового класу не має параметрів (void-конструктор).

Деструктори не вимагають таких строгих правил при їх використаннi. В похiдному класi деструктор потрiбен лише при наявностi

255

членiв, якi необхiдно видалити при виході об’єкта з області видимості.

Розглянемо базовий клас , оголошений наступним чином: class Base

{ private: char * basep; public:

Base (const char * s) {basep=strdup(s);} ~Base() {delete basep;}

const char * GetStr(void) {return basep;}};

Конструктор класу викликає бiблiотечну функцiю strdup() для створення копiї рядкового аргументу у виглядi динамiчної змiнної з присвоєнням адреси видiленої пам'ятi вказiвнику basep. Деструктор звільняє цей блок пам’яті.

При створеннi об'єкта типу:

Base president("Grushevskiy");

конструктору класу передається рядок "Grushevskiy", при цьому вiн створює копію рядка i присвоює її адресу члену basep об'єкта president. Коли president виходить з областi видимостi, деструктор знищує рядок.

Оголосимо похiдний клас наступним чином: class Derived: public Base

{ private:

char * uppercasep; public:

Derived (const char * s) ~Derived() {delete uppercasep;}

const char * GetStr(void) {return uppercasep;}};

До полiв , наслiдуваних з Base, Derived добавляє закритий член, символьний вказiвник uppercasep, який посилаеться на копiю рядка, що зберiгається в Base, з символами, перетвореними на великі. Деструктор похідного класу знищує цей новий рядок.

Конструктор Derived можна реалiзувати так:

Derived()::Derived(const char * s) :Base() {uppercasep=strupr(strdup(s));};

Конструктор Derived викликає конструктор Base , який помiщує рядок, на який посилається s , в кучу та iнiцiалiзує basep адресою копiї цього рядка. Пiсля цього конструктор похiдного класу створює ще одну копiю рядка, перетворюючи її символи в маленьки та присвоюючи uppercase адресу цiєї копiї рядка.

Якщо ми створюємо об'єкт типу Derived, то при цьому створю-

256

ється двi копiї рядка. Розглянемо наступний приклад:

Derived president("Grushevskiy");

cout<<"original string:<<president.GetStr()<<'\n'; cout<<"uppercase string:<<president.GetStr()<<'\n';

Результат:

original string: Grushevskiy uppercase string: GRUSHEVSKIY

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

Розглянемо ще один приклад : class base

{ public:

base (char * n, shint t); ~base();};

class derived : public base { base m;

public:

derived (char * n); ~derived();};

derived::derived(char * n):(n,10),m("member",123){ // ...}

Бачимо , що параметри конструктора члена класу m вказуються при виклиці конструктора похідного класу.

8.9.4 Використання заміщуючих функцій-членів.

Як бачимо, в похiдному класi, як правило, добавляють новi дані тафункцiї-члени. Однак, iснує можливiсть замiни (заміщення) функцiй-членів, наслiдуваних з базового класу. Розглянемо такий клас Base:

class Base

{private: char* sptr; public:

Base (const char *s) {sptr=strdup(s);} ~Base() {delete sptr;}

257

void Display(void){cout <<sptr<<'\n';}};

Можна оголосити обєкт типу Base:

Base region("Rivne”); i вивести рядок : region.Display().

Припустимо, що пiзнiше ми вирiшили , що всi подiбнi рядки повиннi починатися рядком "Region:". Можна добитись цього так:

cout<<"Region:";

region.Display();

Але змiни потрiбно вносити у всю програму. Кращий варiант розв`язку проблеми - вивести новий клас з Base та замiнити наслiдувану функцiю-член Dispay() модифiкованою версiєю.

Можна описати похiдний клас настурним чином: class Region : public Base

{ public:

Region (const char *s):Base(s){ }

void Display (void); //замiщуюча функцiя

};

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

void Region :: Display(void)

{cout<<"Region:";

Base.Display(); // виклик замiщеної функцiї-члена

};

Оголошення та використання об’єкта класу

Region region("Rivne"); region.Display();

приведе до друку рядка "Region:Rivne".

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

class Rivne: public Region {public:

Rivne(): Region("Rivne"){}};

Новий конструктор похідного класу, оголошений без параметрів передає рядок Rivne конструктору базового класy. Далі створюється об‘єкт R та виводиться назва області(міста):

Rivne R; R.Display();

Конструктори можуть мати довiльнi параметри. Нехай нам потрiбен буде клас , який зберiгає крiм назви області , ще й кiлькiсть

258

населення . Можна вивести його з Region: class Population : public TRegion

{private:

long population; public:

Population(long n ,const char* name); void Display(void);};

Population ::Population(long n ,const char * name)

{population=n; Region (name);};

void Population:: Display(void)

{Region:: Display();

cout <<"Population :: "<<population<<'\n';};

Далi використовувати можна його так:

Population Kyiv (3500000, "Kyiv"); Kyiv.Display();

8.9.5 Похідні класи та вказівники.

Якщо похiдний клас derived має вiдкритий базовий клас base, то вказiвник на derived можна присвоювати змiннiй типу вказiвник на Base без використання явного перетворення типу. Обернене перетворення вказiвника на base у вказiвник на derived повинне бути явним.

Приклад:

class base {

//...

};

class derived : public base

{

//....

};

derived m;

base * pb=& m; //неявне перетворення derived * pd;

pd=(derived*)pb; // явне перетворення

Iншими словами, об`єкт похiдного класу при роботi з ним через вказiвник можна розглядать як об`єкт його базового класу. Обернене невiрно.

Якщо base є закритим базовим класом для derived, неявне перетворення derived* в base* не виконується:

class base {int m1; public:

259

int m2;};

class derived : base

{// m2-закритий тип derived

};...

derived d; d.m2=2; //помилка

base* pb=&d; //помилка base* pb; //ok pb->m2=2; //ok

8.9.6 Ієрархія типів

Похiдний клас сам може бути базовим. Наприклад, можна побудувати ієрархію класів:

class student {...};

class programmer: student{...}; class manager : student{...}; class director : manager {...};

Як бачимо з попередніх прикладів, при визначенн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.

class XYValue {int x,y; public:

XYValue(int_x,int_y):x(_x),y(_y) {} };

class XYData

{int x,y; XYData(int_x,int_y) {x=_x;

y=_y;}};

Хоча для бiльшостi елементiв даних список iнiцiалiзацiї необов`язковий , вiн є єдиним методом iнiцiалiзацiї елементiв у деяких випадках, наприклад, якщо елемент класу є об`єктом.

8.9.7 Множинне наслідування

Досі ми розглядали просте наслiдування, тобо коли похiдний

260

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]