Українська
Віртуальні функції
Віртуальні функції, override та приховування
Віртуальний метод дозволяє вибрати реалізацію за динамічним типом об’єкта. Базове посилання на Circle викликає Circle::area, якщо сигнатура коректно заміщує базову. Без virtual вибір визначається статичним типом виразу, тобто типом посилання, відомим під час компіляції.
Завжди записуйте override там, де має бути заміщення. Він перетворює намір на перевірку. Забутий const, інший тип параметра чи неправильний ref-кваліфікатор інакше можуть створити нову функцію замість очікуваного заміщення. З override компілятор повідомляє про невідповідність одразу.
Приховування імені й заміщення – різні явища. Похідний метод із тим самим ім’ям може приховати базові перевантаження, навіть якщо не заміщує жодного. using Base::method повертає потрібні імена в набір пошуку, але не робить невіртуальну функцію віртуальною. Тому аналізуйте і сигнатуру, і правило пошуку імен.
Явний кваліфікований виклик Base::method() звертається до конкретної базової реалізації. Це корисно, коли похідний метод доповнює спільну поведінку, але треба не створити випадкову рекурсію викликом власного заміщення замість базового.
Віртуальний деструктор і володіння
Якщо об’єкт знищується через вказівник на базовий тип, база має підтримувати відповідний поліморфний контракт знищення. Звичайний вибір – публічний віртуальний деструктор virtual ~Base() = default. Тоді знищення через unique_ptr<Base> починається з похідного класу та коректно охоплює всі його поля.
Невіртуальний деструктор бази й видалення похідного об’єкта через базовий вказівник утворюють небезпечний сценарій із невизначеною поведінкою. Не демонструйте його запуском і висновком «повідомлення не надрукувалося, отже лише витік»: наслідки не обмежуються таким спостереженням. Помилку треба усунути в дизайні.
Інша політика – захищений невіртуальний деструктор бази, якщо видалення через неї заборонене. У цьому курсі для контейнерів поліморфних власників використовується перший варіант, бо він дає просте й однозначне володіння.
unique_ptr керує часом життя, але не виправляє неправильний деструктор базового класу магічно. Розумний вказівник виконує задану операцію видалення; типи та контракт ієрархії все одно повинні бути правильними.
Поліморфні колекції та зрізання
vector<Base> зберігає значення Base. Додавання Derived створює базове значення та відкидає похідну частину – зрізання (object slicing). Віртуальні функції в цьому новому об’єкті вже бачать динамічний тип Base: похідний об’єкт не схований усередині, його просто немає.
vector<unique_ptr<Base>> зберігає власників об’єктів різних похідних типів. Кожен елемент має однаковий тип вказівника, але керований об’єкт може бути іншим. Виклик через -> зберігає динамічний вибір, а віртуальний деструктор забезпечує правильне завершення життя.
Копіювання такого контейнера не доступне за замовчуванням: unique_ptr має єдиного власника. Якщо потрібна незалежна копія поліморфного набору, слід спроєктувати окрему операцію clone або іншу модель володіння. Не замінюйте unique_ptr на shared_ptr лише заради зникнення помилки компіляції, не з’ясувавши зміст копії.
Посилання й сирі вказівники можуть бути невласними спостерігачами. Їхній користувач не виконує delete, але мусить гарантувати, що об’єкт існує протягом використання. Поліморфізм і володіння – різні питання, які треба узгодити, а не ототожнювати.
RTTI та перевірювані перетворення
Перетворення Derived до доступної однозначної Base називають upcast; воно не потребує запиту фактичного типу. Зворотний напрямок вимагає гарантії. static_cast не перевіряє в runtime, чи об’єкт справді має потрібний похідний тип; помилкове припущення може призвести до невизначеної поведінки.
dynamic_cast для поліморфної ієрархії використовує інформацію часу виконання. Для вказівника невдалий cast повертає nullptr. Для посилання невдача кидає std::bad_cast. Ці різні форми дають різні контракти помилки: перевірку умовою або обробник винятку.
У MSVC підтримку RTTI задає параметр /GR; у звичайній конфігурації він увімкнений. Не вимикайте її випадково для прикладу з downcast. typeid теж пов’язаний із runtime-типом поліморфних об’єктів, але текст name() залежить від реалізації й не є стабільним форматом для файлів або бізнес-логіки.
Частий ланцюжок dynamic_cast для кожного нового виду сигналізує, що спільну операцію варто перенести в базовий інтерфейс. Рідкісна специфічна дія може виправдовувати перевірку можливості, але основний алгоритм колекції краще будувати на віртуальній операції зі спільним контрактом.
Модель таблиці віртуальних функцій
Поширена реалізація динамічного виклику використовує таблицю віртуальних функцій і приховані вказівники в об’єкті. Налагоджувач MSVC може показувати __vfptr. Це корисна модель, щоб пояснити, як однаковий базовий виклик переходить до різного коду.
Стандарт C++ визначає поведінку виклику, а не обов’язковий порядок байтів vtable. Розмір вказівників, кількість службових полів, розміщення баз і оптимізації залежать від ABI та компілятора. Діаграма в цій лекції є концептуальною, не інструкцією читати чужу пам’ять через довільний cast.
Віртуальність може додати непрямий виклик і вплинути на інлайнінг, але оптимізатор інколи знає фактичний тип і прибирає непрямість. Тому не можна обчислити універсальну «ціну virtual» з одного запуску чи стверджувати, що кожен виклик завжди має однакові машинні інструкції.
Спочатку обирайте модель за вимогами до підстановки й життєвого циклу. Якщо продуктивність стає вимогою, вимірюйте репрезентативний сценарій у Release та пояснюйте обмеження вимірювання. Навчальний скриншот Locals показує конкретне середовище, а не загальний стандартний формат поліморфного об’єкта.