Полиморфная кухня

3. Множественное и виртуальное наследование

В первой части серии мы рассмотрели основы наследования и полиморфизма в языке C++, во второй рассмотрели механизм, благодаря которому всё это работает. Идём дальше: создадим более замысловатую иерархию.

Сегодня реализуем следующую иерархию. Базовым классом будет класс Cmyk, реализующий цветовую схему CMYK. Класс CmykGy будет содержать дополнительный серый, класс CmykLcLm — дополнительные светло-синий и светло-малиновый. Класс CmykGyLcLmLg внизу иерархии будет содержать все упомянутые дополнительные цвета (серый, светло-синий, светло-малиновый), а также светло-серый.

                                          Cmyk
                             ┌──────────────┴──────────────┐
                          CmykGy                        CmykLcLm		
		             └──────────────┬──────────────┘
			               CmykGyLcLmLg

Соответствующий код очень простой.

class Cmyk
{
public:
    virtual ~Cmyk()
    {
    }

    virtual void print() const
    {
        std::cout << "CMYK." << std::endl;
    }

protected:
    unsigned char cyan;
    unsigned char magenta;
    unsigned char yellow;
    unsigned char black;
};



class CmykGy : public Cmyk
{
public:
    virtual void print() const
    {
        std::cout << "CMYK + gray." << std::endl;
    }

protected:
    unsigned char gray;
};



class CmykLcLm : public Cmyk
{
public:
    virtual void print() const
    {
        std::cout << "CMYK + light cyan + light magenta." << std::endl;
    }

protected:
    unsigned char lightCyan;
    unsigned char lightMagenta;
};



class CmykGyLcLmLg : public CmykGy, public CmykLcLm
{
public:
    virtual void print() const
    {
        std::cout << "CMYK + gray " 
            << "+ light cyan + light magenta + light gray." << std::endl;
    }

protected:
    unsigned char lightGray;
};

Как было сказано, производный класс включает все поля, которые содержатся в базовом. Получается, что класс CmykGyLcLmLg содержит в себе поля класса Cmyk дважды! Это не только не соответствует нашим целям, но и расходует память. Такое явление назвается проблемой ромбовидного наследования (diamond problem). (Хотя заметим, что в какой-то другой ситуации такое дублирование может иметь смысл.)

Слегка модифицируем объявления классов CmykGy и CmykLcLm (вы можете скачать получившийся исходный файл).

...
class CmykGy : public virtual Cmyk
{
   ...
...
class CmykLcLm : public virtual Cmyk
{
   ...

Схема принципиально меняется.

При виртуальном наследовании базовый подобъект (давайте назовём его так) хранится в единственном экземпляре (показано стрелками). Но перед этим помещается указатель на него. В случае классов, которые наследуются непосредственно от базового (CmykGy и CmykLcLm), всё более-менее наглядно.

Подробно рассмотрим, как формируется структура класса CmykGyLcLmLg.

  1. CmykGy — это Cmyk. Действительно, первое же поле — указатель на Cmyk (пусть и добавляется один уровень косвенности).
  2. Каждый производный класс включает в себя базовый (все базовые). Можно увидеть, что подобъекты CmykGy и CmykLcLm поочердно включены в производный объект. Сам базовый подобъект располагается далее, он доступен по любому из указателей.
  3. Производный класс может добавлять свои поля.

Заключение

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

Исходные файлы

  1. cmyk.cpp
27 декабря 2016 · наследование и полиморфизм · программирование
Оставить комментарий (отменить)