Лекция 19 (лекции (2002)), страница 3
Описание файла
Файл "Лекция 19" внутри архива находится в папке "лекции (2002)". Документ из архива "лекции (2002)", который расположен в категории "". Всё это находится в предмете "языки программирования" из 7 семестр, которые можно найти в файловом архиве МГУ им. Ломоносова. Не смотря на прямую связь этого архива с МГУ им. Ломоносова, его также можно найти и в других разделах. .
Онлайн просмотр документа "Лекция 19"
Текст 3 страницы из документа "Лекция 19"
x.P( );
Даже по синтаксису и в языке Оберон, и тем более в языках, в которых есть классы, виден только один объект, в зависимости от которого вызывается соответствующая процедура. В Аде может быть случай когда у процедуры есть 2 параметра, и оба параметра являются cw-типом. Для чего может пригодиться такое связывание? Рассмотрим пример с Figure. Для фигур в некоторых случаях очень полезна процедура вычисления пересечений InterSect, у которой есть 2 параметра Figure1, Figure2. Понятно, что абстрактный метод пересечения мы реализовать не может. Т.е. если речь идет о классовом языке
class Figure{
Figure & InterSect(Figure &);
}
У процедуры InterSect фактически будет 2 аргумента: Первый – тот самый указатель this, который передается InterSect. Второй – Figure &.
Figure
Point Line
Мы можем в иерархии классов для каждой фигуры переопределить кучу методов InterSect (для Point мы должны написать реализации метода InterSect для всевозможных фигур, также для Line мы должны переопределить метод InterSect для всех возможных типов). Допустим, что мы хотим вычислить для массива Figure * a[ ] пересечение всех фигур. Как для a[0] и a[1] сделать соответствующий вызов InterSect? Вся беда в том, что мы здесь должны знать конкретно какого типа a[0] и какого типа a[1], и вызывать метод InterSect от двух параметров уже в зависимости от их конкретных типов. Хотелось бы ввести такой механизм
Intersect(a[0], a[1]);
который бы динамически выбирал ту реализацию InterSect, которая соответствует динамическим типам a[0] и a[1]. На языках, в которых есть привязка к единственному типу объекта, подобного рода вещи смоделировать достаточно нелегко. В случае языка Ада у нас есть базовый тип Figure
type Figure is tagged record … end;
procedure InterSect(X1: Figure'class, X2: Figure'class);
Реализуем соответствующие процедуры InterSect.
InterSect(a, b);
Где а и b – 2 объекта типа Figure. Мы ничего не знаем о конкретном типе этих а и b, но будет автоматически выбрана нужная процедура InterSect. Если же ее нельзя выбрать, тогда будет выдано сообщение об ошибке. Естественным образом случай мультиметодов обобщается на любое количество классов. Это последствие достаточного хитрого механизма динамического связывания в языке.
В языке С++ можно было бы реализовать соответствующие мультиметоды, но для этого был необходим дополнительный синтаксис. Подробнее см. книгу Страуструпа "Дизайн и эволюция языка С++", который обсуждал альтернативы, как можно было бы в С++ реализовать мультиметоды. В настоящее время этого нет. Некоторые другие языки, которые мы сейчас не рассматриваем, например COS (Common Object System), также реализуют мультиметоды.
Последнее замечание, которое касается динамического связывания.
Динамическое связывание и множественное наследование.
Само по себе наследование мало что дает без динамического связывания. В свое время считалось, что эффективно реализовать множественное наследование не возможно именно из-за проблем, которые возникают в случае динамиеского связывания.
X Y
virtual void f( ); virtual void g( );
Z
void f( );
void g( );
В классе Х определена виртуальная функция f, в классе Y – g. В классе Z обе эти функции переопределяются.
Z z;
X * p1; Y * p2;
p1= & z;
p2= & z;
Это можно сделать, т.к. от более широкого типа к более узкому типу присваивать можно. Вызываем функцию
p1->f( );
p1, p2
X
Y
Z
Должна быть вызвана функция f из Z потому, что раз она виртуальная, значит связывание должно быть динамическим.
p2->f( );
Никакая функция не будет вызвана. Компилятор выдаст сообщение об ошибке. Аналогичным образом ошибочна конструкция
p1->g( );
p2->g( );
Будет вызвана функция для Z. Функции для Z, которые мы пишем, прекрасно осведомлены о том, что у нас есть объекты класса Х, объекты класса Y и новые объекты класса Z. Представим, что функция g не переопределяется, а наследуется классом Z от класса Y.
p2->g( );
В общем случае компилятор не знает для какого типа будет вызвана функция. Он вызывает виртуальную функцию g (он даже не знает какую потому, что это зависит от динамического типа р2 (Z это или Y)). Функция g написана для класса Y, который о множественном наследовании ничего не знает. Когда мы пишем g, мы еще ничего не знаем о том, что может выводиться из Y. Объекты класса Y смещены относительно указателя p2. Получается, что механизм реализации должен быть следующим: Если функция g не перекрыта, на самом деле ей должен передаваться указатель на Y
p 1
p2 X
Y
Z
потому, что компилятор, программируя функцию, знает, что у него есть указатель with, и если
class Y{ int a, b}
то компилятор знает, что по нулевому смещению ссылка на таблицу виртуальных методов, по следующему смещению а, по следующему смещению b. Если функция g не переопределена в классе Z, и ей передается указатель на X, то все смещения будут не верны. Т.е. в случае множественного наследования механизм вызова виртуального метода должен носить несколько другой характер. На самом деле так оно и есть. Проблем с функциями из первого класса (X) никогда не будет потому, что и в случае единичного наследования, и в случае множественного наследования он стоит в начале. В случае Z тоже проблем нет, поскольку компилятор знает, как выглядит полный объект Z. Но при вызове виртуальных функций для промежуточных классов в цепочке множественного наследования возникают проблемы. Поэтому в ТВМ надо хранить адрес функции и смещение d, которое нужно прибавлять к указателю.
ТВМ
X
& f 0
Y
a & g d
b
Z
& f 0
& g 0
Для X (для первого класса) d всегда равно 0. Особенно неприятно то, о чем говорил Страуструп, что не хорошо заставлять платить пользователя за конструкции, которыми он не пользуется. В этом случае с точки зрения накладных расходов и по памяти, и по времени реализация усложняется. Если программист не использует множественное наследование, он все равно должен платить за то, что кто-то его может использовать. Поэтому естественным выходом будет сделать для множественного наследования одну реализацию, для единичного наследования – другую. Но это существенно усложняет язык. Проблемы при множественном наследовании происходят, когда идет множественное наследование по данным. Если бы в классах X иY никаких данных не было бы, тогда никаких проблем с функциями f и g тоже не было бы, и можно было бы обойтись одной таблицей. Проблемы при реализации возникают только в том случае, когда наследуются данные у нескольких классов. Не случайно в C# и Java множественное наследование есть, но только по данным идет единичное наследование, а от классов, в которых нет данных (интерфейсы), может идти обычное множественное наследование.
Глава 3. Абстрактные классы и интерфейсы.
Абстрактный тип данных – это тип, у которого доступен только функциональный интерфейс, а все данные и реализация закрыты для пользователя. Абстрактные типы данных удобны тем, что мы можем менять их реализацию, а клиентские модули, которые использует соответствующий абстрактный тип данных, совершенно не зависят от того, как реализованы соответствующие процедуры и функции. Это очень удобно.
Пример:
class Figure{
virtual void Draw( );
…
};
Из этого класса выводим новые.
Point
Figure Line
Circle
…
Очевидно, для каждого из этих классов мы пишем свою реализацию метода Draw. Но при перетрансляции программы загрузчик выдаст сообщение о том, что есть ссылка на функцию Draw для типа данных Figure, а самой функции нет. Метод Draw для Figure может вообще не вызываться, но компилятор сгенерирует ТВМ для класса Figure и заполнит ее ссылкой на метод Draw. Загрузчик не обнаружит эту функцию. Поэтому единственный вариант – написать эту функцию. Когда в Turbo Pascal впервые для фирмы Borland появились объектно-ориентированные свойства, там была специальная библиотечная процедура
Abstract( );
Это просто такой интеллигентный способ завершения подпрограмм. При вызове этой процедуры выдается сообщение, вроде: "Этот метод вызывать нельзя." и программа сваливается. Дело в том, что метод Draw вызывать действительно нельзя. Объект собственно класса Figure никакого смысла не имеет, он абстрактный. Figure имеет смысл только с точки зрения того, что из нее будут выведены какие-то новые конкретные классы, в которых виртуальные методы будут переопределены. Просто заводить данные типа Figure никакого смысла нет.
Figure C;
C.Draw( );
Сама по себе Figure никакого смысла не имеет, она имеет смысл только через указатели или ссылки, которые на самом деле будут ссылаться на объекты производных классов. Есть классы, которые вводятся только как вершина в иерархии сами по себе смысла не имеют. Чтобы запретить использование явным образом таких классов и введено понятие абстрактного класса, и связанного с ним понятия чистой виртуальной функции (это в терминологии языка С++). Чистая виртуальная функция – это функция, от которой не требуется никакой реализации. В данном случае мы должны были написать
class Figure{
virtual void Draw( ) = 0;
…
}