Класс может быть объявлен внутри другого класса или даже внутри метода класса. Такой класс называется вложенным.
Вложенный класс можно скрыть от внешнего кода с помощью модификаторов доступа. По умолчанию внешний класс не является другом вложенного класса, и оба эти класса не имеют доступа к закрытым членам друг друга.
Пример:
Описание класса:
В main() была вызвана функция (строка 8). Она в 12 строке перебирает элементы массива объектов из 36-38 строках, заставляя их обращаться к конструктору, тут же каждый раз вызывая из 12 строки функцию в 24 строке. Эта функция в свою очередь каждый раз возвращает string, которая в 12 строке выводится. В 34 строке – этот массив по сути является свойством класса PlanetarySystem.
Результат:
В этой программе напрямую к классу Planet нельзя обратиться, т.к. он находится внутри модификатора private класса PlanetarySystem. Но если перенести private из 15 строки в 35 (или вообще пока что убрать), то можно свободно обратиться к классу Planet и создать объект:
Результат:
Через объект вложенного класса вообще нельзя обратиться к свойствам и методам внешнего класса (даже к public-членам):
Назначение вложенного класса применительно, например, к этой программе следующее. Если есть планеты вне всякой планетарной системы, то нужен еще один класс Planet, не входящий в состав класса PlanetarySystem. Таким образом, закрыв с помощью private вложенный класс Planet, получим возможность работать еще и с другим обычным классом Planet, не вызывая путаницы при обращении к членам этих классов.
Агрегирование и композиция
Под агрегированием, агрегацией (или как его еще называли ранее – делегированием) подразумевают методику создания нового класса из уже существующих классов путём их включения. Об агрегировании также часто говорят как об “отношении принадлежности” по принципу “у машины есть корпус, колёса и двигатель”.
На базе агрегирования реализуется методика делегирования, когда поставленная перед внешним объектом задача перепоручается внутреннему объекту, специализирующемуся на решении задач такого рода.
Агрегация (агрегирование по ссылке) — отношение “часть-целое” между двумя равноправными объектами, когда один объект (контейнер) имеет ссылку на другой объект. Оба объекта могут существовать независимо: если контейнер будет уничтожен, то его содержимое — нет. Например, в агрегации “планета и гравитация” после исчезновения планеты вся остальная гравитация никуда не денется.
Композиция (агрегирование по значению) — это более строгий вариант агрегирования, когда включаемый объект может существовать только как часть контейнера. Если контейнер будет уничтожен, то и включённый объект тоже будет уничтожен. Например, в композиции “планета и атмосфера” при исчезновении планеты исчезнет ее уникальная атмосфера.
Пример агрегации:
Класс Профессор не вложен в класс Университет и не зависит от него.
Сначала создан объект универ класса Университет. Потом вызван метод Работание через объект универ. В классе Университет этот метод выводит “Профессор работает “ и вызывает метод Текст через (созданный как свойство класса Университет) объект проф. Сразу вызывается метод Текст, который возвращает “в любом универ-е\n”.
Если в этой задаче убрать класс Университет, то независимый Профессор продолжит свое существование.
Пример композиции:
Класс Кафедра вложен в класс Университет. У обоих есть метод Работать. Кроме того, у класса Университет есть закрытое свойство кафедра типа данных Кафедра (т.е. это свойство является в то же время объектом вложенного класса Кафедра).
Сначала создан объект универ класса Университет. Потом вызван метод Работать через объект универ. В теле этого метода Работать идет делегирование задачи методу Работать класса Кафедра, который и выводит сообщение.
Если в этой задаче убрать класс Университет, то существование кафедр невозможно и становится бессмысленным.
- Описать в одной программе классы Пирог и Тесто, используя либо агрегацию, либо композицию.
- Использовать агрегацию при описании класса для заданной предметной области.
- Использовать композицию при описании класса для заданной предметной области.
Наследование
Наследование – это один из основных принципов ООП, позволяющий описать новый класс на основе уже существующего (родительского, базового) класса или интерфейса. Потомок может добавить собственные методы и свойства, а также пользоваться родительскими методами и свойствами.
Наследование используется для того, чтобы:
- Выразить связи между классами.
- Уменьшить размер исходного кода.
- Упростить исходный код.
Образец:
class Animal
{};
class Fish : public Animal // класс Fish наследует все члены класса Animal
{};
Пример:
В этой программе класс Cat является классом Animal:
Иерархия классов должна быть внимательно продумана. Например, в классе Кот есть свойство громкость мурлыканья, которое точно не может относиться ко всему классу Животное; а в классе Животное есть свойство, нехарактерное для класса ВсеЖивыеОрганизмы. Но свойство mobility имеет смысл для всех дочерних классов:
Объект myCat имеет доступ ко всем свойствам своих родительских классов.
Если в родительском и дочернем классах имеются свойства или методы с одинаковым именем (сигнатурой), то для объекта дочернего класса сработает свойство или метод из дочернего класса, а для объекта из родительского класса сработает свойство или метод из родительского класса.
Сравните:
1. Здесь работает класс Cat:
2. А здесь работает класс Animal:
Модификаторы доступа при наследовании
private:
protected:
Можно изменить значения защищенных свойств для объекта, обратившись к ним с помощью метода, расположенного внутри класса:
Разница между private и protected
Если модификатор доступа к свойству родительского класса – private, то из дочернего класса доступа к этому свойству нет:
Если модификатор доступа к свойству родительского класса – protected, то из дочернего класса доступ к этому свойству есть:
Изменение доступа при наследовании
Вид наследования | Исходный модификатор доступа | ||
---|---|---|---|
public | private | protected | |
class Cat : public Animal | public | private | protected |
class Cat : private Animal | private | private | private |
class Cat : protected Animal | protected | private | protected |
public:
private:
protected:
private:
Попытка доступа из класса наследника:
Попытка доступа через объект класса наследника:
protected:
Попытка доступа из класса наследника
Попытка доступа через объект класса наследника:
Очередность работы конструкторов при наследовании
Очередность работы деструкторов при наследовании
Перегрузка конструкторов родительского класса
Пример:
При создании объекта cat идет сначала обращение к конструктору класса Cat, который сразу же обращается к конструктору в своем родительском классе. В родительском классе срабатывает конструктор (с параметром или без), затем в дочернем классе срабатывает конструктор и управление возвращается в main().
Чтобы перегрузить конструктор, нужно через двоеточие указать, к какому конструктору родительского класса следует обратиться (25 строка).
Т.о., конструкторов может быть любое реально необходимое количество.
Программист пишет так:
Cat()
Компилятор на самом деле видит так:
Cat() : Animal ()
В любом случае при создании объекта cat здесь сначала идет обращение к конструктору Cat(), но он сразу же обращается к конструктору Animal(), не выполняя до этого инструкций в собственном блоке.
Полиморфизм
Полиморфизм – это способность функции обрабатывать данные разных типов.
Все объектно-ориентированные языки поддерживают полиморфизм. Языки, которые поддерживают классы, но не поддерживают полиморфизм, называются объектно-основанными (например, Ada).
Задача. Написать программу для фирмы, рекламирующей кошачий корм.
Попытка решения. Сначала появилась необходимость реализовать класс Cat:
Затем появилась необходимость осуществить для домашних кошек другую работу метода sound(). Значит нужно реализовать наследование:
Для домашних кошек сработает метод CatHome::sound() , потому что он “перекрывает” метод Cat::sound()
Затем появился корм для очередных представителей семейства кошачьих, поэтому придется реализовать еще один вариант работы метода sound():
В реальной задаче таких “представителей” будет большое множество. К тому же в этом решении ничего не сказано о рекламе корма.
Но с помощью такого принципа ООП, как полиморфизм, можно реализовать решение этой задачи более грамотно.
Если к перегруженному методу базового класса дописать virtual:
то можно создать (29 строка) указатель pKot типа базового класса Cat на объект подкласса catHome.
Тогда (31 строка) при обращении к этому методу через разыменованный указатель сработает метод подкласса.
Если не указать virtual, тогда будет срабатывать метод базового класса, потому что указатель объявлен с типом данных базового класса:
Здесь всё то же самое, но указатель хранит адрес объекта базового класса (29 строка). Соответственно, сработает метод базового класса.
Сравните:
В конечном итоге имеются 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):
Написать программу по заданной предметной области, демонстрирующую концепцию наследования. Программа должна создавать объекты базового класса и подкласса(-ов), производить необходимые расчеты с помощью методов класса и выводить описание (свойства) объектов и результаты расчетов на экран. Использовать различные модификаторы доступа.
Абстрактный класс в ООП – это базовый класс, который не предполагает создания экземпляров.
Абстрактные классы реализуют на практике один из принципов ООП – полиморфизм. Абстрактный класс может содержать (или не содержать) абстрактные методы и абстрактные свойства. Абстрактный метод не реализуется для класса, в котором описан, однако должен быть реализован для его неабстрактных потомков. Абстрактные классы представляют собой наиболее общие абстракции, то есть имеющие наибольший объем и наименьшее содержание.
Абстрактные методы всегда являются виртуальными. Помимо чисто виртуальных методов и свойств абстрактный класс может содержать и обычные.
Класс можно сделать абстрактным, добавив в него чисто виртуальный метод (метод = 0: – делает класс абстрактным):
Теперь компилятор не даст создать объект этого класса:
Кроме класса Animal в предыдущую программу можно добавить класс Dog, который не относится к классу Cat с его наследниками CatHome и Lion, но является наследником класса Animal:
Класс Dog тоже должен иметь доступ к вызову метода sound(), поэтому в описании класса ReklamaKorma работа с этим методом будет идти не через Cat’ов (к которым Dog никак не относится), а через Animal:
Создан объект:
еще + один вызов sound():
Результат:
В классе Animal был написан чисто виртуальный метод sound(), значит обязательно в каждом классе-наследнике метод sound() должен быть переопределен.
Если удалить метод sound() из наследника:
Плюс еще обычный метод в абстрактном классе (всё работает как с обычными классами):
Теперь через любой объект классов наследников можно обращаться к этому обычному методу test():
Результат:
Интерфейс в ООП
Можно далее создать классы Bear, Pig и мн.др., создать их объекты, а реализовывать их работу через одну и ту же строку 47:
Виртуальный деструктор
Виртуальный деструктор нужен, чтобы корректно освобождать память в том случае, если используется указатель базового класса для хранения ссылок на классы наследники (полиморфизм).
Если при использовании абстрактных классов память выделяется динамически для каких-либо элементов, то ее необходимо своевременно освобождать, чтобы избежать утечки памяти.
Здесь обычный пример: сначала для объекта cat конструируется часть базового класса, затем часть подкласса. Удаление в обратном порядке:
Если объект создать динамически:
то тоже всё работает верно: после очистки памяти от тех элементов из конструкторов будет уничтожен и сам объект с помощью delete cat;.
Поскольку базовый класс может хранить ссылку на наследника, можно реализовать следующее:
Но получится:
Произошла утечка памяти (8 байт).
Это случилось, потому что не использовалось ключевое слово virtual, и, соответственно, деструктор базового класса не был виртуальным. Поэтому при выполнении delete cat компилятор “вспомнил”, что объект cat имел тип данных Animal, значит и деструктор вызвался только типа Animal.
Чтобы всё работало корректно без утечки памяти:
Чисто виртуальный деструктор
Если деструктор базового класса (как любой другой метод класса) сделать абстрактным, то абстрактным станет весь класс (т.е. нельзя будет создавать объекты этого класса):
В соответствии с синтаксисом C++ тело деструктора необходимо вынести за скобки в том случае, если имеется объект класса наследника, объявленный с типом базового класса (в 22 строке). Если такового нет, то строка 11 не нужна.
Вызов виртуального метода базового класса из дочернего класса
Пример:
Описание программы:
Есть три класса: 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():
Результат работы программы будет иным:
Виртуальность наследуется подклассами автоматически, поэтому достаточно объявить функцию виртуальной только в базовом классе.
Множественное наследование – это способность порожденного класса наследовать характеристики нескольких базовых классов.
Для создания подкласса из нескольких базовых после имени нового класса и двоеточия указываются имена базовых классов, разделенные запятыми.
Пример:
class SleeperSofa : public Bed, public Sofa{};
Схема наследования (диван-кровать):
Программа работает корректно (пока нет обращения к свойству weight):
Очередность работы конструкторов при множественном наследовании
Если родительские классы перечислить в обратном порядке – сначала Sofa, потом Bed, то сначала вызовется конструктор для Sofa, потом для Bed:
Очередность работы деструкторов при множественном наследовании
Если родительские классы перечислить в обратном порядке – сначала Sofa, потом Bed, то очередность вызова конструкторов и деструкторов Sofa и Bed поменяется:
Как и при обычном наследовании, дочерний класс является своим базовым классом. Т.е. диван-кровать является диваном, и диван-кровать является кроватью. Поэтому так же как и при обычном наследовании, можно создать ссылку на дочерний класс с типом данных базового класса:
Классы Bed и Sofa оба содержат атрибут weight. Значит класс SleeperSofa наследует оба атрибута базовых классов: и Bed::weight, и Sofa::weight. И следующая строка вызовет ошибку:
Для получения доступа к weight придется явно указывать (с помощью имени класса) какая именно переменная weight требуется:
Ошибка устранена, но теперь информация о внутреннем устройстве класса присутствует за пределами класса (может быть и во внешнем приложении).
При наличии одинаковых методов в родительских классах для обращения к конкретному методу можно использовать явное приведение типа – объект ss приводится к типу Bed. Или (так же как в случае одноименных свойств weight) обратиться к методу, явно указав с помощью :: какой метод требуется вызвать:
Ошибка устранена, но теперь информация о внутреннем устройстве класса присутствует за пределами класса (может быть и во внешнем приложении).
Виртуальное наследование при множественном наследовании
Виртуальное наследование – один из вариантов наследования, который необходим для устранения проблем, порождаемых наличием возможности множественного наследования (особенно “ромбовидного наследования”), путем разрешения неоднозначности того, свойства и методы какого из родительских классов необходимо использовать. Виртуальное наследование применяется в тех случаях, когда множественное наследование вместо предполагаемой полной композиции свойств родительских классов приводит к ограничению доступных наследуемых свойств вследствие неоднозначности. Базовый класс, наследуемый множественно, определяется виртуальным с помощью ключевого слова virtual.
Конфликт имен weight в одной из предыдущих программ возник потому, что созданная иерархия классов была неадекватной – разложение на классы было неполным. На самом деле свойство weight является атрибутом более фундаментальной концепции – свойством мебели.
Схема ромбовидного наследования:
Сначала Mebel наследуют классы Bed и Sofa, а уже потом SleeperSofa наследуется от этих классов:
Необходимо, чтобы SleeperSofa наследовал только одну копию Mebel и чтобы Bed и Sofa имели к ней доступ. В C++ это достигается виртуальным наследованием:
При создании объекта ss переход к строке 32, в которой обнаруживается первый родительский класс Bed. Сразу идет переход к 14 строке, в которой обнаруживается родительский класс Mebel. Сразу идет переход к строке 4, и срабатывает конструктор класса Mebel, затем управление возвращается к 14 строке, срабатывает конструктор класса Bed, затем возвращается к 32 строке и обнаруживается еще один родительский класс Sofa. Сразу идет переход к строке 23, в которой опять обнаруживается родительский класс Mebel. Сразу идет переход к строке 4, и опять срабатывает конструктор класса Mebel, затем управление возвращается к 23 строке, срабатывает конструктор класса Sofa, затем возвращается к 32 строке, и срабатывает конструктор SleeperSofa.
Ошибка при попытке обращения к свойству:
Чтобы устранить ошибку, следует добавить ключевое слово virtual:
Теперь класс SleeperSofa выглядит следующим образом:
Класс SleeperSofa включает в себя Mebel, а также части классов Bed и Sofa, не содержащие Mebel, и уникальные для класса SleeperSofa члены. Теперь обращение к члену weight не многозначно, поскольку SleeperSofa содержит только одну копию Mebel.
- Описать фрагмент биологической классификации живых организмов с использованием принципа полиморфизма.
- Описать класс по заданной предметной области с использованием полиморфизма.
- Описать множественное наследование для выбранной предметной области.
- Описать виртуальное наследование при множественном наследовании для выбранной предметной области.