Популярные услуги

Все письменные КМ под ключ за 3 суток! (КМ-6 + КМ-7 + КМ-8 + КМ-9 + КМ-10)
КМ-6. Динамические массивы. Семинар - выполню любой вариант!
КМ-2. Разработка простейших консольных программ с использованием ООП + КМ-4. Более сложные элементы ООП - под ключ!
Любая задача на C/C++
Одно любое задание в mYsql
Сделаю ваше задание: Лабораторная работа на Pascal / Lazarus
Любой тест по базам данных максимально быстро на хорошую оценку - или верну деньги!
Любой реферат по объектно-ориентированному программированию (ООП)
Повышение уникальности твоей работе
Оба семинара по программированию под ключ! КМ-2. Разработка циклических алгоритмов + КМ-3. Функции и многофайловые программы в Си

Лекция 19

2021-03-09СтудИзба

Лекция № 19

  Динамически связанные методы нужны прежде всего для того, чтобы можно было гибко проектировать различного рода библиотеки. Под гибкостью при этом понимается то, что соответствующая библиотека является легко расширяемой. Пример: отрисовка фигур.

                          Figure

   Point               Circle          Rect

Пусть у нас есть некоторые объекты, общее название для которых, а стало быть и название базового класса - Figure. Известно, что каждая фигура умеет сама себя отрисовывать на экране, т.е. обладает методом

   void Draw( );

Каждая фигура умеет себя двигать, т.е. обладает методом

   void Move(int dx, int dy);

Каким образом фигура отрисовывается на экране по абстрактной фигуре ничего сказать нельзя. У каждой фигуры есть координаты точки привязки

Рекомендуемые материалы

   int X, Y;

Для точки – это координаты самой точки. Для окружности – это координаты центра. Для прямоугольника – например, координаты левой верхней вершины. Для каждой конкретной фигуры мы просто переопределяем метод Draw. При этом метод Draw – это метод, который, естественно, является динамически связанным. Если мы объявим в классе Figure метод Draw виртуальным, и потом для каждой фигуры переопределим соответствующий виртуальный метод Draw, то если, например, у нас есть некоторый массив фигур

   Figure * a[];

для того, чтобы отрисовать весь массив

   for(int i=0; i<N; i++)

    a[i]->Draw();

Не смотря на то, что каждый объект массива – это просто указатель на какую-то абстрактную фигуру, за счет динамического связывания этот код будет работать. Гибкость с точки зрения развития заключается в том, что этот код написан в предположении, что у нас есть классы Point, Circle, Rect. Если мы выведем еще какой-то тип данных (например FillRect – заполненный прямоугольник).

                          Figure

   Point               Circle            Rect

 

                                              FillRect

Для него переопределим метод Draw, чтобы он рисовал заполненный прямоугольник. Код, работающий с массивом от того, что мы добавили новый тип данных, никак не изменится. Поэтому во многих случаях не требуется перетрансляция. На языках, в которых нет объектно-ориентированного программирования и динамического связывания, это реализовывалось бы с помощью записи с вариантами. И код, который бы выполнял соответствующую отрисовку, выглядел бы следующим образом

   case …

    when Circle => Draw

    when … => …

    when … => …

Для каждого нового типа мы должны будем добавлять новый when. Это чревато тем, что при появлении нового типа данных (например, новой фигуры) мы должны перелопатить весь проект, отловить там подобного рода места, и сделать в них соответствующую модификацию. В случае объектно-ориентированного программирования с динамическим связыванием этого делать не нужно. Поэтому как программисты, так и менеджеры, начиная с конца 80-ых г.г. с удовольствием ухватились за возможности, которые предоставляют соответствующие языки.

  В языке С++ методы делятся на виртуальные и не виртуальные. Если функция описана в каком-то базовом классе как виртуальная

   virtual void f( ){…}

