Розробка власного класу STRING

Поняття об’єктно-орієнтовного програмування, поява відносин класів. Структури як складені типи даних, побудовані з використанням інших типів. Область дії класу і доступ до його елементів. Конструктор та деструктор класу. Розробка базового класу STRING.

Рубрика Программирование, компьютеры и кибернетика
Вид курсовая работа
Язык украинский
Дата добавления 19.08.2010
Размер файла 280,9 K

Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже

Студенты, аспиранты, молодые ученые, использующие базу знаний в своей учебе и работе, будут вам очень благодарны.

class Z: public Y {

void g (); // перевизначення X:: g

};

Z c; // нормально

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

class character_device {

public:

virtual int open () = 0;

virtual int close (const char*) = 0;

virtual int read (const char*, int) =0;

virtual int write (const char*, int) = 0;

virtual int ioctl (int. .) = 0;

// ...

};

Дійсні драйвери будуть визначатися як похідні від класу character_device.

1.14.8 Множинне входження базового класу

Можливість мати більше одного базового класу спричиняє можливість кількаразового входження класу як базового. Припустимо, класи task і displayed є похідними класу link, тоді в satellite (зроблений на їх основі) він буде входити двічі:

class task: public link {

// link використається для зв'язування всіх

// завдань у список (список диспетчера)

// ...

};

class displayed: public link {

// link використається для зв'язування всіх

// зображуваних об'єктів (список зображень)

// ...

};

Але проблем не виникає. Два різних об'єкти link використаються для різних списків, і ці списки не конфліктують один з одним. Звичайно, без ризику неоднозначності не можна звертатися до членів класу link, але як це зробити коректно, показано в наступному розділі. Графічно об'єкт satellite можна представити так:

Але можна привести приклади, коли загальний базовий клас не повинен представлятися двома різними об'єктами.

1.14.9 Вирішення неоднозначності

Природно, у двох базових класів можуть бути функції-члени з однаковими іменами:

class task {

// ...

virtual debug_info* get_debug ();

};

class displayed {

// ...

virtual debug_info* get_debug ();

};

При використанні класу satellite подібна неоднозначність функцій повинна бути дозволена:

void f (satellite* sp)

{

debug_info* dip = sp->get_debug (); // помилка: неоднозначність

dip = sp->task:: get_debug (); // нормально

dip = sp->displayed:: get_debug (); // нормально

}

Однак, явний дозвіл неоднозначності клопітно, тому для її усунення найкраще визначити нову функцію в похідному класі:

class satellite: public task, public derived {

// ...

debug_info* get_debug ()

{

debug_info* dip1 = task: get_debug ();

debug_info* dip2 = displayed:: get_debug ();

return dip1->merge (dip2);

}

};

Тим самим локалізується інформація з базових для satellite класів. Оскільки satellite:: get_debug () є перевизначенням функцій get_debug () з обох базових класів, гарантується, що саме вона буде викликатися при всякім звертанні до get_debug () для об'єкта типу satellite.

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

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

void manager:: print ()

{

employee:: print ();

// ...

}

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

class employee {

// ...

virtual void print ();

};

class foreman: public employee {

// ...

void print ();

};

class manager: public foreman {

// ...

void print ();

};

Тепер функція foreman:: print () не буде викликатися, хоча майже напевно передбачався виклик саме цієї функції. За допомогою невеликої хитрості можна перебороти ці труднощі:

class foreman: public employee {

typedef employee inherited;

// ...

void print ();

};

class manager: public foreman {

typedef foreman inherited;

// ...

void print ();

};

void manager:: print ()

{

inherited:: print ();

// ...

}

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

1.14.10 Віртуальні базові класи

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

Іноді застосування множинного спадкування припускає досить тісний зв'язок між класами, які розглядаються як "братні" базові класи. Такі класи-брати звичайно повинні проектуватися спільно. У більшості випадків для цього не потрібен особливий стиль програмування, що істотно відрізняється від того, котрий ми тільки що розглядали. Просто на похідний клас покладається деяка додаткова робота. Звичайно вона зводиться до перевизначення однієї або декількох віртуальних функцій. У деяких випадках класи-брати повинні мати загальну інформацію. Оскільки С++ - мову зі строгим контролем типів, спільність інформації можлива тільки при явній вказівці того, що є загальним у цих класах. Способом такої вказівки може служити віртуальний базовий клас.

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

