Тема 4.3. Иерархия и взаимодействие классов 4.3.webp
Агрегирование и композиция

Класс может быть объявлен внутри другого класса или даже внутри метода класса. Такой класс называется вложенным.

Вложенный класс можно скрыть от внешнего кода с помощью модификаторов доступа. По умолчанию внешний класс не является другом вложенного класса, и оба эти класса не имеют доступа к закрытым членам друг друга.

Пример:

431_01

431_02

Описание класса:

431_03

В main() была вызвана функция (строка 8). Она в 12 строке перебирает элементы массива объектов из 36-38 строках, заставляя их обращаться к конструктору, тут же каждый раз вызывая из 12 строки функцию в 24 строке. Эта функция в свою очередь каждый раз возвращает string, которая в 12 строке выводится. В 34 строке – этот массив по сути является свойством класса PlanetarySystem.

Результат:

431_04

В этой программе напрямую к классу Planet нельзя обратиться, т.к. он находится внутри модификатора private класса PlanetarySystem. Но если перенести private из 15 строки в 35 (или вообще пока что убрать), то можно свободно обратиться к классу Planet и создать объект:

431_05

Результат:

431_06

Через объект вложенного класса вообще нельзя обратиться к свойствам и методам внешнего класса (даже к public-членам):

431_07

Назначение вложенного класса применительно, например, к этой программе следующее. Если есть планеты вне всякой планетарной системы, то нужен еще один класс Planet, не входящий в состав класса PlanetarySystem. Таким образом, закрыв с помощью private вложенный класс Planet, получим возможность работать еще и с другим обычным классом Planet, не вызывая путаницы при обращении к членам этих классов.

Агрегирование и композиция

Под агрегированием, агрегацией (или как его еще называли ранее – делегированием) подразумевают методику создания нового класса из уже существующих классов путём их включения. Об агрегировании также часто говорят как об “отношении принадлежности” по принципу “у машины есть корпус, колёса и двигатель”.

На базе агрегирования реализуется методика делегирования, когда поставленная перед внешним объектом задача перепоручается внутреннему объекту, специализирующемуся на решении задач такого рода.

Агрегация (агрегирование по ссылке) — отношение “часть-целое” между двумя равноправными объектами, когда один объект (контейнер) имеет ссылку на другой объект. Оба объекта могут существовать независимо: если контейнер будет уничтожен, то его содержимое — нет. Например, в агрегации “планета и гравитация” после исчезновения планеты вся остальная гравитация никуда не денется.

Композиция (агрегирование по значению) — это более строгий вариант агрегирования, когда включаемый объект может существовать только как часть контейнера. Если контейнер будет уничтожен, то и включённый объект тоже будет уничтожен. Например, в композиции “планета и атмосфера” при исчезновении планеты исчезнет ее уникальная атмосфера.

Пример агрегации:

431_08

Класс Профессор не вложен в класс Университет и не зависит от него.

Сначала создан объект универ класса Университет. Потом вызван метод Работание через объект универ. В классе Университет этот метод выводит “Профессор работает “ и вызывает метод Текст через (созданный как свойство класса Университет) объект проф. Сразу вызывается метод Текст, который возвращает “в любом универ-е\n”.

Если в этой задаче убрать класс Университет, то независимый Профессор продолжит свое существование.

Пример композиции:

431_09

Класс Кафедра вложен в класс Университет. У обоих есть метод Работать. Кроме того, у класса Университет есть закрытое свойство кафедра типа данных Кафедра (т.е. это свойство является в то же время объектом вложенного класса Кафедра).

Сначала создан объект универ класса Университет. Потом вызван метод Работать через объект универ. В теле этого метода Работать идет делегирование задачи методу Работать класса Кафедра, который и выводит сообщение.

Если в этой задаче убрать класс Университет, то существование кафедр невозможно и становится бессмысленным.

Использование агрегации и композиции
  1. Описать в одной программе классы Пирог и Тесто, используя либо агрегацию, либо композицию.
  2. Использовать агрегацию при описании класса для заданной предметной области.
  3. Использовать композицию при описании класса для заданной предметной области.
