Главная страница
Навигация по странице:

  • Таблица 18.1. Виртуальные функции для класса Panda Имя виртуальной функции Активный экземпляр

  • 18.3.1. Наследование и композиция

  • Язык программирования C++. Вводный курс. С для начинающих


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница87 из 93
    1   ...   83   84   85   86   87   88   89   90   ...   93

    931
    Panda
    Рис. 18.3. Иерархия множественного наследования класса Panda
    Конструкторы базовых классов вызываются в порядке объявления в списке базовых классов. Например, для ying_yang эта последовательность такова: конструктор Bear (но поскольку класс Bear – производный от ZooAnimal, то сначала вызывается конструктор
    ZooAnimal
    ), затем конструктор Endangered и в самом конце конструктор Panda.
    Как отмечалось в разделе 17.4, на порядок вызова не влияет ни наличие базовых классов в списке инициализации членов, ни порядок их перечисления. Иными словами, если бы конструктор Bear вызывался неявно и потому не был бы упомянут в списке инициализации членов, как в следующем примере:
    { ... } то все равно конструктор по умолчанию Bear был бы вызван раньше, чем явно заданный в списке конструктор класса Endangered с двумя аргументами.
    Порядок вызова деструкторов всегда противоположен порядку вызова конструкторов. В нашем примере деструкторы вызываются в такой последовательности:

    Panda(),
    Endangered()
    , Bear(), ZooAnimal().
    В разделе 17.3 уже говорилось, что в случае одиночного наследования к открытым и защищенным членам базового класса можно обращаться напрямую (не квалифицируя имя члена именем его класса), как если бы они были членами производного класса. То же самое справедливо и для множественного наследования. Однако при этом можно унаследовать одноименные члены из двух или более базовых классов. В таком случае прямое обращение оказывается неоднозначным и приводит к ошибке компиляции.
    Однако такую ошибку вызывает не
    потенциальная неоднозначность неквалифицированного доступа к одному из двух одноименных членов, а лишь попытка фактического обращения к нему (см. раздел 17.4). Например, если в обоих классах Bear и
    Endangered определена функция-член print(), то инструкция ying_yang.print( cout ); приводит к ошибке компиляции, даже если у двух унаследованных функций-членов разные списки параметров.
    // конструктор по умолчанию класса Bear вызывается до
    //
    êîíñòðóêòîðà êëàññà Endangered ñ äâóìÿ àðãóìåíòàìè ...
    Panda::Panda()
    : Endangered( Endangered::environment,
    Endangered::critical )
    Error: ying_yang.print( cout ) -- ambiguous, one of
    Bear::print( ostream& )

    С++ для начинающих
    932
    Endangered::print( ostream&, int )
    Endangered::print( ostream&, int )
    Причина в том, что унаследованные функции-члены не образуют множество перегруженных функций внутри производного класса (см. раздел 17.3). Поэтому print() разрешается только по имени, а не по типам фактических аргументов. (О том, как производится разрешение, мы поговорим в разделе 18.4.)
    В случае одиночного наследования указатель, ссылка или объект производного класса при необходимости автоматически преобразуются в указатель, ссылку или объект базового класса, которому открыто наследует производный. Это остается верным и для множественного наследования. Так, указатель, ссылку или сам объект класса Panda можно преобразовать в указатель, ссылку или объект ZooAnimal, Bear или Endangered: cout << ying_yang << endl; // правильно
    Однако вероятность неоднозначных преобразований при множественном наследовании намного выше. Рассмотрим, к примеру, две функции: extern void display( const Endangered& );
    Неквалифицированный вызов display() для объекта класса Panda display( ying_yang ); // ошибка: неоднозначность приводит к ошибке компиляции:
    Error: display( ying_yang ) -- ambiguous, one of display( const Bear& ); display( const Endangered& );
    Ошибка: display( ying_yang ) -- неоднозначно, одна из display( const Bear& ); display( const Endangered& );
    Ошибка: ying_yang.print( cout ) -- неоднозначно, одна из
    Bear::print( ostream& ) extern void display( const Bear& ); extern void highlight( const Endangered& );
    Panda ying_yang; display( ying_yang ); //
    ïðàâèëüíî highlight( ying_yang ); //
    ïðàâèëüíî extern ostream& operator<<( ostream&, const ZooAnimal& ); extern void display( const Bear& );
    Panda ying_yang;

    С++ для начинающих
    933
    Компилятор не может различить два непосредственных базовых класса с точки зрения преобразования производного. Равным образом применимы обе трансформации. (Мы покажем способ разрешения этого конфликта в разделе 18.4.)
    Чтобы понять, какое влияние оказывает множественное наследование на механизм виртуальных функций, определим их набор в каждом из непосредственных базовых классов Panda. (Виртуальные функции введены в разделе 17.2 и подробно обсуждались в разделе 17.5.)
    };
    Теперь определим в классе Panda собственный экземпляр print(), собственный деструктор и еще одну виртуальную функцию cuddle():
    };
    Множество виртуальных функций, которые можно напрямую вызывать для объекта
    Panda
    , представлено в табл. 18.1.
    Таблица 18.1. Виртуальные функции для класса Panda
    Имя виртуальной функции
    Активный экземпляр class Bear : public ZooAnimal { public: virtual Bear(); virtual ostream& print( ostream& ) const; virtual string isA() const;
    // ...
    }; class Endangered { public: virtual Endangered(); virtual ostream& print( ostream& ) const; virtual void highlight() const;
    // ... class Panda : public Bear, public Endangered
    { public: virtual Panda(); virtual ostream& print( ostream& ) const; virtual void cuddle();
    // ...

    С++ для начинающих
    934
    Когда ссылка или указатель на объект Bear или ZooAnimal инициализируется адресом объекта Panda или ему присваивается такой адрес, то части интерфейса, связанные с классами Panda и Endangered, становятся недоступны: delete pb; // правильно: Panda::Panda()
    (Обратите внимание, что если бы объекту класса Panda был присвоен указатель на
    ZooAnimal
    , то все показанные выше вызовы разрешались бы так же.)
    Аналогично, если ссылка или указатель на объект Endangered инициализируется адресом объекта Panda или ему присваивается такой адрес, то части интерфейса, связанные с классами Panda и Bear, становятся недоступными: delete pe; // правильно: Panda::Panda()
    Обработка виртуального деструктора выполняется правильно независимо от типа указателя, через который мы уничтожаем объект. Например, во всех четырех инструкциях порядок вызова деструкторов один и тот же – обратный порядку вызова конструкторов: деструктор
    Panda::Panda() print(ostream&) const
    Panda::print(ostream&) isA() const
    Bear::isA() highlight() const
    Endangered::highlight() cuddle()
    Panda::cuddle()
    Bear *pb = new Panda; pb->print( cout ); //
    ïðàâèëüíî: Panda::print(ostream&) pb->isA(); //
    ïðàâèëüíî: Bear::isA() pb->cuddle(); //
    îøèáêà: ýòî íå ÷àñòü èíòåðôåéñà Bear pb->highlight(); //
    îøèáêà: ýòî íå ÷àñòü èíòåðôåéñà Bear
    Endangered *pe = new Panda; pe->print( cout ); // правильно: Panda::print(ostream&)
    //
    îøèáêà: ýòî íå ÷àñòü èíòåðôåéñà Endangered pe->cuddle(); pe->highlight(); // правильно: Endangered::highlight()

    С++ для начинающих
    935
    delete pe;
    Деструктор класса Panda вызывается с помощью механизма виртуализации. После его выполнения по очереди статически вызываются деструкторы Endangered и Bear, а в самом конце – ZooAnimal.
    Почленная инициализация и присваивание объекту производного класса, наследующего нескольким базовым, ведут себя точно так же, как и при одиночном наследовании (см. раздел 17.6). Например, для нашего объявления класса Panda
    { ... }; в результате почленной инициализации объекта ling_ling
    Panda ling_ling = yin_yang; вызывается копирующий конструктор класса Bear (но, так как Bear производный от
    ZooAnimal
    , сначала выполняется копирующий конструктор класса ZooAnimal), затем – класса Endangered и только потом – класса Panda. Почленное присваивание ведет себя аналогично.
    Упражнение 18.1
    Какие из следующих объявлений ошибочны? Почему?
    (a) class CADVehicle : public CAD, Vehicle { ... }; public List, public List { ... };
    // ZooAnimal *pz = new Panda; delete pz;
    // Bear *pb = new Panda; delete pb;
    // Panda *pp = new Panda; delete pp;
    // Endangered *pe = new Panda; class Panda : public Bear, public Endangered
    Panda yin_yang;
    (b) class DoublyLinkedList:

    С++ для начинающих
    936
    private istream, private ostream { ... };
    Упражнение 18.2
    Дана иерархия, в каждом классе которой определен конструктор по умолчанию: class MI : public C, public Z { ... };
    Каков порядок вызова конструкторов в таком определении:
    MI mi;
    Упражнение 18.3
    Дана иерархия, в каждом классе которой определен конструктор по умолчанию: class D : public X, public C { ... };
    Какие из следующих преобразований недопустимы:
    D *pd = new D;
    (a) X *px = pd; (c) B *pb = pd;
    (b) A *pa = pd; (d) C *pc = pd;
    Упражнение 18.4
    Дана иерархия классов, обладающая приведенным ниже набором виртуальных функций:
    (c) class iostream: class A { ... }; class B : public A { ... }; class C : public B { ... }; class X { ... }; class Y { ... }; class Z : public X, public Y { ... }; class X { ... }; class A { ... }; class B : public A { ... }; class C : private B { ... };

    С++ для начинающих
    937
    };
    Какой экземпляр виртуальной функции вызывается в каждом из следующих случаев:
    (b) pb->debug(); (d) pb->writeOn(); (f) delete pb;
    Упражнение 18.5
    На примере иерархии классов из упражнения 18.4 определите, какие виртуальные функции активны при вызове через pd1 и pd2:
    (a) Derived1 *pd1 new MI;
    Derived2 d2 = obj; class Base { public: virtual Base(); virtual ostream& print(); virtual void debug(); virtual void readOn(); virtual void writeOn();
    // ...
    }; class Derived1 : virtual public Base { public: virtual Derived1(); virtual void writeOn();
    // ...
    }; class Derived2 : virtual public Base { public: virtual Derived2(); virtual void readOn();
    // ...
    }; class MI : public Derived1, public Derived2 { public: virtual MI(); virtual ostream& print(); virtual void debug();
    // ...
    Base *pb = new MI;
    (a) pb->print(); (c) pb->readOn(); (e) pb->log();
    (b) MI obj;

    С++ для начинающих
    938
    18.3.
    Открытое, закрытое и защищенное наследование
    Открытое наследование называется еще наследованием типа. Производный класс в этом случае является подтипом базового; он замещает реализации всех функций-членов, специфичных для типа базового класса, и наследует общие для типа и подтипа функции.
    Можно сказать, что производный класс служит примером отношения “ЯВЛЯЕТСЯ”, т.е. предоставляет специализацию более общего базового класса. Медведь (Bear) является животным из зоопарка (ZooAnimal); аудиокнига (AudioBook) является предметом, выдаваемым читателям (LibraryLendingMaterial). Мы говорим, что Bear – это подтип
    ZooAnimal, равно как и Panda. Аналогично AudioBook – подтип LibBook (библиотечная книга), а оба они – подтипы LibraryLendingMaterial. В любом месте программы, где ожидается базовый тип, можно вместо него подставить открыто унаследованный от него подтип, и программа будет продолжать работать правильно (при условии, конечно, что подтип реализован корректно). Во всех приведенных выше примерах демонстрировалось именно наследование типа.
    Закрытое наследование называют также наследованием реализации. Производный класс напрямую не поддерживает открытый интерфейс базового, но пользуется его реализацией, предоставляя свой собственный открытый интерфейс.
    Чтобы показать, какие здесь возникают вопросы, реализуем класс PeekbackStack, который поддерживает выборку из стека с помощью метода peekback(): peekback( int index, type &value ) { ... } где value содержит элемент в позиции index, если peekback() вернула true. Если же peekback()
    возвращает false, то заданная аргументом index позиция некорректна и в value помещается элемент из вершины стека.
    В реализации PeekbackStack возможны два типа ошибок:

    реализация абстракции PeekbackStack: некорректная реализация поведения класса;

    реализация представления данных: неправильное управление выделением и освобождением памяти, копированием объектов из стека и т.п.
    Обычно стек реализуется либо как массив, либо как связанный список элементов (в стандартной библиотеке по умолчанию это делается на базе двусторонней очереди, хотя вместо нее можно использовать вектор, см. главу 6). Хотелось бы иметь гарантированно правильную (или, по крайней мере, хорошо протестированную и поддерживаемую) реализацию массива или списка, чтобы использовать ее в нашем классе PeekbackStack.
    Если она есть, то можно сосредоточиться на правильности поведения стека.
    У нас есть класс IntArray, представленный в разделе 2.3 (мы временно откажемся от применения класса deque из стандартной библиотеки и от поддержки элементов, имеющих отличный от int тип). Вопрос, таким образом, заключается в том, как лучше всего воспользоваться классом IntArray в нашей реализации PeekbackStack. Можно задействовать механизм наследования. (Отметим, что для этого нам придется модифицировать IntArray, сделав его члены защищенными, а не закрытыми.)
    Реализация выглядела бы так: bool
    PeekbackStack::

    С++ для начинающих
    939
    }
    К сожалению, программа, которая работает с нашим новым классом PeekbackStack, может неправильно использовать открытый интерфейс базового IntArray: is[0] = is[512];
    #include "IntArray.h" class PeekbackStack : public IntArray { private: const int static bos = -1; public: explicit PeekbackStack( int size )
    : IntArray( size ), _top( bos ) {} bool empty() const { return _top == bos; } bool full() const { return _top == size()-1; } int top() const { return _top; } int pop() { if ( empty() )
    /*
    îáðàáîòàòü îøèáêó */ ; return _ia[ _top-- ];
    } void push( int value ) { if ( full() )
    /*
    îáðàáîòàòü îøèáêó */ ;
    _ia[ ++_top ] = value;
    } bool peekback( int index, int &value ) const; private: int _top;
    }; inline bool
    PeekbackStack:: peekback( int index, int &value ) const
    { if ( empty() )
    /*
    îáðàáîòàòü îøèáêó */ ; if ( index < 0 || index > _top )
    { value = _ia[ _top ]; return false;
    } value = _ia[ index ]; return true; extern void swap( IntArray&, int, int );
    PeekbackStack is( 1024 );
    //
    íåïðåäâèäåííîå îøèáî÷íîå èñïîëüçîâàíèå PeekbackStack swap(is, i, j); is.sort();

    С++ для начинающих
    940
    Абстракция PeekbackStack должна обеспечить доступ к элементам стека по принципу
    “последним пришел, первым ушел”. Однако наличие дополнительного интерфейса
    IntArray не позволяет гарантировать такое поведение.
    Проблема в том, что открытое наследование описывается как отношение “ЯВЛЯЕТСЯ”.
    Но PeekbackStack не является разновидностью массива IntArray, а лишь включает его как часть своей реализации. Открытый интерфейс IntArray не должен входить в открытый интерфейс PeekbackStack.
    Закрытое наследование от базового класса представляет собой вид наследования, который нельзя описать в терминах подтипов. В производном классе открытый интерфейс базового становится закрытым. Все показанные выше примеры использования объекта PeekbackStack становятся допустимыми только внутри функций-членов и друзей производного класса.
    В приведенном ранее определении PeekbackStack достаточно заменить слово public в списке базовых классов на private. Внутри же самого определения класса public и private следует оставить на своих местах: class PeekbackStack : private IntArray { ... };
    18.3.1.
    Наследование и композиция
    Реализация класса PeekbackStack с помощью закрытого наследования от IntArray работает, но необходимо ли это? Помогло ли нам наследование в данном случае? Нет.
    Открытое наследование – это мощный механизм для поддержки отношения
    “ЯВЛЯЕТСЯ”. Однако реализация PeekbackStack по отношению к IntArray – пример отношения “СОДЕРЖИТ”. Класс PeekbackStack содержит класс IntArray как часть своей реализации. Отношение “СОДЕРЖИТ”, как правило, лучше поддерживается с помощью композиции, а не наследования. Для ее реализации надо один класс сделать членом другого. В нашем случае объект IntArray делается членом PeekbackStack. Вот реализация PeekbackStack на основе композиции:

    С++ для начинающих
    941
    }
    Решая, следует ли использовать при проектировании класса с отношением “СОДЕРЖИТ” композицию или закрытое наследование, можно руководствоваться такими соображениями:

    если мы хотим заместить какие-либо виртуальные функции базового класса, то должны закрыто наследовать ему;

    если мы хотим разрешить нашему классу ссылаться на класс из иерархии типов, то должны использовать композицию по ссылке (мы подробно расскажем о ней в разделе 18.3.4);

    если, как в случае с классом PeekbackStack, мы хотим воспользоваться готовой реализацией, то композиция по значению предпочтительнее наследования. Если class PeekbackStack { private: const int static bos = -1; public: explicit PeekbackStack( int size ) : stack( size ), _top( bos ) {} bool empty() const { return _top == bos; } bool full() const { return _top == size()-1; } int top() const { return _top; } int pop() { if ( empty() )
    /* обработать ошибку */ ; return stack[ _top-- ];
    } void push( int value ) { if ( full() )
    /* обработать ошибку */ ; stack[ ++_top ] = value;
    } bool peekback( int index, int &value ) const; private: int _top;
    IntArray stack;
    }; inline bool
    PeekbackStack:: peekback( int index, int &value ) const
    { if ( empty() )
    /* обработать ошибку */ ; if ( index < 0 || index > _top )
    { value = stack[ _top ]; return false;
    } value = stack[ index ]; return true;

    С++ для начинающих
    942
    требуется отложенное выделение памяти для объекта, то следует выбрать композицию по ссылке (с помощью указателя).
    18.3.2.
    Открытие отдельных членов
    Когда мы применили закрытое наследование класса PeekbackStack от IntArray, то все защищенные и открытые члены IntArray стали закрытыми членами PeekbackStack.
    Было бы полезно, если бы пользователи PeekbackStack могли узнать размер стека с помощью такой инструкции: is.size();
    Разработчик способен оградить некоторые члены базового класса от эффектов неоткрытого наследования. Вот как, к примеру, открывается функция-член size() класса
    IntArray
    :
    };
    Еще одна причина для открытия отдельных членов заключается в том, что иногда необходимо разрешить доступ к защищенным членам закрыто унаследованного базового класса при последующем наследовании. Предположим, что пользователям нужен подтип стека PeekbackStack, который может динамически расти. Для этого классу, производному от PeekbackStack, понадобится доступ к защищенным элементам ia и
    _size класса IntArray:
    };
    Производный класс может лишь вернуть унаследованному члену исходный уровень доступа, но не повысить или понизить его по сравнению с указанным в базовом классе.
    На практике множественное наследование очень часто применяется для того, чтобы унаследовать открытый интерфейс одного класса и закрытую реализацию другого.
    Например, в библиотеку классов Booch Components включена следующая реализация растущей очереди Queue (см. также статью Майкла Вило (Michaeel Vilot) и Грейди Буча
    (Grady Booch) в [LIPPMAN96b]): class PeekbackStack : private IntArray { public:
    // сохранить открытый уровень доступа using IntArray::size;
    // ... template class PeekbackStack : private IntArray { public: using intArray::size;
    // ... protected: using intArray::size; using intArray::ia;
    // ...

    С++ для начинающих
    1   ...   83   84   85   86   87   88   89   90   ...   93


    написать администратору сайта