class window {

// головна інформація

virtual void draw ();

};

Для простоти розглянемо тільки один вид загальної інформації із класу window - функцію draw (). Можна визначати різні більше розвинені класи, що представляють вікна (window). У кожному визначається своя (більше розвинена) функція малювання (draw):

class window_w_border: public virtual window {

// клас "вікно з рамкою"

// визначення, пов'язані з рамкою

void draw ();

};

class window_w_menu: public virtual window {

// клас "вікно з меню"

// визначення, пов'язані з меню

void draw ();

};

Тепер хотілося б визначити вікно з рамкою й меню:

class Clock: public virtual window,

public window_w_border,

public window_w_menu {

// клас "вікно з рамкою й меню"

void draw ();

};

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

Можна в такий спосіб зобразити состав об'єкта класу window_w_border_and_menu:

Щоб побачити різницю між звичайним і віртуальним спадкуванням, зрівняєте цей малюнок з малюнком, що показує состав об'єкта класу satellite. У графі спадкування кожний базовий клас із даним ім'ям, що був зазначений як віртуальний, буде представлений єдиним об'єктом цього класу. Навпроти, кожний базовий клас, що при описі спадкування не був зазначений як віртуальний, буде представлений своїм власним об'єктом.

Тепер треба написати всі ці функції draw (). Це не занадто важко, але для необережного програміста тут є пастка. Спочатку підемо найпростішим шляхом, що саме до неї й веде:

void window_w_border:: draw ()

{

window:: draw ();

// малюємо рамку

}

void window_w_menu:: draw ()

{

window:: draw ();

// малюємо меню

}

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

void clock:: draw () // пастка!

{

window_w_border:: draw ();

window_w_menu:: draw ();

// тепер операції, що ставляться тільки

// до вікна з рамкою й меню

}

На перший погляд все цілком нормально. Як звичайно, спочатку виконуються всі операції, необхідні для базових класів, а потім ті, які ставляться властиво до похідних класів. Але в результаті функція window:: draw () буде викликатися двічі! Для більшості графічних програм це не просто зайвий виклик, а псування картинки на екрані. Звичайно друга видача на екран затирає першу.

Щоб уникнути пастки, треба діяти не так поспішно. Ми відокремимо дії, виконувані базовим класом, від дій, виконуваних з базового класу. Для цього в кожному класі введемо функцію _draw (), що виконує потрібні тільки для нього дії, а функція draw () буде виконувати ті ж дії плюс дії, потрібні для кожного базового класу. Для класу window зміни зводяться до введення зайвої функції:

class window {

// головна інформація

void _draw ();

void draw ();

};

Для похідних класів ефект той же:

class window_w_border: public virtual window {

// клас "вікно з рамкою"

// визначення, пов'язані з рамкою

void _draw ();

void draw ();

};

void window_w_border:: draw ()

{

window:: _draw ();

_draw (); // малює рамку

};

Тільки для похідного класу наступного рівня проявляється відмінність функції, що і дозволяє обійти пастку з повторним викликом window:: draw (), оскільки тепер викликається window:: _draw () і тільки один раз:

class clock

: public virtual window,

public window_w_border,

public window_w_menu {

void _draw ();

void draw ();

};

void clock:: draw ()

{

window:: _draw ();

window_w_border:: _draw ();

window_w_menu:: _draw ();

_draw (); // тепер операції, що ставляться тільки

// до вікна з рамкою й меню

}

Не обов'язково мати обидві функції window:: draw () і window:: _draw (), але наявність їх дозволяє уникнути різних простих описок.

У цьому прикладі клас window служить сховищем загальної для window_w_border і window_w_menu інформації й визначає інтерфейс для спілкування цих двох класів.

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

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

Істотно, щоб у кожному із класів-братів перевизначалися функції, певні в загальному віртуальному базовому класі. У такий спосіб кожний із братів може одержати свій варіант операцій, відмінний від інших. Нехай у класі window є загальна функція уведення get_input ():

