Cpp наследование от нескольких классов

Наследование в C++: beginner, intermediate, advanced

В этой статье наследование описано на трех уровнях: beginner, intermediate и advanced. Expert нет. И ни слова про SOLID. Честно.

Beginner

Что такое наследование?

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

Класс, который наследует данные, называется подклассом (subclass), производным классом (derived class) или дочерним классом (child). Класс, от которого наследуются данные или методы, называется суперклассом (super class), базовым классом (base class) или родительским классом (parent). Термины “родительский” и “дочерний” чрезвычайно полезны для понимания наследования. Как ребенок получает характеристики своих родителей, производный класс получает методы и переменные базового класса.

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

В этом примере, метод turn_on() и переменная serial_number не были объявлены или определены в подклассе Computer . Однако их можно использовать, поскольку они унаследованы от базового класса.

Важное примечание: приватные переменные и методы не могут быть унаследованы.

#include using namespace std; class Device < public: int serial_number = 12345678; void turn_on() < cout private: int pincode = 87654321; >; class Computer: public Device <>; int main() < Computer Computer_instance; Computer_instance.turn_on(); cout 

Типы наследования

В C ++ есть несколько типов наследования:

  • публичный ( public )- публичные ( public ) и защищенные ( protected ) данные наследуются без изменения уровня доступа к ним;
  • защищенный ( protected ) — все унаследованные данные становятся защищенными;
  • приватный ( private ) — все унаследованные данные становятся приватными.

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

#include using namespace std; class Device < public: int serial_number = 12345678; void turn_on() < cout >; class Computer: private Device < public: void say_hello() < turn_on(); cout >; int main() < Device Device_instance; Computer Computer_instance; cout 

Класс Computer теперь использует метод turn_on() как и любой приватный метод: turn_on() может быть вызван изнутри класса, но попытка вызвать его напрямую из main приведет к ошибке во время компиляции. Для базового класса Device , метод turn_on() остался публичным, и может быть вызван из main .

Конструкторы и деструкторы

В C ++ конструкторы и деструкторы не наследуются. Однако они вызываются, когда дочерний класс инициализирует свой объект. Конструкторы вызываются один за другим иерархически, начиная с базового класса и заканчивая последним производным классом. Деструкторы вызываются в обратном порядке.

Важное примечание: в этой статье не освещены виртуальные десктрукторы. Дополнительный материал на эту тему можно найти к примеру в этой статье на хабре.

#include using namespace std; class Device < public: // constructor Device() < cout // destructor ~Device() < cout >; class Computer: public Device < public: Computer() < cout ~Computer() < cout >; class Laptop: public Computer < public: Laptop() < cout ~Laptop() < cout >; int main()

Конструкторы: Device -> Computer -> Laptop .
Деструкторы: Laptop -> Computer -> Device .

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

Множественное наследование происходит, когда подкласс имеет два или более суперкласса. В этом примере, класс Laptop наследует и Monitor и Computer одновременно.

#include using namespace std; class Computer < public: void turn_on() < cout >; class Monitor < public: void show_image() < cout >; class Laptop: public Computer, public Monitor <>; int main()

Проблематика множественного наследования

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

#include using namespace std; class Computer < private: void turn_on() < cout >; class Monitor < public: void turn_on() < cout >; class Laptop: public Computer, public Monitor <>; int main() < Laptop Laptop_instance; // Laptop_instance.turn_on(); // will cause compile time error return 0; >

Несмотря на то, что приватные данные не наследуются, разрешить неоднозначное наследование изменением уровня доступа к данным на приватный невозможно. При компиляции, сначала происходит поиск метода или переменной, а уже после — проверка уровня доступа к ним.

Intermediate

Проблема ромба

Проблема ромба (Diamond problem)- классическая проблема в языках, которые поддерживают возможность множественного наследования. Эта проблема возникает когда классы B и C наследуют A , а класс D наследует B и C .

К примеру, классы A , B и C определяют метод print_letter() . Если print_letter() будет вызываться классом D , неясно какой метод должен быть вызван — метод класса A , B или C . Разные языки по-разному подходят к решению ромбовидной проблем. В C ++ решение проблемы оставлено на усмотрение программиста.

Ромбовидная проблема — прежде всего проблема дизайна, и она должна быть предусмотрена на этапе проектирования. На этапе разработки ее можно разрешить следующим образом:

  • вызвать метод конкретного суперкласса;
  • обратиться к объекту подкласса как к объекту определенного суперкласса;
  • переопределить проблематичный метод в последнем дочернем классе (в коде — turn_on() в подклассе Laptop ).
#include using namespace std; class Device < public: void turn_on() < cout >; class Computer: public Device <>; class Monitor: public Device <>; class Laptop: public Computer, public Monitor < /* public: void turn_on() < cout // uncommenting this function will resolve diamond problem */ >; int main() < Laptop Laptop_instance; // Laptop_instance.turn_on(); // will produce compile time error // if Laptop.turn_on function is commented out // calling method of specific superclass Laptop_instance.Monitor::turn_on(); // treating Laptop instance as Monitor instance via static cast static_cast( Laptop_instance ).turn_on(); return 0; >