то любое ее переопределение (замещение – override) в базовом классе с тем же прототипом обязательно является виртуальной функцией. Т.е. в С++ сделать ее не виртуальной нельзя.

  В языке Java все методы по определению являются виртуальными, поэтому ключевого слова virtual там нет. Все методы в языке Java имеют только динамическое связывание.

  Языки Delphi и C# пошли примерно по пути языка С++, но с некоторыми модификациями. И в Delphi, и в C# методы делятся на виртуальные и не виртуальные. Таким образом в этих языках есть ключевое слово virtual. Но и в Delphi, и в C# нет такого правила, что функция с тем же самым прототипом, которая замещает виртуальную функцию, не обязательно будет замещать ее в производном классе. В случае, если мы хотим, чтобы было динамическое замещение, то в языках C# и Delphi надо писать ключевое слово override. В случае, когда такое объявление встречается в языке С#, то если даже мы напишем

   virtual void f( ){…};

в производном классе, то компилятор выдаст предупреждение потому, что, не смотря на то, что  она объявлена виртуальной, мы явно не сказали, что она замещает соответствующую функцию в базовом классе. Предупреждение не будет выдано, если мы явно напишем override (тогда ключевое слово virtual писать не обязательно), либо если мы напишем new. new говорит о том, что мы не хотим замещать эту функцию. Т.е. начиная с этого нового класса в иерархии f приобретает совершенно другой смысл. Это немного более гибкий механизм. Если мы продумываем реализацию иерархии классов, то подход языка С++ более строг. И в Delphi, и в C# от этого более строгого подхода отказались. Если нас не устраивает иерархия базовых классов в языке С++, единственная возможность – переделать реализацию этих классов. В языке C# можно отказаться от наследия базовых классов и внести свою собственную семантику в соответствующую операцию. Прежде всего это связано с тем, что если поставляется нам какая-то библиотека, даже если она поставляется в исходных текстах, теоретически мы не имеем права менять соответствующие функции в библиотеке. То же самое, если у нас есть NET.Framework, мы не имеем права ничего в ней менять. Но, если нас не устраивает функциональность NET.Framework, мы можем переопределить соответствующие функции.

  Когда мы говорим о динамическом связывание, речь идет исключительно о способе вызова. Т.е. сами по себе реализации функций (тела функций) и в виртуальном, и в не виртуальном случае представляют собой одно и то же. Речь идет исключительно о механизме виртуального вызова. В языке С++, если у нас есть класс Х и в нем виртуальная функция f , объект класса Х

   Х х;

то

   x:f( );

 - это не виртуальный вызов. А вызов через указатель, или, соответственно, через ссылку

   X * px;      X & xx;

   px->f( );    xx.f( );

виртуальные. Т.е. динамический тип в языке С++ имеется только у указателей и ссылок. В Java, C# и Delphi объявление

   Х х;

вводит не сам объект, а ссылку, и инициализируется это следующим образом

   X x = new X( );

то, естественно, что вызовы вида

   x:f( );

по определению все будут динамическими. Тем не менее, иногда динамический вызов можно снимать. В С++ виртуальность вызова можно снимать в функциях - членах того же самого класса. Это совершенно обычная ситуация потому, что довольно часто механизм реализации виртуальных функций делается следующим образом. Иногда мы полностью подменяем. Есть класс Х и в нем виртуальная функция f. Из него выведен класс Y и в нем есть виртуальная функция f. Мы можем либо полностью переписать функцию f, либо, в некоторых случаях, мы можем использовать и функциональность базовых функций, но при этом добавить в них что-нибудь еще. Например, тот же самый метод Draw может реализовываться различным образом. Если у нас есть базовый тип Rect и в нем есть метод Draw. Мы из него выводим FillRect, и, например, (одна из возможных реализаций) может быть следующая: вызвать метод Draw для Rect, который просто обрисует рамочку вокруг прямоугольника, а затем вызвать какой-то метод, который заполняет каким-то шаблоном внутренность этого прямоугольника. Нам необходимо добавить некоторую функциональность в метод Draw, но при этом мы используем и старый метод. Для этого и служит механизм снятия виртуального вызова. Он в языке С++ применяется только к функциям – членам самого этого класса. Т.е. если мы хотим вызвать в методе Draw для FillRect метод Draw для типа данных Rect просто пишем

   Rect::Draw( );