Наследование и полиморфизм

Наследование

433_01

Наследование – это один из основных принципов ООП, позволяющий описать новый класс на основе уже существующего (родительского, базового) класса или интерфейса. Потомок может добавить собственные методы и свойства, а также пользоваться родительскими методами и свойствами.

Наследование используется для того, чтобы:

  1. Выразить связи между классами.
  2. Уменьшить размер исходного кода.
  3. Упростить исходный код.

Образец:

class Animal

{};

class Fish : public Animal // класс Fish наследует все члены класса Animal

{};

Пример:

433_02

В этой программе класс Cat является классом Animal:

433_03

Иерархия классов должна быть внимательно продумана. Например, в классе Кот есть свойство громкость мурлыканья, которое точно не может относиться ко всему классу Животное; а в классе Животное есть свойство, нехарактерное для класса ВсеЖивыеОрганизмы. Но свойство mobility имеет смысл для всех дочерних классов:

433_04

Объект myCat имеет доступ ко всем свойствам своих родительских классов.

Если в родительском и дочернем классах имеются свойства или методы с одинаковым именем (сигнатурой), то для объекта дочернего класса сработает свойство или метод из дочернего класса, а для объекта из родительского класса сработает свойство или метод из родительского класса.

Сравните:

1. Здесь работает класс Cat:

433_05

2. А здесь работает класс Animal:

433_06


Модификаторы доступа при наследовании

private:

433_07

protected:

433_08

Можно изменить значения защищенных свойств для объекта, обратившись к ним с помощью метода, расположенного внутри класса:

433_09

Разница между private и protected

Если модификатор доступа к свойству родительского класса – private, то из дочернего класса доступа к этому свойству нет:

433_10

Если модификатор доступа к свойству родительского класса – protected, то из дочернего класса доступ к этому свойству есть:

433_11


Изменение доступа при наследовании

Вид наследования

Исходный модификатор доступа

 publicprivateprotected

class Cat : public Animal

public

privateprotected

class Cat : private Animal

private

privateprivate

class Cat : protected Animal

protected

privateprotected


public:

433_12_1

private:

433_13

protected:

433_14

private:

Попытка доступа из класса наследника:

433_15

Попытка доступа через объект класса наследника:

433_16

protected:

Попытка доступа из класса наследника

433_17

Попытка доступа через объект класса наследника:

433_18

Очередность работы конструкторов при наследовании

433_19

Очередность работы деструкторов при наследовании

433_20

Перегрузка конструкторов родительского класса

Пример:

433_21

При создании объекта cat идет сначала обращение к конструктору класса Cat, который сразу же обращается к конструктору в своем родительском классе. В родительском классе срабатывает конструктор (с параметром или без), затем в дочернем классе срабатывает конструктор и управление возвращается в main().

Чтобы перегрузить конструктор, нужно через двоеточие указать, к какому конструктору родительского класса следует обратиться (25 строка).

433_22

Т.о., конструкторов может быть любое реально необходимое количество.

Программист пишет так:

Cat()

Компилятор на самом деле видит так:

Cat() : Animal ()

433_23

В любом случае при создании объекта cat здесь сначала идет обращение к конструктору Cat(), но он сразу же обращается к конструктору Animal(), не выполняя до этого инструкций в собственном блоке.

Полиморфизм

433_12

Полиморфизм – это способность функции обрабатывать данные разных типов.

433_24

Все объектно-ориентированные языки поддерживают полиморфизм. Языки, которые поддерживают классы, но не поддерживают полиморфизм, называются объектно-основанными (например, Ada).

Задача. Написать программу для фирмы, рекламирующей кошачий корм.

Попытка решения. Сначала появилась необходимость реализовать класс Cat:

433_25

Затем появилась необходимость осуществить для домашних кошек другую работу метода sound(). Значит нужно реализовать наследование:

433_26


