Производный класс может иметь несколько прямых базовых классов. Подобный тип наследования называется множественным наследованием в отличие от одиночного наследования, при котором используется один базовый класс. Поскольку это несколько усложняет иерархию наследования, то используется гораздо реже, чем одиночное наследование.
Рассмотрим простейший пример:
#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>
#include <string>
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>
#include <string>
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>
#include <string>
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. Применим вирутальные классы:
#include <iostream>
#include <string>
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 вызываются только один раз.