|
Розробка власного класу STRING
т як, цілком ймовірно, буде модифіковане визначення acct усередині main (): // псевдокод на C++, // іллюструючий внутрішню вставку конструктораint main (){Account acct;acct. Account:: Account ("Ethan Stern", 0.0); // ...}Звичайно, якщо конструктор визначений як вбудований, то він підставляється в точці виклику.Обробка оператора new трохи складніше. Конструктор викликається тільки тоді, коли він успішно виділив пам'ять. Модифікація визначення pact у трохи спрощеному виді виглядає так: // псевдокод на C++, // іллюструючий внутрішню вставку конструктора при обробці newint main (){ // ...Account *pact;try {pact = _new (sizeof (Account));pact->Acct. Account:: Account ("Michael Liebarman", 5000.0);}catch (std:: bad_alloc) { // оператор new закінчився невдачею: // конструктор не викликається} // ...}Існує три в загальному випадку еквівалентні форми завдання аргументів конструктора: // загалом ці конструктори еквівалентніAccount acct1 ("Anna Press");Account acct2 = Account ("Anna Press");Account acct3 = "Anna Press";Форма acct3 може використовуватися тільки при завданні єдиного аргументу. Якщо аргументів два або більше, рекомендовано користуватися формою acct1, хоча припустимо й acct2. // рекомендує форма, що, виклику конструктораAccount acct1 ("Anna Press");Визначати об'єкт класу, не вказуючи списку фактичних аргументів, можна в тому випадку, якщо в ньому або об'явлений конструктор за замовчуванням, або взагалі немає об'яв конструкторів. Якщо в класі об'явлений хоча б один конструктор, то не дозволяється визначати об'єкт класу, не викликаючи жодного з них. Зокрема, якщо в класі визначений конструктор, що приймає один або більше параметрів, але не визначений конструктор за замовчуванням, то в кожному визначенні об'єкта такого класу повинні бути присутнім необхідні аргументи. Можна заперечити, що не має змісту визначати конструктор за замовчуванням для класу Account, оскільки не буває рахунків без імені власника. У переглянутій версії класу Account такий конструктор виключений:class Account {public: // імена параметрів в оголошенні вказувати необов'язковоAccount (const char*, double=0.0);const char* name () { return name; } // ...private: // ...};Тепер при оголошенні кожного об'єкта Account у конструкторі обов'язково треба вказати як мінімум аргумент типу C-рядка, але це швидше за все безглуздо. Чому? Контейнерні класи (наприклад, vector) вимагають, щоб для класу елементів, що поміщають у них, був або заданий конструктор за замовчуванням, або взагалі ніяких конструкторів. Аналогічна ситуація має місце при виділенні динамічного масиву об'єктів класу. Так, що інструкція викликала б помилку компіляції для нової версії Account: // помилка: потрібен конструктор за замовчуванням для класуAccount *pact = new Account [new_client_cnt];На практиці часто потрібно задавати конструктор за замовчуванням, якщо є які-небудь інші конструктори.А якщо для класу немає розумних значень за замовчуванням? Наприклад, клас Account вимагає задавати для будь-якого об'єкта прізвище власника рахунку. У такому випадку найкраще встановити стан об'єкта так, щоб було видно, що він ще не ініціалізований коректними значеннями: // конструктор за замовчуванням для класу Accountinline Account:: Account () {_name = 0;_balance = 0.0;_acct_nmbr = 0;}Однак у функції-члени класу Account прийдеться включити перевірку цілісності об'єкта перед його використанням.Існує й альтернативний синтаксис: список ініціалізації членів, у якому через кому вказуються імена й початкові значення. Наприклад, конструктор за замовчуванням можна переписати в такий спосіб: // конструктор за замовчуванням класу Account з використанням // списку ініціалізації членівinline Account::Account (): _name (0),_balance (0.0), _acct_nmbr (0){}Такий список допустимо тільки у визначенні, але не в оголошенні конструктора. Він міститься між списком параметрів і тілом конструктора й відділяється двокрапкою. От як виглядає наш конструктор із двома параметрами при частковому використанні списку ініціалізації членів:inline Account::Account (const char* name, double opening_bal): _balance (opening_bal){_name = new char [strlen (name) +1];strcpy (_name, name);_acct_nmbr = get_unique_acct_nmbr ();}Конструктор не можна об'являти із ключовими словами const або volatile, тому наведені записи невірні:class Account {public:Account () const; // помилкаAccount () volatile; // помилка // ...};Це не означає, що об'єкти класу з такими специфікаторами заборонено ініціалізувати конструктором. Просто до об'єкта застосовується підходящий конструктор, причому без обліку специфікаторів в оголошенні об'єкта. Константність об'єкта класу встановлюється після того, як робота з його ініціалізації завершена, і пропадає в момент виклику деструктора. Таким чином, об'єкт класу зі специфікатором const уважається константним з моменту завершення роботи конструктора до моменту запуску деструктора. Те ж саме ставиться й до специфікатора volatile.Розглянемо наступний фрагмент програми: // у якімсь заголовному файліextern void print (const Account &acct); // ...int main (){ // перетворить рядок "oops" в об'єкт класу Account // за допомогою конструктора Account:: Account ("oops", 0.0)print ("oops"); // ...}За замовчуванням конструктор з одним параметром (або з декількома - за умови, що всі параметри, крім першого, мають значення за замовчуванням) відіграє роль оператора перетворення. У цьому фрагменті програми конструктор Account неявно застосовується компілятором для трансформації літерального рядка в об'єкт класу Account при виклику print (), хоча в даній ситуації таке перетворення не потрібно.Ненавмисні неявні перетворення класів, наприклад трансформація "oops" в об'єкт класу Account, виявилися джерелом помилок, що виявляють важко. Тому в стандарт C++ було додано ключове слово explicit, що говорить компіляторові, що такі перетворення не потрібні:class Account {public:explicit Account (const char*, double=0.0);};Даний модифікатор застосуємо тільки до конструктора.1.8 Конструктор копіюванняІніціалізація об'єкта іншим об'єктом того ж класу називається почленною ініціалізацією за замовчуванням. Копіювання одного об'єкта в іншій виконується шляхом послідовного копіювання кожного нестатичного члена. Проектувальник класу може змінити цей процес, надавши спеціальний конструктор копіювання. Якщо він визначений, то викликається щоразу, коли один об'єкт ініціалізується іншим об'єктом того ж класу.Часто почленна ініціалізація не забезпечує коректну дію класу. Тому ми явно визначаємо конструктор копіювання. У нашому класі Account це необхідно, інакше два об'єкти будуть мати однакові номери рахунків, що заборонено специфікацією класу.Конструктор копіювання приймає як формальний параметр посилання на об'єкт класу (рекомендовано зі специфікатором const). Його реалізація:inline Account::Account (const Account &rhs): _balance (rhs. _balance){_name = new char [strlen (rhs. _name) + 1];strcpy (_name, rhs. _name); // копіювати rhs. _acct_nmbr не можна_acct_nmbr = get_unique_acct_nmbr ();}Коли ми пишемо:Account acct2 (acct1);компілятор визначає, чи оголошений явний конструктор копіювання для класу Account. Якщо він оголошений і доступний, то він і викликається; а якщо недоступний, то визначення acct2 вважається помилкою. У випадку, що коли конструктор копіювання не об'явлений, виконується почленна ініціалізація за замовчуванням. Якщо згодом об'явлення конструктор копіювання буде додане або вилучене, ніяких змін у програми користувачів вносити не прийдеться. Однак перекомпілювати їх все-таки необхідно.1.9 Деструктор класуОдна із цілей, що ставляться перед конструктором, - забезпечити автоматичне виділення ресурсу. Ми вже бачили в прикладі із класом Account конструктор, де за допомогою оператора new виділяється пам'ять для масиву символів і привласнюється унікальний номер рахунку. Можна також представити ситуацію, коли потрібно одержати монопольний доступ до поділюваної пам'яті або до критичної секції потоку. Для цього необхідна симетрична операція, що забезпечує автоматичне звільнення пам'яті або повернення ресурсу після завершення часу життя об'єкта, - деструктор. Деструктор - це спеціальна обумовлена користувачем функція-член, що автоматично викликається, коли об'єкт виходить із області видимості або коли до покажчика на об'єкт застосовується операція delete. Ім'я цієї функції створено з імені класу з попереднім символом “тильда" (~). Деструктор не повертає значення й не приймає ніяких параметрів, а отже, не може бути перевантажений. Хоча дозволяється визначати кілька таких функцій-членів, лише одна з них буде застосовуватися до всіх об'єктів класу. От, наприклад, деструктор для нашого класу 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;};inlineAccount:: ~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 PROFILETimer 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_Hclass Time { public:Time (int = 0, int = 0, int = 0); // конструктор за замовчуванням // функції запису setvoid 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-23int minute; // 0-59int 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: SSvoid 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 functionTime:: setHour (int) called for const object Warning FXG7 l. CPP: Non-const functionTime:: setMinute (int) callers for const object Warning FIG7 1. CPP: Non-const functionTime:: 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, 2, 3, 4, 5
|
|