Для домашних кошек сработает метод CatHome::sound() , потому что он “перекрывает” метод Cat::sound()

Затем появился корм для очередных представителей семейства кошачьих, поэтому придется реализовать еще один вариант работы метода sound():

433_27

В реальной задаче таких “представителей” будет большое множество. К тому же в этом решении ничего не сказано о рекламе корма.

Но с помощью такого принципа ООП, как полиморфизм, можно реализовать решение этой задачи более грамотно.

Если к перегруженному методу базового класса дописать virtual:

433_28

то можно создать (29 строка) указатель pKot типа базового класса Cat на объект подкласса catHome.

Тогда (31 строка) при обращении к этому методу через разыменованный указатель сработает метод подкласса.

Если не указать virtual, тогда будет срабатывать метод базового класса, потому что указатель объявлен с типом данных базового класса:

433_29

Здесь всё то же самое, но указатель хранит адрес объекта базового класса (29 строка). Соответственно, сработает метод базового класса.

Сравните:

  433_30   433_31  

В конечном итоге имеются 4 класса: базовый класс Cat, два его наследника: CatHome и Lion, а также класс ReklamaKorma. В каждом классе реализован метод sound(); созданы 4 объекта.

Класс ReklamaKorma в своем методе sound() принимает в качестве параметра адрес объекта и записывает его в созданный указатель cat, который имеет тип данных базового класса Cat. В теле этого метода с помощью операции разыменования идет вызов того из методов sound(), чей объект был передан с помощью указателя.

В 35 строке создан объект rk класса ReklamaKorma. Далее в 36-38 строках через rk вызывается метод sound() класса ReklamaKorma. Но в качестве параметра этому методу передается адрес нужного объекта.

Например, в 38 строке через объект rk класса ReklamaKorma вызывается метод sound() и в метод передается адрес созданного ранее объекта lion. Этот адрес записывается в 25 строке в параметр cat, и значит именно для lion в инструкции cat->sound() вызывается его метод из 19 строки.

Т.о., полиморфизм здесь реализован через способность метода sound() класса Cat обрабатывать данные других типов – его наследников (CatHome и Lion):

433_32

Создание наследованного класса

Написать программу по заданной предметной области, демонстрирующую концепцию наследования. Программа должна создавать объекты базового класса и подкласса(-ов), производить необходимые расчеты с помощью методов класса и выводить описание (свойства) объектов и результаты расчетов на экран. Использовать различные модификаторы доступа.

Абстрактный класс

Абстрактный класс в ООП – это базовый класс, который не предполагает создания экземпляров.

Абстрактные классы реализуют на практике один из принципов ООП – полиморфизм. Абстрактный класс может содержать (или не содержать) абстрактные методы и абстрактные свойства. Абстрактный метод не реализуется для класса, в котором описан, однако должен быть реализован для его неабстрактных потомков. Абстрактные классы представляют собой наиболее общие абстракции, то есть имеющие наибольший объем и наименьшее содержание.

Абстрактные методы всегда являются виртуальными. Помимо чисто виртуальных методов и свойств абстрактный класс может содержать и обычные.

Класс можно сделать абстрактным, добавив в него чисто виртуальный метод (метод = 0: – делает класс абстрактным):

435_01

Теперь компилятор не даст создать объект этого класса:

435_02


Кроме класса Animal в предыдущую программу можно добавить класс Dog, который не относится к классу Cat с его наследниками CatHome и Lion, но является наследником класса Animal:

435_03


Класс Dog тоже должен иметь доступ к вызову метода sound(), поэтому в описании класса ReklamaKorma работа с этим методом будет идти не через Cat’ов (к которым Dog никак не относится), а через Animal:

435_04


Создан объект:

435_05


еще + один вызов sound():

435_06


Результат:

435_07


В классе Animal был написан чисто виртуальный метод sound(), значит обязательно в каждом классе-наследнике метод sound() должен быть переопределен.

Если удалить метод sound() из наследника:

435_08