class window {

// головна інформація

virtual void draw ();

virtual void get_input ();

};

В одному з похідних класів можна використати цю функцію, не замислюючись про те, де вона визначена:

class window_w_banner: public virtual window {

// клас "вікно із заголовком"

void draw ();

void update_banner_text ();

};

void window_w_banner:: update_banner_text ()

{

// ...

get_input ();

// змінити текст заголовка

}

В іншому похідному класі функцію get_input () можна визначати, не замислюючись про те, хто її буде використати:

class window_w_menu: public virtual window {

// клас "вікно з меню"

// визначення, пов'язані з меню

void draw ();

void get_input (); // перевизначає window:: get_input ()

};

Всі ці визначення збираються разом у похідному класі наступного рівня:

class clock

: public virtual window,

public window_w_banner,

public window_w_menu

{

void draw ();

};

Контроль неоднозначності дозволяє переконатися, що в класах-братах визначені різні функції:

class window_w_input: public virtual window {

// ...

void draw ();

void get_input (); // перевизначає window:: get_input

};

class clock

: public virtual window,

public window_w_input,

public window_w_menu

{ // помилка: обидва класи window_w_input і

// window_w_menu перевизначають функцію

// window:: get_input

void draw ();

};

Транслятор виявляє подібну помилку, а усунути неоднозначність можна звичайним способом: ввести в класи window_w_input і window_w_menu функцію, що перевизначає "функції-порушника", і якимось чином усунути неоднозначність:

class window_w_input_and_menu

: public virtual window,

public window_w_input,

public window_w_menu

{

void draw ();

void get_input ();

};

У цьому класі window_w_input_and_menu:: get_input () буде перевизначати всі функції get_input ().

1.14.11 Контроль доступу

Член класу може бути приватним (private), захищеним (protected) або загальним (public):

Приватний член класу X можуть використати тільки функції-члени й друзі класу X.

Захищений член класу X можуть використати тільки функції-члени й друзі класу X, а так само функції-члени й друзі всіх похідних від X класів.

Загальний член можна використати в будь-якій функції.

Ці правила відповідають розподілу функцій, що звертаються до класу, на три види: функції, що реалізують клас (його друзі й члени), функції, що реалізують похідний клас (друзі й члени похідного класу) і всі інші функції.

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

class X {

private:

enum { A, B };

void f (int);

int a;

};

void X:: f (int i)

{

if (i<A) f (i+B);

a++;

}

void g (X& x)

{

int i = X:: A; // помилка: X:: A приватний член

x. f (2); // помилка: X:: f приватний член

x. a++; // помилка: X:: a приватний член

}

1.14.12 Захищені члени

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

З іншого боку, функції draw () призначалися для загального застосування.

Це розходження можна виразити, розбивши інтерфейси класів window на дві частини - захищений інтерфейс і загальний інтерфейс:

class window {

public:

virtual void draw ();

// ...

protected:

void _draw ();

// інші функції, що служать будівельним матеріалом

private:

// подання класу

};

Така розбивка можна проводити й у похідних класах, таких, як window_w_border або window_w_menu.

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

От менш практичний, але більше докладний приклад:

class X {

// за замовчуванням приватна частина класу

int priv;

protected:

int prot;

public:

int publ;

void m ();

};

Для члена X:: m доступ до членів класу необмежений:

void X:: m ()

{

priv = 1; // нормально

prot = 2; // нормально

publ = 3; // нормально

}

Член похідного класу має доступ тільки до загальних і захищених членів:

class Y: public X {

void mderived ();

};

Y:: mderived ()

{

priv = 1; // помилка: priv приватний член

prot = 2; // нормально: prot захищений член, а

// mderived () член похідного класу Y

publ = 3; // нормально: publ загальний член

}

У глобальній функції доступні тільки загальні члени:

void f (Y* p)

