Тема множественного наследования с одной стороны простая, в то же время есть кое какие нюансы.

Приступим сразу к проблеме. Допустим у нас есть класс звуковой карты SoundCard, который умеет как то проигрывать звуки play_sound. Кроме того, есть класс видеокарты GraphicCard, который умеет как то запускать игры play_game.

class SoundCard
{
public:
    void play_sound()
    {
        cout << "play sound" << endl;
    }
};
 
class GraphicCard
{
public:
    void play_game()
    {
        cout << "play game" << endl;
    }
};

Теперь, мы хотим их поместить в единый объект — компьютер (Computer). Для этого просто отнаследуемся от этих классов, теперь внутри компьютера мы можем проигрывать звуки (play_sound) и играть в игры (play_game).

class Computer : public SoundCard, public GraphicCard
{
public:
    Computer()
    {
        play_sound();
        play_game();
    }
};

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

class Power
{
public:
    Power()
    {
        cout << "power" << endl;
    }
 
};
 
class SoundCard : public Power
{
public:
    void play_sound()
    {
        cout << "play sound" << endl;
    }
};
 
class GraphicCard : public Power
{
public:
    void play_game()
    {
        cout << "play game" << endl;
    }
};
 
class Computer : public SoundCard, public GraphicCard
{
public:
    Computer()
    {
        play_sound();
        play_game();
    }
};

Если запустить программу на этом этапе, то в выведется следующий текст:

power
power
play sound
play game

Что это значит? При вызове конструктора для класса Computer вызвались конструкторы базовых классов Power два раза, сначала для Power который внутри Sound, а затем для того что внутри GraphicCard. Очень важно в этой истории понять как будет лежать в памяти объект Computer.

При данном типе наследования вначале будут лежать данные базового класса Power, который относится к звуковой карте. Затем сама звуковая карта, а затем данные базового класса Power, который относится к видеокарте. Важно что при публичном наследовании у каждого объекта базовый класс будет свой.

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

Computer pc; //создаем объект класса Computer 
SoundCard *sound_card = &pc; //приводим класс Computer к классу SoundCard

В этом куске кода нет сомнений, если сделать подобное приведение, то указатель на SoundCard будет указывать на начало данных Computer, т.е. на SoundCard. В данном случае преобразование выполнится корректно.

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

Теперь вопрос, а что если привести к Power, на какие данные будет указывать указатель, на те что в SoundCard или в GraphicCard?

Computer pc;
Power *power = &amp;pc;

С точки зрения компилятора такое преобразование не однозначно, так как базовых классов два и не понятно к какому Power нужно привести, к тому что внутри SoundCard или в GraphicCard. Поэтому это приведет к ошибке компиляции. Нужно помочь компилятору и сказать к какому из объектов мы хотим обратиться, правильно в данном случае кастануть к нужному типу

 Power *power_sound = static_cast<SoundCard *>(&pc);
 Power *power_graphics = static_cast<GraphicCard *>(&pc);

В итоге получим указатели на базовый класс Power , которые выставлены на правильные участки памяти. Ну и в качестве примера, добавим в Power возможность устанавливать напряжение set_voltage и печатать его get_voltage.

#include <iostream>
 
using namespace std;
 
class Power
{
public:
    Power()
    {
        cout << "power" << endl;
    }
    void set_voltage(int voltage)
    {
       m_voltage = voltage;
    }
 
    void get_voltage()
    {
       cout << "voltage=" << m_voltage << endl;
    }
private:
    int m_voltage;
};
 
class SoundCard : public Power
{
public:
    void play_sound()
    {
        cout << "play sound" << endl;
    }
};
 
class GraphicCard : public Power
{
public:
    void play_game()
    {
        cout << "play game" << endl;
    }
};
 
class Computer : public SoundCard, public GraphicCard
{
public:
    Computer()
    {
        play_sound();
        play_game();
    }
};
 
int main(int argc, char *argv[])
{
    Computer pc;
    Power *power_sound = static_cast<SoundCard *>(&pc);
    Power *power_graphics = static_cast<GraphicCard *>(&pc);
 
    power_sound->set_voltage(5);
    power_graphics->set_voltage(12);
 
    power_sound->get_voltage();
    power_graphics->get_voltage();
    return 0;
}

Если запустить программу, то мы увидим следующий вывод:

power
power
play sound
play game
voltage=5
voltage=12

Подводя итог, у класса SoundCard свой базовый класс Power, который печатает 5В, у класса GraphicCard свой базовый класс Power, который печатает 12В, они физически лежат в разных местах и никак не пересекаются.

Но предположим, что у нас один блок питания Power и для SoundCard и для GraphicCard, у которого как то меняется напряжение и эти изменения мы хотим видеть в этих классах одновременно. На этот случай можно отнаследовать Power виртуально, добавив ключевое слово virtual и это сделает ровно то что мы хотим. Если поменять наш предыдущий пример добавив в определение классов виртуальное наследование, то при запуске программы мы увидим, что выводится напряжение 12В два раза.

class SoundCard : virtual public Power
class GraphicCard : virtual public Power

Снова важно понимать, как будет располагаться в памяти Power

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

Итого, при множественном наследовании потомок будет иметь несколько базовых классов, как в первом случае где было несколько источников питания. Где то это может пригодиться. Если необходимо, чтобы базовый класс был общий, используется виртуальное наследование. При виртуальном наследовании используется таблица виртуальных функций, что снижает производительность при доступе к объекту базового класса.

2 комментария: Множественное наследование

  • &pc; это опечатка, да?

  • нет не опечатка, это операция взятия адреса, допустим строчка

    Power *power_sound = static_cast<SoundCard *>(&pc);

    читаем ее справа налево так: возьмем адрес от pc, преобразуем его в указатель на sound_card и засунем в указатель power_sound

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

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

Последние комментарии
  • Загрузка...
Счетчик
Яндекс.Метрика