Такая конструкция снимает виртуальность. Такого рода конструкцию нельзя применять извне функций – членов.

  В C# и Java в принципе тоже можно снимать виртуальный вызов, но в некоторых частных случаях. В языке Java при описании метода можно задать ключевое слово final:

   final void f( ){…}

Поскольку в языке Java все методы связаны только динамически, при любом переопределении метода речь идет именно о его замещении. Ключевое слово final говорит о том, что этот метод в производных классах нельзя замещать. В языке С# есть аналогичное по смыслу ключевое слово sealed (запечатанный). Это означает, что любая попытка переопределить в производном классе соответствующий метод приводит к выдаче ошибки компиляции. Т.е. компилятор не позволит переопределить соответствующий метод. final и sealed нужны для двух вещей: во-первых, из соображений безопасности. Например, если у нас есть какой-то механизм проверки, то мы можем в произвольном классе его заместить и ввести свой, и таким образом обойти тот алгоритм, который нам задан с самого начала. Если создателю класса это не по нутру, он может запечатать или закрыть соответствующий метод для расширения, и тогда волей-неволей мы можем использовать только его реализацию. Кроме этого есть еще второй момент. Как мы увидим чуть позже, динамическое связывание сводится к косвенной ссылке. Страуструп приводит для реализации языка С++ такую цифру: порядка 8 "лишних" ассемблерных операций с точки зрения машинного кода добавляется при виртуальном вызове (при динамическом связывании). Т.е. в общем случае динамическое связывание влечет за собой определенного рода расходы. В принципе возможны случаи, если компилятор видит, что в классе Х метод f является финальным или запечатанным, и если есть определение

   X x = …;

И вызов

   x.f( );

То компилятор как в С#, так и в Java имеет право вставлять здесь не виртуальный вызов. Потому, что, т.к. f является финальным методом или соответственно запечатанным, поскольку речь идет об объекте класса Х, ясно, что в данном случае вызывается именно эта функция. final говорит о том, что в классах, производных от Х, функции f не может быть. Поэтому, даже если динамический тип х является больше, чем Х, все равно f будет браться из класса Х. Поэтому компилятор вполне может в данном случае выполнить вызов

   X.f( );

Это некоторый вариант оптимизации. Все доступные реализации Java и C# используют эту оптимизацию. Т.е. механизм виртуальности иногда можно снимать, в тех случаях, когда это оказывается выгодным.

  Еще есть некоторые тонкости, связанные с замещением. Например, и в языке Java, и в языке C++ рассмотрим такую ситуацию:

   class X{

    virtual void f(int);

    …

   };

   class Y: public X{

    virtual void f(string &);

   };

Класс Y не замещает функцию f. И в языке С++, и в языке Java такая ситуация является допустимой потому, что в данном случае не будет замещения, а будет перекрытие. У нас есть объект у.

   Y y;  String s;

В данном случае, не смотря на то, что речь идет о двух вложенных областях действия, вполне можно вызывать и

   y.f(1);

и

   y.f(s);

И это не будет являться ошибкой.

  В случае языка C# ситуация следующая: замещение в языке C# идет не по сигнатурам (сигнатура – это прототип), а по именам. Т.е. в случае, если функции являются виртуальными, и здесь имеет место механизм замещения, то в языке C# в рассмотренном случае будет выдана ошибка. Т.е., если мы переопределяем функцию f даже с другой сигнатурой, то новая функция f скрывает функцию f из базового класса. Механизм скрытия. И у той, и у другой точки зрения есть свои противники, и есть свои защитники. В любом случае (даже в случае языка C# можно получить доступ к старой функции f за счет механизма преобразования типа. Т.е., если для объекта у нам хочется вызвать функцию y.f(1), нам нужно преобразовать ссылку на производный класс в ссылку на базовый класс

   ((X)y).f(1);

