Лекції з C
БлогNot. Лекції з C/C++: Класи, частина 3 (поліморфізм)
Лекції з C/C++: Класи, частина 3 (поліморфізм)
Поліморфізмом у сенсі називається здатність функції обробляти дані різних типів. Основні сфери застосування поліморфізму в C++ наступні:
1.Перевантаження функцій. C + + дозволяє визначати кілька функцій з однаковим ім'ям в одному просторі імен. Такі функції називаються перевантаженими та компілятор розрізняє їх за списками параметрів. Перевантаження дозволяє використовувати універсальні імена для однотипних дій.
У класичному Сі підтримка поліморфізму обмежена, наприклад, функція взяття модуля числа має різні назви в залежності від типу свого аргументу - abs, fabs або labs. У C++ для сумісності ці імена збережені, але не пам'ятати 3 різних назви функцій, а користуватися поліморфною функцією abs . Компілятор сам вибере, яка конкретна версія функції виконується:
Наведемо приклад навантаження користувача функції виведення значення об'єкта з ім'ям print :
Для прикладу потрібно підключити такі стандартні заголовки:
Багато функцій C++-бібліотек, таких як .NET або QT, також мають кілька перевантажень, що працюють з різними типами або різними списками аргументів.
2.Віртуальні методи базових класів, які перевизначаються у похідних класах. Тобто, похідні класи надають свої власні визначення та реалізацію певної стандартної дії ("встановити", "намалювати", "стерти" тощо). Під час виконання, коли клієнт викликає віртуальний метод, виконується пошук типу об'єкта та викликається перезапис віртуального методу. Як приклад наведемо клас Number (число), що має нащадків IntNumber (цілечисло) та DoubleNumber (речове число):
Як видно з коду, ми поводимося з об'єктами різних класів (числами різних типів) схожим чином.
3.Перевантаження операторів у класах. Оператор в С++ - це деяка дія або функція , позначена спеціальним символом. Для того щоб поширювати ці дії на нові типи даних, при цьому зберігаючи природний синтаксис, С++ була введена можливість перевантаження операторів.
Перевантажувати можна наступні оператори:
Відповідно навіть серед існуючих операторів є такі, які не можна перевантажити. Наприклад, оператор дозволу області видимості :: або оператор . (точка), що використовується для звернення до полів та методів об'єкта.
Для вбудованих типів даних перевантажувати оператори також не можна, тому що тип - це набір даних та операцій над ними. Змінюючи операції, що виконуються над даними типу, ми змінюємо визначення типу. Також не можна змінити пріоритет існуючого оператора або визначити новий оператор, який немає у мові.
Перевизначаючи оператори " , " " && " " " ми можемо втратити їх " ліниві " властивості (можливість не обчислювати вираз остаточно, наприклад, true умова == true , відповідно, після отримання першого значення true умова перестає обчислюватися).
Оператори "->", "[]", "()", "=" та "(type)" можна перевизначити тільки як методи класу.
Перевантаження операторів буває корисним у таких типових випадках:
- замість синтаксису get(i) зручніше використовувати [i];
- a.plus(b) або plus(a,b) зручніше замінити на a + b;
- замість p.get()->print() зручніше використовувати p->print() .
І т.д., наприклад, у класі "матриця" зручно і природно перевизначити оператор "*"для множення матриць, як зроблено в Mathcad.
Слід розуміти, що оператори діляться наунарні(застосовувані до одного об'єкта, наприклад, інкремент і декремент) ібінарні(що мають два операнди, наприклад, додавання або множення). Єдину в C/C++ тернарну операцію умовного оператора (… ? … : …) перевизначити також не можна.
4.Використання шаблонів. Шаблони функцій - це інструкції, згідно з якими створюються локальні версії шаблонизованої функції для певного набору параметрів та типів даних. Наприклад, без використання шаблонів для вирішення типового завдання виведення в консоль елементів масиву нам довелося створити стільки функцій, скільки типів масивів ми обробляємо:
Функції відрізняються лише типом параметрів, такий підхід є вкрай нераціональним за витратами часу. За допомогою ключового слова template ми можемо створити універсальний шаблон функції друку масиву з будь-яким типом елементів:
Компілятор сам створить локальні копії функції-шаблону.
Усі шаблони функцій починаються зі слова template , після якого йдуть кутові дужки, у яких перераховується перелік параметрів. Кожному параметру має передувати зарезервоване слово class або typename:
Ключове слово typename говорить про те, що в шаблоні використовуватиметься вбудований тип даних, такий як int , double , float , char і т.д. А ключове слово class повідомляє компілятору, що в шаблоні функції як параметр будуть використовуватися типи даних користувача, тобто, перш за все, класи.
5.Контейнери з різнотипних об'єктів. За допомогою поліморфізму можна помістити об'єкти різних класів в один масив із типом базового класу. Наприклад, створимо три класи із двома методами – невіртуальний метод Info виводитьінформацію про тварину, а віртуальний метод Say повідомляє, що ця тварина "каже". Віртуальний метод перевизначений (override) у класах-спадкоємцях. Невіртуальний метод просто прихований у спадкоємцях новою реалізацією (невіртуальні методи не можна перевизначати).
При створенні об'єкта буде мати значення, в змінну якого типу записаний об'єкт:
А тепер часта ситуація, коли поліморфізм потрібний - при створенні масиву об'єктів:
Ключовим у розумінні поліморфізму є те, що він дозволяє вам маніпулювати об'єктами різного ступеня складності шляхом створення загального для них стандартного інтерфейсу для реалізації схожих дій (принцип "один інтерфейс - безліч методів"). Загалом поліморфізм дозволяє писати абстрактніші програми та підвищити ступінь повторного використання коду.
Приклад 1. Тепер додамо до класу, створеного у попередній лекції (проект прикріплений внизу за посиланням), дві основні можливості з перерахованих:
- створення списку, куди можуть входити екземпляри як предка, і нащадка (нащадків);
- перевизначення операторів у межах класу.
До опису класу Student з попередньої лекції додамо такі елементи (файл student.h):
Поля у розділі public:
Ключове слово static у застосуванні до властивості класу означає, що властивість існує в однині для всіх екземплярів класу (є статичним). Такі властивості можна використовувати, наприклад, як лічильники числа об'єктів або вказівники їх списки.
Поза всіма функціями, тобто, глобально додамо у файл student.cpp ініціалізацію статичних членів класу. Вона виконується саме таким чином, подібно до ініціалізації глобальних змінних:
Змінна size тут службова тапризначений для ініціалізації максимального розміру списку.
Опишемо прототипи нових методів, також у розділі public базового класу (файл student.h):
Функція print, як видно з лістингу, також оголошена статичною. Зокрема, це означає, що її можна викликати створення екземпляра класу кодом виду Student::print();
Зрештою, передбачимо в нашому класі перевизначення операторів. Як сказано вище, перевизначений оператор розуміється просто як перевантажена функція, а перевизначати можна багато, але не всі оператори C++.
При перевизначенні оператора його пріоритет і порядок виконання операцій не змінюються, але в ряді випадків діють спеціальні правила написання перевизначеної функції, наприклад, щоб відрізняти постфіксний ++ від префіксного.
Загальний вид перевизначення оператора наступний:
Додамо до публічної секції опису класу Student типові приклади функцій перевизначення операторів:
Тут ми перевизначили унарний оператор! (заперечення). Його результат - величина типу int, яка прийматиме значення 1 (істина) або 0 (брехня). Це відповідає звичайній логіці даного оператора. Параметри такої функції не потрібні, оскільки вона працюватиме з поточним об'єктом this .
Прототип функції, перевизначає оператор += , який створювати нового об'єкта, лише змінювати поточний, " причіплюючи " рядок-параметр до властивості Name поточного об'єкта. Тому тип функції вказуємо void. Не означає, що не можна було реалізувати функцію += , повертає значення деякого типу. У загальному випадку потрібно пам'ятати про обчислення "ланцюжком" і уникати операторів, які нічого не повертають. Наприклад, наш оператор працюватиме у синтаксисі
З іншого боку, ніщо незаважає написати кілька реалізацій функції-оператора, що відрізняються списком параметрів і значень, що повертаються, так само, як ми надходили зі звичайними функціями (писати перевантажені функції, які відрізняютьсятількитипом значення, що повертається, не можна).
Перевизначили бінарний оператор додавання, він отримує посилання на об'єкт, що додається, що стоїть праворуч від знака "+" (Student & ) і повертає новий об'єкт, отриманий в результаті додавання. Об'єкт зліва від знака "+" доступний через це.
У ряді випадків економічніше повертати тільки посилання на об'єкт класу, а не сам об'єкт (Student & amp; замість Student).
У публічній секції опису дочірнього класу Hobbit також перевизначимо 2 операції - префіксний і постфіксний оператори ++ правила їх перевизначення видно з коду:
Напишемо реалізацію нових та змінених властивостей та методів для обох класів.
Метод add додає об'єкт батьківського або дочірнього класу до єдиного списку (з контролем граничного заповнення списку):
Видаляти об'єкт із динамічного списку буде деструктор базового класу, відповідно, його реалізація зміниться:
Деструктор посилається на новий метод search , який шукає прізвище студента у поточному списку:
Статичний метод друку виводить весь поточний список:
Зрештою, напишемо реалізацію функцій, що перевизначають стандартні оператори в наших класах.
Оператор! ми пристосували для перевірки того, чи є об'єкт, до якого він застосовується, у списку:
Оператор += додаватиме рядок, переданий параметром, до прізвища студента:
Нагадаємо, що метод setName повинен викликатися тільки для створеного та проініціалізованого об'єкта, інакше явне звільнення пам'яті, яке виконує оператор delete [] Nameу цьому методі може призвести до "передчасного" видалення об'єкта, наприклад, при неявному виклику конструктора копіювання. Одним з рішень проблем, пов'язаних з такими помилками, стала фактична ліквідація деструкторів у мовах Java, PHP та ін. Можливість виклику деструктора в цих мовах замінюється процедурою "складання сміття" (garbage collection), що періодично виконується системою.
Більш коректне визначення += , що працює по ланцюжку, було б, наприклад, таким:
Приклад виклику такого методу:
"Складання" студентів у нашому випадку означатиме зчеплення їхніх прізвищ та складання номерів груп. Сама по собі ця дія безглуздо, але вона ілюструє, як перевизначати бінарний оператор:
У класі-нащадку Hobbit префіксний оператор ++, який повинен повертати посилання на об'єкт, збільшує на 1 код символу, що відповідає властивості Hobby:
Оператор постфіксного інкременту відрізняється тим, що має параметр типу int , що не використовується . Оператор повертає тимчасовий об'єктз колишнімзначенням поля Hobby, але все одно збільшує значення Hobby поточного об'єкта this. Саме так буде забезпечена коректна робота постфіксного оператора, який спрацьовує після обчислення виразу, в якому він зустрівся:
Продемонструємо запрограмовані дії в main.cpp:
Приклад 2. Визначимо невеликий клас комплексних чисел і проілюструємо деякі особливості перевизначення операторів.
Приклад 3. У класі Class , що є "обгорткою" для звичайного цілого чисельного значення, проілюстровано навантаження таких операторів, як привласнення і круглі дужки, а також показане навантаження операторів функціями-друзями класу (на практиці не рекомендується перевантажувати"друзями" будь-які оператори, що змінюють стан об'єкта).
Приклади перевірені у безкоштовному збиранні Visual Studio 2010 Express.
Усі лекції з C/C++