Если метод turn_on() не был переопределен в Laptop, вызов Laptop_instance.turn_on() , приведет к ошибке при компиляции. Объект Laptop может получить доступ к двум определениям метода turn_on() одновременно: Device:Computer:Laptop.turn_on() и Device:Monitor:Laptop.turn_on() .

Проблема ромба: Конструкторы и деструкторы

Поскольку в С++ при инициализации объекта дочернего класса вызываются конструкторы всех родительских классов, возникает и другая проблема: конструктор базового класса Device будет вызван дважды.

#include using namespace std; class Device < public: Device() < cout >; class Computer: public Device < public: Computer() < cout >; class Monitor: public Device < public: Monitor() < cout >; class Laptop: public Computer, public Monitor <>; int main()

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

Виртуальное наследование (virtual inheritance) предотвращает появление множественных объектов базового класса в иерархии наследования. Таким образом, конструктор базового класса Device будет вызван только единожды, а обращение к методу turn_on() без его переопределения в дочернем классе не будет вызывать ошибку при компиляции.

#include using namespace std; class Device < public: Device() < cout void turn_on() < cout >; class Computer: virtual public Device < public: Computer() < cout >; class Monitor: virtual public Device < public: Monitor() < cout >; class Laptop: public Computer, public Monitor <>; int main()

Примечание: виртуальное наследование в классах Computer и Monitor не разрешит ромбовидное наследование если дочерний класс Laptop будет наследовать класс Device не виртуально ( class Laptop: public Computer, public Monitor, public Device <>; ).

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

В С++, класс в котором существует хотя бы один чистый виртуальный метод (pure virtual) принято считать абстрактным. Если виртуальный метод не переопределен в дочернем классе, код не скомпилируется. Также, в С++ создать объект абстрактного класса невозможно — попытка тоже вызовет ошибку при компиляции.

#include using namespace std; class Device < public: void turn_on() < cout virtual void say_hello() = 0; >; class Laptop: public Device < public: void say_hello() < cout >; int main() < Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.say_hello(); // Device Device_instance; // will cause compile time error return 0; >

Интерфейс

С++, в отличии от некоторых ООП языков, не предоставляет отдельного ключевого слова для обозначения интерфейса (interface). Тем не менее, реализация интерфейса возможна путем создания чистого абстрактного класса (pure abstract class) — класса в котором присутствуют только декларации методов. Такие классы также часто называют абстрактными базовыми классами (Abstract Base Class — ABC).

#include using namespace std; class Device < public: virtual void turn_on() = 0; >; class Laptop: public Device < public: void turn_on() < cout >; int main() < Laptop Laptop_instance; Laptop_instance.turn_on(); // Device Device_instance; // will cause compile time error return 0; >

Advanced

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

Наследование от реализованного или частично реализованного класса

Если наследование происходит не от интерфейса (чистого абстрактного класса в контексте С++), а от класса в котором присутствуют какие-либо реализации, стоит учитывать то, что класс наследник связан с родительским классом наиболее тесной из возможных связью. Большинство изменений в классе родителя могут затронуть наследника что может привести к непредвиденному поведению. Такие изменения в поведении наследника не всегда очевидны — ошибка может возникнуть в уже оттестированом и рабочем коде. Данная ситуация усугубляется наличием сложной иерархии классов. Всегда стоит помнить о том, что код может изменяться не только человеком который его написал, и пути наследования очевидные для автора могут быть не учтены его коллегами.

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

Интерфейс

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

Интерфейс: Пример использования

Прежде всего стоит заметить, что пример тесно связан с понятием полиморфизма, но будет рассмотрен в контексте наследования от чистого абстрактного класса.

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

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

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

Заключение

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

Источник

Наследование (C++)

В этом разделе рассматривается использование производных классов для создания расширяемых программ.

Общие сведения

Новые классы могут быть производными от существующих классов с помощью механизма под названием "наследование" (см. сведения, начиная с одного наследования). Классы, используемые для наследования, называются "базовыми классами" определенного производного класса. Производный класс объявляется с помощью следующего синтаксиса:

class Derived : [virtual] [access-specifier] Base < // member list >; class Derived : [virtual] [access-specifier] Base1, [virtual] [access-specifier] Base2, . . . < // member list >; 

После тега (имени) класса следует двоеточие и список базовых спецификаций. Названные таким образом базовые классы, вероятно, были объявлены ранее. Базовые спецификации могут содержать описатель доступа, который является одним из ключевых слов public , protected или private . Эти описатели доступа отображаются перед именем базового класса и применяются только к базовому классу. Эти описатели контролируют разрешение производного класса на использование членов базового класса. Сведения о доступе к членам базового класса см. в разделе Member-контроль доступа. Если описатель доступа опущен, доступ к этой базе считается private . Базовые спецификации могут содержать ключевое слово virtual , указывающие на виртуальное наследование. Это ключевое слово может отображаться до или после описателя доступа, если таковые имеются. Если используется виртуальное наследование, базовый класс называется виртуальным базовым классом.

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

В этой статье содержатся следующие разделы:

Ключевые слова __super и __interface описаны в этом разделе.

Источник

Читайте также:  Embedding html in markdown
Оцените статью