Плюс еще обычный метод в абстрактном классе (всё работает как с обычными классами):

435_09


Теперь через любой объект классов наследников можно обращаться к этому обычному методу test():

435_10


Результат:

435_11


Интерфейс в ООП

Можно далее создать классы Bear, Pig и мн.др., создать их объекты, а реализовывать их работу через одну и ту же строку 47:

435_12


435_13


Виртуальный деструктор

Виртуальный деструктор нужен, чтобы корректно освобождать память в том случае, если используется указатель базового класса для хранения ссылок на классы наследники (полиморфизм).

Если при использовании абстрактных классов память выделяется динамически для каких-либо элементов, то ее необходимо своевременно освобождать, чтобы избежать утечки памяти.

Здесь обычный пример: сначала для объекта cat  конструируется часть базового класса, затем часть подкласса. Удаление в обратном порядке:

435_14


Если объект создать динамически:

435_15


то тоже всё работает верно: после очистки памяти от тех элементов из конструкторов будет уничтожен и сам объект с помощью delete cat;.

Поскольку базовый класс может хранить ссылку на наследника, можно реализовать следующее:

435_16


Но получится:

435_17


Произошла утечка памяти (8 байт).

Это случилось, потому что не использовалось ключевое слово virtual, и, соответственно, деструктор базового класса не был виртуальным. Поэтому при выполнении delete cat компилятор “вспомнил”, что объект cat имел тип данных Animal, значит и деструктор вызвался только типа Animal.

Чтобы всё работало корректно без утечки памяти:

435_18


435_19


Чисто виртуальный деструктор

Если деструктор базового класса (как любой другой метод класса) сделать абстрактным, то абстрактным станет весь класс (т.е. нельзя будет создавать объекты этого класса):

435_20


В соответствии с синтаксисом C++ тело деструктора необходимо вынести за скобки в том случае, если имеется объект класса наследника, объявленный с типом базового класса (в 22 строке). Если такового нет, то строка 11 не нужна.

Вызов виртуального метода базового класса из дочернего класса

Пример:

435_21


435_22


Описание программы:

Есть три класса: Animal, Cat и ReklamaKorma. Класс Cat является дочерним по отношению к классу Animal. В 46 строке создается объект cat класса Cat и в 7 строку в конструктор передается “Мяу” в whatHeSay. В 9 строке из параметра whatHeSay этот “Мяу” записывается в закрытое свойство whatHeSay для объекта cat. Затем в 48 строке создается объект rk класса ReklamaKorma. Далее в 49 строке через объект rk вызывается метод  Display() с передаваемым параметром – адрес объекта cat. В 36 строке адрес этого объекта записывается в указатель AnyObject, этот указатель имеет тип базового класса Animal. Срабатывает 38 строка, в которой через разыменованный указатель AnyObject (т.е. через объект cat) вызывается метод Sound(). Этот метод описан и с 12, и с 27 строк. Но раз вызывается этот метод через объект cat класса Cat, значит и сработать должен метод именно из класса Cat, т.е. с 27 строки. Здесь в 29 строке выполняется конкатенация, при этом вызывается метод Sound() класса Animal. Как раз здесь и вызывается метод базового класса изнутри дочернего класса. Метод описан с 12 строки, в 13 строке этот метод просто возвращает то, что было записано в свойстве whatHeSay для объекта cat (там было записано “Мяу”). Этот “Мяу” возвращается в 29 строку и участвует в конкатенации. Результат конкатенации (“Мур Мяу Мявк”) возвращается в 38 строку и попадает в cout. Далее всё возвращается в main(), и программа завершает свою работу.

Способность решать на этапе выполнения, какую именно из нескольких перегружаемых функций в зависимости от текущего типа следует вызвать, называется полиморфизмом, или поздним связыванием. Выбор перегружаемой функции на этапе компиляции называется ранним связыванием.

Перегрузка функции базового класса называется переопределением (overriding) функции базового класса. Это название используется, чтобы отличать этот более сложный случай от обычной перегрузки функции.

