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

  Юрий  | 

  |

  Обновл. 15 Сен 2021  | 

 84317

 ǀ   1 

На этом уроке мы рассмотрим чистые виртуальные функции, интерфейсы и абстрактные классы.

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

До этого момента мы записывали определения всех наших виртуальных функций. Однако 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, должен предоставить свою реализацию всех 3 методов класса IErrorLog. Вы можете создать дочерний класс с именем FileErrorLog, где openLog() открывает файл на диске, closeLog() — закрывает файл, а writeError() — записывает сообщение в файл. Вы можете создать еще один дочерний класс с именем ScreenErrorLog, где openLog() и closeLog() ничего не делают, а writeError() выводит сообщение во всплывающем окне.

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

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

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

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

Интерфейсы чрезвычайно популярны, так как они просты в использовании, удобны в поддержке, и их функционал легко расширять. Некоторые языки, такие как Java и C#, даже добавили в свой синтаксис ключевое слово interface, которое позволяет программистам напрямую определять интерфейсный класс, не указывая явно, что все методы являются абстрактными.

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

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

Тест


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

Ответ

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

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

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

Комментариев: 1

  1. Kris:

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

    А теперь, самое интересное. Как вызвать чистую виртуальную функцию? Чтобы это сделать, нужно, чтобы был сконструирован объект какого-то класса, явно не абстрактного, который умудрялся бы как-то обращаться к методам абстрактного класса, от которого он наследуется, чтобы те методы вызвали чистую вирт. функцию. Но как это сделать? Если немного подумать, то становится очевидным, что чтобы вызвать именно чистую вирт. функцию, нужно, чтобы в в момент обращения к методам абстрактного класса, в нашей таблице виртуальных функций был записан именно абстрактный класс. А это возможно только в момент вызова конструктора. Тогда, внутри конструктора нужно вызвать виртуальную функцию. Но компилятор не позволит настолько наглеть, ибо это явная ошибка. Тогда, можно обмануть компилятор, если в абстрактном классе создать и определить метод, который уже и вызывает чистую виртуальную функцию, а из конструктора вызывать именно этот метод. Вот код, который на моем компиляторе скомпилировался и вывел на экран после выполнения сообщение:

    Сообщение:

    "pure virtual method called
    terminate called without an active exception"

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

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