Лекція № 11 Тема: Класи та об’єкти в мові С++ План Опис класів Опис об’єктів Конструктори Конструктор копіювання Дружні функції Дружні класи Деструктор Опис класу Клас є абстрактним типом даних, який визначається користувачем, і є моделлю реального об'єкту у вигляді даних і функцій для роботи з ними. Дані класу називаються полями (по аналогії з полями структури), а функції класу — методами. Поля і методи називаються елементами класу. Опис класу в першому наближенні виглядає так: class <ім'я> { [ private: ] <опис прихованих элементів> public: <опис доступних элементів> }; // Опис закінчується крапкою з комою Специфікатори доступу private і publіс управляють видимістю елементів класу. Елементи, описані після службового слова private, видимі тільки всередині класу. Цей вид доступу прийнятий в класі за замовчуванням. Інтерфейс класу описаний після специфікатора public. Дія будь-якого специфікатора розповсюджується до наступного специфікатора або до кінця класу. Можна задавати декілька секцій private і public, порядок їх проходження значення не має. Поля класу: можуть мати будь-який тип, окрім типу цього ж класу (але можуть бути покажчиками або посиланнями на цей клас); можуть бути описані з модифікатором const, при цьому вони ініціалізуються тільки один раз (за допомогою конструктора) і не можуть змінюватися; можуть бути описані з модифікатором static але ні як auto, extern і register.Ініціалізація полів при описі не допускається. Класи можуть бути глобальними (оголошеними зовні будь-якого блоку) і локальними (оголошеними всередині блоку, наприклад, функції або іншого класу). Нижче перераховані деякі особливості локального класу: всередині локального класу можна використовувати типи, статичні (static) і зовнішні (extern) змінні, зовнішні функції і елементи перерахувань з області, в якій він описаний; забороняється використовувати автоматичні змінні з цієї області; локальний клас не може мати статичних елементів; методи цього класу можуть бути описані тільки всередині класу; якщо один клас вкладений в інший клас, вони не мають яких-небудь особливих прав доступу до елементів один до одного і можуть звертатися до них тільки за загальними правилами. Як приклад створимо клас, моделюючий персонаж комп'ютерної гри. Для цього вимагається задати його властивості (наприклад, кількість щупалець, силу або наявність гранатомета) і поведінку. class monstr { int health, ammo; public: monstr(int he = 100, int am= 10){ health = he; ammo = am;} void draw(int x, int у, int scale, int роsition); int get_health(){return health;} int get_ammo(){return ammo;} }: В цьому класі два приховані поля — health і ammo, отримати значення яких зовні можна за допомогою методів get_health() і get_ammo(). Доступ до полів за допомогою методів в даному випадку здається штучним ускладненням, але треба враховувати, що полями реальних класів можуть бути складні динамічні структури, і отримання значень їх елементів не так тривіальні. Крім того, дуже важливою є можливість вносити в ці структури зміни, не торкаючись інтерфейсу класу. Всі методи класу мають безпосередній доступ до його прихованих полів, іншими словами, тіла функцій класу входять в область видимості private елементів класу. В приведеному класі міститься три визначення методів і одне оголошення (метод draw). Якщо тіло методу визначено всередині класу, він є вбудованим (inline). Як правило, вбудованими роблять короткі методи. Якщо всередині класу записано тільки оголошення (заголовок) методу, сам метод повинен бути визначений у іншому місці програми за допомогою операції доступу до області видимості (::): void monstr::draw(int x, int у, int scale, int роsition) { /*тіло методу */ } Метод можна визначити як вбудований і зовні класу за допомогою директиви inline (як і для звичайних функцій, вона несе рекомендаційний характер): inline int monstr::get_ammo() { return ammo; } В кожному класі є хоча б один метод, ім'я якого співпадає з ім'ям класу. Він називається конструктором і викликається автоматично при створенні об'єкту класу. Конструктор призначений для ініціалізації об'єкту. Автоматичний виклик конструктора дозволяє уникнути помилок, зв'язаних з використанням неініціалізованих змінних. Типи даних struct і union є видами класу. Опис об'єктів Конкретні змінні типу «клас» називаються екземплярами класу, або об'єктами. Час життя і видимість об'єктів залежить від вигляду і місця їх опису і підкоряється загальним правилам C++: monstr Vasia;// Об'єкт класу monstr з параметрами за замовчуванням monstr Super(200, 300); // Об'єкт з явною ініціалізацією monstr stado[100]; // Масив об'єктів з параметрами за умовчанням monstr *beavis = new monstr (10); // Динамічний об'єкт //(другий параметр задається за замовчуванням) monstr &butthead = Vasia; // Посилання на об'єкт При створенні кожного об'єкту виділяється пам'ять, достатня для зберігання всіх його полів, і автоматично викликається конструктор, який виконує їх ініціалізацію. Методи класу не тиражуються. При виході об'єкту з області дії він знищується, при цьому автоматично викликається деструктор. Доступ до елементів об'єкту аналогічний доступу до полів структури. Для цього використовуються операція .(крапка) при зверненні до елемента через ім'я об'єкту і операція -> при зверненні через покажчик, наприклад: int n = Vasia.get_ammo(); stado[5].draw; cout << beavis->get_health(); Звернутися таким чином можна тільки до елементів із специфікатором publіc. Отримати або змінити значення елементів із специфікатором private можна тільки через звернення до відповідних методів. Можна створити константний об'єкт, значення полів якого змінювати забороняється. До нього повинні застосовуватися тільки константні методи: class monstr { int get_health() const {return health;} }; const monstr Dead(0,0); // Константний об'єкт cout << Dead.get_health(): Константний метод: оголошується з ключовим словом const після списку параметрів; не може змінювати значення полів класу; може викликати тільки константні методи; може викликатися для будь-яких (не тільки константних) об'єктів. Рекомендується описувати як константні ті методи, які призначені для отримання значень полів. Конструктори Конструктор призначений для ініціалізації об'єкту і викликається автоматично при його створенні. Основні властивості конструкторів. Конструктор не повертає значення, навіть типу void. Не можна отримати покажчик на конструктор. Клас може мати декілька конструкторів з різними параметрами для різних видів ініціалізації (при цьому використовується механізм перевантаження). Конструктор, який викликається без параметрів, називається конструктором за замовчуванням. Параметри конструктора можуть мати будь-який тип, окрім цього ж класу. Можна задавати значення параметрів за замовчуванням. Їх може містити тільки один з конструкторів. Якщо програміст не вказав жодного конструктора, компілятор створює його автоматично. У разі, коли клас містить константи або посилання, при спробі створення об'єкту класу буде видана помилка, оскільки їх необхідно ініціалізувати конкретними значеннями, а конструктор за замовчуванням цього робити не вміє. Конструктори не успадковуються. Конструктори не можна описувати з модифікаторами const, virtual і static. Конструктори глобальних об'єктів викликаються до виклику функції main. Локальні об'єкти створюються, як тільки стає активною область їх дії. Конструктор викликається, якщо в програмі зустрілася яка-небудь з синтаксичних конструкцій: ім'я_класу ім'я_об'єкту [(список параметрів)]; // Список параметрів не повинен бути порожнім ім’я_класу (список параметрів); // Створюється об'єкт без імені (список може бути порожнім) ім’я_класу ім’я_об’єкту = вираз; // Створюється об'єкт без імені і копіюється Приклади. monstr Super(200, 300), Vasia(50), Z; monstr X = monstr(1000); monstr У = 500; Як приклад класу з декількома конструкторами удосконалимо описаний раніше клас monstr, додавши в нього поля, задаючі колір (skin) і ім'я (name): enum color (red, green, blue); // Можливі значення кольору class monstr { int health, ammo; color skin; char *name; public; monstr(int he = 100, int am = 10); monstr(color sk); monstr(char * nam); int get_health() {return health;} int get_ammo() {return ammo;} }; //.............................. monstr::monstr(int he, int am) { health = he; ammo = am; skin = red; name = 0; } //.......................... monstr::monstr(color sk) { switch (sk) { case red : health = 100; ammo = 10; skin = red; name = 0; break; case green: health = 100; ammo = 20; skin = green; name = 0; break; case blue : health = 100; ammo = 40; skin = blue; name = 0; break; } } //------------------------------------------ monstr::monstr(char * nam) { name = new char [strlen(nam)+ 1]; // До довжини рядка додається 1 для зберігання нуль-символа strcpy(name, nam); health = 100; ammo =10; skin = red; } //................................ monstr *m = new monstr ("Ork"); monstr Green (green); Перший з приведених вище конструкторів є конструктором по замовчуванню, оскільки його можна викликати без параметрів. Об'єкти класу monstr теперь можна ініціалізувати різними способами, необхідний конструктор буде викликаний залежно від списку значень в дужках. При заданні декількох конструкторів слід дотримуватися тих правил, що і при написанні перевантажених функцій — у компілятора повинна бути можливість розпізнати потрібний варіант. Конструктор копіювання Конструктор копіювання — це спеціальний вид конструктора, одержуючий як єдиний параметр покажчик на об'єкт цього ж класу: Т::T(const T&) { ... /* Тіло конструктора */ } де Т — ім'я класу. Цей конструктор викликається в тих випадках, коли новий об'єкт створюється шляхом копіювання існуючого: при описі нового об'єкту з ініціалізацією іншим об'єктом; при передачі об'єкта в функцію по значенню; при поверненні об'єкту з функції. Якщо програміст не вказав жодного конструктора копіювання, компілятор створює його автоматично. Такий конструктор виконує по елементне копіювання полів. Якщо клас містить покажчики або посилання, це, швидше за все, буде неправильним, оскільки і копія, і оригінал вказуватимуть на одну і ту ж область пам'яті. Конструктор копіювання для класу monstr. Оскільки в ньому є поле name, яке містить покажчик на рядок символів, конструктор копіювання повинен виділяти пам'ять під новий рядок і копіювати в нього початковий: monstr::monstr(const monstr &M) { if (M.name) { name = new char [strlen(M.name)+ 1]; strcpy(name, M.name); } else name = 0; health = M.health; ammo = M.ammo; skin = M.skin; } monstr Vasia (blue); monstr Super = Vasia; // Працює конструктор копіювання monstr *m = new monstr ("Orc"); monstr Green = *m; // Працює конструктор копіювання Іноді бажано мати безпосередній доступ ззовні до прихованих полів класу, тобто розширити інтерфейс класу. Для цього використовують дружні функції і дружні класи. Дружні функції Дружні функції застосовуються для доступу до прихованих полів класу і є альтернативою методам. Метод, як правило, використовується для реалізації властивостей об'єкту, а у вигляді дружніх функцій оформляються дії, не представляючі властивості класу, але концептуально що входять в його інтерфейс і потребуючі в доступі до його прихованих полів, наприклад, перевизначення операції виведення об'єктів. Правила опису і особливості дружніх функцій: 1. Дружня функція оголошується всередині класу, до елементів якого їй потрібен доступ, з ключовим словом friend. Як параметр їй повинен передаватися об'єкт або посилання на об'єкт класу. 2. Дружня функція може бути звичайною функцією або методом другого раніше визначеного класу. На неї не розповсюджується дія специфікаторів доступу, місце розміщення її оголошення в класі байдуже. Одна функція може бути дружньою відразу декільком класам. Приклад опису двох функцій, дружніх класу monstr. Функція kill є методом класу hero, а функція steal_ammo не належить жодному класу. Обом функціям як параметр передається посилання на об'єкт класу monstr. class monstr; // Попереднє оголошення класу class hero { public: void kill(monstr &); ….. }; class monstr { friend int steal_ammo(monstr &); friend void hero::kill(monstr &); // Клас hero повинен бути визначений раніше }; int steal_ammo(monstr &M){return --M.ammo;} void hero::kill(monstr &M){M.health = 0: M.ammo = 0;} Використання дружніх функцій потрібно по можливості уникати, оскільки вони порушують принцип інкапсуляції і, таким чином, утрудняють відладку і модифікацію програми. Дружній клас Якщо всі методи якого-небудь класу повинні мати доступ до прихованих полів іншого, весь клас оголошується дружнім за допомогою ключового слова friend. В наведеному прикладі клас mistress оголошується дружнім класу hero: class hero{ friend class mistress; } class mistress{ void fl(); void f2(); } Функції fl і f2 є дружніми по відношенню до класу hero (хоча і описані без ключового слова friend) і мають доступ до всіх його полів. Оголошення friend не є специфікатором доступу і не успадковується. Деструктор Деструктор — це особливий вид методу, який застосовується для звільнення памяти, займаної об'єктом. Деструктор викликається автоматично, коли об'єкт виходить з області бачення: 1. для локальних об'єктів — при виході з блоку, в якому вони оголошені; 2. для глобальних — як частина процедури виходу з main: 3. для об'єктів, заданих через покажчики, деструктор викликається неявно при використанні операції delete. Ім'я деструктора починається з тільди (~), безпосередньо за якою слідує ім'я класу. Деструктор: 1. не має аргументів і значення, яке повертається; не може бути оголошений як const або static; 2. не успадковується; 3. може бути віртуальним . Якщо деструктор явним чином не визначено, компілятор автоматично створює порожній деструктор. Описувати в класі деструктор явним чином потрібно у разі, коли об'єкт містить покажчики на пам'ять, яка виділяється динамічно — інакше при знищенні об'єкту пам'ять, на яку посилалися його поля-покажчики, не буде помічена як вільна. Покажчик на деструктор визначити не можна. Деструктор для даного прикладу повинен виглядати так: monstr::~monstr( ) {delete [] name;} Деструктор можна викликати явним чином шляхом вказання повністю уточненого імені, наприклад: monstr *m; ... m -> -monstr(); Це може знадобитися для об'єктів, яким за допомогою перевантаження операції new виділялася конкретна адреса пам'яті. Без необхідності явно викликати деструктор об'єкту не рекомендується.