{

p->priv = 1; // помилка: priv приватний член

p->prot = 2; // помилка: prot захищений член, а f ()

// не друг або член класів X і Y

p->publ = 3; // нормально: publ загальний член}

1.14.13 Доступ до базових класів

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

class X {

public:

int a;

// ...

};

class Y1: public X { };

class Y2: protected X { };

class Y3: private X { };

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

void f (Y1* py1, Y2* py2, Y3* py3)

{

X* px = py1; // нормально: X - загальний базовий клас Y1

py1->a = 7; // нормально

px = py2; // помилка: X - захищений базовий клас Y2

py2->a = 7; // помилка

px = py3; // помилка: X - приватний базовий клас Y3

py3->a = 7; // помилка

}

Тепер нехай описані

class Y2: protected X { };

class Z2: public Y2 { void f (); };

Оскільки X - захищений базовий клас Y2, тільки друзі й члени Y2, а також друзі й члени будь-яких похідних від Y2 класів (зокрема Z2) можуть при необхідності перетворювати (неявно) Y2* в X*. Крім того вони можуть звертатися до загальних і захищених членів класу X:

void Z2:: f (Y1* py1, Y2* py2, Y3* py3)

{

X* px = py1; // нормально: X - загальний базовий клас Y1

py1->a = 7; // нормально

px = py2; // нормально: X - захищений базовий клас Y2, // а Z2 - похідний клас Y2

py2->a = 7; // нормально

px = py3; // помилка: X - приватний базовий клас Y3

py3->a = 7; // помилка

}

Нарешті, розглянемо:

class Y3: private X { void f (); };

Оскільки X - приватний базовий клас Y3, тільки друзі й члени Y3 можуть при необхідності перетворювати (неявно) Y3* в X*. Крім того вони можуть звертатися до загальних і захищених членів класу X:

void Y3:: f (Y1* py1, Y2* py2, Y3* py3)

{

X* px = py1; // нормально: X - загальний базовий клас Y1

py1->a = 7; // нормально

px = py2; // помилка: X - захищений базовий клас Y2

py2->a = 7; // помилка

px = py3; // нормально: X - приватний базовий клас Y3, // а Y3:: f член Y3

py3->a = 7; // нормально

}

1.14.14 Вільна пам'ять

Якщо визначити функції operator new () і operator delete (), керування пам'яттю для класу можна взяти у свої руки. Це також можна, (а часто й більш корисно), зробити для класу, що служить базовим для багатьох похідних класів. Допустимо, нам потрібні були свої функції розміщення й звільнення пам'яті для класу employee ($$6.2.5) і всіх його похідних класів:

class employee {

// ...

public:

void* operator new (size_t);

void operator delete (void*, size_t);

};

void* employee:: operator new (size_t s)

{

// відвести пам'ять в 's' байтів

// і повернути покажчик на неї

}

void employee:: operator delete (void* p, size_t s)

{

// 'p' повинне вказувати на пам'ять в 's' байтів,

// відведену функцією employee:: operator new ();

// звільнити цю пам'ять для повторного використання

}

Призначення до цієї пори загадкового параметра типу size_t стає очевидним. Це - розмір об'єкта, що звільняє. При видаленні простого службовця цей параметр одержує значення sizeof (employee), а при видаленні керуючого - sizeof (manager). Тому власні функції класи для розміщення можуть не зберігати розмір кожного розташованого об'єкта. Звичайно, вони можуть зберігати ці розміри (подібно функціям розміщення загального призначення) і ігнорувати параметр size_t у виклику operator delete (), але тоді навряд чи вони будуть краще, ніж функції розміщення й звільнення загального призначення.

Як транслятор визначає потрібний розмір, якому треба передати функції operator delete ()? Поки тип, зазначений в operator delete (), відповідає щирому типу об'єкта, все просто; але розглянемо такий приклад:

class manager: public employee {

int level;

// ...

};

void f ()

{

employee* p = new manager; // проблема

delete p;

}

У цьому випадку транслятор не зможе правильно визначити розмір. Як і у випадку видалення масиву, потрібна допомога програміста.

Він повинен визначити віртуальний деструктор у базовому класі employee:

class employee {

// ...

public:

// ...

void* operator new (size_t);

void operator delete (void*, size_t);

virtual ~employee ();

};

Навіть порожній деструктор вирішить нашу проблему:

employee:: ~employee () { }

Тепер звільнення пам'яті буде відбуватися в деструкторі (а в ньому розмір відомий), а будь-який похідний від employee клас також буде змушений визначати свій деструктор (тим самим буде встановлений потрібний розмір), якщо тільки користувач сам не визначить його. Тепер наступний приклад пройде правильно:

void f ()

{

employee* p = new manager; // тепер без проблем

delete p;

}

Розміщення відбувається за допомогою (створеного транслятором) виклику

employee:: operator new (sizeof (manager))

а звільнення за допомогою виклику

employee:: operator delete (p,sizeof (manager))

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

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

class X {

// ...

public:

// ...

virtual void f (); // в X є віртуальна функція, тому

// визначаємо віртуальний деструктор

virtual ~X ();

};

1.14.15 Віртуальні конструктори

Довідавшись про віртуальні деструктори, природно запитати: "Чи можуть конструктори те ж бути віртуальними?" Якщо відповісти коротко - немає. Можна дати більше довга відповідь: "Ні, але можна легко одержати необхідний ефект".

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

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

Як правило "віртуальні конструктори" є стандартними конструкторами без параметрів або конструкторами копіювання, параметром яких служить тип результату:

class expr {

// ...

public:

expr (); // стандартний конструктор

virtual expr* new_expr () { return new expr (); }

};

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

class conditional: public expr {

// ...

public:

conditional (); // стандартний конструктор

expr* new_expr () { return new conditional (); }

};

Це означає, що, маючи об'єкт класу expr, користувач може створити об'єкт в "точності такого ж типу":

void user (expr* p1, expr* p2)

{

expr* p3 = p1->new_expr ();

expr* p4 = p2->new_expr ();

// ...

}

Змінним p3 і p4 привласнюються вказівники невідомого, але підходящого типу.

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

class expr {

// ...

expr* left;

expr* right;

public:

// ...

// копіювати 's' в 'this'

inline void copy (expr* s);

// створити копію об'єкта, на який дивиться this

virtual expr* clone (int deep = 0);

};

Параметр deep показує розходження між копіюванням властивому об'єкту (поверхневе копіювання) і копіюванням усього піддерева, коренем якого служить об'єкт (глибоке копіювання). Стандартне значення 0 означає поверхневе копіювання.

Функцію clone () можна використати, наприклад, так:

void fct (expr* root)

{

expr* c1 = root->clone (1); // глибоке копіювання

expr* c2 = root->clone (); // поверхневе копіювання

// ...

}

Будучи віртуальної, функція clone () здатна розмножувати об'єкти будь-якого похідного від expr класу. Дійсне копіювання можна визначити так:

void expr:: copy (expression* s, int deep)

{

if (deep == 0) { // копіюємо тільки члени

*this = *s;

}

else { // пройдемося по вказівником:

left = s->clone (1);

right = s->clone (1);

// ...

}

}

Функція expr:: clone () буде викликатися тільки для об'єктів типу expr (але не для похідних від expr класів), тому можна просто розмістити в ній і повернути з її об'єкт типу expr, що є власною копією:

expr* expr:: clone (int deep)

{

expr* r = new expr (); // будуємо стандартне вираження

r->copy (this,deep); // копіюємо '*this' в 'r'

return r;

}

Таку функцію clone () можна використати для похідних від expr класів, якщо в них не з'являються члени-дані (а це саме типовий випадок):

class arithmetic: public expr {

// ...

// нових член-член-даних немає =>

// можна використати вже певну функцію clone

};

З іншого боку, якщо додані члени-дані, то потрібно визначати власну функцію clone ():

class conditional: public expression {

expr* cond;

public:

inline void copy (cond* s, int deep = 0);

expr* clone (int deep = 0);

// ...

};

Функції copy () і clone () визначаються подібно своїм двійникам з expression:

expr* conditional:: clone (int deep)

{

conditional* r = new conditional ();

r->copy (this,deep);

return r;

}

void conditional:: copy (expr* s, int deep)

{

if (deep == 0) {

*this = *s;

}

else {

expr:: copy (s,1); // копіюємо частину expr

cond = s->cond->clone (1);

}

}

Визначення останньої функції показує відмінність дійсного копіювання в expr:: copy () від повного розмноження в expr:: clone () (тобто створення нового об'єкта й копіювання в нього). Просте копіювання виявляється корисним для визначення більш складних операцій копіювання й розмноження. Розходження між copy () і clone () еквівалентно розходженню між операцією присвоювання й конструктором копіювання і еквівалентно розходженню між функціями _draw () і draw (). Відзначимо, що функція copy () не є віртуальною. Їй і не треба бути такою, оскільки віртуальна викликаюча її функція clone (). Очевидно, що прості операції копіювання можна також визначати як функції-підстановки.

1.15 Перевантаження операцій

Звичайно в програмах використовуються об'єкти, що є конкретним поданням абстрактних понять. Наприклад, у С++ тип даних int разом з операціями +, - , *, / і т.д. реалізує (хоча й обмежено) математичне поняття цілого. Звичайно з поняттям зв'язується набір дій, які реалізуються в мові у вигляді основних операцій над об'єктами, що задають у стислому, зручному й звичному виді. На жаль, у мовах програмування безпосередньо представляється тільки мале число понять. Так, поняття комплексних чисел, алгебри матриць, логічних сигналів і рядків у С++ не мають безпосереднього вираження. Можливість задати подання складних об'єктів разом з набором операцій, котрі виконуються над такими об'єктами, реалізують у С++ класи. Дозволяючи програмістові визначати операції над об'єктами класів, ми одержуємо більше зручну й традиційну систему позначень для роботи із цими об'єктами в порівнянні з тієї, у якій всі операції задаються як звичайні функції. Приведемо приклад:

class complex {

double re, im;

public:

complex (double r, double i) { re=r; im=i; }

friend complex operator+ (complex, complex);

friend complex operator* (complex, complex);

};

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

void f ()

{

complex a = complex (1,3.1);

complex b = complex (1.2,2);

complex c = b;

a = b+c;

b = b+c*a;

c = a*b+complex (1,2);

}

Зберігаються звичайні пріоритети операцій, тому другий вираз виконується як b=b+ (c*a), а не як b= (b+c) *a.

1.15.1 Операторні функції

Можна описати функції, що визначають інтерпретацію наступних операцій:

+ - * /% ^ & | ~!

= < > += - = *= /=%= ^= &=

|= << >> >>= <<= ==! = <= >= &&

|| ++ - і - >*, - > [] () new delete

Останні п'ять операцій означають: непряме звертання, індексацію, виклик функції, розміщення у вільній пам'яті й звільнення. Не можна змінити пріоритети цих операцій, так само як і синтаксичні правила для виразів. Так, не можна визначити унарну операцію%, також як і бінарну операцію!. Не можна ввести нові лексеми для позначення операцій, але якщо набір операцій вас не влаштовує, можна скористатися звичним позначенням виклику функції. Тому використайте pow (), а не **. Ці обмеження можна ввжати драконівськими, але більш вільні правила легко приводять до неоднозначності. Припустимо, ми визначимо операцію ** як піднесення до степеня, що на перший погляд здається очевидним і простим завданням. Але якщо як варто подумати, то виникають питання: чи належні операції ** виконуватися ліворуч праворуч або праворуч ліворуч? Як інтерпретувати вираження a**p як a* (*p) або як (a) ** (p)?

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

void f (complex a, complex b)

{

complex c = a + b; // коротка форма

complex d = operator+ (a,b); // явний виклик

}

З урахуванням наведеного опису типу complex ініціалізатори в цьому прикладі є еквівалентними.

1.15.2 Бінарні й унарні операції

Бінарну операцію можна визначити як функція-член з одним параметром, або як глобальну функцію із двома параметрами. Виходить, для будь-якої бінарної операції @ вираження aa @ bb інтерпретується або як aa. operator (bb), або як operator@ (aa,bb). Якщо визначені обидві функції, то вибір інтерпретації відбувається за правилами зіставлення параметрів. Префіксна або постфіксна унарна операція може визначатися як функція-член без параметрів, або як глобальна функція з одним параметром. Для будь-якої префиксної унарної операції @ вираження @aa інтерпретується або як aa. operator@ (), або як operator@ (aa). Якщо визначені обидві функції, то вибір інтерпретації відбувається за правилами зіставлення параметрів. Для будь-якої постфіксної унарної операції @ вираз @aa інтерпретується або як aa. operator@ (int), або як operator@ (aa, int). Якщо визначені обидві функції, то вибір інтерпретації відбувається за правилами зіставлення параметрів. Операцію можна визначити тільки відповідно до синтаксичних правил, наявними для неї в граматиці С++. Зокрема, не можна визначити% як унарну операцію, а + як тернарну. Проілюструємо сказане прикладами:

class X {

// члени (неявно використається покажчик 'this'):

X* operator& (); // префіксна унарная операція &

// (узяття адреси)

X operator& (X); // бінарна операція &

X operator++ (int); // постфіксний інкремент

X operator& (X,X); // помилка: & не може бути тернарною

X operator/ (); // помилка: / не може бути унарною

};

// глобальні функції (звичайно друзі)

X operator- (X); // префіксний унарный мінус

X operator- (X,X); // бінарний мінус

X operator-і (X&, int); // постфіксний інкремент

X operator- (); // помилка: немає операнда

X operator- (X,X,X); // помилка: тернарна операція

X operator% (X); // помилка: унарна операція%

1.15.3 Операторні функції й типи користувача

Операторна функція повинна бути або членом, або мати принаймні один параметр, що є об'єктом класу (для функцій, що перевизначають операції new і delete, це не обов'язково). Це правило гарантує, що користувач не зуміє змінити інтерпретацію виразів, що не містять об'єктів типу користувача. Зокрема, не можна визначити операторну функцію, що працює тільки з вказівниками. Цим гарантується, що в ++ можливі розширення, але не мутації (не вважаючи операцій =, &, і, для об'єктів класу).