Назначение полиморфизма

Наследование не может в полной мере реализовать свою функциональность без полиморфизма. Например, программистом написана программа, использующая класс Dog. Через некоторое время в программу требуется добавить возможность работать не только обычными, но и с дрессированными собаками. В программе с одним классом Dog была ранее создана функция fn(), которая вызывает метод speed():

void fn(Dog &d)

{

// действия функции

d.speed();

// еще какие-то действия функции

}

Если бы C++ не поддерживал позднее связывание, то пришлось бы отредактировать функцию fn() и добавить ее в класс TrainedDog (директива #define определяет идентификатор и последовательность символов, которая будет подставляться вместо идентификатора каждый раз, когда он встретится в исходном файле):

#define DOG 1

#define TRAINEDDOG 2

void fn(Dog &d)

{

// Добавление типа члена, который будет 

//индицировать текущий тип объекта

switch (d.type)

{

case DOG:

d.Dog::speed();

break;

case TRAINEDDOG:

td.TrainedDog::speed();

break;

}

// продолжение функции

}

Таким образом пришлось бы добавить в класс переменную type. После этого программист вынужден был бы добавить присвоение type = DOG к конструктору Dog и type = TRAINEDDOG к конструктору TrainedDog. Значение переменной type отражало бы текущий тип объекта d. Затем пришлось бы добавить проверяющие команды, показанные ранее, везде, где вызываются переопределяемые функции. Это сложно, потому что speed() вызывается из нескольких мест и этой функции придется выбирать между множеством классов, и нужно найти все места в программе, которые надо отредактировать. Придется изменять код, который был отлажен и работал, на что потребуется много времени, сил и внимания, чтобы изменения не начали конфликтовать с другим кодом. Кроме того, придется поддерживать несколько версий программы. А спустя некоторое время может появиться необходимость в еще одном классе и т.д. При наличии полиморфизма всё, что потребуется сделать – это добавить новый подкласс, в одном месте изменить базовый класс и перекомпилировать программу. Изменения в коде приложения в этом случае будут сведены к минимуму.

Полиморфные функции

Любой язык программирования поддерживает раннее или позднее связывание. Старые языки (например, C) в основном поддерживают раннее связывание. Более поздние языки (например, Java) поддерживают позднее связывание. C++ поддерживает оба типа связывания.

По умолчанию C++ использует раннее связывание:

1. для достижения максимальной обратной совместимости с языком C;

2. позднее связывание требует как выполнения дополнительного кода, так и дополнительных затрат памяти.

Чтобы сделать функцию-член полиморфной, нужно пометить ее ключевым словом virtual:

virtual float speed()

{

cout<< " Функция Dog::speed" << endl;

return 0;

}

Такое виртуальное объявление speed() означает, что вызовы этой функции-члена будут связаны позже, если сомнения по поводу типа объекта, для которого эта функция будет вызываться на этапе выполнения. Когда функции fn() передается объект подкласса, вызов обращается к функции TrainedDog::speed():

435_23


Результат работы программы будет иным:

435_24


Виртуальность наследуется подклассами автоматически, поэтому достаточно объявить функцию виртуальной только в базовом классе.

Множественное наследование

Множественное наследование – это способность порожденного класса наследовать характеристики нескольких базовых классов.

Для создания подкласса из нескольких базовых после имени нового класса и двоеточия указываются имена базовых классов, разделенные запятыми.

Пример:

class SleeperSofa : public Bed, public Sofa{};

Схема наследования (диван-кровать):

436_01

Программа работает корректно (пока нет обращения к свойству weight):

436_02


Очередность работы конструкторов при множественном наследовании

Если родительские классы перечислить в обратном порядке – сначала Sofa, потом Bed, то сначала вызовется конструктор для Sofa, потом для Bed:

436_03


Очередность работы деструкторов при множественном наследовании

Если родительские классы перечислить в обратном порядке – сначала Sofa, потом Bed, то очередность вызова конструкторов и деструкторов Sofa и Bed поменяется:

436_04


Как и при обычном наследовании, дочерний класс является своим базовым классом. Т.е. диван-кровать является диваном, и диван-кровать является кроватью. Поэтому так же как и при обычном наследовании, можно создать ссылку на дочерний класс с типом данных базового класса:

436_05


Классы Bed и Sofa оба содержат атрибут weight. Значит класс SleeperSofa наследует оба атрибута базовых классов: и Bed::weight, и Sofa::weight. И следующая строка вызовет ошибку:

436_06


Для получения доступа к weight придется явно указывать (с помощью имени класса) какая именно переменная weight требуется:

436_07


Ошибка устранена, но теперь информация о внутреннем устройстве класса присутствует за пределами класса (может быть и во внешнем приложении).

При наличии одинаковых методов в родительских классах для обращения к конкретному методу можно использовать явное приведение типа – объект ss приводится к типу Bed. Или (так же как  в случае одноименных свойств weight) обратиться к методу, явно указав с помощью :: какой метод требуется вызвать:

436_08


Ошибка устранена, но теперь информация о внутреннем устройстве класса присутствует за пределами класса (может быть и во внешнем приложении).

Виртуальное наследование при множественном наследовании

Виртуальное наследование – один из вариантов наследования, который необходим для устранения проблем, порождаемых наличием возможности множественного наследования (особенно “ромбовидного наследования”), путем разрешения неоднозначности того, свойства и методы какого из родительских классов необходимо использовать. Виртуальное наследование применяется в тех случаях, когда множественное наследование вместо предполагаемой полной композиции свойств родительских классов приводит к ограничению доступных наследуемых свойств вследствие неоднозначности. Базовый класс, наследуемый множественно, определяется виртуальным с помощью ключевого слова virtual.

Конфликт имен weight в одной из предыдущих программ возник потому, что созданная иерархия классов была неадекватной – разложение на классы было неполным. На самом деле свойство weight является атрибутом более фундаментальной концепции – свойством мебели.

Схема ромбовидного наследования:

436_09


Сначала Mebel наследуют классы Bed и Sofa, а уже потом SleeperSofa наследуется от этих классов:

436_10


Необходимо, чтобы SleeperSofa наследовал только одну копию Mebel и чтобы Bed и Sofa имели к ней доступ. В C++ это достигается виртуальным наследованием:

436_11


436_12


При создании объекта ss переход к строке 32, в которой обнаруживается первый родительский класс Bed. Сразу идет переход к 14 строке, в которой обнаруживается родительский класс Mebel. Сразу идет переход к строке 4, и срабатывает конструктор класса Mebel, затем управление возвращается к 14 строке, срабатывает конструктор класса Bed, затем возвращается к 32 строке и обнаруживается еще один родительский класс Sofa. Сразу идет переход к строке 23, в которой опять обнаруживается родительский класс Mebel. Сразу идет переход к строке 4, и опять срабатывает конструктор класса Mebel, затем управление возвращается к 23 строке, срабатывает конструктор класса Sofa, затем возвращается к 32 строке, и срабатывает конструктор SleeperSofa.

Ошибка при попытке обращения к свойству:

436_13


Чтобы устранить ошибку, следует добавить ключевое слово virtual:

436_14


436_15


Теперь класс SleeperSofa выглядит следующим образом:

436_16


Класс SleeperSofa включает в себя Mebel, а также части классов Bed и Sofa, не содержащие Mebel, и уникальные для класса SleeperSofa члены. Теперь обращение к члену weight не многозначно, поскольку SleeperSofa содержит только одну копию Mebel.

Работа с полиморфизмом и множественным наследованием
  1. Описать фрагмент биологической классификации живых организмов с использованием принципа полиморфизма.
  2. Описать класс по заданной предметной области с использованием полиморфизма.
  3. Описать множественное наследование для выбранной предметной области.
  4. Описать виртуальное наследование при множественном наследовании для выбранной предметной области.
Тренажёр