Урок №170. Вказівники, Посилання і Спадкування

  Юрій  | 

  Оновл. 22 Бер 2021  | 

 110

На попередніх уроках ми розглянули використання спадкування для отримання нових класів з існуючих. На уроках цього розділу ми зосередимося на одному з найважливіших і найпотужніших аспектів спадкування — віртуальних функціях. Але, перш ніж ми перейдемо до вивчення віртуальних функцій, давайте спочатку визначимося, навіщо це взагалі нам потрібно.

Вказівники, посилання і дочірні класи

З уроку №163 ми знаємо, що при створенні об’єкта дочірнього класу виконується побудова 2-х частин, з яких цей об’єкт і складається: батьківська і дочірня. Наприклад:

При створенні об’єкта класу Child спочатку виконується побудова частини Parent, а потім вже частини Child. Пам’ятайте, що тип відносин в спадкуванні — «є». Оскільки Child «є» Parent, то логічно, що Child містить частину Parent.

Ми можемо дати команду вказівникам і посиланням класу Child вказувати на інші об’єкти класу Child:

Результат:

child is a Child and has value 7
rChild is a Child and has value 7
pChild is a Child and has value 7

Цікаво, оскільки Child має частину Parent, то чи можемо ми дати команду вказівнику або посиланню класу Parent вказувати на об’єкт класу Child? Виявляється, що можемо!

Результат:

child is a Child and has value 7
rParent is a Parent and has value 7
pParent is a Parent and has value 7

Але це може бути не зовсім те, що ви очікували побачити!

Оскільки rParent і pParent є посиланням і вказівником класу Parent, то вони можуть бачити тільки члени класу Parent (і члени будь-яких інших класів, які наслідує Parent). Таким чином, вказівник/посилання класу Parent не може побачити Child::getName(). Отже, викликається Parent::getName(), а rParent і pParent повідомляють, що вони відносяться до класу Parent, а не до класу Child.

Зверніть увагу, це також означає, що неможливо викликати Child::getValueDoubled() через rParent або pParent. Вони нічого не можуть бачити в класі Child.

Ось складніший приклад:

Результат виконання програми:

cat is named Matros, and it says Meow
dog is named Barsik, and it says Woof
pAnimal is named Matros, and it says ???
pAnimal is named Barsik, and it says ???

Ми бачимо тут ту ж проблему. Оскільки pAnimal є вказівником типу Animal, то він може бачити тільки частину Animal. Отже, pAnimal->speak() викликає Animal::speak(), а не Dog::Speak() чи Cat::speak().

Вказівники, посилання і батьківські класи

Тепер ви можете сказати: «Вищенаведені приклади здаються безглуздими. Чому я повинен використовувати вказівник або посилання батьківського класу на об’єкт дочірнього класу, якщо я можу просто використовувати дочірній об’єкт?». Виявляється, на це є кілька вагомих причин.

По-перше, припустимо, що ви хочете написати функцію, яка виводитиме ім’я та звук тварини. Без використання вказівника на батьківський клас, вам доведеться реалізувати це через перевантаження функцій. Наприклад:

Не дуже важко, але уявіть, що у нас 30 різних типів тварин. Нам довелося б написати 30 перевантажень! Крім того, якщо ви коли-небудь додасте новий тип тварини, то вам також доведеться написати нову функцію для цього типу тварини. Це величезна трата часу.

Однак, оскільки Cat і Dog наслідують Animal, Cat і Dog мають частину Animal, тому ми можемо зробити наступне:

Це дозволить нам передавати будь-який клас, який є дочірнім класу Animal! Замість окремого методу на кожен дочірній клас ми записали один метод, який працює відразу з усіма дочірніми класами!

Проблема, звичайно, в тому, що, оскільки rAnimal є посиланням класу Animal, то rAnimal.speak() викличе Animal::speak() замість методу speak() дочірнього класу.

По-друге, припустимо, що у нас є 3 кішки і 3 собаки, яких ми б хотіли зберегти в масиві для легкого доступу до них. Оскільки масиви можуть містити об’єкти тільки одного типу, то без вказівників/посилань на батьківський клас, нам би довелося створювати окремий масив для кожного дочірнього класу. Наприклад:

Тепер уявіть, що у нас 30 різних типів тварин. Нам довелося б створити 30 масивів — по одному на кожен тип тварини!

Однак, оскільки Cat і Dog наслідують Animal, то можна зробити наступне:

Хоча це скомпілюється і виконається, але, на жаль, той факт, що кожен елемент масиву animals є вказівником на Animal, означає, що animals[iii]->speak() викликатиме Animal::speak(), замість методів speak() дочірніх класів.

Хоча обидва ці способи можуть заощадити нам багато часу і енергії, вони мають одну і ту ж проблему: вказівник або посилання батьківського класу викликає батьківську версію функції, а не дочірню. От якби був якийсь спосіб змусити батьківські вказівники викликати методи дочірніх класів.

Вгадайте тепер, навіщо потрібні віртуальні функції? 🙂

Тест

Наш приклад з Animal/Cat/Dog не працює так, як ми хочемо, тому що посилання/вказівник класу Animal не може отримати доступ до методів speak() дочірніх класів. Один із способів обійти цю проблему — зробити так, щоб дані, які повертаються методом speak(), стали доступними у вигляді батьківської частини класу Animal (так само, як name класу Animal доступний через член m_name).

Оновіть класи Animal, Cat і Dog у вищенаведеному коді, додавши новий член m_speak в клас Animal. Ініціалізуйте його відповідним чином. Наступна програма повинна працювати коректно:

Відповідь

Примітка: Ви також можете зробити m_speak типу std::string, але недоліком є те, що кожен об’єкт класу Animal міститиме зайву копію рядка speak, а побудова об’єктів Animal займе більше часу, тому що глибоке копіювання std::string виконуватиметься повільніше, ніж копіювання вказівника, який вказує на константний рядок C-style.

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (5 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

Ваш E-mail не буде опублікований. Обов'язкові поля відмічені *