лекции (2010) (by Ульянов Алексей_ Лихогруд Николай_ Сергеев Николай) (1160852), страница 15
Текст из файла (страница 15)
следовательно, таблица виртуальныхфункций)class X{public:virtual void f(); //Объявлена, но не определенаvoid g();//Объявлена, но не определена}class Y: public X{publicvirtual void f() {…;}//Замещена}….X * py = new Y(); // уже здесь компилятор выдаст ошибку из-за того, т.к. непонятно чтотаблицу виртуальных функцийзаписывать вpy -> f();X a; // здесь компилятор также выдаст ошибку из-за того, из-за того, что не сможетзаполнить таблицувиртуальных функций. Если бы функция X:: f() не былавиртуальной, то ошибки здесь не было бы.a.g();// ошибка, т.к X::g() не определенаРешение проблемы – языковая поддержкаВ C#, Java:abstract перед классом и перед функцией, для которой не существует реализации на данном уровнедетализации.
Такие функции и классы, их содержащие, называют аббстрактнымиabstract class Figure //Абстрактный класс{abstract public void Draw(); // Функция без реализацииabstract public void Move();// Функция без реализации}В C++:Чисто виртуальная функция (абстрактная) – virtual «прототип» = 0;В Аде:procedure P( X : T ) is abstract;где T – тегированный тип.Объекты абстрактных классов нельзя создавать.При вызове виртуальной функции в конструкторе виртуальность вызова снимается:сlass X{…}class Y: public X{…}class Z: public Y {…}При создании объекта класса Z сначала вызовется конструктор класса X, потом класса Y и в самом концекласса Z.
Нельзя вызвать в конструкторе класса Y замещённую функцию из Z, потому что объект Z ещё не доконца создан и для него ещё даже нет таблицы виртуальных функций.Существует метод, который не должен быть чисто виртуальным – деструктор. Он всегда должен бытьвиртуальным и реализованным.Base * px = new Derived;«использование»delete(px); // уничтожиться должен объект класса DerivedРазличие между абстрактными классами и абстрактными типами данных:В абстрактных классах абстрагируются от реализаций некоторых методов.
В абстрактных типах данныхабстрагируются от всей структуры.Например множество – каждое множество должно поддерживать операции• include(const T &)• exclude( const T &)При этом, естественно, реализация этих методов зависит от типа элементов, способа хранения и т.д.class Iset{virtual void include(const T &) = 0;virtual exclude(const T &) = 0;«статические члены»}Такой класс называется класс-интерфейс.
В таких классах не имеет смысл объявлять нестатические поля, т.к.не реализовано ни одного методы для работы с ними.class Slist{…}class Slist_Set: public Iset, private Slist {…; } // Iset – интерфейсIset * Move(){….return new Slist_set(«параметры»);}В C# и Java, в отличие от C++, существует языкового понятия интерфейса:interface «имя»{«объявления членов»}Внутри интерфейса могут быть статические поля. Поля без «static» будут восприняты как статические. Такжечленами интерфейса могут быть методы и свойства, которые считаются чисто виртуальными и публичными.Т.е. интерфейс – чистый контракт, не реализующий структуру.Если класс наследует интерфейс и не определяет все методы интерфейса, то он становится абстрактным.Множественное наследованиеТолько в C++ поддерживается множественное наследование. В C# и Java множественное наследованиеподдерживается только для интерфейсов.C#: class D: [«класс»]{, «интерфейс»}Java: class D extends Base implements «интерфейс» {, «интерфейс»}Проблемы, связанные с множественным наследованием:•Конфликт имёнJava:interface Icard{void Draw(){ …; }// Раздать карты}interface IGUIControl{{void Draw(){ …; } //нарисовать колоду}class Sample implements ICard, IGUIControl {…; }// Ошибка!!C++class D public I1, public I2{virtual void I1::f(){…;} // Операция разрешения видимостиvirtual void I2::f(){…;} // Операция разрешения видимости}…D * px = new D;px-> f(); // ошибкаpx->I1::f(); // ошибка((I1 *)px) -> f(); //Работает! Явное приведениеC#: Неявная и явная реализация интерфейсовinterface ISample{void f();}class CoClass: ISample{public void f(){…} // неявная реализация, «public» перед void f() обязателен}class CoClass2: ISample{void ISample.f(){…} // явная реализация.
Запрещено указывать public, private, protected.}CoClass x = new CoClass();x.f();// работает!CoClass2 x = new CoClass2()’x.f();/// ошибка! Цитата компилятора: «CoClass2' does not contain a definition for 'f'((ISample)x).f(); // работает! Явное приведениеВ платформе .Net имеется классclass FileStream:IDisposable{….void IDisposable.Dispose(){…;}void Close(){ ((IDisposable)this).Dispose();}….}FileStream реализует интерфейс IDisposable явным образом, т.е. через объект FileStream нельзя вызвать методDispose.
Вместо этого имеется не виртуальный метод Close, который явно вызывается Dispose внутри себя.class IControl{void Paint();}interface IEdit:IControl{…..}interface IDropList:Icontrol{…..}class ComboBox : IDropList, IEdit // Элемент управления ComboBox реализует оба//интерфейса. И для каждого из них должен быть свой//Paint;{void IDropList.Paint(){…; } //Явная реализацияvoid IEdit.Paint(){….;}//Явная реализацияpublic void Paint() //Этот метод будет вызываться по умолчанию{.…((IEdit)this).Paint(); // Явный вызов…..}}В C# реализованные методы интерфейсов считаются по умолчанию «sealed» - запечатанными.
В наследникахони перекрываются.class ExampleClass : IControl{public void Paint(){ … ;}}class FancyComBox: ExampleClass{public void Paint(){….; } // Компилятор выдаст предупреждение для этой строчки, вкотором сообщит,что «FancyComBox.Paint() скрываетунаследованный метод ExampleClass.Paint(). Используйтеключевое слово «new», если это сделано целенаправленно.»Т.е. еслипоставить «new» перед определением, то предупреждение исчезнет.public void override Paint (){… ; } // Для этой строчки компилятор выдаст ошибку, т.к.нельзязамещать(переопределять) методы, не указанные в базовых классах как virtual, overrideили abstact}В целом множественное наследование можно заменить включением (агрегацией).Как было показано выше, конфликт имён решается через явное приведение и уточнение класса.Но существует ещё одна проблема – эффективность динамического полиморфизма (виртуальных функций)Эта проблема возникает только при наследовании по данным.Рассмотрим одиночное наследование:class A{public:A(){};int a1, a2, a3;};a1a2a3c1class C : public Aобъект класса Сvirtual void second(){..;}Подъобъект Avirtual void a(){ a1 = 1;};vptrТаблицавиртуальныхфункций A + Cадрес C::a(), котораязамещает функциюA::a()адрес A::second()адрес C::goo(){public:C() : A(){};virtual void goo(){};void a(){}; // переопределениеint c1;};….C c;Если в классе C переопределить метод, то в соответствующую ячейку в таблице виртуальных функций будетзаписан указатель на новый метод.
Если же в классе C добавляются новые функции – они дописываются вконец таблицы. При вызове методов никаких лишних действий не происходит.Таблица виртуальныхнаследование:функций A + Cvptr{public:virtual void a(){ a1 = 1;};virtual void second(){..;}int a1, a2, a3;};class B{public:B(){};virtual void bar(){};virtual void bbar(){};a2a3vptrb1Подъобъект BA(){};a1объект класса Сclass AПодъобъект AА теперь рассмотрим множественноеадрес C::a(), котораязамещает функциюA::a()адрес A::second()адрес C::goo()Таблица виртуальныхфункций Bb2b3c1адрес C::bar(), котораязамещает функциюB::bar()адрес B::bar()int b1, b2, b3;};class C : public A{public:C() : A(){};virtual void goo(){};// Собственная новая виртуальная функцияvoid a(){}; // переопределениеvoid bar();// переопределениеint c1;};….C c;Тут надо обратить внимание на следующее:••Таблица виртуальных методов самого нижнего класса в иерархии доступна через первый указательvptr.Каждый подобъект, который содержит виртуальные методы, имеет свою таблицу виртуальныхфункций.Если в классе C переопределить метод, то в соответствующую ячейку в таблице родительского объекта будетзаписан указатель на новый метод.
Если же в классе C добавляются новые функции – они дописываются вконец первой таблицы.Такой алгоритм становится понятен, если рассмотреть возможные преобразования типов:••С -> A. Через указатель на класс A можно вызывать только методы, которые прописаны в этом классе.C -> B. Ситуация аналогична, только мы можем вызывать виртуальные методы, определенные в классеB.Новые виртуальные методы (которых нет в родительских классах) можно использовать только черезуказатель на класс C. В этом случае всегда используется первая таблица виртуальных функций.Сложность реализации заключается в следующем:Во время преобразования типов меняется адрес указателя:C c;B *p = &c;Указатель p будет содержать адрес объекта c + смещение подобъекта B. Т.е.
все вызовы методов черезтакой указатель будут использовать вторую таблицу виртуальных методов объекта C. Но ведь в такойситуации при вызове переопределённой в C функции через указатель на B в эту функцию передастсянеправильный указатель this! Он будет указывать не на C, как это нужно, а на B.Приходится расширять таблицу виртуальных функций добавлением в неё смещения от указателя на объекткласса до таблицы виртуальных функций для каждой функции.
Если виртуальная функция из Bпереопределена в C, то для неё такое смещение будет равно (-смещение подобъекта B). Если же не былапереопределена, то оно будет равно нулю. Для всех виртуальных функций из класса A это смещение будетнулевым, т.к. указатель на подобъект A совпадает с указателем на весь объект C(объект А находится в началеобъекта C). Теперь в функцию можно передать правильный указатель:this = current_this + offsetгде current_this – на подобъект, через который вызывается функция. offset – значение, которое берётся израсширенной таблицы виртуальных функций.Без наследования по данным таких проблем не возникает, т.к.
указатель на таблицу виртуальных функцийвсегда один.Ромбовидное и не ромбовидное наследованиеAсlass A{ .. ;}class X:public A{ …; }XПодобъект XНе ромбовидное: : В объекте Z будет два экземпляра объекта A с разными реализациями таблицывиртуальных функцийclass Z: public X, public Y {…;}AYZПодобъект Yclass Y:public A{… ; }AAXYZРомбовидное: В объекте Z будет только один экземпляр объекта Aсlass A{ .. ;}Aclass X: public virtual A{ …; }class Y: public virtual A{… ; }Xclass Z: public X, public Y {…;}YAZXYZ.