В C# взяли синтаксис приведения типов С.

          Методы реализация динамического связывания в различных ЯП.

  Оно реализовано достаточно простым способом. Рассмотрим прототип реализации для языка С++, который, в принципе, используется и в других языках программирования. Для каждого типа, в котором есть виртуальный метод, создается так называемая таблица виртуальных методов (ТВМ). Она одна (!) для каждого типа (не для экземпляра). В объект, котором содержит виртуальный метод, добавляется одно невидимое для программиста поле – ссылка на соответствующую ТВМ. Виртуальные методы перечислены в описании типа в каком-то порядке. ТВМ состоит из указателей на функции – на реализации соответствующих виртуальных функций.

   class X{

    virtual void f( );

    virtual void g( );

   }

В этом классе 2 виртуальных функции. ТВМ для Х

px


             f    X::f

             g   X::g 

                 

                  

Пусть есть класс Y, выведенный из Х, в котором мы переопределяем функцию f, и класс Z, выведенный из Y, в котором мы переопределяем функцию g. Значит Y наследует g от Х, а класс Z наследует f от Y. В классе Y также определяется виртуальная функция h, которая переопределяется в Z. ТВМ для Y

   f   Y::f

   g  X::g

   h  Y::h

                   

Т.к. класс Y наследуется от Х, то начало ТВМ для Y будет таким же как и для Х, только f переопределяется. ТВМ для класса Z


  f    Y::f

  g   Z::g

  h   Z::h

                

Вызов. У нас есть указатель

   px->f( );

Поскольку в каждом объекте класса Х имеется указатель на таблицу виртуальных имен, он, разумеется, наследуется. Считая от начала класса ссылка на ТВМ всегда имеет  фиксированное смещение для всех объектов. Поэтому компилятор знает, что под фиксированным смещением стоит ссылка на ТВМ, и какой вид имеет ТВМ для класса Х: по нулевому смещению в ней стоит адрес f. Задача компилятора косвенно добраться до таблицы виртуальных имен, взять из нулевого смещения адрес f и косвенным образом вызвать f. Понятно, откуда берутся накладные расходы: мы должны по этому указателю получить адрес соответствующей функции и сделать косвенный вызов. Что особенно приятно, эти накладные расходы фиксированы для любого типа данных, в отличии от языка Smalltalk. Если во всех языках, которые мы рассматриваем, мы объявляем динамику связывания метода при трансляции, равно как и его сигнатуру, то в языке Smalltalk таблица методов может пополняться динамически. Память в языке Smalltalk распределяется не линейным образом, а цепным. Если у нас есть некоторая иерархия

Object

Х            

               

                 

                                         

                  Y                    

                                      

                                

      

                                   Z                

                                                 

Если ссылка указывает на объект класса Z и есть вызов какого-то объекта, то поиск метода производится следующим образом: динамически в таблице описывается существует реализация такого метода или нет. Если не существует мы идем на суперкласс Y, если нет в таблице методов для этого класса, то идем на класс Х и т.д. до тех пор, пока мы не дойдем до типа данных Object, который является вершиной соответствующей иерархии. Если и в Object нет реализации этого метода, тогда динамически выдается сообщение об ошибке. Т.е. компилятор языка Smalltalk, во-первых, в принципе не может проверить есть такой метод или нет, поскольку речь идет о чистой динамике. Чем длинней иерархия классов, тем больше может занять поиск соответствующего метода. С практической точки зрения подобного рода реализация является очень гибкой. Но для реальных промышленных приложений, где требуется эффективность, такой метод реализации не является приемлемым. В С++ и всех языках, которые на нем основаны, ест небольшие накладные расходы (порядка 6 – 9 ассемблерных команд), но они во многих случаях являются вполне приемлемыми. Они никак не зависят от глубины иерархии. Правда есть еще  один интересный побочный эффект. ТВМ создается одна для всех экземпляров класса. Вроде бы это не большие расходы по памяти – для каждого типа данных своя ТВМ. В некоторых случаях такой расход является слишком накладным. Рассмотрим типичный пример: система графического многооконного интерфейса пользователя. Типичная для этого случая иерархия классов

                            CWindow

