|
Розробка власного класу STRING
/b>Ця глава присвячена поняттю похідного класу. Похідні класи - це простий, гнучкий і ефективний засіб визначення класу. Нові можливості додаються до вже існуючого класу, не вимагаючи його перепрограмування або перетрансляції. За допомогою похідних класів можна організувати загальний інтерфейс із декількома різними класами так, що в інших частинах програми можна буде одноманітно працювати з об'єктами цих класів. Вводиться поняття віртуальної функції, що дозволяє використати об'єкти належним чином навіть у тих випадках, коли їхній тип на стадії трансляції невідомий. Основне призначення похідних класів - спростити програмістові завдання вираження спільності класів.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 і т.д.). Графічно відношення спадкування звичайно зображується у вигляді стрілки від похідних класів до базового:employeemanagerЗвичайно говорять, що похідний клас успадковує базовий клас, тому й відношення між ними називається успадкуванням. Іноді базовий клас називають суперкласом, а похідний - підлеглим класом. Але ці терміни можуть викликати здивування, оскільки об'єкт похідного класу містить об'єкт свого базового класу. Взагалі похідний клас більше свого базового в тому розумінні, що в ньому утримується більше даних і визначено більше функцій.Маючи визначення employee і manager, можна створити список службовців, частина з яких є й керуючими:void f (){manager m1, m2;employee e1, e2;employee* elist;elist = &m1; // помістити m1 в elistm1. next = &e1; // помістити e1 в eliste1. next = &m2; // помістити m2 в elistm2. next = &e2; // помістити m2 в eliste2. 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 типу managerpm->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 1234level 2J. 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:: rotatevoid draw (); // нормально: // перевизначення shape:: drawcircle (point p, int r);};Якщо чиста віртуальна функція не визначається в похідному класі, то вона й залишається такою, а значить похідний клас теж є абстрактним. При такому підході можна реалізовувати класи поетапно:class X {public:virtual void f () = 0;virtual void g () = 0;};X b; // помилка: опис об'єкта абстрактного класу Xclass Y: public X {void f (); // перевизначення X:: f};Y b; // помилка: опис об'єкта абстрактного класу Yclass 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_inputvoid 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 ();
Страницы: 1, 2, 3, 4, 5
|
|