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

Последнее обновление: 14.03.2023

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

Рассмотрим простейший пример:

#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 вызываются только один раз.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850