CMenu  CFrameWind  CDlgWindow  CViewWindow

CFrameWind  - главное окно. Типичное приложение – это Frame Window, внутри которого всавлено View Window и иногда может возникать Dialog Window и т.д.  – по одному экземпляру каждого класса. Все нормальные системы интерфейса пользователя являются событийно-ориентированными. Для каждого вида событий должна быть задана в базовом классе функция – обработчик этого события. В базовой версии Windows было 144 события. Если реализовывать такой механизм обработки событий с классической объектно-ориентированной точки зрения, то каждая таблица виртуальных методов должна иметь по меньшей мере 144 метода (реакции на данные события). 144 метода для каждого типа окон и для базового типа. Причем у нас всего по одному экземпляру каждого, и с каждым экземпляром, в результате, у нас связана своя ТВМ. Т.е. накладные расходы по памяти на ТВМ становятся больше, чем на объекты самого класса. Это не пример реальной иерархии. Реальные иерархии классов, например, в MFC, значительно длиннее, и как правило по одному  - два объекта заводится для каждого класса такой иерархии. Накладные расходы на ТВМ становятся слишком большими. Отсюда, как правило, событийно-ориентированные интерфейсы с огромным количеством событий, где каждый объект в иерархии должен реагировать на каждое возможное событие, реализуются с помощью механизма динамической диспетчеризации, когда заводится некоторая табличка (она как правило является хеш-таблицей), в которую включаются только те указатели, которые реально есть. Если CWindow умеет отвечать только на 10 из этих событий, а остальные просто игнорирует, то в соответствующей таблице будет 10 указателей. В свое время фирма Borland стала расширять реализацию языка С++, чтобы допускать подобного рода вещи. И в языке Delphi, и в языке Borland C++ появились так называемые динамические методы, которые были реализованы через динамическую диспетчеризацию. А фирма Microsoft пошла по другому пути: она стала создавать так называемые таблицы сообщений с помощью макросов, оставаясь в рамках стандартной реализации языка С++. Достаточно много менеджеров программных проектов не захотели принимать идеологию Borland потому, что Borland специфическим образом расширял язык С++.

                                                          Оберон.

  В первой версии языка Оберон, которая вышла в 1988 г. динамического связывания не было в принципе. Через год или два появилась реализация языка Оберон-2, в котором динамическое связывание уже было. (Оберон-2 – расширение языка Оберон динамическим связыванием.) Оберон-2 вводит понятие процедур динамически привязанных к типу.

   TYPE T = RECORD

                      …

                     END

Обычная процедура выглядит так

    PROCEDURE P(VAR X:T);

Никакой динамической привязки здесь нет.

   PROCEDURE (VAR X:T) P( );

Это означает, что, во-первых, процедуру Р можно переопределять, если ее динамическим параметром будет производный от Т класс. Т.е. если мы выведем новый класс Т1

   TYPE T1 = RECORD(T)

                        …

                       END

то для этого класса Т1 мы имеем право написать свою реализацию процедуры Р

   PROCEDURE (VAR X:T1) P( );

  В языке Оберон любое перекрытие, как и замещение имен запрещено. Но именно для динамически связанных процедур и функций это допустимо. От того, какой будет динамический тип соответствующего аргумента, будет вызываться нужная процедура Р. Если есть какая-то другая процедура

   PROCEDURE F(VAR X: T);

    …

   X.P( );

Вызов очень похож на вызов методов из ООП.

   VAR A: T;

            B:T1;

