Урок №168. Чистые виртуальные функции. Интерфейсы и Абстрактные классы

  Юрий Ворон  | 

    | 

  Обновлено 24 Окт 2018  | 

 477

До этого момента мы записывали определения всех наших виртуальных функций.

Абстрактные функции и классы

Однако C++ позволяет создавать особый вид виртуальных функций, так называемых чистых виртуальных функций (или еще «абстрактных функций»), которые вообще не имеют определения! Переопределяют их дочерние классы.

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

Таким образом мы сообщаем компилятору: «Реализацией этой функции займутся дочерние классы».

Использование чистой виртуальной функции имеет два основных последствия. Во-первых, любой класс с одной и более чистыми виртуальными функциями становится абстрактным классом, объекты которого создавать нельзя! Подумайте, что произойдет, если мы создадим объект класса Parent:

Поскольку мы не определяли метод getValue(), то каков результат выполнения parent.getValue()?

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

Пример чистой виртуальной функции

Рассмотрим пример чистой виртуальной функции на практике. В одном из предыдущих уроков мы создавали родительский класс Animal и дочерние классы Cat и Dog. Код:

Мы запретили создавать объекты класса Animal, сделав конструктор protected. Однако остаются две проблемы:

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

   По-прежнему могут быть дочерние классы, которые не переопределяют метод speak().

Например:

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



John says ???

Что случилось? Мы забыли переопределить метод speak(), поэтому lion.Speak() вызвал Animal.speak() и получили то, что получили.

Решение — использовать чистую виртуальную функцию:

Здесь есть несколько вещей на которые следует обратить внимание. Во-первых, speak() теперь является чистой виртуальной функцией. Это означает, что Animal теперь абстрактный родительский класс и нам уже не нужен спецификатор protected (хотя он и не будет лишним). Во-вторых, поскольку наш класс Lion является дочерним классу Animal, но мы не определили Lion::speak(), то Lion считается также абстрактным классом. Поэтому, если мы попытаемся скомпилировать следующий код:

То получим ошибку, что Lion является абстрактным классом, а создавать объекты абстрактного класса нельзя. Из этого можно сделать вывод, что для того, чтобы создать объект класса Lion нам нужно переопределить метод speak():

Теперь уже другое дело:

John says RAWRR!

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

Чистые виртуальные функции с определениями

Оказывается, мы можем определить чистые виртуальные функции:

В этом случае speak() по-прежнему считается чистой виртуальной функцией (хотя позже мы её определили), а Animal по-прежнему считается абстрактным родительским классом (и, следовательно, объекты этого класса не могут быть созданы). Любой класс, который наследует класс Animal, должен переопределить метод speak() или он также будет считаться абстрактным классом.

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

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

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

Barbara says buzz

Хотя это используется редко.

Интерфейсы

Интерфейс — это класс, который не имеет переменных-членов и все методы которого являются чистыми виртуальными функциями! Интерфейсы еще называют «классы-интерфейсы» или «интерфейсные классы».

Интерфейсные классы принято называть с I вначале. Например:

Любой класс, который наследует IErrorLog, должен предоставить свою реализацию всех трех методов класса IErrorLog. Вы можете создать дочерний класс с именем FileErrorLog, где openLog() открывает файл на диске, closeLog() закрывает файл, а writeError() записывает сообщение в файл. Вы можете создать еще один дочерний класс с именем ScreenErrorLog, где openLog() и closeLog() ничего не делают, а writeError() выводит сообщение во всплывающем окне на экран.

Теперь, допустим, вам нужно написать программу, которая использует журнал ошибок. Если вы будете писать классы FileErrorLog или ScreenErrorLog напрямую, то это не эффективно. Например, следующая функция заставляет всех объектов, вызывающих mySqrt(), использовать FileErrorLog, что может быть не всегда уместно.

Намного лучшим вариантом будет реализация через IErrorLog:

Теперь пользователь через передачу объектов может определить самостоятельно, какой класс следует вызывать. Если он хочет, чтобы ошибка была записана в файле, то он передаст в функцию mySqrt объект класса FileErrorLog. Если он хочет, чтобы ошибка выводилась на экран, то передаст объект класса ScreenErrorLog. Или, если он хочет сделать то, что вы не предусмотрели, например, отправить кому-то Email-ом сообщение ошибки, то он может создать новый дочерний класс EmailErrorLog, который будет наследовать IErrorLog, и передавать объект этого класса! Таким образом, реализация через IErrorLog делает нашу функцию более гибкой и независимой.

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

Интерфейсы чрезвычайно популярны, так как они просты в использовании, удобны в поддержке и их функционал легко расширять. Даже некоторые языки, такие как Java и C#, добавили в свой синтаксис ключевое слово «interface», которое позволяет программистам напрямую определять интерфейсный класс, не указывая явно, что все методы являются абстрактными. Кроме того, хотя Java (до 8-ой версии) и C# не позволяют использовать множественное наследование с обычными классами, в случае с интерфейсными классами это ограничение снято. Так как интерфейсы не имеют переменных-членов и определений методов, то они избегают многих традиционных проблем, которые возникают с множественным наследованием, сохраняя при этом свою гибкость.

Чистые виртуальные функции и виртуальная таблица

Абстрактные классы имеют виртуальные таблицы, которые могут использоваться, если у вас есть указатель или ссылка на абстрактный класс. Запись чистой виртуальной функции в виртуальной таблице обычно содержит либо нулевой указатель, либо указывает на общую функцию, которая выводит ошибку (иногда эта функция называется __purecall), если не было обнаружено переопределения.

Тест

Чем отличается абстрактный класс от интерфейса в C++?

Ответ

Абстрактный класс может иметь переменные-члены и имеет от одной чистой виртуальной функции, в то время как интерфейс не имеет переменных-членов и все его методы должны быть чистыми виртуальными функциями.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (5 оценок, среднее: 5,00 из 5)
Загрузка...

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

ВОЛШЕБНАЯ ТАБЛЕТКА ПО С++