лекции (2011) (1160854), страница 9
Текст из файла (страница 9)
X * py = new Y(); // уже здесь компилятор выдаст ошибку из-за того, т.к. непонятно что записывать в таблицу виртуальных функций
py -> f();
X a; // здесь компилятор также выдаст ошибку из-за того, из-за того, что не сможет заполнить таблицу виртуальных функций. Если бы функция X:: f() не была виртуальной, то ошибки здесь не было бы.
a.g();// ошибка, т.к X::g() не определена
Решение проблемы – языковая поддержка
В C#, Java:
abstract перед классом и перед функцией, для которой не существует реализации на данном уровне детализации. Такие функции и классы, их содержащие, называют аббстрактными
abstract class base //Абстрактный класс
{
abstract public void Draw(); // Функция без реализации
}
С#
class D: base{
public override void draw(){……};
};
Java
class D: extends base{
public override void draw(){……};
};
//обязательно поставить тело Draw!
В C++:
Чисто виртуальная функция (абстрактная) – virtual «прототип» = 0;
В Аде:
procedure P( X : T ) is abstract;
где T – тегированный тип.
Объекты абстрактных классов нельзя создавать.
При вызове виртуальной функции в конструкторе виртуальность вызова снимается.
Существует метод, который не должен быть чисто виртуальным – деструктор. Он всегда должен быть виртуальным и реализованным.
Различие между абстрактными классами и абстрактными типами данных:
В абстрактных классах абстрагируются от реализаций некоторых методов. В абстрактных типах данных абстрагируются от всей структуры.
Абстрактный тип данных (АТД) — это тип с полностью инкапсулированной структурой. Использовать объекты АТД возможно только при помощи явно определенных в интерфейсе типа операций.
Абстрактный класс (АК) — это класс, содержащий хотя бы один абстрактный метод.
class Iset
{
virtual void include(const T &) = 0;
virtual exclude(const T &) = 0;
«статические члены»
}
Такой класс называется класс-интерфейс. В таких классах не имеет смысл объявлять нестатические поля, т.к. не реализовано ни одного методы для работы с ними.
Множественное наследование интерфейсов. Реализация интерфейсов и ее
особенности современных ЯП. Явная и неявная реализация интерфейсов.
В C# и Java, в отличие от C++, существует языкового понятия интерфейса:
interface «имя»
{
«объявления членов»
}
Внутри интерфейса могут быть статические поля. Поля без «static» будут восприняты как статические. Также членами интерфейса могут быть методы и свойства, которые считаются чисто виртуальными и публичными.
Т.е. интерфейс – чистый контракт, не реализующий структуру.
Если класс наследует интерфейс и не определяет все методы интерфейса, то он становится абстрактным.
Если написать
int pi{gte; set}
то оно превратится в свойство.
Кстати, перед определением свойства может стоять слово virtual. То есть свойства могут быть виртуальными или чисто виртуальными.
В Java вместо get и set есть getpair и setpair.
Методы в интерфейсах объявляются без модификаторов – все, что внутри интерфейса, обязано быть публичным по умолчанию. Однако protected все-таки может стоять.
C#
class D: имя, интерфейсы:
Java
Class D extends Base implements именя интерфейсов
Интерфейсы могут содержать внутри себя любые вложенные классы.
В С# появляется так называемая явная и неявная реализация интерфейсов.
Неявная реализация – это то, что мы всегда называем обычной реализацией.
Пример неявной реализации интерфейсов:
interface ISample{
void f(); };
class CoClass: ISample{
public void f(){………………..}; //если тут не поставить public, компилятор заругается.
};
Явная реализация интерфейсов:
class CoClass2: ISample{
void ISample f() {…………….}
//тут спецификатор public отсутствует, потому что попытка написать тут public карается
};
Как же вызвать данный метод, если он не публичный?
D * px;
px->f(); //непонятно, какой метд я хочу вызывать – ошибка.
Но px->I1::f(); //снова ошибка: попытка вызова чисто виртуальной функции.
Мы же хотим вызвать заместителя для I1. Это длается так:
((I1*)px)->f();
Замечание. Слова «явный» и «неявный» должны относиться к приведению типов, а не к классам(интерфейсам).
CoClass2 x;
(ISample)x.f();//явное приведение
При явной реализации вызов возможен лишь ри явом приведении.
Явная реализация интерфейса означает, что вызов метода
интерфейса может происходить только через ссылку на интерфейс, но неможет происходить через ссылку на класс, реализующий интерфейс. Перед
вызовом интерфейсного метода необходимо явно преобразовать ссылку на
объект реализующего класса к ссылке на интерфейс. Концепция явной
реализации полезна, например, при конфликте имен между унаследованными
интерфейсами.
В C# реализованные методы интерфейсов считаются по умолчанию «sealed» - запечатанными. В наследниках они перекрываются.
class ExampleClass : IControl
{
public void Paint(){ … ;}
}
class FancyComBox: ExampleClass
{
public void Paint(){….; } // Компилятор выдаст предупреждение для этой строчки, в котором сообщит, что «FancyComBox.Paint() скрывает унаследованный метод ExampleClass.Paint(). Используйте ключевое слово «new», если это сделано целенаправленно. Т.е. если поставить «new» перед определением, то предупреждение исчезнет.
Замечание2. Интерфейсы-маркеры.Реализованы в Ада. Являюются разновидностью стандартных интерфейсов, относящихся к категории ссылочных типов. Цель маркеров состоит в том, чтобы вызов через интерфейс был равносиелн по эффективности вызову виртуального метода.
Java: Iterable.
Интерфейсы-маркеры – это стандартные интерфейсы, доведенные до абсолюта. Контракт интерфейсов-маркеров описан только в документации. Это пустые интерфейсы, мы не можем увидеть их с своем коде.
Пример: интерфейс Cloneable – класс, поддерживающий этот интерфейс, обязан реализовать метод Clone
12. Множественное наследование
Множественное наследование в языке Си++. Ромбовидное
наследование и его примеры. Проблемы множественного наследования:
конфликт имен, реализация динамического связывания. Особенности
реализации множественного наследования при наследовании интерфейсов.
С какими проблемами мы сталкиваемся при множественном наследовании?
-
Конфликт имен
-
Виртуальные методы.
Конфликт имен решается через явное указание имени базы или через приведение к ссылке на базу.
Но существует ещё одна проблема – эффективность динамического полиморфизма (виртуальных функций)
А теперь рассмотрим множественное
class A
{
public:
A(){};
virtual void a(){ a1 = 1;};
virtual void second(){..;}
int a1, a2, a3;
};
class B
{
public:
B(){};
virtual void bar(){};
virtual void bbar(){};
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 – значение, которое берётся из расширенной таблицы виртуальных функций.
Без наследования по данным таких проблем не возникает, т.к. указатель на таблицу виртуальных функций всегда один.
Ромбовидное и не ромбовидное наследование
Не ромбовидное: : В объекте Z будет два экземпляра объекта A с разными реализациями таблицы виртуальных функций
сlass A{ .. ;}
class X:public A{ …; }
class Y:public A{… ; }
class Z: public X, public Y {…;}
Ромбовидное: В объекте Z будет только один экземпляр объекта A
сlass A{ .. ;}
class X: public virtual A{ …; }
class Y: public virtual A{… ; }
class Z: public X, public Y {…;}
13. Динамическая идентификация типа
Понятие о динамической идентификации типа (ДИТ). Достоинства и
недостатки использования ДИТ. Особенности ДИТ в современных языках
программирования.
14. Понятие о родовых объектах. Обобщенное программирование
Понятие о статической параметризации и родовых объектах.
Достоинства статической параметризации. Статическая параметризация и
ООП.
Родовые модули и подпрограммы в языке Ада.
Порождение нового пакета порождало новый экземпляр данного типа.
package Stack – два параметра – тип элемента размер стека:
generic
type T is private;
size: integer;
package Stack is
Push
Pop
end Stack;
Можно ли так запрограммировать процедуры, чтобы они эффективно работали для любого размера стека? Конечно, да!
Конкретизация:
package IStack is new Stack(integer, 128);
Тривиальная реализация – простая макроподстановка integer и 128. Плюс – крайняя простота. Минус – очень сильное разбухание кода – сколько объявлений, столько и различных процедур.
В Аде большое разнообразие типов формальных параметров родового модуля. Формальные параметры родовых модулей:
параметр-переменная <=> любое константное выражение
type T is private <=> любой тип с операцией присваивания
type T is range <=> любой дискретный тип с упорядоченностью и функциями «следующий» и «предыдущий»
type T is delta <=> любое плавающее выражение
< > <=> при конкретизации процедуры мы можем не указывать этот вариант (параметр по умолчанию).
Пусть у нас есть:
procedure StrSort is new G_SORT(String, Integer, TARR);
C помощью < > компилятор находит функцию < для строк и подставляет как параметр по умолчанию. Родовые сегменты – абсолютно необходимая в Аде конструкция, потому что там нет передачи процедур и функций по параметру. Компилятор должен видеть не только спецификацию данной абстракции, но и тело. Гибкость повышается, но тело родовой абстракции должно быть доступно в любой момент конкретизации. В Аде-83 механизм родовых модулей – единственный, который поддерживал ОО. Как вы думаете, что самое главное, что появилось в Аде-95? Правильно, класс – тегированная запись:
type T is tagged
Механизм шаблонов в языке Си++. Шаблоны-классы и шаблоны-
функции. Параметры шаблонов. Вывод параметров шаблонов. Генерация
кода по шаблонам. Частичная специализация шаблонов. Обобщенное
программирование на языке Си++: функторы, свойства, стратегии, шаблоны
выражений.