Компилятор, когда транслирует функцию F, не знает какого реально динамического типа будет Х (Х передается по ссылке). При вызове F(A) будет вызываться Р для типа Т, при вызове  F(B) будет вызываться Р для типа Т1. Естественно, это зависит от того каким будет фактический параметр. Не зная фактического параметра, оттранслировать вызов не можем, компилятор вставляет соответствующий динамический вызов. Даже по синтаксису они очень напоминают виртуальные методы в таких языках, как С++, C#, Java, Delphi.

                    Динамическое связывание методов в языке Ада 95.

   Необходимо было, чтобы новая система типов, т.е. объектно-ориентированное расширение языка, включалась бы в язык мягким образом. Т.е. программы на языке Ада 83 одновременно должны быть программами на языке Ада 95. Достаточно сложная задача, и создатели Ада 95 решили ее достаточно адекватно.

   tagged record …end

Такие записи можно расширять.

   type T1 is new T with record … end;

Т обязан быть помеченной записью или наследником помеченной записи. Само наследование без динамического связывания практически не работает. Поскольку без динамического связывания с тем же успехом можно было бы говорить об отношении включения. В языке Ада введено понятие class-wide types (cw-типы или типы класса). Вводится обобщение понятия типа данных. Если Т – расширяемый класс (т.е. либо это помеченная запись, либо запись, унаследованная от помеченной), то тогда вводится понятие

   T'class

class – новое ключевое слово (программы на языке Ада 83, которые использовали одноименные идентификаторы стали непроходимыми, нужно один раз перекомпилировать). Это чем-то похоже на понятие неограниченных типов в Аде (например, массив, у которого не фиксировался диапазон, записи с вариантами). Переменные таких типов имели смысл только как формальные параметры процедур. Объектами этого типа могут являться переменные типа Т + переменные всех типов, которые могут быть выведены из Т.

   X:T'class;

Х – переменная, которой можно присваивать любой объект типа Т + любой объект типа производного от Т. С точки зрения реализации Х выступает как ссылка на объект типа Т. Если у нас есть какая-то иерархия

   Т => T1 => T2

   A: T;  B: T1;  C: T2;

Можно производить присваивания

   X:= A;  X:= B;  X:= C;

Ссылки могут иметь динамический тип. Представим, что у нас есть пакет М, в котором определяется тип данных Т и некоторая процедура

   procedure P(X: T);

Пусть в пакете М1 из типа Т выводится тип Т1 и

   procedure P(X: T1);

(это можно было делать и в Ада 83). Пусть в пакете М2 из типа данных Т1 выводится тип данных Т2 и переопределяется процедура

   procedure P(X: T2);