Операторна функція, що має першим параметр основного типу, не може бути функцією-членом. Так, якщо ми додаємо комплексну змінну aa до цілого 2, то при підходящому описі функції-члена aa+2 можна інтерпретувати як aa. operator+ (2), але 2+aa так інтерпретувати не можна, оскільки не існує класу int, для якого + визначається як 2. operator+ (aa). Навіть якби це було можливо, для інтерпретації aa+2 і 2+aa довелося мати справа із двома різними функціями-членами. Цей приклад тривіально записується за допомогою функцій, що не є членами.

1.15.4 Конструктори

Замість того, щоб описувати кілька функцій для кожного випадку виклику (наприклад, комбінації типу double та complex з усіма операціями), можна описати конструктор, що з параметра double створює complex:

class complex {

// ...

complex (double r) { re=r; im=0; }

};

Цим визначається як одержати complex, якщо задано double. Конструктор з єдиним параметром не обов'язково викликати явно:

complex z1 = complex (23);

complex z2 = 23;

Обидві змінні z1 і z2 будуть ініціалізовані викликом complex (23).

Конструктор є алгоритмом створення значення заданого типу. Якщо потрібне значення деякого типу й існує його конструктор, параметром якого є це значення, то тоді цей конструктор і буде використатися. Так, клас complex можна було описати в такий спосіб:

class complex {

double re, im;

public:

complex (double r, double i =0) { re=r; im=i; }

friend complex operator+ (complex, complex);

friend complex operator* (complex, complex);

complex operator+= (complex);

complex operator*= (complex);

// ...

};

Всі операції над комплексними змінними й цілими константами з урахуванням цього опису стають законними. Ціла константа буде інтерпретуватися як комплексне число із мнимою частиною, рівної нулю. Так, a=b*2 означає

a = operator* (b, complex (double (2), double (0)))

Нові версії операцій таких, як +, має сенс визначати тільки для підвищення ефективності за рахунок відмови від перетворень типу коштує того. Наприклад, якщо з'ясується, що операція множення комплексної змінної на речовинну константу є критичної, то до безлічі операцій можна додати operator*= (double):

class complex {

double re, im;

public:

complex (double r, double i =0) { re=r; im=i; }

friend complex operator+ (complex, complex);

friend complex operator* (complex, complex);

complex& operator+= (complex);

complex& operator*= (complex);

complex& operator*= (double);

// ...

};

Операції присвоювання типу *= і += можуть бути дуже корисними для роботи з типами користувача, оскільки звичайно запис із ними коротше, ніж з їх звичайними "двійниками" * і +, а крім того вони можуть підвищити швидкість виконання програми за рахунок виключення тимчасових змінних:

inline complex& complex:: operator+= (complex a)

{

re += a. re;

im += a. im;

return *this;

}

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

inline complex operator+ (complex a, complex b)

{

return complex (a. re+b. re, a. im+b. im);

}

Тут в операторі return використається конструктор, що дає транслятору коштовну підказку на предмет оптимізації. Але для більше складних типів і операцій, наприклад таких, як множення матриць, результат не можна задати як одне вираження, тоді операції * і + простіше реалізувати за допомогою *= і +=, і вони будуть легше піддаватися оптимізації:

matrix& matrix:: operator*= (const matrix& a)

{

// ...

return *this;

}

matrix operator* (const matrix& a, const matrix& b)

{

matrix prod = a;

prod *= b;

return prod;

}

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

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

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

1.15.5 Присвоювання й ініціалізація

Розглянемо простий строковий клас string:

struct string {

char* p;

int size; // розмір вектора, на який указує p

string (int size) { p = new char [size=sz]; }

~string () { delete p; }

};

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

void f ()

{

string s1 (10);

string s2 (20)

s1 = s2;

}

Тут будуть розміщені два символьних вектори, але в результаті присвоювання s1 = s2 вказівник на один з них буде знищений, і заміниться копією другого. Після виходу з f () буде викликаний для s1 і s2 деструктор, що двічі видалить той самий вектор, результати чого по всій видимості будуть жалюгідні. Для рішення цієї проблеми потрібно визначити відповідне присвоювання об'єктів типу string:

