лекция 20 (1161115)
Текст из файла
Глава 2: Динамическое связывание (продолжение)
В прошлый раз говорили про организацию виртуальных методов (таблица виртуальных методов фактически это неявный статический член виртуального класса).
И когда вызывается виртуальный метод (они могут вызываться через указатель или ссылку, поскольку только указатели и ссылки имеют динамический тип). Компилятор через эту ссылку добирается до ТВМ. Если вызывается виртуальный метод f(), компилятор знает, какое у него смещение (всегда фиксированное смещение) от начала этой виртуальной таблицы, и запускает функцию по этому адресу (получатся 2 косвенных преобразований). Накладные расходы есть, но они более-менее терпимы, гибкость, появляющаяся при этом, гораздо больше.
Классовые языки
В языках ориентированных на классы динамическое связывание очень похоже на Си++, есть определенные нюансы.
В языке Java понятие виртуального метода вообще нет. Там, в силу специфики языка, все методы по умолчанию связаны динамически, не виртуальных методов нет. В языках еще сохранились невитруальные методы, потому как они более эффективны при вызове. При дизайне Java соображение гибкости и переносимости превалировали над соображениям эффективности.
В Си# и Дельфи есть как виртуальные методы
Виртуальные методы описываются:
1.Cи#:
virtual void f() {...}
2.Delphi:
procedure F; virtual;
2.
Если в Си++ метод является виртуальным и объявлен как виртуальный, то стать невиртуальным он уже не может!
class X {
int f();
virtual void g();
}
class Y public X {
void g();
virtual int f(); // не имеем право так делать
}
Если функция объявлена как виртуальная, то ее переопределение (т.е. функция, которая имеет тот же профиль и тот же тип возвращаемого значения) уже не может стать виртуальной. Но с точки зрения реализации через ТВМ все равно, что первое ограничение виртуальная функция остается виртуально, что второе - невиртуальная функция не может стать виртуальной. Такое ограничение – из идеологических соображений (в других языках таких ограничений нет). Хотя обычно не приходится невиртуальную функцию объявлять как виртуальную, если доступна вся иерархия классов, проще в самом начале объявить нужную функцию виртуальной (можно сказать, что потребность делать невиртуальную функцию виртуальной объясняется плохим проектированием соответствующей иерархии классов).
И в языках Си# и Дельфи этих ограничений нет. Чтобы подменить функцию, надо это явно указать:
1.Cи#: override void f() {...}
2.Delphi: procedure F; override;
Если ключевого слова override не стоит, то это считается, что такие функции закрывают соответствующие виртуальные предыдущие функции, и после этого механизма виртуального вызова не будет. Хотя из производных классов потом можно будет унаследовать виртуальную функцию void f(), сделав ключевое слова override. Функция может быть виртуальной, потом стать невиртуальной, а потом снова может стать виртуальной, если мы это укажем явно. С точки зрения практического программирования это исправление недостатков существующих иерархий классов, которых программист поменять не может. С точки зрения дизайна собственных классов применение таких трюков некрасиво. В остальном механизм динамического связывания работает как в Си++.
Неклассовые языки
(Ада 95 и Оберон-2):
Оберон.
В Обероне (1988 год, Оберон 2 появился окончательно в 1993 году) было наследование (расширение типа), но не было динамического связывания (оно реализовывалось при помощи специальных обработчиков - вместо виртуального метода мы создавали поле функционального типа данных, и при инициализации экземпляра каждого типа мы должны были нужной функцией инициализировать нужное поле)1.
С точки зрения ООП объект класса представляет:
- члены-данные (определяют текущее состояние объекта класса);
- члены-функции (определяют поведение экземпляра объекта класса;)
Во всех настоящих ООЯ: не зависит от конкретного экземпляра этого объекта, а зависит только от типа, поскольку члены-функции, они привязываются к типу соответствующего объекта. В первом Обероне динамическое связывание реализуется полями указателей на функции, и эти поля специфичны для каждого метода (для каждого метода мы должны инициализировать их своими функциями, ничто не мешает для одного экземпляра провести инициализацию одними функциями, а для другого экземпляра другими). Поведение в первом Обероне является не атрибутом класса, а атрибутом отдельного экземпляра этого класса, что не есть хорошо (противоречит общей тенденции). Обратим внимании, что в рассматриваемых нами языках, поведение (набор членов-функций) – задается при описании, и при описании мы говорим – эти методы связаны статически (поведение может быть предсказано в момент компиляции), а эти динамически (поведение зависит от динамического типа объекта). Поведение связано с типом в целом.
SmallTalk. В этом языки динамическое поведение может динамически меняться в зависимости от того, что мы сделали с соответствующим типом. В SmallTalk все методы связаны динамически. Мы говорили, что наличие ТВМ позволяет свести поиск виртуального метода к 6-7 ассемблерным командам, поскольку мы знаем профиль каждого метода и знаем смещение адреса этого метода в ТВМ. В SmallTalk совершенно другая ситуация - там цепное распределение памяти.
Х | Объект базового класса |
У | Объект производного класса |
Z | Ссылка на объект суперкласса |
Видим иерархию их 3-х классов, каждый объект содержит свой указатель на ТВМ.
Замечание: Если для такого яп как Си++ (или Java) для производного класса У ТВМ напоминает сам объект (поскольку при наследовании объект класса У содержит часть для Х и часть для У), то же самое и для ТВМ класса У – она в начале содержит таблицу методов для класса Х (ТВМ Х), а потом функции, которые добавлены в классе У. Если соответствующая функция подменяется в классе У, то меняется соответствующий адрес в ТВМ (ТВМ у нас как бы наследуется).
В SmallTalk:
Х | Ссылка на ТВМ для Х |
У | Ссылка на ТВМ только для добавленных в классе У |
Z | Ссылка на ТВМ только для Z |
Как осуществляется поиск виртуального метода? Когда мы вызываем для Z функцию z.f(), то объекту z посылается сообщение f (в терминологии SmallTalk).
Замечание: В языке Java (или Си#, Си++) мы знаем, что для ТВМ этого класса обязательно где-то должен найтись адрес этого обработчика. Наша задача добраться только до ТВМ этого класса, поскольку они у нас наследуются.
В SmallTalk мы сначала ищем соответствующий обработчик в ТВМ Z, потом если мы там не нашли, то идём к нашему родителю и в его ТВМ ищем эту функцию, если и там нет, то идём к родителю нашего родителя и т.д. (пока не дойдем до корневого класса в иерархии, если не нашли, выдается сообщение об ошибке). Это влечёт большие накладные расходы, но что-то там нам всё-таки даёт. В SmallTalk сделана достаточно хитрая вещь, ТВМ – ее можно пополнять и изменять динамически, поэтому есть возможность осуществлять динамический поиск. Когда мы меняем ТВМ, мы меняем его для всего типа в целом, а вовсе не для экземпляров.
Оберон 2.
Язык Оберон-2 - это то же самое, что и Оберон, но с небольшими изменениями, самое главное из которых – появление процедур динамически привязанных к типу.
В Обероне нет понятия перекрытия операций, т.е. одному имени водной области видимости может соответствовать только одна сущность. Для процедур динамически привязанных к типу это уже не совсем так. Если у нас есть какой-то тип:
TYPE T = RECORD
...
END;
Поскольку можно наследовать любой тип данных:
T1 = RECORD (T1)
...
END;
Синтаксис процедуры динамически привязанной к типу:
PROCEDURE (VAR X:T) Draw(); // Draw – это имя, обратите внимание, что оно находится не сразу после
ключевого слова Procedure
X - аргумент - ссылка или указатель. И точно так же динамическая привязка осуществляется не для объектов типа, а только для ссылок или указателей.
В одной и той же области видимости мы можем описать другую процедуру Draw:
PROCEDURE (VAR X:T1) Draw(); // заметим что здесь должен быть объект Х – унаследованный от Т1
Как вызывается соответствующая вещь? Пусть у нас есть нединамическая процедура:
PROCEDURE P(VAR A:T) //нединамическая
BEGIN
A.Draw();
END;
У процедуры динамически привязанной к типу есть один выделенный аргумент – ссылка на объект. Вызов процедуры динамически привязанной к типу даже синтаксически один в одни соответствует вызову методу в яп, ориентированных на классы: A.Draw(). Поскольку речь идет о ссылке (var – переменная передается по адресу), а ссылка обладает динамическим типом. В зависимости от того, какой динамический тип имеет А, будет вызвана соответствующая Draw().
VAR B:T;
B1:T1;
P(B); (* будет вызван T.Draw *)
P(B1); (* будет вызван T1.Draw *)
Замечание: Очень часто получается, что вызываемый виртуальный метод обращается к своему аналогу из базового класса.2 Для этого нужны специальные языковые средства.
-
В Си++ квалификация имени класса: имя_класса::имя_функции
Этот механизм снимает виртуальность вызова, он четко говорит, что нужно вызвать имя функции из такого-то класса. Такое можно применять только внутри функций самого класса, потому что вначале по умолчанию приписывается this->. Если мы вызываем функцию извне, мы должны явным образом указать соответствующий класс.
P.X::F //Р – сам объект, так и ссылка на объект. Четко указываем, что должны вызвать метод F из класса Х (Х должен совпадать с Р, либо быть базовым).
-
Такая же возможность есть и в Обероне: Draw^ (может появиться только внутри функции динамически связанной с типом, выведенного из типа Т)- вызов Draw для соответствующего базового класса.
Ада 95:
Замечание:
До этого момента мы рассматривали одну технику реализации ДС. Процедуры динамически привязаны к типу; виртуальные методы – это методы, которые вызываются динамически в зависимости от динамического типа соответствующей ссылки или указателя. Всегда речь идет об одном объекте – и виртуальные методы в Си++, и процедуры динамически привязанные в Си# - они все привязаны к одному объекту (это монометоды, которые всегда определяются динамическим типом одного объекта). Этот объект, например, мы и выделяем процедурой Draw().
PROCEDURE (VAR X:T) Draw(); // Х тут есть старый добрый this или self.
Рассмотрим, как реализовано динамическое связывание в Ада95 (никаких this или self нет):
В Аде: СWT(ClanWideType) – классовые типы, Т – тегированная запись. Именно из тегированных записей выводятся унаследуемые типы. При этом любой унаследованный тип по умолчанию является тегированной записью.
T - typed record
...
end record;
Type T1 is new T
with record ... //новые поля или ключевое слово no record – все это указывает, что наследование в ОО
смысле
end record
Классовый тип для такого Т: T ' class
Вспомним концепцию неограниченных типов данных, неограниченные тд в Ада они могут содержат потенциально бесконечное количество членов (пример: неограниченный массив с неограниченным диапазоном). T ' class – тоже является неограниченным типом, а именно множество значений типа T ' class - все объекты типа Т + все производные. Потенциально такое множество не ограниченное - в любой момент мы можем добавить новые производные объекты.
PROCEDURE P(A:T) is // Т – тегированная запись
begin
f(A);
end P;
-
Пусть у нас есть типы Т (базовый) и Т1 (производный от него) и пусть у нас есть некоторая процедура F:
PROCEDURE F(X:T);
И ещё один её вариант (имеем право):
PROCEDURE F(X:T1);
-
Статическое перекрытие методов (компилятор знает тип и вызывает нужную функцию):
Y:T;
Y1:T1;
F(Y); //имеем право
F(Y1); // имеем право вызывать
-
Рассмотрим другой вызов:
P(Y); // f() for T
P(Y1); // f() for T – компилятор, когда генерирует код по вызову f(), он ничего не знает о том, как будет вызываться эта процедура Р. Даже если Т - тегированная запись, ничего особого не происходит.
2) Пусть теперь:
PROCEDURE P(A:T ' class) is //это меняет ситуацию
begin
f(A); //будет вызываться в зависимости от конкретного динамического типа А
end P;
Характеристики
Тип файла документ
Документы такого типа открываются такими программами, как Microsoft Office Word на компьютерах Windows, Apple Pages на компьютерах Mac, Open Office - бесплатная альтернатива на различных платформах, в том числе Linux. Наиболее простым и современным решением будут Google документы, так как открываются онлайн без скачивания прямо в браузере на любой платформе. Существуют российские качественные аналоги, например от Яндекса.
Будьте внимательны на мобильных устройствах, так как там используются упрощённый функционал даже в официальном приложении от Microsoft, поэтому для просмотра скачивайте PDF-версию. А если нужно редактировать файл, то используйте оригинальный файл.
Файлы такого типа обычно разбиты на страницы, а текст может быть форматированным (жирный, курсив, выбор шрифта, таблицы и т.п.), а также в него можно добавлять изображения. Формат идеально подходит для рефератов, докладов и РПЗ курсовых проектов, которые необходимо распечатать. Кстати перед печатью также сохраняйте файл в PDF, так как принтер может начудить со шрифтами.