В других языках (в Обероне, в C#, в Delphi специально помечаются методы, которые могут быть динамическими. Рассмотрим

   PROCEDURE F1(X: in out T);

   PROCEDURE F2(X: T'class);

В процедурах F1 и F2 вызывается процедура

    P(X);

Это все вполне допустимо. В случае вызова P(X) в процедуре F1 никакой динамической привязки. Для всех случаев

   F1(A);  F1(B);  F1(C);

поскольку речь идет о типе данных Т, то будет вызываться процедура Р для Т (те. Р из модуля М) потому, что компилятор в этом месте просто вставит статическую привязку. Для F2 ситуация другая.

   F2(A);  F2(B);  F2(C);

Здесь будет вызываться Р(Х) в зависимости от динамического типа. В случае F2(А) будет вызвана процедура Р для типа Т, в случае F2(B) – процедура Р для типа Т1, в случае F2(C) – процедура Р для типа Т2. Компилятор, видя, что Х – объект классового типа, передаст соответствующую динамическую ссылку, и вызов Р(х) в F2 будет динамически привязан. Т.е. одну и ту же процедуру можно вызывать как статически привязанной, так и динамически привязанной – все зависит от параметра.

  Это выглядит очень схожим, но на самом деле такой механизм привязки оказывается несколько богаче, чем может показаться на первый взгляд. Для всех методов, которые мы писали для С++ и прочих языков, аналогичные вещи можно сделать и для языка Ада. Но язык Ада (Ада 95) позволяет еще некоторое расширение, которое не позволяют все те языки, о которых мы говорили. Это так называемые мультиметоды. Фактически мультиметоды реализованы как понятие только вв языке Ада 95. В С++ Страуструп в своей книге обсуждал потребность в мультиметодах, но тем не менее мультиметоды в языке С++ не реализованы. Слово мультиметоды не имеет никакого отношения к множественному наследованию. Мультиметод – это динамически связанный метод, но который вызывается в зависимости от динамического типа нескольких объектов. До этого динамическая привязка всегда шла по одному типу объекта. В классовых языках метод класса вызывается в зависимости от динамического типа соответствующего объекта х

   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 от двух параметров уже в зависимости от их конкретных типов. Потому что, скажем, для Point мы не можем реализовать одну процедуру 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, p2

                  X

                  Y

                  Z


   p1->f( );

Должна быть вызвана функция 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

p1

p2       X

           Y

           Z    

               

потому, что компилятор, программируя функцию, знает, что у него есть указатель with, и если

   class Y{ int a,b}

то компилятор знает, что по нулевому смещению ссылка на таблицу виртуальных методов, по следующему смещению а, по следующему смещению b. Если функция g не переопределена в классе Z, и ей передается указатель на X, то она начнет работать с объектом 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;

    …

   }

Лучше всего было бы ввести какое-то ключевое слово. Но в 86-м г. Страуструп очень боялся новых ключевых слов, чтобы оказаться как можно ближе к языку С. Им был выбран такой синтаксис. От чистой виртуальной функции, во-первых, не требуется никакой ссылки, во-вторых, абстрактный класс – это класс, в котором есть хотя бы одна чистая виртуальная функция. Компилятор следит за тем, чтобы объекты абстрактных классов не могли создаваться в программе. Т.е. мы не можем объявить

   Figure C;

Но мы можем написать так

   Figure * pf;

потому, что реально объект типа Figure в памяти не размещается. Но мы не имеем права писать

   pf = new Figure( );

В этом месте компилятор тоже выдаст ошибку:"Инициализируется объект абстрактного класса." Мы можем писать так

   pf = new Line( );

Здесь заводится объект конкретного типа. Абстрактные классы – это классы, которые нужны только как вершины в некоторой иерархии. Они нужны для того, чтобы из них потом вывести конкретные классы. Как правило, контейнерные процедуры обработки имеют дело с указателями ил ссылками на этот базовый абстрактный класс, но с точки зрения реализации все эти указатели и ссылки указывают на объекты производных классов. Чистые виртуальные функции могут тела не иметь. Но может быть ситуация, когда чистая виртуальная функция все-таки будет вызвана. На первый взгляд это не возможно потому, что если она будет выдана, то динамический тип соответствующего указателя должен совпадать с типом Figure. А это вроде бы не возможно потому, что статический тип Figure компилятор запрещает. Тем не менее существует случай, достаточно редкий, но не приятный, когда мы можем вызвать эту чистую виртуальную функцию.

   class X{…

Вам также может быть полезна лекция "4 Заливка областей".

    virtual void f( ) = 0;

    X( ){… f( ); …}

   }

В конструкторе класса вызывается виртуальная функция. Если бы метод не был конструктором f должна была бы вызываться исходя из динамического типа. Если внутри вызова конструктора есть вызов виртуальной функции самого объекта, компилятор всегда вставляет статический вызов (конструируется объект класса Х, и, очевидно, его тип должен совпадать с классом Х => компилятор вставит X::f( )). Выводится класс Y. В нем есть свой конструктор Y( ), который вызывает конструктор класса Х. В объекте Y тоже есть виртуальная функция f.

  Поэтому на самом деле в ТВМ чистым виртуальным функциям соответствуют не нули, а стоят ссылки на некоторую функцию, типа метода Abstract из Delphi. Эта функция программируется, грубо говоря, автором компилятора. Поэтому, не смотря на то, что функция объявлена как чистая виртуальная, программист теоретически имеет право вставить ее тело. Хотя вызов чистой виртуальной функции – это чаще всего ошибка.

  Абстрактный класс, у которого открытым является только функциональный интерфейс, является абстрактным типом данных. Но в то же время есть примеры АТД, не являющихся абстрактным классом, и абстрактных классов, не являющихся АТД.

Свежие статьи
Популярно сейчас
Почему делать на заказ в разы дороже, чем купить готовую учебную работу на СтудИзбе? Наши учебные работы продаются каждый год, тогда как большинство заказов выполняются с нуля. Найдите подходящий учебный материал на СтудИзбе!
Ответы на популярные вопросы
Да! Наши авторы собирают и выкладывают те работы, которые сдаются в Вашем учебном заведении ежегодно и уже проверены преподавателями.
Да! У нас любой человек может выложить любую учебную работу и зарабатывать на её продажах! Но каждый учебный материал публикуется только после тщательной проверки администрацией.
Вернём деньги! А если быть более точными, то автору даётся немного времени на исправление, а если не исправит или выйдет время, то вернём деньги в полном объёме!
Да! На равне с готовыми студенческими работами у нас продаются услуги. Цены на услуги видны сразу, то есть Вам нужно только указать параметры и сразу можно оплачивать.
Отзывы студентов
Ставлю 10/10
Все нравится, очень удобный сайт, помогает в учебе. Кроме этого, можно заработать самому, выставляя готовые учебные материалы на продажу здесь. Рейтинги и отзывы на преподавателей очень помогают сориентироваться в начале нового семестра. Спасибо за такую функцию. Ставлю максимальную оценку.
Лучшая платформа для успешной сдачи сессии
Познакомился со СтудИзбой благодаря своему другу, очень нравится интерфейс, количество доступных файлов, цена, в общем, все прекрасно. Даже сам продаю какие-то свои работы.
Студизба ван лав ❤
Очень офигенный сайт для студентов. Много полезных учебных материалов. Пользуюсь студизбой с октября 2021 года. Серьёзных нареканий нет. Хотелось бы, что бы ввели подписочную модель и сделали материалы дешевле 300 рублей в рамках подписки бесплатными.
Отличный сайт
Лично меня всё устраивает - и покупка, и продажа; и цены, и возможность предпросмотра куска файла, и обилие бесплатных файлов (в подборках по авторам, читай, ВУЗам и факультетам). Есть определённые баги, но всё решаемо, да и администраторы реагируют в течение суток.
Маленький отзыв о большом помощнике!
Студизба спасает в те моменты, когда сроки горят, а работ накопилось достаточно. Довольно удобный сайт с простой навигацией и огромным количеством материалов.
Студ. Изба как крупнейший сборник работ для студентов
Тут дофига бывает всего полезного. Печально, что бывают предметы по которым даже одного бесплатного решения нет, но это скорее вопрос к студентам. В остальном всё здорово.
Спасательный островок
Если уже не успеваешь разобраться или застрял на каком-то задание поможет тебе быстро и недорого решить твою проблему.
Всё и так отлично
Всё очень удобно. Особенно круто, что есть система бонусов и можно выводить остатки денег. Очень много качественных бесплатных файлов.
Отзыв о системе "Студизба"
Отличная платформа для распространения работ, востребованных студентами. Хорошо налаженная и качественная работа сайта, огромная база заданий и аудитория.
Отличный помощник
Отличный сайт с кучей полезных файлов, позволяющий найти много методичек / учебников / отзывов о вузах и преподователях.
Отлично помогает студентам в любой момент для решения трудных и незамедлительных задач
Хотелось бы больше конкретной информации о преподавателях. А так в принципе хороший сайт, всегда им пользуюсь и ни разу не было желания прекратить. Хороший сайт для помощи студентам, удобный и приятный интерфейс. Из недостатков можно выделить только отсутствия небольшого количества файлов.
Спасибо за шикарный сайт
Великолепный сайт на котором студент за не большие деньги может найти помощь с дз, проектами курсовыми, лабораторными, а также узнать отзывы на преподавателей и бесплатно скачать пособия.
Популярные преподаватели
Добавляйте материалы
и зарабатывайте!
Продажи идут автоматически
5137
Авторов
на СтудИзбе
440
Средний доход
с одного платного файла
Обучение Подробнее