Розробка власного класу STRING
Поняття об’єктно-орієнтовного програмування, поява відносин класів. Структури як складені типи даних, побудовані з використанням інших типів. Область дії класу і доступ до його елементів. Конструктор та деструктор класу. Розробка базового класу STRING.
Рубрика | Программирование, компьютеры и кибернетика |
Вид | курсовая работа |
Язык | украинский |
Дата добавления | 19.08.2010 |
Размер файла | 280,9 K |
Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже
Студенты, аспиранты, молодые ученые, использующие базу знаний в своей учебе и работе, будут вам очень благодарны.
Хоча дозволяється визначати кілька таких функцій-членів, лише одна з них буде застосовуватися до всіх об'єктів класу. От, наприклад, деструктор для нашого класу Account:
class Account {
public:
Account ();
explicit Account (const char*, double=0.0);
Account (const Account&);
~Account ();
// ...
private:
char *_name;
unsigned int _acct_nmbr;
double _balance;
};
inline
Account:: ~Account ()
{
delete [] _name;
return_acct_number (_acct_nnmbr);
}
Зверніть увагу, що в нашому деструкторі не скидаються значення членів:
inline Account:: ~Account ()
{
// необхідно
delete [] _name;
return_acct_number (_acct_nnmbr);
// необов'язково
_name = 0;
_balance = 0.0;
_acct_nmbr = 0;
}
Робити це необов'язково, оскільки відведена під члени об'єкта пам'ять однаково буде звільнена. Розглянемо наступний клас:
class Point3d {
public:
// ...
private:
float x, y, z;
};
Конструктор тут необхідний для ініціалізації членів, що представляють координати точки. Чи потрібний деструктор? Немає. Для об'єкта класу Point3d не потрібно звільняти ресурси: пам'ять виділяється й звільняється компілятором автоматично на початку й наприкінці його життя.
В загальному випадку, якщо члени класу мають прості значення, скажімо, координати точки, то деструктор не потрібний. Не для кожного класу необхідний деструктор, навіть якщо в нього є один або більше конструкторів. Основною метою деструктора є звільнення ресурсів, виділених або в конструкторі, або під час життя об'єкта, наприклад звільнення пам'яті, виділеної оператором new.
Але функції деструктора не обмежені тільки звільненням ресурсів. Він може реалізовувати будь-яку операцію, що за задумом проектувальника класу повинна бути виконана відразу по закінченні використання об'єкта. Так, широко розповсюдженим прийомом для виміру продуктивності програми є визначення класу Timer, у конструкторі якого запускається та або інша форма програмного таймера. Деструктор зупиняє таймер і виводить результати вимірів. Об'єкт даного класу можна умовно визначати в критичних ділянках програми, які ми хочемо профілювати, у такий спосіб:
{
// початок критичної ділянки програми
#ifdef PROFILE
Timer t;
#endif
// критична ділянка
// t знищується автоматично
// відображається витрачений час...
}
Щоб переконатися в тім, що ми розуміємо поводження деструктора (та й конструктора теж), розберемо наступний приклад:
(1) #include "Account. h"
(2) Account global ("James Joyce");
(3) int main ()
(4) {
(5) Account local ("Anna Livia Plurabelle", 10000);
(6) Account &loc_ref = global;
(7) Account *pact = 0;
(8)
(9) {
(10) Account local_too ("Stephen Hero");
(11) pact = new Account ("Stephen Dedalus");
(12) }
(13)
(14) delete pact;
(15) }
Скільки тут викликається конструкторів? Чотири: один для глобального об'єкта global у рядку (2); по одному для кожного з локальних об'єктів local і local_too у рядках (5) і (10) відповідно, і один для об'єкта, розподіленого в купі, у рядку (11). Ні об'явлення посилання loc_ref на об'єкт у рядку (6), ні об'явлення вказівника pact у рядку (7) не приводять до виклику конструктора. Посилання - це псевдонім для вже сконструйованого об'єкта, у цьому випадку для global. Вказівника також лише адресує об'єкт, створений раніше (у цьому випадку розподілений у купі, рядок (11)), або не адресує ніякого об'єкта (рядок (7)).
Аналогічно викликаються чотири деструктори: для глобального об'єкта global, об'явленого в рядку (2), для двох локальних об'єктів і для об'єкта в купі при виклику delete у рядку (14). Однак у програмі немає інструкції, з якої можна зв'язати виклик деструктора. Компілятор просто вставляє ці виклики за останнім використанням об'єкта, але перед закриттям відповідної області видимості.
Конструктори й деструктори глобальних об'єктів викликаються на стадіях ініціалізації й завершення виконання програми. Хоча такі об'єкти нормально поводяться при використанні в тім файлі, де вони визначені, але їхнє застосування в ситуації, коли виробляються посилання через границі файлів, стає в C++ серйозною проблемою.
Деструктор не викликається, коли з області видимості виходить посилання або вказівник на об'єкт (сам об'єкт при цьому залишається).
С++ за допомогою внутрішніх механізмів перешкоджає застосуванню оператора delete до вказівника, що не адресує ніякого об'єкта, так що відповідні перевірки коду необов'язкові:
// необов'язково: неявно виконується компілятором
if (pact! = 0) delete pact;
Щораз, коли усередині функції цей оператор застосовується до окремого об'єкта, розміщеному в купі, краще використати об'єкт класу auto_ptr, а не звичайний вказівник. Це особливо важливо тому, що пропущений виклик delete (скажемо, у випадку, коли збуджується виключення) веде не тільки до витоку пам'яті, але й до пропуску виклику деструктора. Нижче приводиться приклад програми, переписаної з використанням auto_ptr (вона злегка модифікована, тому що об'єкт класу auto_ptr може бути явно із для адресації іншого об'єкта тільки присвоюванням його іншому auto_ptr):
#include <memory>
#include "Account. h"
Account global ("James Joyce");
int main ()
{
Account local ("Anna Livia Plurabelle", 10000);
Account &loc_ref = global;
auto_ptr<Account> pact (new Account ("Stephen Dedalus"));
{
Account local_too ("Stephen Hero");
}
// об'єкт auto_ptr знищується тут
}
1.10 Явний виклик деструктора
Іноді викликати деструктор для деякого об'єкта доводиться явно. Особливо часто така необхідність виникає у зв'язку з оператором new. Розглянемо приклад.
Коли ми пишемо:
char *arena = new char [sizeof Image];
то з купи виділяється пам'ять, розмір якої дорівнює розміру об'єкта типу Image, вона не ініціалізована й заповнена випадковими бітами.
Якщо ж написати:
Image *ptr = new (arena) Image ("Quasimodo");
то ніякої нової пам'яті не виділяється. Замість цього змінної ptr привласнюється адреса, асоційованою зі змінною arena. Тепер пам'ять, на яку вказує ptr, інтерпретується як займана об'єктом класу Image, і конструктор застосовується до вже існуючої області. Таким чином, оператор розміщення new () дозволяє сконструювати об'єкт у раніше виділеній області пам'яті.
Закінчивши працювати із зображенням Quasimodo, ми можемо зробити якісь операції із зображенням Esmerelda, розміщеним по тій же адресі arena у пам'яті:
Image *ptr = new (arena) Image ("Esmerelda");
Однак зображення Quasimodo при цьому буде затерто, а ми його модифікували й хотіли б записати на диск. Звичайне збереження виконується в деструкторі класу Image, але якщо ми застосуємо оператор delete:
// погано: не тільки викликає деструктор, але й звільняє пам'ять
delete ptr;
то, крім виклику деструктора, ще й повернемо в купу пам'ять, чого робити не варто було б. Замість цього можна явно викликати деструктор класу Image:
ptr->~Image ();
зберігши відведену під зображення пам'ять для наступного виклику оператора розміщення new.
Відзначимо, що, хоча ptr і arena адресують ту саму область пам'яті в купі, застосування оператора delete до arena
// деструктор не викликається
delete arena;
не приводить до виклику деструктора класу Image, тому що arena має тип char*, а компілятор викликає деструктор тільки тоді, коли операндом в delete є вказівник на об'єкт класу, що має деструктор.
1.11 Небезпека збільшення розміру програми
Вбудований деструктор може стати причиною непередбаченого збільшення розміру програми, оскільки він вставляється в кожній точці виходу всередині функції для кожного активного локального об'єкта. Наприклад, у наступному фрагменті
Account acct ("Tina Lee");
int swt;
// ...
switch (swt) {
case 0:
return;
case 1:
// щось зробити
return;
case 2:
// зробити щось інше
return;
// і так далі
}
компілятор підставить деструктор перед кожною інструкцією return. Деструктор класу Account невеликий, і витрати часу й пам'яті на його підстановку теж малі. У противному випадку прийдеться або об'явити деструктор невбудованим, або реорганізувати програму. У прикладі вище інструкцію return у кожній мітці case можна замінити інструкцією break для того, щоб у функції була єдина точка виходу:
// переписано для забезпечення єдиної точка виходу
switch (swt) {
case 0:
break;
case 1:
// щось зробити
break;
case 2:
// зробити щось інше
break;
// і так далі
}
// єдина точка виходу
return;
1.12 Константні об'єкти й функції-елементи
Ми ще раз особливо відзначаємо принцип найменших привілеїв як один з найбільш фундаментальних принципів створення гарного програмного забезпечення. Розглянемо один зі способів застосування цього принципу до об'єктів.
Деякі об'єкти повинні допускати зміни, інші - ні. Програміст може використовувати ключове слово const для вказівки на те, що об'єкт незмінний - є константним і що будь-яка спроба змінити об'єкт є помилкою. Наприклад,
const Time noon (12, 0, 0);
об'являє як константний об'єкт noon класу Time і присвоює йому початкове значення 12 годин пополудні.
Компілятори C++ сприймають оголошення const настільки неухильно, що в підсумку не допускають ніяких викликів функцій-елементів константних об'єктів (деякі компілятори дають у цих випадках тільки попередження). Це жорстоко, оскільки клієнти об'єктів можливо захочуть використати різні функції-елементи читання "get", а вони, звичайно, не змінюють об'єкт. Щоб обійти це, програміст може оголосити константні функції-елементи; тільки вони можуть оперувати константними об'єктами. Звичайно, константні функції-елементи не можуть змінювати об'єкт - це не дозволить компілятор.
Константна функція вказується як const і в об'яві, і в описі за допомогою ключового слова const після списку параметрів функції, але перед лівою фігурною дужкою, що починає тіло функції. Наприклад, у наведеному нижче прикладі об'являється як константна функція-елемент деякого класу А
int A:: getValue () const {return privateDateMember};
яка просто повертає значення одного з даних-елементів об'єкта. Якщо константна функція-елемент описується поза об'явою класу, то як об'ява функції-елемента, так і її опис повинні включати const.
Тут виникає цікава проблема для конструкторів і деструкторів, які звичайно повинні змінювати об'єкт. Для конструкторів і деструкторів константних об'єктів оголошення const не потрібно. Конструктор повинен мати можливість змінювати об'єкт із метою присвоювання йому відповідних початкових значень. Деструктор повинен мати можливість виконувати підготовку завершення робіт перед знищенням об'єкта.
Програма на мал.4 створює константний об'єкт класу Time і намагається змінити об'єкт не константними функціями-елементами setHour, setMinute і setSecond. Як результат показані згенеровані компілятором Borland C++ попередження.
// TIME5. H
// Оголошення класу Time.
// Функції-елементи описані в TIMES. CPP
#ifndef TIME5_H idefine TIME5_H
class Time { public:
Time (int = 0, int = 0, int = 0); // конструктор за замовчуванням
// функції запису set
void setTime (int, int, int); // установкачасу
void setHour (int); // установкагодин
void setMinute (int); // установкахвилин
void setSecond (int); // установкасекунд
// функції читання get (звичайно об'являється const)
int getHour () const; // повертає значення годин
int getMinute () const; // повертає значення хвилин
int getSecondf) const; // повертає значення секунд
// функції друк (звичайно об'являється const)
void printMilitary () const; // друк військового часу void printStandard () const; // друк стандартного часу
private:
int hour; // 0-23
int minute; // 0-59
int second; // 0-59
};
#endif
// TIME5. CPP
// Опис функцій-елементів класу Time.
finclude <iostream. h>
iinclude "time5. h"
// Функція конструктор для ініціалізації закритих даних. // За замовчуванням значення рівні 0 (дивися опис класу). Time:: Time (int hr, int min, int sec) { setTime (hr, min, sec); }
// Встановка значень години, хвилин і секунд, void Time:: setTime (int h, int m, int s) {
hour = (h >= 0 && h < 24)? h: 0;
minute = (m >= 0 && m < 60)? m: 0;
second = (s >= 0 && s < 60)? s: 0; }
// Установка значення годин
void Time:: setHour (int h) { hour = (h >= 0 && h < 24)? h: 0; }
// Установка значення хвилин void Time:: setMinute (int m)
{ minute = (m >= 0 && m < 60)? m: 0; }
// Установка значення секунд void Time:: setSecond (int s)
{ second = (s >= 0 && s < 60)? s: 0; }
// Читання значення годин
int Time:: getHour () const { return hour; }
// Читання значення хвилин
int Time:: getMinute () const { return minute; }
// Читання значення секунд
int Time:: getSecond () const { return second; }
// Відображення часу у військовому форматі: HH: MM: SS
void Time:: printMilitary () const
{
cout " (hour < 10?"0": "")" hour " ": "
" (minute < 10?"0": "")" minute" ": "
" (second < 10?"0": "")" second; }
// Відображення часу в стандартному форматі: HH: MM: SS AM // (або РМ)
void Time:: printStandard () const {
cout " ( (hour == 12)? 12: hour% 12)" ": "
" (minute < 10?"0": "")" minute " ": " " (second < 10?"0": "")" second " (hour< 12?"AM": "PM"); }
// FIG7_1. CPP
// Спроба одержати доступ до константного об'єкта
// з не-константними функціями-елементами.
#include <iostream. h>
#include "time5. h"
main () {
const Time t (19, 33, 52); // константний об'єкт
t. setHour (12); // ПОМИЛКА: не-константна функція елемент t. setMinute (20); // ПОМИЛКА: не-константна функція елемент t. setSecond (39); // ПОМИЛКА: не-константна функція елемент
return 0; }
Compiling FIG7_1. CPP:
Warning FIG7_1. CPP: Non-const function
Time:: setHour (int) called for const object Warning FXG7 l. CPP: Non-const function
Time:: setMinute (int) callers for const object Warning FIG7 1. CPP: Non-const function
Time:: setSecond (int) called for const object
Мал.4. Використання класу Time з константними об'єктами й константними функціями-елементами
Зауваження: Константна функція-елемент може бути перевантажена неконстантним варіантом. Вибір того, яка з перевантажених функцій-елементів буде використатися, виконується компілятором автоматично залежно від того, був об'явлений об'єкт як const чи ні.
Константный об'єкт не може бути змінений за допомогою присвоювання, так що він повинен мати початкове значення. Якщо дані-елементи класу об'явлені як const, то треба використати ініціалізатор елементів, щоб забезпечити конструктор об'єкта цього класу початковими значенням даних-елементів. Мал.7 демонструє використання ініціалізатора елементів для завдання початкового значення константному елементу increment класу Increment. Конструктор для Increment змінюється в такий спосіб:
Increment:: Increment (int c, int i): increment (i) { count = c; }
Запис: increment (i) викликає завдання початкового значення елемента increment, рівного i. Якщо необхідно задати початкові значення відразу декільком елементам, просто включіть їх у список після двокрапки, розділяючи комами. Використовуючи ініціатори елементів, можна присвоїти початкові значення всім даним-елементам.
// Використання ініціалізатора елементів для
// ініціалізації даних константного вбудованого типу.
#include <iostream. h>
class Increment { public:
Increment (int з = 0, int i = 1);
void addlncrement () { count += increment; }
void print () const;
private:
int count;
const int increment; // константний елемент даних };
// Конструктор класу Increment Increment:: Increment (int c, int i)
: increment (i) // ініціали затор константного елемента
{ count = с; }
// друк даних
void Increment:: print () const
{
cout << "count = " << count
"", increment = " " increment << endl; }
main ()
{
Increment value (10,5);
cout << "Перед збільшенням: "; value. print ();
for (int j = 1; j <= 3;) }
value. addlncrement ();
cout << "Після збільшення " << j "": "; value. print ();
}
return 0; }
Перед збільшенням: count = 10, increment = 5
Після збільшення 1: count = 15, increment = 5
Після збільшення 2: count = 20, increment = 5
Після збільшення 3: count = 25, increment = 5
Мал.7. Використання ініціалізаторів елементів для ініціалізації даних константного типу убудованого типу
1.13 Друзі
Нехай визначені два класи: vector (вектор) і matrix (матриця). Кожний з них приховує своє подання даних, але дає повний набір операцій для роботи з об'єктами його типу. Допустимо, треба визначити функцію, що множить матрицю на вектор. Для простоти припустимо, що вектор має чотири елементи з індексами від 0 до 3, а в матриці чотири вектори теж з індексами від 0 до 3. Доступ до елементів вектора забезпечується функцією elem (), і аналогічна функція є для матриці. Можна визначити глобальну функцію multiply (помножити) у такий спосіб:
vector multiply (const matrix& m, const vector& v);
{
vector r;
for (int i = 0; i<3; i++) { // r [i] = m [i] * v;
r. elem (i) = 0;
for (int j = 0; j<3; j++)
r. elem (i) +=m. elem (i,j) * v. elem (j);
}
return r;
}
Це цілком природнє рішення, але воно може виявитися дуже неефективним. При кожному виклику multiply () функція elem () буде викликатися 4* (1+4*3) раз. Якщо в elem () проводиться контроль границь масиву, то на такий контроль буде витрачено значно більше часу, ніж на виконання самої функції, і в результаті вона виявиться непридатної для користувачів. З іншого боку, якщо elem () є якийсь спеціальний варіант доступу без контролю, то тим самим ми засмічуємо інтерфейс із вектором і матрицею особливою функцією доступу, що потрібна тільки для обходу контролю.
Якщо можна було б зробити multiply членом обох класів vector і matrix, ми могли б обійтися без контролю індексу при звертанні до елемента матриці, але в той же час не вводити спеціальної функції elem (). Однак, функція не може бути членом двох класів. Треба мати в мові можливість надавати функції, що не є членом, право доступу до приватних членів класу. Функція - не член класу, але має доступ до його закритої частини, називається другом цього класу. Функція може стати другом класу, якщо в його описі вона описана як friend (друг). Наприклад:
class matrix;
class vector {
float v [4];
// ...
friend vector multiply (const matrix&, const vector&);
};
class matrix {
vector v [4];
// ...
friend vector multiply (const matrix&, const vector&);
};
Функція-друг не має ніяких особливостей, за винятком права доступу до закритої частини класу. Зокрема, у такій функції не можна використати вказівник this, якщо тільки вона дійсно не є членом класу. Опис friend є дійсним описом. Воно вводить ім'я функції в область видимості класу, у якому вона була описана, і при цьому відбуваються звичайні перевірки на наявність інших описів такого ж імені в цій області видимості. Опис friend може перебуває як у загальній, так і в приватній частинах класу, це не має значення.
Тепер можна написати функцію multiply, використовуючи елементи вектора й матриці безпосередньо:
vector multiply (const matrix& m, const vector& v)
{
vector r;
for (int i = 0; i<3; i++) { // r [i] = m [i] * v;
r. v [i] = 0;
for (int j = 0; j<3; j++)
r. v [i] +=m. v [i] [j] * v. v [j];
}
return r;
}
Відзначимо, що подібно функції-члену дружня функція явно описується в описі класу, з яким дружить. Тому вона є невід'ємною частиною інтерфейсу класу нарівні з функцією-членом.
Функція-член одного класу може бути другом іншого класу:
class x {
// ...
void f ();
};
class y {
// ...
friend void x:: f ();
};
Цілком можливо, що всі функції одного класу є друзями іншого класу. Для цього є коротка форма запису:
class x {
friend class y;
// ...
};
У результаті такого опису всі функції-члени y стають друзями класу x.
1.14 Ядро ООП: Успадкування та поліморфізм
Ця глава присвячена поняттю похідного класу. Похідні класи - це простий, гнучкий і ефективний засіб визначення класу. Нові можливості додаються до вже існуючого класу, не вимагаючи його перепрограмування або перетрансляції. За допомогою похідних класів можна організувати загальний інтерфейс із декількома різними класами так, що в інших частинах програми можна буде одноманітно працювати з об'єктами цих класів. Вводиться поняття віртуальної функції, що дозволяє використати об'єкти належним чином навіть у тих випадках, коли їхній тип на стадії трансляції невідомий. Основне призначення похідних класів - спростити програмістові завдання вираження спільності класів.
1.4.1 Похідні класи
Обговоримо, як написати програму обліку службовців деякої фірми. У ній може використатися, наприклад, така структура даних:
struct employee { // службовець
char* name; // ім'я
short age; // вік
short department; // відділ
int salary; // оклад
employee* next;
// ...
};
Поле next потрібно для зв'язування в список записів про службовців одного відділу (employee). Тепер спробуємо визначити структуру даних для керуючого (manager):
struct manager {
employee emp; // запис employee для керуючого
employee* group; // підлеглий колектив
short level;
// ...
};
Керуючий також є службовцем, тому запис employee зберігається в члені emp об'єкта manager. Для людини ця спільність очевидна, але для транслятора член emp нічим не відрізняється від інших членів класу. Вказівник на структуру manager (manager*) не є вказівником на employee (employee*), тому не можна вільно використати один замість іншого. Зокрема, без спеціальних дій не можна об'єкт manager включити до списку об'єктів типу employee. Доведеться або використати явне приведення типу manager*, або в список записів employee включити адресу члена emp. Обоє рішень некрасиві й можуть бути досить заплутаними. Правильне рішення полягає в тому, щоб тип manager був типом employee з деякою додатковою інформацією:
struct manager: employee {
employee* group;
short level;
// ...
};
Клас manager є похідним від employee, і, навпаки, employee є базовим класом для manager. Крім члена group у класі manager є члени класу employee (name, age і т.д.). Графічно відношення спадкування звичайно зображується у вигляді стрілки від похідних класів до базового:
employee
manager
Звичайно говорять, що похідний клас успадковує базовий клас, тому й відношення між ними називається успадкуванням. Іноді базовий клас називають суперкласом, а похідний - підлеглим класом. Але ці терміни можуть викликати здивування, оскільки об'єкт похідного класу містить об'єкт свого базового класу. Взагалі похідний клас більше свого базового в тому розумінні, що в ньому утримується більше даних і визначено більше функцій.
Маючи визначення employee і manager, можна створити список службовців, частина з яких є й керуючими:
void f ()
{
manager m1, m2;
employee e1, e2;
employee* elist;
elist = &m1; // помістити m1 в elist
m1. next = &e1; // помістити e1 в elist
e1. next = &m2; // помістити m2 в elist
m2. next = &e2; // помістити m2 в elist
e2. next = 0; // кінець списку
}
Оскільки керуючий є також службовцем, вказівник manager* можна використати як employee*. У той же час службовець не обов'язково є керуючим, і тому employee* не можна використати як manager*.
У загальному випадку, якщо клас derived має загальний базовий клас base, то вказівник на derived можна без явних перетворень типу привласнювати змінній, що має тип вказівника на base. Зворотне перетворення від вказівника на base до вказівника на derived може бути тільки явним:
void g ()
{
manager mm;
employee* pe = &mm; // нормально
employee ee;
manager* pm = ⅇ // помилка:
// не всякий службовець є керуючим
pm->level = 2; // катастрофа: при розміщенні ee
// пам'ять для члена 'level' не виділялася
pm = (manager*) pe; // нормально: насправді pe
// не настроєно на об'єкт mm типу manager
pm->level = 2; // відмінно: pm указує на об'єкт mm
// типу manager, а в ньому при розміщенні
// виділена пам'ять для члена 'level'
}
Іншими словами, якщо робота з об'єктом похідного класу йде через вказівник, то його можна розглядати як об'єкт базового класу. Зворотне невірно. Відзначимо, що у звичайній реалізації С++ не передбачається динамічного контролю над тим, щоб після перетворення типу, подібного тому, що використовувалося в присвоюванні pe в pm, отриманий у результаті вказівник дійсно був налаштований на об'єкт необхідного типу.
1.14.2 Функції-члени
Прості структури даних начебто employee і manager самі по собі не занадто цікаві, а часто й не дуже корисні. Тому додамо до них функції:
class employee {
char* name;
// ...
public:
employee* next; // перебуває в загальній частині, щоб
// можна було працювати зі списком
void print () const;
// ...
};
class manager: public employee {
// ...
public:
void print () const;
// ...
};
Треба відповісти на деякі питання. Яким чином функція-член похідного класу manager може використати члени базового класу employee? Які члени базового класу employee можуть використати функції-члени похідного класу manager? Які члени базового класу employee може використати функція, що не є членом об'єкта типу manager? Які відповіді на ці питання повинна давати реалізація мови, щоб вони максимально відповідали завданню програміста?
Розглянемо приклад:
void manager:: print () const
{
cout << " ім'я " << name << '\n';
}
Член похідного класу може використати ім'я із загальної частини свого базового класу нарівні з усіма іншими членами, тобто без вказівки імені об'єкта. Передбачається, що є об'єкт, на який настроєний this, тому коректним звертанням до name буде this->name. Однак, при трансляції функції manager:: print () буде зафіксована помилка: члену похідного класу не надане право доступу до приватних членів його базового класу, значить name недоступно в цій функції.
Можливо багатьом це здасться дивним, але давайте розглянемо альтернативне рішення: функція-член похідного класу має доступ до приватних членів свого базового класу. Тоді саме поняття частки (закритого) члена втрачає всякий зміст, оскільки для доступу до нього досить просто визначити похідний клас. Тепер уже буде недостатньо для з'ясування, хто використає приватні члени класу, переглянути всі функції-члени й друзів цього класу. Прийдеться переглянути всі вихідні файли програми, знайти похідні класи, потім досліджувати кожну функцію цих класів. Далі треба знову шукати похідні класи від уже знайдених і т.д. Це, принаймні, утомливо, а швидше за все нереально. Потрібно всюди, де це можливо, використати замість приватних членів захищені (protected).
Як правило, саме надійне рішення для похідного класу - використати тільки загальні члени свого базового класу:
void manager:: print () const
{
employee:: print (); // друк даних про службовців
// друк даних про керуючих
}
Відзначимо, що операція:: необхідна, оскільки функція print () перевизначена в класі manager. Таке повторне використання імен типово для С++. Необережний програміст написав би:
void manager:: print () const
{
print (); // печатка даних про службовців
// печатка даних про керуючих
}
У результаті він одержав би рекурсивну послідовність викликів manager:: print ().
1.14.3 Конструктори й деструктори
Для деяких похідних класів потрібні конструктори. Якщо конструктор є в базовому класі, то саме він і повинен викликатися із вказівкою параметрів, якщо такі в нього є:
class employee {
// ...
public:
// ...
employee (char* n, int d);
};
class manager: public employee {
// ...
public:
// ...
manager (char* n, int i, int d);
};
Параметри для конструктора базового класу задаються у визначенні конструктора похідного класу. У цьому змісті базовий клас виступає як клас, що є членом похідного класу:
manager:: manager (char* n, int l, int d)
: employee (n,d), level (l), group (0)
{
}
Конструктор базового класу employee:: employee () може мати таке визначення:
employee:: employee (char* n, int d)
: name (n), department (d)
{
next = list;
list = this;
}
Тут list повинен бути описаний як статичний член employee.
Об'єкти класів створюються знизу вверх: спочатку базові, потім члени й, нарешті, самі похідні класи. Знищуються вони у зворотному порядку: спочатку самі похідні класи, потім члени, а потім базові. Члени й базові створюються в порядку опису їх у класі, а знищуються вони у зворотному порядку.
1.14.4 Ієрархія класів
Похідний клас сам у свою чергу може бути базовим класом:
class employee {/*... */ };
class manager: public employee {/*... */ };
class director: public manager {/*... */ };
Така безліч зв'язаних між собою класів звичайно називають ієрархією класів. Звичайно вона представляється деревом, але бувають ієрархії з більш загальною структурою у вигляді графа:
class temporary {/*... */ };
class secretary: public employee {/*... */ };
class tsec
: public temporary, public secretary { /*... */ };
class consultant
: public temporary, public manager { /*... */ };
Бачимо, що класи в С++ можуть утворювати спрямований ациклічний граф.
1.14.5 Поля типу
Щоб похідні класи були не просто зручною формою короткого опису, у реалізації мови повинно бути вирішено питання: якому з похідних класів ставиться об'єкт, на який дивиться вказівник base*? Існує три основних способи відповіді:
[1] Забезпечити, щоб вказівник міг посилатися на об'єкти тільки одного типу;
[2] Помістити в базовий клас поле типу, що зможе перевіряти функції;
[3] використати віртуальні функції.
Вказівники на базові класи, звичайно, використаються при проектуванні контейнерних класів (вектор, список і т.д.). Тоді у випадку [1] ми одержимо однорідні списки, тобто списки об'єктів одного типу.
Способи [2] і [3] дозволяють створювати різнорідні списки, тобто списки об'єктів декількох різних типів (насправді, списки вказівників на ці об'єкти).
Спосіб [3] - це спеціальний надійний у сенсі типу варіант спосіб [2]. Особливо цікаві й потужні варіанти дають комбінації способів [1] і [3].
Спочатку обговоримо простий спосіб з полем типу, тобто спосіб [2]. Приклад із класами manager/employee можна перевизначити так:
struct employee {
enum empl_type { M, E };
empl_type type;
employee* next;
char* name;
short department;
// ...
};
struct manager: employee {
employee* group;
short level;
// ...
};
Маючи ці визначення, можна написати функцію, що друкує дані про довільного службовця:
void print_employee (const employee* e)
{
switch (e->type) {
case E:
cout << e->name << '\t' << e->department << '\n';
// ...
break;
case M:
cout << e->name << '\t' << e->department << '\n';
// ...
manager* p = (manager*) e;
cout << "level" << p->level << '\n';
// ...
break;
}
}
Надрукувати список службовців можна так:
void f (const employee* elist)
{
for (; elist; elist=elist->next) print_employee (elist);
}
Це цілком гарне рішення, особливо для невеликих програм, написаних однією людиною, але воно має істотний недолік: транслятор не може перевірити, наскільки правильно програміст поводиться з типами. У більших програмах це приводить до помилок двох видів. Перша - коли програміст забуває перевірити поле типу. Друга - коли в перемикачі вказуються не всі можливі значення поля типу. Цих помилок досить легко уникнути в процесі написання програми, але зовсім нелегко уникнути їх при внесенні змін у нетривіальну програму, а особливо, якщо це велика програма, написана кимось іншим. Ще сутужніше уникнути таких помилок тому, що функції типу print () часто пишуться так, щоб можна було скористатися спільністю класів:
void print (const employee* e)
{
cout << e->name << '\t' << e->department << '\n';
// ...
if (e->type == M) {
manager* p = (manager*) e;
cout << "level" << p->level << '\n';
// ...
}
}
Оператори if, подібні наведеним у прикладі, складно знайти у великій функції, що працює з багатьма похідними класами. Але навіть коли вони знайдені, нелегко зрозуміти, що відбувається насправді. Крім того, при всякім додаванні нового виду службовців потрібні зміни у всіх важливих функціях програми, тобто функціях, що перевіряють поле типу. У результаті доводиться правити важливі частини програми, збільшуючи тим самим час на налагодження цих частин.
Іншими словами, використання поля типу чревате помилками й труднощами при супроводі програми. Труднощі різко зростають по мірі росту програми, адже використання поля типу суперечить принципам модульності й приховування даних. Кожна функція, що працює з полем типу, повинна знати подання й специфіку реалізації всякого класу, котрий є похідним для класу, що містить поле типу.
1.14.6 Віртуальні функції
За допомогою віртуальних функцій можна перебороти труднощі, що виникають при використанні поля типу. У базовому класі описуються функції, які можуть перевизначатися в будь-якому похідному класі. Транслятор і завантажник забезпечать правильну відповідність між об'єктами й застосовуваними до них функціями:
class employee {
char* name;
short department;
// ...
employee* next;
static employee* list;
public:
employee (char* n, int d);
// ...
static void print_list ();
virtual void print () const;
};
Службове слово virtual (віртуальна) показує, що функція print () може мати різні версії в різних похідних класах, а вибір потрібної версії при виклику print () - це завдання транслятора. Тип функції вказується в базовому класі й не може бути перевизначений у похідному класі. Визначення віртуальної функції повинне даватися для того класу, у якому вона була вперше описана (якщо тільки вона не є чисто віртуальною функцією). Наприклад:
void employee:: print () const
{
cout << name << '\t' << department << '\n';
// ...
}
Ми бачимо, що віртуальну функцію можна використати, навіть якщо немає похідних класів від її класу. У похідному ж класі не обов'язково перевизначити віртуальну функцію, якщо вона там не потрібна. При побудові похідного класу треба визначати тільки ті функції, які в ньому дійсно потрібні:
class manager: public employee {
employee* group;
short level;
// ...
public:
manager (char* n, int d);
// ...
void print () const;
};
Місце функції print_employee () зайняли функції-члени print (), і вона стала не потрібна. Список службовців будує конструктор employee. Надрукувати його можна так:
void employee:: print_list ()
{
for (employee* p = list; p; p=p->next) p->print ();
}
Дані про кожного службовця будуть друкуватися відповідно до типу запису про нього. Тому програма
int main ()
{
employee e ("J. Brown",1234);
manager m ("J. Smith",2,1234);
employee:: print_list ();
}
надрукує
J. Smith 1234
level 2
J. Brown 1234
Зверніть увагу, що функція друку буде працювати навіть у тому випадку, якщо функція employee_list () була написана й трансльована ще до того, як був задуманий конкретний похідний клас manager! Очевидно, що для правильної роботи віртуальної функції потрібно в кожному об'єкті класу employee зберігати деяку службову інформацію про тип. Як правило, реалізація як така інформація використовується просто вказівник. Цей вказівник зберігається тільки для об'єктів класу з віртуальними функціями, але не для об'єктів всіх класів, і навіть для не для всіх об'єктів похідних класів. Додаткова пам'ять виділяється тільки для класів, у яких описані віртуальні функції. Помітимо, що при використанні поля типу, для нього однаково потрібна додаткова пам'ять.
Якщо у виклику функції явно зазначена операція дозволу області видимості::, наприклад, у виклику manager:: print (), то механізм виклику віртуальної функції не діє. Інакше подібний виклик привів би до нескінченної рекурсії. Уточнення імені функції дає ще один позитивний ефект: якщо віртуальна функція є підстановкою (у цьому немає нічого незвичайного), те у виклику з операцією:: відбувається підстановка тіла функції. Це ефективний спосіб виклику, якому можна застосовувати у важливих випадках, коли одна віртуальна функція звертається до іншої з тим самим об'єктом. Приклад такого випадку - виклик функції manager:: print (). Оскільки тип об'єкта явно задається в самому виклику manager:: print (), немає потреби визначати його в динаміку для функції employee:: print (), що і буде викликатися.
1.14.7 Абстрактні класи
Багато класів подібні із класом employee тим, що в них можна дати розумне визначення віртуальним функціям. Однак, є й інші класи. Деякі, наприклад, клас shape, представляють абстрактне поняття (фігура), для якого не можна створити об'єкти. Клас shape набуває сенсу тільки як базовий клас у деякому похідному класі. Причиною є те, що неможливо дати осмислене визначення віртуальних функцій класу shape:
class shape {
// ...
public:
virtual void rotate (int) { error ("shape:: rotate"); }
virtual void draw () { error ("shape:: draw"): }
// не можна не обертати, не малювати абстрактну фігуру
// ...
};
Створення об'єкта типу shape (абстрактної фігури) законна, хоча зовсім безглузда операція:
shape s; // нісенітниця: ''фігура взагалі''
Вона безглузда тому, що будь-яка операція з об'єктом s приведе до помилки.
Краще віртуальні функції класу shape описати як чисто віртуальні. Зробити віртуальну функцію чисто віртуальної можна, додавши ініціалізатор = 0:
class shape {
// ...
public:
virtual void rotate (int) = 0; // чисто віртуальна функція
virtual void draw () = 0; // чисто віртуальна функція
};
Клас, у якому є віртуальні функції, називається абстрактним. Об'єкти такого класу створити не можна:
shape s; // помилка: змінна абстрактного класу shape
Абстрактний клас можна використати тільки в якості базового для іншого класу:
class circle: public shape {
int radius;
public:
void rotate (int) { } // нормально:
// перевизначення shape:: rotate
void draw (); // нормально:
// перевизначення shape:: draw
circle (point p, int r);
};
Якщо чиста віртуальна функція не визначається в похідному класі, то вона й залишається такою, а значить похідний клас теж є абстрактним. При такому підході можна реалізовувати класи поетапно:
class X {
public:
virtual void f () = 0;
virtual void g () = 0;
};
X b; // помилка: опис об'єкта абстрактного класу X
class Y: public X {
void f (); // перевизначення X:: f
};
Y b; // помилка: опис об'єкта абстрактного класу Y
Подобные документы
Концепції об'єктно-орієнтованого програмування. Методи створення класів. Доступ до методів базового класу. Структура даних, функції. Розробка додатку на основі діалогових вікон, програми меню. Засоби розробки програмного забезпечення мовами Java та С++.
курсовая работа [502,5 K], добавлен 01.04.2016Об’єктно-орієнтоване програмування мовою С++. Основні принципи об’єктно-орієнтованого програмування. Розробка класів з використанням технології візуального програмування. Розробка класу classProgressBar. Базовий клас font. Методи тестування програми.
курсовая работа [211,3 K], добавлен 19.08.2010Поняття абстрактного класу в об’єктно-орієнтованому програмуванні. Описання класу pruzhyna, створення консольної програми для обчислення коефіцієнта передачі пружини стискання з циліндричної проволоки круглого перетину за заданою формулою і параметрами.
курсовая работа [100,7 K], добавлен 25.11.2011Поняття та види векторів. Прості математичні операції над ними. Векторний добуток, його геометричні та алгебраїчні властивості. Визначення та реалізація програмного класу багатовимірних векторів. Перевантажені оператори та дружні оператор-функції.
курсовая работа [110,1 K], добавлен 15.01.2012Розробка методу-члену класу для створення нового одновимірного масиву з кількості всіх негативних елементів кожного рядка заданого двовимірного динамічного масиву. Особливість виводу змісту масиву на екран. Аналіз перевірки правильності роботи програми.
лабораторная работа [131,2 K], добавлен 18.11.2021Редагування за допомогою текстового редактора NotePad вхідного файлу даних. Програмна реалізація основного алгоритму з використанням засобів об'єктно-орієнтованого програмування. Об’ява та опис класів і об'єктів. Розробка допоміжних програмних засобів.
курсовая работа [69,4 K], добавлен 14.03.2013Принципи об'єктно-орієнтованого підходу. Розробка програмного комплексу з використанням цього алгоритму і користувальницьких класів на мові програмування С++. Реалізація простого відкритого успадкування. Тестування працездатності системи класів.
курсовая работа [98,0 K], добавлен 06.05.2014Визначення сили взаємодії двох точкових зарядів. С++ як універсальна мова програмування. Клас - ключове поняття С++. Стандартні бібліотеки С++. Функція конструктора і деструктора. Опис базового класу Objcalc. Лістинг та результат роботи програми.
курсовая работа [107,6 K], добавлен 27.12.2011Реалізація операцій ініціалізації (завдання початкових властивостей), запису класу у файл та читання класу з файлу. Функції інформаційної системи "Фермерське господарство", модулі створення та перегляду файлу, додавання даних в файл, пошуку даних.
курсовая работа [4,9 M], добавлен 16.03.2012Розробка програми на мові програмування С++ з використанням об’єктно-орієнтованого програмування, яка включає в себе роботу з файлами, класами, обробку числової інформації і роботу з графікою. Структура класів і об’єктів. Лістинг та алгоритм програми.
курсовая работа [104,4 K], добавлен 14.03.2013