Міністерство освіти інауки України
Дніпропетровськийнаціональний університет
Факультет фізики,електроніки та комп’ютерних систем
Курсова робота
з дисципліни
об’єктно-орієнтовнепрограмування
на тему: "Розробка власного класу String"
Виконав:
ст. гр. РС-05-1
Тимощенко П.А.
Перевірив:
доц. Вовк С.М.
Дніпропетровськ 2007
Содержание
Завдання
1. Теоретична частина
1.1 Введення воб’єктно-орієнтовну технологію
1.2 Визначення структур
1.3 Доступ до елементівструктури
1.4 Використаннявизначеного користувачем типу Time за допомогою Struct
1.5 Використанняабстрактного типу даних Time за допомогою класу
1.6 Область дії клас ідоступ до елементів класу
1.7 Конструктор класу
1.8 Конструктор копіювання
1.9 Деструктор класу
1.10 Явний виклик деструктора
1.11 Небезпека збільшеннярозміру програми
1.12 Константні об'єкти й функції-елементи
1.13 Друзі
1.14 Ядро ООП: Успадкування та поліморфізм
1.4.1 Похідні класи
1.14.2 Функції-члени
1.14.3 Конструктори йдеструктори
1.14.4 Ієрархія класів
1.14.5 Поля типу
1.14.6 Віртуальні функції
1.14.7 Абстрактні класи
1.14.8 Множинне входженнябазового класу
1.14.9 Вирішеннянеоднозначності
1.14.10 Віртуальні базові класи
1.14.11 Контроль доступу
1.14.12 Захищені члени
1.14.13 Доступ до базовихкласів
1.14.14 Вільна пам'ять
1.14.15 Віртуальніконструктори
1.15 Перевантаженняоперацій
1.15.1 Операторні функції
1.15.2 Бінарні й унарніоперації
1.15.3 Операторні функціїй типи користувача
1.15.4 Конструктори
1.15.5 Присвоювання йініціалізація
1.15.6 Інкремент ідекремент
1.15.7 Перевантаженняоперацій помістити в потік і взяти з потоку
2. Розробка власного класу clsString
2.1 Загальний алгоритмвирішення
2.2 Детальний анализ
2.3 Тестування
Висновки
Література
Додатки
/>Завдання
Розробити клас classString, на основі якого можнастворювати об'єкти типу «рядок символів». Цей клас повинен надаватиможливість створення програм, в яких реалізуються обробка рядків символів. Вкласі повинні бути визначені методи присвоєння рядків, додавання рядків,вставки рядка в рядок з заданого місця та вилучення певної кількості символів зрядка, звертання до окремого елементу рядка, операції відношень для порівняннярядків (більше, менше, рівно, нерівно), операції вставки рядка в потіквведення/виведення та його вилучення з потоку, метод визначення довжини рядка,тощо. Розробку виконувати в середовищі Borland C++ Builder або MS Visual StudioC++.
/>/>1. Теоретична частина 1.1 Введення в об’єктно-орієнтовнутехнологію
Подивіться навколо себе на реальній світ. Куди б ви не подивились завждизнаходяться об’єкти! Люди, тварини, рослини, автомобілі, літаки, комп’ютери ітощо. Людина кумекає в термінах об’єктів. Мі володіємо чудовою можливістюабстрагувати, що дозволяє нам бачити картинки на екрані (людей, дерева, літаки)саме у вигляді об’єктів, а не у вигляді окремих кольорових точок.
Як би ми не класифікували ці об’єкти, всі вони мають спільні атрибути: форма,колір, маса тощо. Кожен з них має свій набір рухів, наприклад, м’яч котиться, підстрибує,спускає, дитина кричить, сміється, спить, їсть, блимає очима тощо.
Людство пізнає об’єкти шляхом вивчення їх атрибутів. Різні об’єктиможуть мати багато однакових атрибутів та представляти схожу поведінку.
Об’єктно-орієнтовне програмування (ООП) моделює об'єкти реального світуза допомогою програмних аналогів. Це приводить до появи відносин класів, колиоб'єкти певного класу — такого, як клас засобів пересування — мають однаковіхарактеристики. Це висуває відносини спадкування й навіть відносини множинногоспадкування, коли знову створювані класи здобувають наслідуваніхарактеристики існуючих класів, а також містять свої власні унікальніхарактеристики. Об'єкти класу автомобілів з відкидним верхом виразно маютьхарактеристики класу автомобілів, але дах у них відкидається й закривається.
Об’єктно-орієнтовне програмування дає нам найбільш природний іінтуїтивний спосіб розгляду процесу програмування як моделювання реальноіснуючих об'єктів, їхніх атрибутів і поводження. ООП моделює такожзв'язок між об'єктами. Подібно тому, як люди посилають один одному повідомлення(наприклад, сержант, командуючий групі стояти струнко), об'єкти теж зв'язуютьсяодин з одним за допомогою повідомлень.
ООП інкапсулює дані (атрибути) і функції (способи поводження) у пакети,називані об'єктами; дані й функції об'єктів тісно взаємозалежні. Об'єкти маютьвластивість приховування інформації. Це означає, що хоча об'єкти можуть знати,як зв'язатися один з одним за допомогою добре визначених інтерфейсів, вони незнають, як реалізовані інші об'єкти — деталі реалізації заховані всерединісамих об'єктів. Безсумнівно, можна ефективно їздити на автомобілі, не знаючидеталей того, як працює його мотор, трансмісія й система вихлопу.
У С та інших мовах процедурного програмування програмування маєтенденцію бути орієнтованим на дії, тоді як в C++ програмування прагне бутиорієнтованим на об'єкти. У С одиницею програмування є функція. В C++ одиницеюпрограмування є клас, на основі якого в кінцевому результаті створюютьсяекземпляри об'єктів.
Програмісти, що використають С, зосереджені на написанні функцій. Групидій, що виконують деяке загальне завдання, формуються у вигляді функцій, афункції групуються так, щоб сформувати програму. Дані звичайно важливі в С, алеіснує думка, що дані призначені в першу чергу для підтримки виконуванихфункціями дій. Дієслова в оголошенні системи допомагають програмістові на С прирозробці системи визначити набір функцій, які, працюючи спільно, і забезпечуютьфункціонування системи.
Програмісти на C++ зосереджені на створенні своїх власних обумовленихкористувачем типів, названих класами. Кожний клас містять дані й набір функцій,які маніпулюють цими даними. Компоненти дані класу називаються даними-елементами(елементами даних). Компоненти функції класу називаються функціями-елементами. Точнотак само, як екземпляр вбудованого типу, такого як int, називається змінної,екземпляр певного користувачем типу (тобто класу) називається об'єктом. Програміствикористає вбудовані типи як блоки для конструювання певних користувачем типів.В C++ увага фокусується скоріше на об'єктах, чим на функціях. Імена іменники вописі системи допомагають програмістові на C++ при створенні системи визначитинабір класів, з яких будуть створені об'єкти, які, працюючи спільно, ізабезпечують функціонування системи.
Класи для об’єктів є тим же самим, що і проекти для домів. Можназбудувати багато домів згідно одного проекту, і можна реалізувати багатооб’єктів з одного класу. Наприклад, в об’єктно-орієнтовному проектуванні банкуклас BankTeller, повинен співвідноситися з класом BankAccount. Ці співвідношенняназивають асоціативними.
Класи в С++ є природнім продовженням структури struck в мові С. Тому,перш ніж, розглядати специфіку розробки класів на С++, мі розглянемо тапобудуємо визначений користувачем тип, оснований на структурі. Недоліки, які мипобачимо при цьому, допоможуть пояснити запис класу.
1.2 Визначення структур
Структури — це складені типи даних, побудовані з використанням іншихтипів. Розглянемо наступне визначення структури:
struct Time
{
int hour; // 0-23
int minute; // 0-59
int second; // 0-59
}
Ключове слово struct починає визначення структури. Ідентифікатор Time — тег (позначення, ім'я-етикетка) структури. Тег структури використається при об’явленнізмінних структур даного типу. У цьому прикладі ім'я нового типу — Time. Імена, об’явленніу фігурних дужках опису структури — це елементи структури. Елементи однієї йтієї ж структури повинні мати унікальні імена, але дві різні структури можутьмістити не конфліктуючі елементи з однаковими іменами. Кожне визначенняструктури повинне закінчуватися крапкою з комою. Наведене пояснення, як минезабаром побачимо, вірно й для класів.
Визначення Time містить три елементи типу int — hour, minute і second (годинники,хвилини й секунди). Елементи структури можуть бути будь-якого типу й однаструктура може містити елементи багатьох різних типів. Структура не може,однак, містити екземпляри самої себе. Наприклад, елемент типу Time не може бутиоголошений у визначенні структури Time. Однак, може бути включений вказівник наіншу структуру Time. Структура, що містить елемент, котрий є вказівником натакий же структурний тип, називається структурою із самоадресацією. Структуриіз самоадресацією корисні для формування зв'язних структур даних.
Попереднє визначення структури даних не резервує ніякого простору впам'яті; визначення тільки створює новий тип даних, що використається для об’явленнязмінних. Змінні структури об’явленні так само, як змінні інших типів. Об’явлення
Time timeObject, timeArray [10], *timePtr;
повідомляє timeObject змінна типу Time, timeArray — масив з 10 елементівтипу Time, a timePtr — вказівник на об'єкт типу Time.
1.3 Доступ до елементів структури
Для доступу до елементів структури (або класу) використовуютьсяоперації доступу до елементів — операція крапка () і операція стрілка (->).
Операція крапка звертається до елемента структури (або класу) по іменізмінної об'єкта або по посиланню на об'єкт.
Наприклад, щоб надрукувати елемент hour структури timeObjectвикористається оператор
cout
Операція стрільця, що складається зі знака мінус (-) і знака більше (>),записаних без пропусків, забезпечує доступ до елемента структури (або класу) черезвказівник на об'єкт. Припустимо, що вказівник timePtr був уже об’явлений якпосилання на об'єкт типу Time і що адреса структури timeObiect була вжеприсвоєна timePtr. Тоді, щоб надрукувати елемент hour структури timeObject звказівником timePtr, можна використати оператор
cout hour;
Вираз timePtr->hour еквівалентний (*timePtr). hour, що розіменовує вказівникі робить доступним елемент hour через операцію крапка. Дужки потрібні тому, щооперація крапка має більш високий пріоритет, ніж операція розіменуваннявказівника (*). Операції стрілка й крапка поряд із круглими й квадратнимидужками мають другий найвищий пріоритет (після операції дозволу області дії) іасоціативності зліва направо.
1.4 Використання визначеного користувачемтипу Time за допомогою Struct
Програма на мал.1 створює визначений користувачем тип структури Time ізтрьома цілими елементами: hour, minute і second. Програма визначає єдинуструктуру типу Time, названу dinnerTime, і використовує операцію крапка дляприсвоєння елементам структури початкових значень 18 для hour, 30 для minute і0 для second. Потім програма друкує час у військовому (24-годинному) істандартному (12-годинному) форматах. Помітимо, що функції друку приймаютьпосилання на постійні структури типу Time. Це є причиною того, що структуриTime передаються друкуючим функціям по посиланню — цим виключаються накладнівитрати на копіювання, пов'язані з передачею структур функціям за значенням, авикористання const запобігає зміні структури типу Time функціями друку. Далі миобговоримо об'єкти const і функції-елементи const.
Порада з підвищення ефективності: Щоб уникнутинакладних витрат, пов’язаних із передачею по значенню й одержати користьзахисту початкових даних від зміни, передавайте аргументи великого розміру якпосилання const.
Існують перешкоди створенню нових типів данихзазначеним способом за допомогою структур. Оскільки ініціалізація структурспеціально не потрібна, можна мати дані без початкових значень і випливаючизвідси проблеми. Навіть якщо дані одержали початкові значення, можливо, це булозроблено невірно. Неправильні значення можуть бути привласнені елементамструктури (як ми зробили на мал.1), тому що програма має прямий доступ до даних.Програма присвоїла невірні значення всім трьом елементам об'єкта dinnerTimeтипу Time. Якщо реалізація struct зміниться (наприклад, час тепер будепредставляється як число секунд після півночі), то всі програми, яківикористовують struct, потрібно буде змінити. Не існує ніякого "інтерфейсу",гарантуючого те, що програміст правильно використає тип даних і що дані єнесуперечливими.
// Створення структури, завдання й друк її елементів.
#include
struct Time { // визначення структури
int hour; // 0-23
int minute; // 0-59
int second; // 0-59 };
void printMilitary (const Time &); // прототип void printStandard (constTime &); // прототип
main ()
{
Time dinnerTime; // змінна нового типу Time
// завдання елементам правильні значення dinnerTime. hour = 18; dinnerTime.minute = 30; dinnerTime. second = 0;
cout " «Обід відбудеться в »;
printMilitary (dinnerTime);
cout " " за військовим часом," " endl
// завдання елементам неправильних значень
dinnerTime. hour = 29;
dinnerTime. minute = 73; dinnerTime. second = 103;
cout " endl
// Друк часу у військовому форматі void printMilitary (const Time&t)
{
cout " (t. hour
"": "" (t. minute
"": "" (t. second
}
// друк часу в стандартному форматі
void printStandard (const Time &t)
{
cout " ( (t. hour == 0 || t. hour == 12)? 12: t. hour%12)"": "" (t. minute
}
Обід відбудеться в 18: 30: 00 за військовим часом,
що відповідає 6: 30: 00 РМ за стандартним часом.
Час із неправильними значеннями: 29: 73: 103
Мал.1. Створення структури, завдання й друк її елементів
Існують і інші проблеми, пов'язані зі структурами в стилі С. У Сструктури не можуть бути надруковані як єдине ціле, тільки по одному елементу звідповідним форматом кожного. Для друку елементів структури в якому-небудьпотрібному форматі повинна бути написана функція. «Перевантаження операцій»покаже, як перевантажити операцію ", щоб надати можливість простого друкуоб'єктів типу структура (C++ розширює поняття структури) або типу клас. У Сструктури не можна порівнювати в цілком, їх потрібно порівнювати елемент заелементом. Далі покажемо, як перевантажити операції перевірки рівності йвідношення, щоб можна було в С++ порівнювати об'єкти типів структура й клас.
У наступному розділі ми знову використаємо нашу структуру Time, але вжеяк клас, і продемонструємо деякі переваги створення таких, так званихабстрактних типів даних, як класи. Ми побачимо, що класи й структури в C++можна використовувати майже однаково. Різниця між ними складається вдоступності за замовчуванням елементів кожного із цих типів. Це буде більшдетально пояснено пізніше.
1.5 Використання абстрактного типуданих Time за допомогою класу
Класи надають програмістові можливість моделювати об'єкти, які маютьатрибути (представлені як дані-елементи) і варіанти поведінки або операції (представленіяк функції-елементи). Типи, що містять дані-елементи й функції-елементи,звичайно визначаються в C++ за допомогою ключового слова class.
Функції-елементи іноді в інших об’єктно-орієнтовних мовах називаютьметодами, вони викликаються у відповідь на повідомлення, що посилаються об'єкту.Повідомлення відповідає виклику функції-елемента.
Коли клас визначений, ім'я класу може бути використане для об’явленняоб'єкта цього класу. Мал.1 містить просте визначення класу Time.
Визначення нашого класу Time починається із ключового слова class. Тіловизначення класу береться у фігурні дужки ({ }). Визначення класу закінчуєтьсякрапкою з комою. Визначення нашого класу Time, як і нашої структури Time,містить три цілих елементи hour, minute і second.
сlass Time {
public:
Time ();
void setTime (int, int, int);
void printMilitary ();
void printStandatd (); private:
int hour; // 0-23
int minute; // 0 — 59
int second; // 0-59
};
Мал.1 Просте визначення класу
Інші частини визначення класу — нові. Мітки public: (відкрита) іprivate: закрита) називаються специфікаторами доступу до елементів. Будь-якідані-елементи й функції-елементи, об’явлені після специфікатора доступу доелементів public: (і до наступного специфікатора доступу до елементів),доступні при будь-якому звертанні програми до об'єкта класу Time. Будь-якідані-елементи й функції-елементи, об’явлені після специфікатора доступу доелементів private: (і до наступного специфікатора доступу до елементів),доступні тільки функціям-елементам цього класу. Специфікатори доступу доелементів завжди закінчуються двокрапкою (:) і можуть з'являтися у визначеннікласу багато разів і в будь-якому порядку. Надалі в тексті нашої роботи мибудемо використовувати записи специфікаторів доступу до елементів у виглядіpublic і private (без двокрапки).
Гарний стиль програмування: Використовуйте при визначенні класу кожнийспецифікатор доступу до елементів тільки один раз, що зробить програму більшясною й простій для читання. Розміщайте першими елементи public, що єзагальнодоступними.
Визначення класу в нашій програмі містить після специфікатора доступудо елементів public прототипи наступних чотирьох функцій-елементів: Time,setTime, printMilitary і printStandard. Це — відкриті функції-елементи абовідкритий інтерфейс послуг класу. Ці функції будуть використовуватися клієнтамикласу (тобто частинами програми, що грають роль користувачів) для маніпуляцій зданими цього класу.
Зверніть увагу на функцію-елемент із тим же ім'ям, що й клас. Вонаназивається конструктором цього класу. Конструктор — це спеціальнафункція-елемент, що ініціалізує дані-елементи об'єкта цього класу. Конструкторкласу викликається автоматично при створенні об'єкта цього класу. Ми побачимо,що звичайно клас має декілька конструкторів; це досягається за допомогоюперевантаження функції.
Після специфікатора доступу до елементів private слідують три цілихелементи. Це говорить про те, що ці дані-елементи класу є доступними тількифункціям-елементам класу й, як ми побачимо далі, «друзям» класу. Такимчином, дані-елементи можуть бути доступні тільки чотирьом функціям, прототипияких включені у визначення цього класу (або друзів цього класу). Звичайнодані-елементи перераховуються в частині private, а функції-елементи — у частиніpublic. Як ми побачимо далі, можна мати функції-елементи private і дані public;останнє не типовим й вважається в програмуванні поганим тоном.
Коли клас визначений, його можна використати як тип в оголошеннях,наприклад, у такий спосіб:
Time sunset, // об'єкт типу Time
arrayOfTimes [5], // масив об'єктів типу Time
*pointerToTime, // вказівник на об’єкт типу Time
&dinnerTime = sunset; // посилання на об'єкт типу Time
Ім'я класу стає новим специфікатором типу. Може існувати безліч об'єктівкласу як і безліч змінних типу, наприклад, такого, як int. Програміст по мірінеобхідності може створювати нові типи класів. Це одна з багатьох причин, зяких C++ є розширюваною мовою.
Програма на мал.2 використовує клас Time. Ця програма створює єдинийоб'єкт класу Time, названий t. Коли об'єкт створюється, автоматичновикликається конструктор Time, що явно привласнює нульові початкові значеннявсім даним-елементам закритої частини private. Потім друкується час увійськовому й стандартному форматах, щоб підтвердити, що елементи одержалиправильні початкові значення. Після цього за допомогою функцій-елементівsetTime встановлюється час і воно знову друкується в обох форматах. Потімфункція-елемент setTime намагається дати даним-елементам неправильні значення йчас знову друкується в обох форматах.
Знову відзначимо, що дані-елементи hour, minute і second об’явленіспецифікатором доступу до елементів private. Ці закриті дані-елементи класузвичайно недоступні поза класом. Глибокий зміст такого підходу полягає в тому,що реальне становище даних усередині класу не стосується клієнтів класу. Наприклад,було б цілком можливо змінити внутрішню структуру даних і представляти,наприклад, час усередині класу як число секунд після опівночі. Клієнти могли бвикористати ті ж самі відкриті функції-елементи й одержувати ті ж самірезультати, навіть не усвідомлюючи про зроблені зміни. У цьому сенсі, говорять,що реалізація класу схована від клієнтів. Таке приховання інформації сприяємодифікаційності програм і спрощує сприйняття класу клієнтами.
// FIG 3. CPP // Клас Time.
#include
// Визначення абстрактного типу даних (АТД) Time
class Time{
public:
Time{); // конструктор
void setTime (int, int, int); // установка годин, хвилин
// та секунд
void printMilitary (); // часу у військовому форматі
void printStandard (); // друк часу
// у стандартному форматі
private:
int hour; // 0-23
int minute; // 0-59
int second; // 0-59
// Конструктор Time привласнює нульові початкові значення // кожномуелементу даних. Забезпечує погоджене
// початковий стан всіх об'єктів
Time Time:: Time () { hour = minute = second =0; }
// Завдання нового значення Time у вигляді воєнного часу. // Перевіркаправильності значень даних.
// Обнуління неправельних значень,
void Time:: setTime (int h, int m, int s) {
hour = (h>=0&&h
minute = (m >= 0 && m
second ~ (s > — 0 && s
// Друк часу у військовому форматі
void Time:: printMilitary ()
{
cout " {hour
" (minute
" (second
// Друк часу в стандартному форматі void Time:: printStandard ()
{
cout " ( (hour == 0 || hour == 12)? 12: hour% 12)
"": " " (minute
"": " " (second
" (hour
}
/> // Формуванняперевірки простого класу Time
main ()
{
Time t; // визначення екземпляра об'єкта t класу Time
cout " «Початкове значення воєнного часу дорівнює »; t. printMilitary(); cout
t. setTime (13, 27,6):
cout " endl " endl
cout
t. setTime (99, 99, 99); // спроба встановити неправильні значення cout
cout
Мал.2. Використання абстрактного типу даних Time як класу
Початкове значення воєнного часу дорівнює 00: 00: 00Початкове значення стандартного часу дорівнює 12: 00: 00 AM
Воєнний час після setTime дорівнює 13: 27: 06
Після спроби неправильної установки: Воєнний час: 00;00: 00 Стандартний час: 12: 00: 00 AM
У нашій програмі конструктор Time просто встановлює початкові значення,рівні 0, даним-елементам, (тобто задає воєнний час, еквівалентний 12AM). Цегарантує, що об'єкт при його створенні перебуває у відомому стані. Неправильнізначення не можуть зберігатися в даних-елементах об'єкта типу Time, оскількиконструктор автоматично викликається при створенні об'єкта типу Time, а всінаступні спроби змінити дані-елементи ретельно розглядаються функцією setTime.
Відзначимо, що дані-елементи класу не можуть одержувати початковізначення в тілі класу, де вони оголошуються. Ці дані-елементи повинніодержувати початкові значення за допомогою конструктора класу або їм можнаприсвоїти значення через функції.
Функція з тим же ім'ям, що й клас, але з символом-тильда (~) перед нею,називається деструктором цього класу (наш приклад не включає деструктор). Деструкторробить «завершальні службові дії над кожним об'єктом класу перед тим, якпам'ять, відведена під цей об'єкт, буде повторно використана системою.
Помітимо, що функції, якими клас постачає зовнішній світ, визначаютьсяміткою public. Відкриті функції реалізують всі можливості класу, необхідні дляйого клієнтів. Відкриті функції класу називають інтерфейсом класу або відкритимінтерфейсом.
Об’ява класу містить об’яви даних-елементів і функцій-елементів класу. Об’явафункцій-елементів є прототипами функцій. Функції-елементи можуть бути описанівсередині класу, але гарний стиль програмування полягає в описі функцій позавизначенням класу.
Відзначимо використання бінарної операції дозволу області дії (::) укожному визначенні функції-елемента, що випливає за визначенням класу на мал.3.Після того, як клас визначений і його функції-елементи Об’явлені, ціфункції-елементи повинні бути описані. Кожна функція-елемент може бути описанапрямо в тілі класу (замість включення прототипу функції класу) або після тілакласу. Коли функція-елемент описується після відповідного визначення класу,ім'я функції випереджається ім'ям класу та бінарною операцією дозволу областідії (::). Оскільки різні класи можуть мати елементи з однаковими іменами,операція дозволу області дії „прив'язує“ ім'я елемента до іменікласу, щоб однозначно ідентифікувати функції-елементи даного класу.
Незважаючи на те, що функція-елемент, об’явлена у визначенні класу,може бути описана поза цим визначенням, ця функція-елемент однаково має областюдії клас, тобто її ім'я відомо тільки іншим елементам класу поки до неїзвертаються за допомогою об'єкта класу, посилання на об'єкт класу або покажчикана об'єкт класу. Про області дії класу ми більш докладно ще поговоримо пізніше.
Якщо функція-елемент описана у визначенні класу, вона автоматичновбудовується inline. Функція-елемент, описана поза визначенням класу, може бутиinline за допомогою явного використання ключового слова inline. Нагадаємо, щокомпілятор резервує за собою право не вбудовувати ніяких функцій.
Цікаво, що функції-елементи printMilitary і printStandard не одержуютьніяких аргументів. Це відбувається тому, що функції-елементи неявно знають, щовони друкують дані-елементи певного об'єкта типу Time, для якого вониактивізовані. Це робить виклики функцій-елементів більш короткими, ніжвідповідні виклики функцій у процедурному програмуванні. Це зменшує такожймовірність передачі неправильних аргументів, неправильних типів аргументів абонеправильної кількості аргументів.
Класи спрощують програмування, тому що клієнт (або користувач об'єктакласу) має справу тільки з операціями, інкапсульованими або вбудованими воб'єкт. Такі операції звичайно проектуються орієнтовними саме на клієнта, а нена зручну реалізацію. Інтерфейси міняються, але не так часто, як реалізації. Призміні реалізації відповідно повинні змінюватися орієнтовані на реалізацію коди.А шляхом приховання реалізації ми виключаємо можливість для інших частинпрограми виявитися залежними від особливостей реалізації класу.
Часто класи не створюються „на порожнім місці“. Звичайно вониє похідними від інших класів, що забезпечують нові класи необхідними їмопераціями. Або класи можуть включати об'єкти інших класів як елементи. Такеповторне використання програмного забезпечення значно збільшує продуктивністьпрограміста. Створення нових класів на основі вже існуючих класів називаєтьсяуспадкуванням. Включення класів як елементів інших класів називаєтьсякомпозицією./>1.6 Областьдії клас і доступ до елементів класу
Дані-елементи класу(змінні, об’явлені у визначенні класу) і функції-елементи (функції, об’явлені увизначенні класу) мають областю дії клас. Функції, що не є елементами класу, мають областю дії файл.
При області дії клас елементи класу безпосередньо доступні всімфункціям-елементам цього класу й на них можна посилатися просто по імені. Позаобластю дії клас до елементів класу можна звертатися або через ім'я об'єкта,або посиланням на об'єкт, або за допомогою вказівника на об'єкт.
Функції-елементи класу можна перевантажувати, але тільки за допомогоюінших функцій-елементів класу. Для перевантаження функції-елемента простозабезпечте у визначенні класу прототип для кожної версії перевантаженої функціїй позначить кожну версію функції окремим описом.
Але, не можна перевантажити функцію-елемент класу за допомогою функціїне з області дії цього класу.
Функції-елементи мають всередині класу область діїфункцію: змінні, об’явлені у функції-елементі, відомі тільки цій функції. Якщофункція-елемент об’являє змінну з тим же ім'ям, що й змінна в області дії клас,остання робиться невидимої в області дії функції. Така схована змінна може бутидоступна за допомогою операції дозволу області. Невидимі глобальні змінніможуть бути доступні за допомогою унарної операції дозволу області дії.
Операції, для доступу до елементів класу, аналогічні операціям, длядоступу до елементів структури. Операція вибору елемента крапка () комбінуєтьсядля доступу до елементів об'єкта з ім'ям об'єкта або з посиланням на об'єкт. Операціявибору елемента стрілка (->) комбінується для доступу до елементів об'єкта звказівником на об'єкт.
Програма на мал.3 використає простий клас, названий Count, з відкритимелементом даних х типу int і відкритої функцією-елементом print, щобпроілюструвати доступ до елементів класу за допомогою операції вибору елемента.Програма створює три екземпляри змінних типу Count — counter, counterRef (посиланняна об'єкт типу Count) і counterPtr (покажчик на об'єкт типу Count). ЗміннаcounterRef об’явлена, щоб/> посилатися на counter, змінна counterPtrоб’явлена, щоб указувати на counter. Важливо відзначити, що тут елемент даних хзроблений відкритим просто для того, щоб продемонструвати способи доступу довідкритих елементів. Як ми вже встановили, дані звичайно робляться закритими (private).
// FIG6_4. CPP
// Демонстрація операцій доступу до елементів класу. і — >
#include
// Простий клас Count class Count { public:
int x;
void print () { cout
main ()
{
Count counter, // створюється об'єкт counter
*counterPtr = &counter, // покажчик на counter &counterRef =counter; // посиланя на counter
cout " «Присвоювання х значення 7 і друк по імені об'єкта:»;
counter. х =7; // присвоювання 7 елементу даних х
counter. print (); // виклик функції-елемента для друку
cout
counterRef. x = 8; // присвоювання 8 елементу даних х
counterRef. print (); // виклик функції-елемента для друку
cout x = 10; // присвоювання 10 елементу даних хcounterPtr->print (); // виклик функції-елемента для друку
return 0;
}
Мал.3. Доступ до даних-елементів об'єкта й функціям-елементам задопомогою імені об'єкта, посилання й вказівника на об'єкт
Присвоювання х значення 7 і друк по імені об'єкта: 7
Присвоювання х значення 8 і друк по посиланню: 8
Присвоювання х значення 10 і друк по покажчику: 10/> 1.7 Конструктор класу
Серед інших функцій-членів конструктор виділяється тим, що його ім'язбігається з ім'ям класу. Для оголошення конструктора за замовчуванням мипишемо:
class Account {
public:
// конструктор за замовчуванням...
Account ();
// ...
private:
char *_name;
unsigned int _acct_nmbr;
double _balance;
};
Єдине синтаксичне обмеження, що накладає на конструктор, полягає втому, що він не повинен мати тип значення, що повертає, навіть void.
Кількість конструкторів в одного класу може бути будь-яким, аби тількивсі вони мали різні списки формальних параметрів.
Звідки ми знаємо, скільки і які конструктори визначити? Як мінімум,необхідно присвоїти початкове значення кожному члену, що це потребує. Наприклад,номер рахунку або задається явно, або генерується автоматично таким чином, щобгарантувати його унікальність. Припустимо, що він буде створюватися автоматично.Тоді ми повинні дозволити ініціализувати два члени, що залишилися _nameі _balance:
Account (const char *name, double open_balance);
Об'єкт класу Account, ініціалізуємий конструктором, можнаоб’явити в такий спосіб:
Account newAcct («Mikey Matz», 0);
Якщо ж є багато рахунків, для яких початковий баланс дорівнює 0,то корисно мати конструктор, що задає тільки ім'я власника й автоматичноініцілізує _balance нулем. Один зі способів зробити це — надатиконструктор виду:
Account (const char *name);
Інший спосіб — включити в конструктор із двома параметрами значення зазамовчуванням, рівне нулю:
Account (const char *name, double open_balance = 0.0);
Обоє конструктора володіють необхідної користувачеві функціональністю,тому обоє рішення прийнятні. Ми воліємо використати аргумент за замовчуванням,оскільки в такій ситуації загальне число конструкторів класу скорочується.
Потрібно чи підтримувати також завдання одного лише початкового балансубез вказівки імені клієнта? У цьому випадку специфікація класу явно забороняєце. Наш конструктор із двома параметрами, з яких другий має значення зазамовчуванням, надає повний інтерфейс для задання початкових значень тих членівкласу Account, які можуть бути ініціалізовані користувачем:
class Account {
public:
// конструктор за замовчуванням...
Account ();
// імена параметрів в оголошенні вказувати необов'язково
Account (const char*, double=0.0);
const char* name () { return name; }
// ...
private:
// ...
};
Нижче наведені два приклади правильного визначення об'єкта класу Account,де конструкторові передається один або два аргументи:
int main ()
{
// правильно: в обох випадках викликається конструктор
// с двома параметрами
Account acct («Ethan Stern»);
Account *pact = new Account («Michael Lieberman», 5000);
if (strcmp (acct. name (), pact->name ()))
// ...
}
C++ вимагає, щоб конструктор застосовувався до певного об'єкта до йогопершого використання. Це означає, що як для acct,так і для об'єкта, на який указує pact, конструктор будевикликаний перед перевіркою в інструкції if.
Компілятор перебудовує нашу програму, вставляючи виклики конструкторів.
От як, цілком ймовірно, буде модифіковане визначення acctусередині main ():
// псевдокод на C++,
// іллюструючий внутрішню вставку конструктора
int main ()
{
Account acct;
acct. Account:: Account («Ethan Stern», 0.0);
// ...
}
Звичайно, якщо конструктор визначений як вбудований, то вінпідставляється в точці виклику.
Обробка оператора new трохи складніше. Конструктор викликається тількитоді, коли він успішно виділив пам'ять. Модифікація визначення pactу трохи спрощеному виді виглядає так:
// псевдокод на C++,
// іллюструючий внутрішню вставку конструктора при обробці new
int 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 вимагає задавати для будь-якого об'єктапрізвище власника рахунку.
У такому випадку найкраще встановити стан об'єкта так, щоб було видно,що він ще не ініціалізований коректними значеннями:
// конструктор за замовчуванням для класу Account
inline 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;
};
inline
Account:: ~Account ()
{
delete [] _name;
return_acct_number (_acct_nnmbr);
}
Зверніть увагу, що в нашому деструкторі не скидаються значення членів:
inline Account:: ~Account ()
{
// необхідно
delete [] _name;
return_acct_number (_acct_nnmbr);
// необов'язково
_name = 0;
_balance = 0.0;
_acct_nmbr = 0;
}
Робити це необов'язково, оскільки відведена під члени об'єкта пам'ятьоднаково буде звільнена. Розглянемо наступний клас:
class Point3d {
public:
// ...
private:
float x, y, z;
};
Конструктор тут необхідний для ініціалізації членів, що представляютькоординати точки. Чи потрібний деструктор? Немає. Для об'єкта класу Point3dне потрібно звільняти ресурси: пам'ять виділяється й звільняється компіляторомавтоматично на початку й наприкінці його життя.
В загальному випадку, якщо члени класу мають прості значення, скажімо,координати точки, то деструктор не потрібний. Не для кожного класу необхіднийдеструктор, навіть якщо в нього є один або більше конструкторів. Основною метоюдеструктора є звільнення ресурсів, виділених або в конструкторі, або під часжиття об'єкта, наприклад звільнення пам'яті, виділеної оператором new.
Але функції деструктора не обмежені тільки звільненням ресурсів. Вінможе реалізовувати будь-яку операцію, що за задумом проектувальника класуповинна бути виконана відразу по закінченні використання об'єкта. Так, широкорозповсюдженим прийомом для виміру продуктивності програми є визначення класу Timer,у конструкторі якого запускається та або інша форма програмного таймера. Деструкторзупиняє таймер і виводить результати вимірів. Об'єкт даного класу можна умовновизначати в критичних ділянках програми, які ми хочемо профілювати, у такийспосіб:
{
// початок критичної ділянки програми
#ifdef PROFILE
Timer t;
#endif
// критична ділянка
// t знищується автоматично
// відображається витрачений час...
}
Щоб переконатися в тім, що ми розуміємо поводження деструктора (та йконструктора теж), розберемо наступний приклад:
(1) #include «Account. h»
(2) Account global («James Joyce»);
(3) int main ()
(4) {
(5) Account local («Anna Livia Plurabelle», 10000);
(6) Account &loc_ref = global;
(7) Account *pact = 0;
(8)
(9) {
(10) Account local_too («Stephen Hero»);
(11) pact = new Account («Stephen Dedalus»);
(12) }
(13)
(14) delete pact;
(15) }
Скільки тут викликається конструкторів? Чотири: один для глобальногооб'єкта global у рядку (2); по одному для кожного злокальних об'єктів local і local_too у рядках (5) і (10)відповідно, і один для об'єкта, розподіленого в купі, у рядку (11). Ніоб’явлення посилання loc_ref на об'єкт у рядку (6), ні об’явленнявказівника pact у рядку (7) не приводять до викликуконструктора. Посилання — це псевдонім для вже сконструйованого об'єкта, уцьому випадку для global. Вказівника також лише адресує об'єкт,створений раніше (у цьому випадку розподілений у купі, рядок (11)), або неадресує ніякого об'єкта (рядок (7)).
Аналогічно викликаються чотири деструктори: для глобального об'єкта global,об’явленого в рядку (2), для двох локальних об'єктів і для об'єкта в купі привиклику delete у рядку (14). Однак у програмі немаєінструкції, з якої можна зв'язати виклик деструктора. Компілятор простовставляє ці виклики за останнім використанням об'єкта, але перед закриттямвідповідної області видимості.
Конструктори й деструктори глобальних об'єктів викликаються на стадіяхініціалізації й завершення виконання програми. Хоча такі об'єкти нормальноповодяться при використанні в тім файлі, де вони визначені, але їхнє застосуванняв ситуації, коли виробляються посилання через границі файлів, стає в C++серйозною проблемою.
Деструктор не викликається, коли з області видимості виходить посиланняабо вказівник на об'єкт (сам об'єкт при цьому залишається).
С++ за допомогою внутрішніх механізмів перешкоджає застосуваннюоператора delete до вказівника, що не адресує ніякого об'єкта,так що відповідні перевірки коду необов'язкові:
// необов'язково: неявно виконується компілятором
if (pact! = 0) delete pact;
Щораз, коли усередині функції цей оператор застосовується до окремогооб'єкта, розміщеному в купі, краще використати об'єкт класу auto_ptr,а не звичайний вказівник. Це особливо важливо тому, що пропущений виклик delete(скажемо, у випадку, коли збуджується виключення) веде не тільки до витокупам'яті, але й до пропуску виклику деструктора. Нижче приводиться прикладпрограми, переписаної з використанням auto_ptr(вона злегка модифікована, тому що об'єкт класу auto_ptrможе бути явно із для адресації іншого об'єкта тільки присвоюванням його іншомуauto_ptr):
#include
#include «Account. h»
Account global («James Joyce»);
int main ()
{
Account local («Anna Livia Plurabelle», 10000);
Account &loc_ref = global;
auto_ptr pact (new Account («Stephen Dedalus»));
{
Account local_too («Stephen Hero»);
}
// об'єкт auto_ptr знищується тут
}
/>1.10 Явнийвиклик деструктора
Іноді викликати деструктор для деякого об'єкта доводиться явно. Особливочасто така необхідність виникає у зв'язку з оператором new. Розглянемоприклад.
Коли ми пишемо:
char *arena = new char [sizeof Image];
то з купи виділяється пам'ять, розмір якої дорівнює розміру об'єктатипу Image, вона не ініціалізована й заповненавипадковими бітами.
Якщо ж написати:
Image *ptr = new (arena) Image («Quasimodo»);
то ніякої нової пам'яті не виділяється. Замість цього змінної ptrпривласнюється адреса, асоційованою зі змінною arena. Теперпам'ять, на яку вказує ptr, інтерпретується як займана об'єктом класу Image,і конструктор застосовується до вже існуючої області. Таким чином, операторрозміщення new () дозволяє сконструювати об'єкт у ранішевиділеній області пам'яті.
Закінчивши працювати із зображенням Quasimodo,ми можемо зробити якісь операції із зображенням Esmerelda,розміщеним по тій же адресі arena у пам'яті:
Image *ptr = new (arena) Image («Esmerelda»);
Однак зображення Quasimodo при цьому буде затерто, а ми йогомодифікували й хотіли б записати на диск. Звичайне збереження виконується вдеструкторі класу Image, але якщо ми застосуємо оператор delete:
// погано: не тільки викликає деструктор, але й звільняє пам'ять
delete ptr;
то, крім виклику деструктора, ще й повернемо в купу пам'ять, чогоробити не варто було б. Замість цього можна явно викликати деструктор класу Image:
ptr->~Image ();
зберігши відведену під зображення пам'ять для наступного викликуоператора розміщення new.
Відзначимо, що, хоча ptr і arena адресують ту самуобласть пам'яті в купі, застосування оператора deleteдо arena
// деструктор не викликається
delete arena;
не приводить до виклику деструктора класу Image,тому що arena має тип char*,а компілятор викликає деструктор тільки тоді, коли операндом в deleteє вказівник на об'єкт класу, що має деструктор.
1.11 Небезпека збільшення розмірупрограми
Вбудований деструктор може стати причиною непередбаченого збільшеннярозміру програми, оскільки він вставляється в кожній точці виходу всерединіфункції для кожного активного локального об'єкта. Наприклад, у наступномуфрагменті
Account acct («Tina Lee»);
int swt;
// ...
switch (swt) {
case 0:
return;
case 1:
// щось зробити
return;
case 2:
// зробити щось інше
return;
// і так далі
}
компілятор підставить деструктор перед кожною інструкцією return.Деструктор класу Account невеликий, і витрати часу й пам'яті на йогопідстановку теж малі. У противному випадку прийдеться або об’явити деструкторневбудованим, або реорганізувати програму. У прикладі вище інструкцію returnу кожній мітці case можна замінити інструкцією breakдля того, щоб у функції була єдина точка виходу:
// переписано для забезпечення єдиної точка виходу
switch (swt) {
case 0:
break;
case 1:
// щось зробити
break;
case 2:
// зробити щось інше
break;
// і так далі
}
// єдина точка виходу
return;
1.12 Константні об'єкти йфункції-елементи
Ми ще раз особливо відзначаємо принцип найменших привілеїв як один знайбільш фундаментальних принципів створення гарного програмного забезпечення. Розглянемоодин зі способів застосування цього принципу до об'єктів.
Деякі об'єкти повинні допускати зміни, інші — ні. Програміст можевикористовувати ключове слово const для вказівки на те, що об'єктнезмінний — є константним і що будь-яка спроба змінити об'єкт є помилкою. Наприклад,
const Time noon (12, 0, 0);
об’являє як константний об'єкт noon класу Time і присвоює йомупочаткове значення 12 годин пополудні.
Компілятори C++ сприймають оголошення const настільки неухильно, що впідсумку не допускають ніяких викликів функцій-елементів константних об'єктів (деякікомпілятори дають у цих випадках тільки попередження). Це жорстоко, оскількиклієнти об'єктів можливо захочуть використати різні функції-елементи читання«get», а вони, звичайно, не змінюють об'єкт. Щоб обійти це,програміст може оголосити константні функції-елементи; тільки вони можутьоперувати константними об'єктами. Звичайно, константні функції-елементи неможуть змінювати об'єкт — це не дозволить компілятор.
Константна функція вказується як const і в об’яві, і в описі задопомогою ключового слова const після списку параметрів функції, алеперед лівою фігурною дужкою, що починає тіло функції. Наприклад, у наведеномунижче прикладі об’являється як константна функція-елемент деякого класу А
int A:: getValue () const {return privateDateMember};
яка просто повертає значення одного з даних-елементів об'єкта. Якщоконстантна функція-елемент описується поза об’явою класу, то як об’явафункції-елемента, так і її опис повинні включати const.
Тут виникає цікава проблема для конструкторів і деструкторів, якізвичайно повинні змінювати об'єкт. Для конструкторів і деструкторів константнихоб'єктів оголошення const не потрібно. Конструктор повинен матиможливість змінювати об'єкт із метою присвоювання йому відповідних початковихзначень. Деструктор повинен мати можливість виконувати підготовку завершенняробіт перед знищенням об'єкта.
Програма на мал.4 створює константний об'єкт класу Time і намагаєтьсязмінити об'єкт не константними функціями-елементами setHour, setMinute іsetSecond. Як результат показані згенеровані компілятором Borland C++попередження.
// TIME5. H
// Оголошення класу Time.
// Функції-елементи описані в TIMES. CPP
#ifndef TIME5_H idefine TIME5_H
class Time { public:
Time (int = 0, int = 0, int = 0); // конструктор за замовчуванням
// функції запису set
void setTime (int, int, int); // установкачасу
void setHour (int); // установкагодин
void setMinute (int); // установкахвилин
void setSecond (int); // установкасекунд
// функції читання get (звичайно об’являється const)
int getHour () const; // повертає значення годин
int getMinute () const; // повертає значення хвилин
int getSecondf) const; // повертає значення секунд
// функції друк (звичайно об’являється const)
void printMilitary () const; // друк військового часу voidprintStandard () const; // друк стандартного часу
private:
int hour; // 0-23
int minute; // 0-59
int second; // 0-59
};
#endif
// TIME5. CPP
// Опис функцій-елементів класу Time.
finclude
iinclude «time5. h»
// Функція конструктор для ініціалізації закритих даних. // Зазамовчуванням значення рівні 0 (дивися опис класу). Time:: Time (int hr, intmin, int sec) { setTime (hr, min, sec); }
// Встановка значень години, хвилин і секунд, void Time:: setTime (inth, int m, int s) {
hour = (h >= 0 && h
minute = (m >= 0 && m
second = (s >= 0 && s
// Установка значення годин
void Time:: setHour (int h) { hour = (h >= 0 && h
// Установка значення хвилин void Time:: setMinute (int m)
{ minute = (m >= 0 && m
// Установка значення секунд void Time:: setSecond (int s)
{ second = (s >= 0 && s
// Читання значення годин
int Time:: getHour () const { return hour; }
// Читання значення хвилин
int Time:: getMinute () const { return minute; }
// Читання значення секунд
int Time:: getSecond () const { return second; }
// Відображення часу у військовому форматі: HH: MM: SS
void Time:: printMilitary () const
{
cout " (hour
" (minute
" (second
// Відображення часу в стандартному форматі: HH: MM: SS AM // (або РМ)
void Time:: printStandard () const {
cout " ( (hour == 12)? 12: hour% 12)" ": "
" (minute
// FIG7_1. CPP
// Спроба одержати доступ до константного об'єкта
// з не-константними функціями-елементами.
#include
#include «time5. h»
main () {
const Time t (19, 33, 52); // константний об'єкт
t. setHour (12); // ПОМИЛКА: не-константна функція елемент t. setMinute(20); // ПОМИЛКА: не-константна функція елемент t. setSecond (39); // ПОМИЛКА: не-константнафункція елемент
return 0; }
Compiling FIG7_1. CPP:
Warning FIG7_1. CPP: Non-const function
Time:: setHour (int) called for const object Warning FXG7 l. CPP: Non-constfunction
Time:: setMinute (int) callers for const object Warning FIG7 1. CPP: Non-constfunction
Time:: setSecond (int) called for const object
Мал.4. Використання класу Time з константними об'єктами й константнимифункціями-елементами
Зауваження: Константна функція-елемент може бути перевантаженанеконстантним варіантом. Вибір того, яка з перевантажених функцій-елементівбуде використатися, виконується компілятором автоматично залежно від того, був об’явленийоб'єкт як const чи ні.
Константный об'єкт не може бути змінений за допомогою присвоювання, такщо він повинен мати початкове значення. Якщо дані-елементи класу об’явлені якconst, то треба використати ініціалізатор елементів, щоб забезпечитиконструктор об'єкта цього класу початковими значенням даних-елементів. Мал.7демонструє використання ініціалізатора елементів для завдання початковогозначення константному елементу increment класу Increment. Конструктор дляIncrement змінюється в такий спосіб:
Increment:: Increment (int c, int i): increment (i) { count = c; }
Запис: increment (i) викликає завдання початкового значення елементаincrement, рівного i. Якщо необхідно задати початкові значення відразудекільком елементам, просто включіть їх у список після двокрапки, розділяючикомами. Використовуючи ініціатори елементів, можна присвоїти початкові значеннявсім даним-елементам.
// Використання ініціалізатора елементів для
// ініціалізації даних константного вбудованого типу.
#include
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
"", increment = " " increment
main ()
{
Increment value (10,5);
cout
for (int j = 1; j
value. addlncrement ();
cout
}
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
r. elem (i) = 0;
for (int j = 0; 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
r. v [i] = 0;
for (int j = 0; j
r. v [i] +=m. v [i] [j] * v. v [j];
}
return r;
}
Відзначимо, що подібно функції-члену дружня функція явно описується вописі класу, з яким дружить. Тому вона є невід'ємною частиною інтерфейсу класунарівні з функцією-членом.
Функція-член одного класу може бути другом іншого класу:
class x {
// ...
void f ();
};
class y {
// ...
friend void x:: f ();
};
Цілком можливо, що всі функції одного класу є друзями іншого класу. Дляцього є коротка форма запису:
class x {
friend class y;
// ...
};
У результаті такого опису всі функції-члени y стають друзями класу x.
1.14 Ядро ООП: Успадкування та поліморфізм
Ця глава присвячена поняттю похідного класу. Похідні класи — цепростий, гнучкий і ефективний засіб визначення класу. Нові можливості додаютьсядо вже існуючого класу, не вимагаючи його перепрограмування або перетрансляції.За допомогою похідних класів можна організувати загальний інтерфейс іздекількома різними класами так, що в інших частинах програми можна будеодноманітно працювати з об'єктами цих класів. Вводиться поняття віртуальноїфункції, що дозволяє використати об'єкти належним чином навіть у тих випадках,коли їхній тип на стадії трансляції невідомий. Основне призначення похіднихкласів — спростити програмістові завдання вираження спільності класів.
/>/>
1.4.1 Похідні класи
Обговоримо, як написати програму обліку службовців деякої фірми. У нійможе використатися, наприклад, така структура даних:
struct employee { // службовець
char* name; // ім'я
short age; // вік
short department; // відділ
int salary; // оклад
employee* next;
// ...
};
Поле next потрібно для зв'язування в список записів про службовціводного відділу (employee). Тепер спробуємо визначити структуру даних длякеруючого (manager):
struct manager {
employee emp; // запис employee для керуючого
employee* group; // підлеглий колектив
short level;
// ...
};
Керуючий також є службовцем, тому запис employee зберігається в членіemp об'єкта manager. Для людини ця спільність очевидна, але для трансляторачлен emp нічим не відрізняється від інших членів класу. Вказівник на структуруmanager (manager*) не є вказівником на employee (employee*), тому не можнавільно використати один замість іншого. Зокрема, без спеціальних дій не можнаоб'єкт manager включити до списку об'єктів типу employee. Доведеться абовикористати явне приведення типу manager*, або в список записів employeeвключити адресу члена emp. Обоє рішень некрасиві й можуть бути доситьзаплутаними. Правильне рішення полягає в тому, щоб тип manager був типомemployee з деякою додатковою інформацією:
struct manager: employee {
employee* group;
short level;
// ...
};
Клас manager є похідним від employee, і, навпаки, employee є базовимкласом для manager. Крім члена group у класі manager є члени класу employee (name,age і т.д.). Графічно відношення спадкування звичайно зображується у виглядістрілки від похідних класів до базового:
employee
manager
Звичайно говорять, що похідний клас успадковує базовий клас, тому йвідношення між ними називається успадкуванням. Іноді базовий клас називаютьсуперкласом, а похідний — підлеглим класом. Але ці терміни можуть викликатиздивування, оскільки об'єкт похідного класу містить об'єкт свого базового класу.Взагалі похідний клас більше свого базового в тому розумінні, що в ньомуутримується більше даних і визначено більше функцій.
Маючи визначення employee і manager, можна створити список службовців,частина з яких є й керуючими:
void f ()
{
manager m1, m2;
employee e1, e2;
employee* elist;
elist = &m1; // помістити m1 в elist
m1. next = &e1; // помістити e1 в elist
e1. next = &m2; // помістити m2 в elist
m2. next = &e2; // помістити m2 в elist
e2. next = 0; // кінець списку
}
Оскільки керуючий є також службовцем, вказівник manager* можнавикористати як employee*. У той же час службовець не обов'язково є керуючим, ітому employee* не можна використати як manager*.
У загальному випадку, якщо клас derived має загальний базовий класbase, то вказівник на derived можна без явних перетворень типу привласнюватизмінній, що має тип вказівника на base. Зворотне перетворення від вказівника наbase до вказівника на derived може бути тільки явним:
void g ()
{
manager mm;
employee* pe = &mm; // нормально
employee ee;
manager* pm = ⅇ // помилка:
// не всякий службовець є керуючим
pm->level = 2; // катастрофа: при розміщенні ee
// пам'ять для члена 'level' не виділялася
pm = (manager*) pe; // нормально: насправді pe
// не настроєно на об'єкт mm типу manager
pm->level = 2; // відмінно: pm указує на об'єкт mm
// типу manager, а в ньому при розміщенні
// виділена пам'ять для члена 'level'
}
Іншими словами, якщо робота з об'єктом похідного класу йде черезвказівник, то його можна розглядати як об'єкт базового класу. Зворотне невірно.Відзначимо, що у звичайній реалізації С++ не передбачається динамічногоконтролю над тим, щоб після перетворення типу, подібного тому, щовикористовувалося в присвоюванні pe в pm, отриманий у результаті вказівникдійсно був налаштований на об'єкт необхідного типу.
1.14.2 Функції-члени
Прості структури даних начебто employee і manager самі по собі незанадто цікаві, а часто й не дуже корисні. Тому додамо до них функції:
class employee {
char* name;
// ...
public:
employee* next; // перебуває в загальній частині, щоб
// можна було працювати зі списком
void print () const;
// ...
};
class manager: public employee {
// ...
public:
void print () const;
// ...
};
Треба відповісти на деякі питання. Яким чином функція-член похідногокласу manager може використати члени базового класу employee? Які членибазового класу employee можуть використати функції-члени похідного класуmanager? Які члени базового класу employee може використати функція, що не єчленом об'єкта типу manager? Які відповіді на ці питання повинна даватиреалізація мови, щоб вони максимально відповідали завданню програміста?
Розглянемо приклад:
void manager:: print () const
{
cout
}
Член похідного класу може використати ім'я із загальної частини свогобазового класу нарівні з усіма іншими членами, тобто без вказівки імені об'єкта.Передбачається, що є об'єкт, на який настроєний 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 name department
// ...
break;
case M:
cout name department
// ...
manager* p = (manager*) e;
cout level
// ...
break;
}
}
Надрукувати список службовців можна так:
void f (const employee* elist)
{
for (; elist; elist=elist->next) print_employee (elist);
}
Це цілком гарне рішення, особливо для невеликих програм, написаниходнією людиною, але воно має істотний недолік: транслятор не може перевірити,наскільки правильно програміст поводиться з типами. У більших програмах цеприводить до помилок двох видів. Перша — коли програміст забуває перевіритиполе типу. Друга — коли в перемикачі вказуються не всі можливі значення полятипу. Цих помилок досить легко уникнути в процесі написання програми, алезовсім нелегко уникнути їх при внесенні змін у нетривіальну програму, аособливо, якщо це велика програма, написана кимось іншим. Ще сутужніше уникнутитаких помилок тому, що функції типу print () часто пишуться так, щоб можна булоскористатися спільністю класів:
void print (const employee* e)
{
cout name department
// ...
if (e->type == M) {
manager* p = (manager*) e;
cout level
// ...
}
}
Оператори 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
// ...
}
Ми бачимо, що віртуальну функцію можна використати, навіть якщо немаєпохідних класів від її класу. У похідному ж класі не обов'язково перевизначитивіртуальну функцію, якщо вона там не потрібна. При побудові похідного класутреба визначати тільки ті функції, які в ньому дійсно потрібні:
class manager: public employee {
employee* group;
short level;
// ...
public:
manager (char* n, int d);
// ...
void print () const;
};
Місце функції print_employee () зайняли функції-члени print (), і вонастала не потрібна. Список службовців будує конструктор employee. Надрукуватийого можна так:
void employee:: print_list ()
{
for (employee* p = list; p; p=p->next) p->print ();
}
Дані про кожного службовця будуть друкуватися відповідно до типу записупро нього. Тому програма
int main ()
{
employee e («J. Brown»,1234);
manager m («J. Smith»,2,1234);
employee:: print_list ();
}
надрукує
J. Smith 1234
level 2
J. Brown 1234
Зверніть увагу, що функція друку буде працювати навіть у тому випадку,якщо функція employee_list () була написана й трансльована ще до того, як бувзадуманий конкретний похідний клас manager! Очевидно, що для правильної роботивіртуальної функції потрібно в кожному об'єкті класу employee зберігати деякуслужбову інформацію про тип. Як правило, реалізація як така інформаціявикористовується просто вказівник. Цей вказівник зберігається тільки дляоб'єктів класу з віртуальними функціями, але не для об'єктів всіх класів, інавіть для не для всіх об'єктів похідних класів. Додаткова пам'ять виділяєтьсятільки для класів, у яких описані віртуальні функції. Помітимо, що привикористанні поля типу, для нього однаково потрібна додаткова пам'ять.
Якщо у виклику функції явно зазначена операція дозволу областівидимості::, наприклад, у виклику manager:: print (), то механізм викликувіртуальної функції не діє. Інакше подібний виклик привів би до нескінченноїрекурсії. Уточнення імені функції дає ще один позитивний ефект: якщо віртуальнафункція є підстановкою (у цьому немає нічого незвичайного), те у виклику зоперацією:: відбувається підстановка тіла функції. Це ефективний спосібвиклику, якому можна застосовувати у важливих випадках, коли одна віртуальнафункція звертається до іншої з тим самим об'єктом. Приклад такого випадку — викликфункції manager:: print (). Оскільки тип об'єкта явно задається в самомувиклику manager:: print (), немає потреби визначати його в динаміку для функціїemployee:: print (), що і буде викликатися.
1.14.7 Абстрактні класи
Багато класів подібні із класом employee тим, що в них можна датирозумне визначення віртуальним функціям. Однак, є й інші класи. Деякі,наприклад, клас shape, представляють абстрактне поняття (фігура), для якого неможна створити об'єкти. Клас shape набуває сенсу тільки як базовий клас удеякому похідному класі. Причиною є те, що неможливо дати осмислене визначеннявіртуальних функцій класу shape:
class shape {
// ...
public:
virtual void rotate (int) { error («shape:: rotate»); }
virtual void draw () { error («shape:: draw»): }
// не можна не обертати, не малювати абстрактну фігуру
// ...
};
Створення об'єкта типу shape (абстрактної фігури) законна, хоча зовсімбезглузда операція:
shape s; // нісенітниця: ''фігура взагалі''
Вона безглузда тому, що будь-яка операція з об'єктом s приведе допомилки.
Краще віртуальні функції класу shape описати як чисто віртуальні. Зробитивіртуальну функцію чисто віртуальної можна, додавши ініціалізатор = 0:
class shape {
// ...
public:
virtual void rotate (int) = 0; // чисто віртуальна функція
virtual void draw () = 0; // чисто віртуальна функція
};
Клас, у якому є віртуальні функції, називається абстрактним. Об'єктитакого класу створити не можна:
shape s; // помилка: змінна абстрактного класу shape
Абстрактний клас можна використати тільки в якості базового для іншогокласу:
class circle: public shape {
int radius;
public:
void rotate (int) { } // нормально:
// перевизначення shape:: rotate
void draw (); // нормально:
// перевизначення shape:: draw
circle (point p, int r);
};
Якщо чиста віртуальна функція не визначається в похідному класі, товона й залишається такою, а значить похідний клас теж є абстрактним. При такомупідході можна реалізовувати класи поетапно:
class X {
public:
virtual void f () = 0;
virtual void g () = 0;
};
X b; // помилка: опис об'єкта абстрактного класу X
class Y: public X {
void f (); // перевизначення X:: f
};
Y b; // помилка: опис об'єкта абстрактного класу Y
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++;
}
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 (constX&). Особливо це важливо в тих випадках, коли визначений деструктор. Якщо вкласі X є нетривіальний деструктор, наприклад, що робить звільнення об'єкта увільній пам'яті, найімовірніше, у цьому класі буде потрібно повний набірфункцій, щоб уникнути копіювання об'єктів по членах:
class X {
// ...
X (something); // конструктор, що створює об'єкт
X (const X&); // конструктор копіювання
operator= (const X&); // присвоювання:
// видалення й копіювання
~X (); // деструктор
};
Є ще два випадки, коли доводиться копіювати об'єкт: передача параметрафункції й повернення нею значення. При передачі параметра неініціалізованазмінна, тобто формальний параметр ініціалізується. Семантика цієї операціїідентична іншим видам ініціалізації. Теж відбувається й при поверненні функцієюзначення, хоча цей випадок не такий очевидний. В обох випадках використаєтьсяконструктор копіювання:
string g (string arg)
{
return arg;
}
main ()
{
string s = «asdf»;
s = g (s);
}
Очевидно, після виклику g () значення s повинне бути «asdf». Неважко записати в параметр s копію значення s, для цього треба викликатиконструктор копіювання для string. Для одержання ще однієї копії значення s повиходу з g () потрібний ще один виклик конструктора string (const string&).Цього разу ініціалізується тимчасова змінна, котра потім привласнюється s. Дляоптимізації одну, але не обидві, з подібних операцій копіювання можна забрати. Природно,тимчасові змінні, використовувані для таких цілей, знищуються належним чиномдеструктором string:: ~string ().
Якщо в класі X операція присвоювання X:: operator= (const X&) іконструктор копіювання X:: X (const X&) явно не задані програмістом, щобракують операції будуть створені транслятором. Ці створені функції будутькопіювати по членах для всіх членів класу X. Якщо члени приймають простізначення, як у випадку комплексних чисел, це, те, що потрібно, і створеніфункції перетворяться в просте й оптимальне поразрядное копіювання. Якщо длясамих членів визначені користувальницькі операції копіювання, вони й будутьвикликатися відповідним чином:
class Record {
string name, address, profession;
// ...
};
void f (Record& r1)
{
Record r2 = r1;
}
Тут для копіювання кожного члена типу string з об'єкта r1 будевикликатися string:: operator= (const string&).
У нашому першому й неповноцінному варіанті строковий клас маєчлен-вказівник і деструктор. Тому стандартне копіювання по членах для ньогомайже напевно невірно. Транслятор може попереджати про такі ситуації. 1.15.6 Інкремент і декремент
Нехай є програма з розповсюдженою помилкою:
void f1 (T a) // традиційне використання
{
T v [200];
T* p = &v [10];
p--;
*p = a; // Приїхали: `p' настроєні поза масивом,
// і це не виявлено
++p;
*p = a; // нормально
}
Природне бажання замінити вказівник p на об'єкт класу CheckedPtrTo, поякому непряме звертання можливо тільки за умови, що він дійсно вказує на об'єкт.Застосовувати інкремента і декремента до такого вказівника буде можна тільки втому випадку, що вказівник настроєний на об'єкт у границях масиву й урезультаті цих операцій вийде об'єкт у границях того ж масиву:
class CheckedPtrTo {
// ...
};
void f2 (T a) // варіант із контролем
{
T v [200];
CheckedPtrTo p (&v [0],v, 200);
p--;
*p = a; // динамічна помилка:
// 'p' вийшов за межі масиву
++p;
*p = a; // нормально
}
Інкремент і декремент є єдиними операціями в С++, які можна використатияк постфіксні так і префіксні операції. Отже, у визначенні класу CheckedPtrToми повинні передбачити окремі функції для префіксних і постфіксних операційінкремента й декремента:
class CheckedPtrTo {
T* p;
T* array;
int size;
public:
// початкове значення 'p'
// зв'язуємо з масивом 'a' розміру 's'
CheckedPtrTo (T* p, T* a, int s);
// початкове значення 'p'
// зв'язуємо з одиночним об'єктом
CheckedPtrTo (T* p);
T* operator++ (); // префіксна
T* operator++ (int); // постфіксна
T* operator-- (); // префіксна
T* operator-- (int); // постфісна
T& operator* (); // префіксна
};
Параметр типу int служить вказівкою, що функція буде викликатися дляпостфісної операції. Насправді цей параметр є штучним і ніколи невикористається, а служить тільки для розходження постфіксної і префіксноїоперації. Щоб запам'ятати, яка версія функції operator++ використається якпрефіксна операція, досить пам'ятати, що префіксна є версія без штучногопараметра, що вірно й для всіх інших унарних арифметичних і логічних операцій. Штучнийпараметр використається тільки для «особливих» постфіксних операцій++ і — -. За допомогою класу CheckedPtrTo приклад можна записати так:
void f3 (T a) // варіант із контролем
{
T v [200];
CheckedPtrTo p (&v [0],v, 200);
p. operator-і (1);
p. operator* () = a; // динамічна помилка:
// 'p' вийшов за межі масиву
p. operator++ ();
p. operator* () = a; // нормально
}
1.15.7 Перевантаження операційпомістити в потік і взяти з потоку
C++ здатний вводити й виводити стандартні типи даних, використовуючиоперацію помістити в потік " і операцію взяти з потоку ". Ці операціївже перевантажені в бібліотеках класів, якими постачені компілятори C++, щобобробляти кожний стандартний тип даних, включаючи рядки й адреси пам'яті. Операціїпомістити в потік і взяти з потоку можна також перевантажити для того, щобвиконувати введення й вивід типів користувача. Програма на малюнку 8 демонструєперевантаження операцій помістити в потік і взяти з потоку для обробки данихпевного користувачем класу телефонних номерів PhoneNumber. У цій програміпередбачається, що телефонні номери вводяться правильно. Перевірку помилок мизалишаємо для вправ.
На мал.8 функція-операція взяти з потоку (operator") одержує якаргументи посилання input типу istream, і посилання, названу num, на заданийкористувачем тип PhoneNumber; функція повертає посилання типу istream. Функція-операція(operator") використається для введення номерів телефонів у вигляді
(056) 555-1212
в об'єкти класу PhoneNumber. Коли компілятор бачить вираження
cin >> phone
в main, він генерує виклик функції
operator>> (cin, phone);
Після виконання цього виклику параметр input стає псевдонімом для cin,а параметр num стає псевдонімом для phone. Функція-операція використовуєфункцію-елемент getline класу istream, щоб прочитати з рядка три частинителефонного номера викликаного об'єкта класу PhoneNumber (num уфункції-операції й phone в main) в areaCode (код місцевості), exchange (комутатор)і line (лінія). Символи круглих дужок, пробілу й дефіса пропускаються привиклику функції-елемента ignore класу istream, що відкидає зазначену кількістьсимволів у вхідному потоці (один символ за замовчуванням). Функція operator"повертає посилання input типу istream (тобто cin). Це дозволяє операціямвведення об'єктів PhoneNumber бути зчепленими з операціями уведення іншихоб'єктів PhoneNumber або об'єктів інших типів даних. Наприклад, два об'єктиPhoneNumber могли б бути уведені в такий спосіб:
cin >> phonel >> phone2;
Спочатку було б виконане вираження cin " phonel шляхом виклику
operator>> (cin, phonel);
// Перевантаження операцій помістити в потік і взяти з потоку.
#include
class PhoneNumber{
friend ostream soperator > (istream &, PhoneNumber &);
private:
char areaCode [4]; // трицифровий код місцевості й нульовий символ
char exchange [4]; // трицифровий комутатор і нульовий символ
char line [5]; // чотирицифрова лінія й нульовий символ
};
// Перевантажена операція помістити в потік
// (вона не може бути функцією-елементом).
ostream &operator
{
output
«num. exchange
return output; // дозволяє cout
}
// Перевантажена операція взяти зпотоку
istream &operator>> (istream sinput, PhoneNumber &num)
{
input. ignore (); // пропуск (
input. getline (num. areaCode,
4); // введення коду місцевості
input. ignore (2); // пропуск) і пробілу
input. getline (num. exchange,
4); // введення комутатора
input. ignore (); // пропуск дефіса (-)
input. getline (num. line,
5); // введення лінії
return input; // дозволяє cin >> a >>b >>c;
}
main () {
PhoneNumber phone; // створення об'єкта phone
cout
“» вигляді (123) 456-7890: " " endl;
// cin >> phone активізує функцію operator>> // шляхомвиклику operator>> (cin, phone). cin >> phone;
// cout
cout
return 0; }
Введіть номер телефону у вигляді (123) 456-7890: (800) 555-1212
Був введений номер телефону: (800) 555-1212
Мал.8 Задані користувачем операції “помістити в потік» і«взяти з потоку»
Цей виклик міг би потім повернути посилання на cin як значення cin" phonel, так що частина, що залишилася, вираження була б інтерпретованапросто як cin " phone2. Це було б виконане шляхом виклику
operator" (cin, phone2);
Операція помістити в потік одержує як аргументи посилання output типуostream і посилання пшп на певний користувачем тип PhoneNumber і повертаєпосилання типу ostream. Функція operator" виводить на екран об'єкти типуPhoneNumber. Коли компілятор бачить вираження
cout
в main, він генерує виклик функції
operator
Функція operator" виводить на екран частини телефонного номера якрядка, тому що вони зберігаються у форматі рядка (функція-елемент getline класуistream зберігає нульовий символ після завершення введення).
Помітимо, що функції operator" і operator" об’явлені в classPhoneNumber не як функції-елементи, а як дружні функції. Ці операції не можутьбути елементами, тому що об'єкт класу PhoneNumber з'являється в кожному випадкуяк правий операнд операції; а для перевантаженої операції, записаної якфункція-елемент, операнд класу повинен з'являтися ліворуч. Перевантаженіоперації помістити в потік і взяти з потоку повинні об’являтися як дружні, якщовони повинні мати прямий доступ до закритих елементів класу з міркуваньпродуктивності.
/>2.Розробка власного класу clsString
2.1 Загальний алгоритм вирішення
Створимо базовий клас TPString у якому розмістимо мінімальнонеобхіднікомпоненти, але при цьому цей клас вже буде функціональною одиницею. На основікласу TPString створимо два нащадки TPStrThread-відповідатиме за потоковуобробку рядка, а клас TPStrCompare-відповідатиме за порівнння. Обидва класибудуть абстрактними, так як представляють логічно незавершений результатвиконання завдання. Використовуючи множинне успадкування створимо результуючийклас clsString, додавши іще декілька методів.
Загальна UML діаграма пропонованого варіанту
/>
/>2.2 Детальнийанализ
Почнемо аналізувати програму з класу TPString. Цей клас є базовим, акрім того може бути і віртуальним (для нащадків), тобто потрібен конструктор зазамовченням. Цю функцію краще всього виконає конструктор перетворення. Йогопрототип:
TPString (const char * = "");
Цей конструктор виконуватиме перетворення рядків у стилі С. Кодконструктору
TPString:: TPString (const char *s)
{
len=strlen (s);
BuffLen=0;
symb=NULL;
setString (s);
}
Захищена змінна len зберігає довжину рядка, а змінна BuffLen зберігаєдовжину буферу пам’яті, на який посилається вказівник symb. Функція setStringвиконує всю роботу зі збереження рядка.
Конструктор копіювання реалізований майже однаковоз конструктором перетворення, окрім того, що аргументом є посилання на об’єктTPString.
TPString:: TPString (TPString & copy)
{
len (copy. len)
BuffLen=0;
symb=NULL;
setString (copy. symb);
}
Головне завдання деструктору-звільнити пам’ять, де зберігається рядок.
TPString:: ~TPString ()
{
delete [] symb;
}
Операція індексації реалізована 2 функціями
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)
(12)
(13)
(14)
char&TPString:: operator [] (int index)
{
if ( (index=len))
FatalError ();
return * (symb+index);
}
const char &TPString:: operator [] (int index) const
{
if ( (index=len))
{
FatalError ();
}
return symb [index];
}
(1) даємо посилання на відповідний до індексусимвол для того, щоб у нього можна було записати необхідну інформацію. Аленедоліком є те, що замість присвоєннясимволу, ми можемо за посиланням зберегти адресу цього символа, а це можевикликати помилки в роботі. Для недопущення цієї помилки використовуємо другуоперацію індексації (7), котра повертає константний символ і, до того ж, єконстантною.
У разі спроби прочитати/записати символ занеіснуючим індексом, программа викликаєфункцію FatalError (), котра завершить виконання.
Перевантаження оператора писвоєння є аналогічною доконструктора копіювання, за виключенням того, що вона повертає вказівник на об’єкт,котрому присвоюється значення. Це даєможливість наступного коду:
TPString a,b,c;
· · ·
a = b = c;
Операцій додавання рядків, або конкатенаціяреалізовані наступним чином:
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)
(12)
(13)
(14)
(15)
(16)
(17)
(18)
(19)
(20)
(21)
(22)
(23)
TPString &TPString:: operator+= (const TPString& part)
{
if (BuffLen
BuffLen=len+1+part. len;
char *ptr=new char [BuffLen];
strcpy (ptr,symb);
strcpy (ptr+len,part. symb);
ptr [BuffLen-1] =0;
delete [] symb;
symb=ptr;
} else {
strcpy (symb+len,part. symb);
}
len+=part. len;
return *this;
}
TPString &TPString:: operator+ (const TPString& part)
{
TPString temp (*this);
temp+=part;
return temp;
}
В (1) об’являється оператор += котрий і виконуєконкатенацію. Параметром є посилання на об’єкт типу TPString. сonst гарантуєнезмінність об’єкту. (3) перевірка на достатність виділеної пам’яті длярозміщення рядка. (4) обчислення необхідго розміру буферу. (5) видіенянеобхідної пам’яті. (6) та (7) копіювання рядків до нового буферу. (8) встановленнясимволу кінця рядка. (9) знищення старого буферу.
Оператор + загалом об’влений у (18) також має константний параметр. У (20) створюєтьсятимчасовий об’єкт на основі існуючого. Далі виклик += і повернення результату. Завдякипервантаженню операції = уникаємо помлок копіювання.
Функція Clear () присвоює значення рядку “”
void TPString:: Clear ()
{
len=0;
if (symb! =NULL) symb [0] ='\0';
}
Функція видалення певної кількості символів приймаєдва параметра: стартова позиція та кількість символів.
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
void TPString:: TPdelete (int startpos, int count)
{
if (startpos>=len||startpos
if (startpos+count>=len||count==0) count=len-startpos+1;
int st=startpos+count;
for (; st
len=len-count;
}
Алгоритм видалення досить простий: символи з кінця рядкапереписуються замість тих, що підлягають видаленню (6). Перевірки (3) та (4) реалізуютькоректність роботи алгоритму.
Алгоритм вставки рядка в рядок є більш складним,але подібний до операції інкременту.
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)
(12)
(13)
(14)
(15)
(16)
(17)
(18)
(19)
(20)
(21)
(22)
(23)
(24)
(25)
(26)
(27)
void TPString:: insert (TPString& part, int pos, int count)
{
if (pos>len) return;
if (count>part. len) count=part. len;
if (part. len
if (BuffLen>=len+count+1) {
for (int i=len; i>=pos; — -i)
{
symb [i+count] =symb [i];
}
for (int i=0; i
{
symb [pos] =part. symb [i];
}
} else {
char *temp=new char [len+part. len+1];
strncpy (temp,symb,pos);
strncpy (temp,part. symb,count);
strncpy (temp,symb+pos,len-pos);
delete [] symb;
symb=temp;
BuffLen=len+part. len+1;
}
len+=count;
}
Рядки (3) — (5) реалізують коректність роботиалгоритму. У рядках (7) — (24) реалізований алгоритм конкатенації, оснований напопередньому «роздвиганні» базового рядка у який виконується ставка. У(26) ми встановлюємо поточну довжину рядка.
Захищена функція
void TPString:: setString (const char* s)
{
if (BuffLen
{
if (symb! =NULL) delete [] symb;
BuffLen=len+1;
symb=new char [BuffLen];
}
strcpy (symb,s);
}
виконує виділення необхідного об’єму буферу такопіювання у нього нове значення.
Об’ява нащадку TPStrThread від TPString маєнаступний вигляд:
class TPStrThread abstract: virtual public TPString
{
···
}
Ключове слово abstract говорить про те, що об’єктикласу неможна створювати, проте ми додали конструктор, що ініціалізуватиме уподальшому усю систему.
Головною відмінністю є наявність двох дружніхфункцій, що є перевантаженням операцій введення/виведення.
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)
(12)
(13)
(14)
(15)
(16)
(17)
(18)
(19)
(20)
(21)
(22)
(23)
(24)
(25)
(26)
(27)
(28)
(29)
(30)
(31)
ostream &operator
{
for (int i=0; i
out
return out;
}
istream &operator>> (istream& input, TPStrThread& tp)
{
int i=256;
int k=-1;
char *temp=new char [i];
do{
k++;
if (k>i) {
i
char * t=new char [i];
strncpy (t,temp,k);
delete [] temp;
temp=t;
}
input. get (temp [k]);
}while (temp [k]! ='\n');
temp [k] =0;
if (tp. symb! =NULL) delete [] tp. symb;
tp. symb=temp;
tp. BuffLen=i;
tp. len=strlen (temp);
return input;
}
У рядку (1)перевантажується операція виведення, вона подібна до операції виведення рядка устилі С. (5) повертає посилання на потік, що дозволяє виконувати наступнуоперацію:
cout
Операція введення (8) реалізована на основідинамічного алгоритму: виділяється буфер, якщо не вистачає, то виділяється удва рази більший (у нього копіюється попередній і звільняється) і т.д. Замістьмноження на 2 використовуюємо швидшу операцію зсуву. Це дозволяє максимальнозбалансувати швидкість виконання та використання ресурсів.
Об’ява нащадку TPStrCompare від TPString маєнаступний вигляд:
class TPStrCompare abstract: virtual publicTPString
{···}
У ньому ми перевантажимо всі операції порівняння:
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)
(12)
(13)
(14)
(15)
(16)
(17)
(18)
(19)
(20)
(21)
(22)
(23)
(24)
(25)
(26)
(27)
(28)
(29)
bool TPStrCompare:: operator! () const
{
if (len==0) return true; else return false;
}
bool TPStrCompare:: operator! = (const TPStrCompare& part) const
{
return (strcmp (symb,part. symb)! =0);
}
bool TPStrCompare:: operator== (const TPStrCompare& part) const
{
return! (*this! = part);
}
bool TPStrCompare:: operator
{
return strcmp (symb,part. symb)
}
bool TPStrCompare:: operator> (const TPStrCompare& part) const
{
return (strcmp (symb,part. symb) >0);
}
bool TPStrCompare:: operator
{
return! (*this> part);
}
bool TPStrCompare:: operator>= (const TPStrCompare& part) const
{
return! (*this
}
У рядках (1), (6), (10), (14), (16), (18), (22), (26)об’являються оператори порівнняння. Кожен з них є константною функцією таприймає константні аргументи, що гарантує захищеність, та щожливістьвикористання, коли об’єкт був об’явлений константно.
У рядках (16) та (20) викорисовубться функііїпорівняння у стилі С. Останні функції порівняння (крім (1)) створювалися наоснові заданих.
Розробка результуючого класу пов’язана зуспадкуванням від двох абстрактних. Має наступну об’яву:
class clsString: public TPStrThread, publicTPStrCompare
{ ··· }
Зауважимо, що базові класи не є віртуальними інемає наслідування від базового класу (як це зазначено у теоретичній частині). Викликиконструкторів будуть проходити так:
Конструктор за замовчуванням TPString
Конструктор за замовчуванням TPStrThread абоTPStrCompare
Конструктор за замовчуванням TPStrCompare абоTPStrThread
Конструктор clsString
Пункти 2 і 3 рівносильні і їх порядок залежить від компілятору (хоча в стандарті сказано,що вони викликаються в порядку об’яви).
В звязку з тим, що конструктори та операціїприсвоєння не наслідубться потрібно їх створювати зоново. Конструкторикопіювання та перетворення аналогічні TPString. Розглянемо добавленіконструктори.