Производный класс может иметь несколько прямых базовых классов. Подобный тип наследования называется множественным наследованием в отличие от одиночного наследования, при котором используется один базовый класс. Поскольку это несколько усложняет иерархию наследования, то используется гораздо реже, чем одиночное наследование.
Рассмотрим простейший пример:
#include <iostream> class Camera // класс фотокамеры { public: void makePhoto() { std::cout << "making photo" << std::endl; } }; class Phone // класс телефона { public: void makeCall() { std::cout << "making call" << std::endl; } }; // класс смартфона class Smartphone : public Phone, public Camera { }; int main() { Smartphone iphone; iphone.makePhoto(); // making photo iphone.makeCall(); // making call }
Здесь класс Camera представляет фотокамеру и для съемки фото предоставляет функцию makePhoto. Класс Phone представляет телефон и для звонков предоставляет функцию makeCall. Оба эти класса наследуются классом Smartphone, который представляет смартфон и может и делать фото, и выполнять звонки.
Стоит обратить внимание, что при установке наследования для каждого базового класса указывается спецификатор доступа:
class Smartphone : public Phone, public Camera
В итоге через объект Smartphone мы сможем вызывать функции обоих базовых классов:
Smartphone iphone; iphone.makePhoto(); // making photo iphone.makeCall(); // making call
При множественном наследовании также необходимо вызывать конструкторы базовых классов, если они имеют параметры. Например, пусть у нас есть класс книги Book, класс компьютерного файла File и класс электронной книги Ebook, который наследуется от этих классов:
#include <iostream> class Book // класс книги { public: Book(unsigned pages): pages(pages) { std::cout << "Book created" << std::endl; } ~Book() { std::cout << "Book deleted" << std::endl; } void printPageCount() { std::cout << pages << " pages" << std::endl; } private: unsigned pages; // количество страниц }; class File // класс электронного файла { public: File(double size): size(size) { std::cout << "File created" << std::endl; } ~File() { std::cout << "File deleted" << std::endl; } void printSize() { std::cout << size << "Mb" << std::endl; } private: double size; // размер файла }; // класс электронной книги class Ebook : public Book, public File { public: Ebook(std::string title, unsigned pages, double size): Book{pages}, File{size}, title{title} { std::cout << "Ebook created" << std::endl; } ~Ebook() { std::cout << "Ebook deleted" << std::endl; } void printTitle() { std::cout << "Title: " << title << std::endl; } private: std::string title; // название книги }; int main() { Ebook cppbook {"About C++", 320, 5.6}; cppbook.printTitle(); cppbook.printPageCount(); cppbook.printSize(); }
Оба базовых класса имеют конструкторы с одним параметром. И в конструкторе Ebook вызываем эти конструкторы:
class Ebook : public Book, public File { public: Ebook(std::string title, unsigned pages, double size): Book{pages}, File{size}, title{title}
Причем стоит обратить внимание на порядок вызов конструкторов. В определении класса Ebook первым базовым классом указан класс Book, поэтому сначала вызываем конструктор класса Book и только потом конструктор класса File.
Для каждого класса также определен деструктор. Посмотрим на очередность вызова конструкторов и деструкторов. И для этого в функции main создадим один объект Ebook, вызывая у него все функции базовых классов:
int main() { Ebook cppbook {"About C++", 320, 5.6}; cppbook.printTitle(); cppbook.printPageCount(); cppbook.printSize(); }
В итоге мы получим следующий консольный вывод
Book created File created Ebook created Title: About C++ 320 pages 5.6Mb Ebook deleted File deleted Book deleted
Мы видим, что первым вызывается конструктор класса Book, который указан первым среди базовых классов. Деструкторы вызываются в обратном порядке. Таким образом, деструктор Book выполнится последним.
В примере выше все классы имели функции, которые называются по разному. Но посмотрим, что будет в следующем случае:
#include <iostream> class Book // класс книги { public: Book(unsigned pages): pages(pages) { } void print() { std::cout << pages << " pages" << std::endl; } private: unsigned pages; // количество страниц }; class File // класс электронного файла { public: File(double size): size(size) { } void print() { std::cout << size << "Mb" << std::endl; } private: double size; // размер файла }; // класс электронной книги class Ebook : public Book, public File { public: Ebook(std::string title, unsigned pages, double size): Book{pages}, File{size}, title{title} { } void printTitle() { std::cout << "Title: " << title << std::endl; } private: std::string title; }; int main() { Ebook cppbook {"About C++", 320, 5.6}; cppbook.print(); // Ошибка компиляции }
Здесь базовые классы Book и File имеют функцию с одним и тем же именем - print()
. В итоге у нас получается двойственность, и такой код просто не скомпилируется.
Чтобы решить проблему, мы можем указать, из какого конкретного класса мы хотим вызвать функцию print:
int main() { Ebook cppbook {"About C++", 320, 5.6}; cppbook.Book::print(); // 320 pages cppbook.File::print(); // 5.6Mb }
В качестве альтернативы мы можем выполнять операцию преобразования к нужному типу и затем вызывать функцию:
int main() { Ebook cppbook {"About C++", 320, 5.6}; static_cast<Book&>(cppbook).print(); // 320 pages static_cast<File&>(cppbook).print(); // 5.6Mb }
Еще одной формой двойственности при наследовании может быть наследование от нескольких классов, которые косвенно или напрямую наследуются от одного и того же класса. Например:
#include <iostream> class Person { public: Person(std::string name): name{name} { std::cout << "Person created" << std::endl; } ~Person() { std::cout << "Person deleted" << std::endl; } void print() const { std::cout << "Person " << name << std::endl; } private: std::string name; }; class Student: public Person { public: Student(std::string name): Person{name} {} }; class Employee: public Person { public: Employee(std::string name): Person{name} {} }; // работающий студент class StudentEmployee: public Student, public Employee { public: StudentEmployee(std::string name): Student{name}, Employee{name} {} }; int main() { StudentEmployee bob{"Bob"}; //bob.print(); }
Здесь в основе иерархии классов находится класс человека - Person, от которого наследуются класс рабочего Employee и класс студента Student. Но у нас может быть работающий студент. И для этого определяем класс StudentEmployee, который наследуется от Student и Employee. Подобных ситуаций, конечно, лучше избегать, но тем не менее они то же могут встречаться. И если мы запустим программу, то увидим, что для одного объекта StudentEmployee два раза вызывается конструктор и деструктор класса Person:
Person created Person created Person deleted Person deleted
Более того, мы видим, что вызов bob.print()
не компилируется.
Для решения этой проблемы в C++ применяются виртуальные базовые классы - при установке наследования перед именем базового класса указывается ключевое слово virtual. Применим вирутальные классы:
class Person { public: Person(std::string name): name{name} { std::cout << "Person created" << std::endl; } ~Person() { std::cout << "Person deleted" << std::endl; } void print() const { std::cout << "Person " << name << std::endl; } private: std::string name; }; class Student: public virtual Person { public: Student(std::string name): Person{name} {} }; class Employee: public virtual Person { public: Employee(std::string name): Person{name} {} }; // работающий студент class StudentEmployee: public Student, public Employee { public: StudentEmployee(std::string name): Person{name}, Student{name}, Employee{name} {} }; int main() { StudentEmployee bob{"Bob"}; bob.print(); }
Теперь при определении классов Student и Employee базовый класс Person указан как виртуальный:
class Student: public virtual Person class Employee: public virtual Person
В итоге для объекта StudentEmployee мы сможем вызвать функцию print:
int main() { StudentEmployee bob{"Bob"}; bob.print(); }
А консольный вывод будет следующим:
Person created Person Bob Person deleted
Таким образом, мы видим, что теперь конструктор и деструктор класса Person вызываются только один раз.