struct string {

char* p;

int size; // розмір вектора, на який указує p

string (int size) { p = new char [size=sz]; }

~string () { delete p; }

string& operator= (const string&);

};

string& string:: operator= (const string& a)

{

if (this! =&a) { // небезпечно, коли s=s

delete p;

p = new char [size=a. size];

strcpy (p,a. p);

}

return *this;

}

При такім визначенні string попередній приклад пройде як задумано. Але після невеликої зміни в f () проблема виникає знову, але в іншому виді:

void f ()

{

string s1 (10);

string s2 = s1; // ініціалізація, а не присвоювання

}

Тепер тільки один об'єкт типу string будується конструктором string:: string (int), а знищуватися буде два рядки. Справа в тому, що користувальницька операція присвоювання не застосовується до неініціалізованого об'єкта. Досить глянути на функцію string:: operator (), щоб зрозуміти причину цього: вказівник p буде тоді мати невизначене, по суті випадкове значення. Як правило, в операції присвоювання передбачається, що її параметри проініціалізовані. Отже, щоб упоратися з ініціалізацією потрібна схожа, але своя функція:

struct string {

char* p;

int size; // розмір вектора, на який указує p

string (int size) { p = new char [size=sz]; }

~string () { delete p; }

string& operator= (const string&);

string (const string&);

};

string:: string (const string& a)

{

p=new char [size=sz];

strcpy (p,a. p);

}

Ініціалізація об'єкта типу X відбувається за допомогою конструктора X (const X&). Особливо це важливо в тих випадках, коли визначений деструктор. Якщо в класі X є нетривіальний деструктор, наприклад, що робить звільнення об'єкта у вільній пам'яті, найімовірніше, у цьому класі буде потрібно повний набір функцій, щоб уникнути копіювання об'єктів по членах:

class X {


Подобные документы

  • Концепції об'єктно-орієнтованого програмування. Методи створення класів. Доступ до методів базового класу. Структура даних, функції. Розробка додатку на основі діалогових вікон, програми меню. Засоби розробки програмного забезпечення мовами 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

Работы в архивах красиво оформлены согласно требованиям ВУЗов и содержат рисунки, диаграммы, формулы и т.д.
PPT, PPTX и PDF-файлы представлены только в архивах.
Рекомендуем скачать работу.