Что такое позднее связывание. Раннее и позднее связывание

30.10.2019 Сотовые операторы

«Позднее связывание» – это один из таких же терминов компьютерных наук, что «строгая типизация», который означает разные вещи для разных людей. Я думаю, что смогу описать, что я под ним понимаю.

Прежде всего, что такое «связывание»? Мы не сможем понять, что означает позднее связывание, если мы не знаем, что вообще означает термин «связывание».

По определению, компилятор – это такое устройство, которое принимает текст, написанный на одном языке, и выдает код на другом языке, «который означает то же самое». Я, например, разрабатываю компилятор, который принимает на вход текст на языке C# и выдает CIL (*). Все важные задачи, выполняемые компилятором можно разделить на три крупные группы:

  • Синтаксический анализ входного текста
  • Семантический анализ синтаксиса
  • Генерация выходного текста – в этой статье этот этап нам не интересен

Синтаксический анализ входного текста ничего не знает о значении анализируемого текста; синтаксический анализ беспокоится, прежде всего, о лексической структуре программы (т.е. о границах комментариев, идентификаторах, операторах и т.п.), а затем по этой лексической структуре определяется грамматическая структура программы: границы классов, методов, операторов, выражений и т.п.

Затем семантический анализатор берет результаты синтаксического анализатора и связывает значения различных синтаксических элементов. Например, когда вы пишите:

class X {}
class B {}
class D: B
{
public static void X() { }
public static void Y() { X(); }
}

то синтаксический анализатор определяет наличие трех классов, что один из них содержит два метода, второй метод содержит оператор, который является выражением вызова метода. Семантический анализатор определяет, что X в выражении X(); ссылается на метод D.X(), а не, скажем, на тип X, объявленный выше. Это и есть пример «связывания» в наиболее широком смысле этого слова: связывание – это ассоциация синтаксического элемента, содержащего имя метода, с логической частью программы .

Когда речь заходит о «раннем» или «позднем» «связывании», то речь всегда идет об определении имени для вызова метода. Однако, с моей точки зрения это определение слишком строгое. Я буду использовать термин «связывание» при описании процесса определения семантическим анализатором компилятора, что класс D наследует класс B и что имя «B» связано с именем класса.

Более того, я буду использовать термин «связывание» для описания и других видов анализа. Если у вас в программе есть выражение 1 * 2 + 1.0, тогда я могу сказать, что оператор «+» связан со встроенным оператором, который принимает два числа с плавающей запятой, складывает их и возвращает третье число. Обычно люди не думают о связи имени «+» с определенным методом, но я, все же, считаю это «связыванием».

Говоря еще менее строго, я могу использовать термин «связывание» для нахождения ассоциации типов с выражениями, которые не используют имя этого типа напрямую. Если говорить неформально, то в приведенном выше примере выражение 1 * 2 «связано» с типом int, хотя, очевидно, имя этого типа в нем не указано. Синтаксическое выражение строго связано с этим семантическим элементом, хотя и не использует соответствующее имя напрямую.

Так что, говоря в общем случае, я бы сказал, что «связывание» – это любая ассоциация некоторого фрагмента синтаксического дерева с некоторым логическим элементом программы. (**)

Тогда в чем разница между «ранним» и «поздним» связыванием? Люди часто говорят об этих понятиях так, будто это взаимоисключающий выбор: связывание либо раннее, либо позднее. Как мы вскоре увидим, это не так; некоторые виды связывания полностью ранние, некоторые частично ранние и частично – поздние, а некоторые – и правда, полностью поздние. Но прежде чем переходить к этому, давайте рассмотрим, по отношению к чему связывание бывает ранним или поздним?

Обычно, когда мы говорим о «раннем связывании» мы имеем ввиду «связывание, выполняемое компилятором и результат связывания «зашивается» в сгенерированный код»; если связывание завершается неудачно, то программа не запускается, поскольку компилятор не может перейти к фазе генерации кода. Под «поздним связыванием» мы подразумеваем, что «некоторая часть связывания будет выполняться во время выполнения» и, таким образом, ошибки связывания проявятся только во время выполнения. Раннее и позднее связывание иногда называют «статическим» и «динамическим связыванием»; статическое связывание выполняется на основе «статической» информации, известной компилятору, а динамическое связывание выполняется на основе «динамической» информации, известной только во время выполнения.

Какое из этих видов связывания лучше? Очевидно, что ни один из вариантов не является однозначно лучше другого; если бы один из вариантов всегда превосходил другой, то мы бы с вами ничего сейчас не обсуждали. Преимущество раннего связывания в том, что мы можем быть уверены в отсутствии ошибок времени выполнения; недостатком же является отсутствие гибкости позднего связывания. Раннее связывание предполагает, что вся информация, необходимая для принятия правильного решения будет известна перед выполнением программы; но иногда эта информация не доступна до момента выполнения.

Я уже говорил, что связывание образует спектр от раннего до позднего. Давайте рассмотрим некоторые примеры на языке C#, которые покажут, как мы можем перейти от раннего связывания к позднему.

Мы начали с примера вызова статического метода Х. Этот анализ однозначно является ранним. Нет никакого сомнения в том, что при вызове метода Y, будет вызван метод D.X. Никакая часть этого анализа не откладывается до времени выполнения, поэтому данный вызов будет однозначно успешным.

Теперь, давайте рассмотрим следующий пример:

class B
{
public void M(double x) {}
public void M(int x) {}
}
class C
{
public static void X(B b, int d) { b.M(d); }
}

Теперь у нас меньше информации. Мы выполняем много раннего связывания; мы знаем, что переменная b типа B, и что вызывается метод B.M(int). Но, в отличие от предыдущего примера, у нас нет никаких гарантий компилятора, что вызов будет успешным, поскольку переменная b может быть null. По сути, мы откладываем до времени выполнения анализ того, будет ли приемник вызова валидным или нет. Многие не рассматривает это решение, как «связывание», поскольку мы не связываем синтаксис с программным элементом . Давайте сделаем вызов внутри метода C немного более поздним, путем изменения класса B:

class B
{
public virtual void M(double x) {}
public virtual void M(int x) {}
}

Теперь мы выполняем часть анализа во время компиляции; мы знаем, что будет вызван виртуальный метод B.M(int). Мы знаем, что вызов метода будет успешен, в том плане, что такой метод существует. Но мы не знаем, какой именно метод будет вызван во время выполнения! Это может быть переопределенный метод в наследнике; может быть вызван совершенно другой код, определенный в другой части программы. Диспетчеризация виртуальных методов является формой позднего связывания; решение о том, какой метод связан с синтаксической конструкцией b.M(d) частично принимается компилятором, а частично – во время выполнения.

А как насчет такого примера?

class C
{
public static void X(B b, dynamic d) { b.M(d); }
}

Теперь связывание практически полностью отложено до времени выполнения. В этом случае компилятор генерирует код, который говорит динамической среде времени выполнения (Dynamic Language Runtim), что статический анализ определил, что статическим типом переменной b является класс B и что вызываемой метод называется M, но реальное разрешение перегрузки для определения метода B.M(int) или B.M(double) (или никакого из них, если d, например, будет типа string) будет выполнено во время выполнения на основе этой информации. (***)

class C
{
public static void X(dynamic b, dynamic d) { b.M(d); }
}

Теперь, на этапе компиляции определяется лишь то, что для некоторого типа вызывается метод с именем M. Это практически наиболее позднее связывание, но, на самом деле, мы можем пойти еще дальше:

class C
{
public static void X(object b, object d, string m, BindingFlags f)
{
b.GetType().GetMethod(m, f).Invoke(b, d);
}
}

Теперь весь анализ выполняется во время позднего связывания; мы даже не знаем, какое имя мы собираемся связывать с вызываемым методом. Все, что мы можем знать, так это то, что автор X ожидает, что в переданном объекте b есть метод, имя которого определяет m, соответствующий флагам, переданным в f, принимающий аргументы, переданные в d. В этом случае мы ничего не можем сделать во время компиляции. (****)

(*) Конечно же, результат кодируется в двоичный формат, а не в читабельный для человека CIL формат.

(**) Вы можете спросить: являются ли «связывание» и «семантический анализ» синонимами; конечно, семантический анализ – это не более чем ассоциация синтаксических элементов с их значениями! Связывание является большей частью фазы семантического анализа компилятора, но есть много других форм анализа, которые нужно выполнить уже после того, как тела методов полностью «связаны». Например, анализ определенного присваивания (definite assignment) никак нельзя назвать «связыванием»; он не является ассоциацией синтаксических элементов с конкретными элементами программы. Скорее, этот анализ связывает лексические места с фактами о программных элементах, типа «локальная переменная blah не является определенно присвоенной в начале этого блока». Аналогично, оптимизация арифметических выражений является формой семантического анализа и явно не относится к «связыванию».

(***) Компилятор все еще может выполнить значительную часть статического анализа. Предположим, что класс B является закрытым (sealed) классом без методов с именем M. Даже при наличии динамических аргументов мы уже во время компиляции знаем, что связывание с методом M завершится неудачно, и мы можем сказать вам об этом во время компиляции. И компилятор на самом деле выполняет подобный анализ; а как именно – это хорошая тема для еще одного разговора.

(****) В некотором смысле этот пример является хорошим контрпримером моего определения связывания; мы даже не связываем синтаксические элементы с методом; мы связываем содержимое строки с методом.

Применение рефлексии, позднего связывания и атрибутов

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

Что именно подразумевается под расширяемым приложением? Рассмотрим IDE-среду Visual Studio 2010. При разработке в этом приложении были предусмотрены специальные "ловушки" (hook) для предоставления другим производителям ПО возможности подключать свои специальные модули. Понятно, что разработчики Visual Studio 2010 не могли добавить ссылки на несуществующие внешние сборки.NET (т.е. воспользоваться ранним связыванием), тогда как же им удалось обеспечить в приложении необходимые методы-ловушки? Ниже описан один из возможных способов решения этой проблемы.

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

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

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

Если расширяемое приложение изначально программируется так, чтобы запрашивать определенные интерфейсы, оно получает возможность определять во время выполнения, может ли активизироваться интересующий тип, и после успешного прохождения типом такой проверки позволять ему поддерживать дополнительные интерфейсы и получать доступ к их функциональным возможностям полиморфным образом. Именно такой подход и предприняли разработчики Visual Studio 2010, причем ничего особо сложного в нем нет.

В первую очередь необходимо создать сборку с типами, которые должна обязательно использовать каждая оснастка, чтобы иметь возможность подключаться к расширяемому приложению. Для этого создадим проект типа Class Library (Библиотека классов), и определим в нем два следующих типа:

Using System; namespace PW_CommonType { public interface IApplicationFunc { void Go(); } public class InfoAttribute: System.Attribute { public string CompanyName { get; set; } public string CompanyUrl { get; set; } } }

Далее потребуется создать тип, реализующий интерфейс IApplicationFunc. Чтобы не усложнять пример создания расширяемого приложения, давайте сделаем этот тип простым. Создадим новый проект типа Class Library на C# и определим в нем тип класса по имени MyCompanyInfo:

Using System; using PW_CommonType; using System.Windows..Go() { MessageBox.Show("Важная информация!"); } } }

И, наконец, последний шаг заключается в создании самого расширяемого приложения Windows Forms, которое позволит пользователю выбирать желаемую оснастку с помощью стандартного диалогового окна открытия файлов Windows.

Теперь нужно добавить в него ссылку на сборку PW_CommonType.dll, но не на библиотекy кода CompanyInfo.dll. Кроме того, необходимо импортировать в главный файл кода формы (для его открытия щелкните правой кнопкой мыши в визуальном конструкторе формы и выберите в контекстном меню пункт View Code (Просмотреть код)) пространства имен System.Reflection и PW_CommonType. Вспомните, что цель создания данного приложения состоит в том, чтобы увидеть, как использовать позднее связывание и рефлексию для проверки отдельных двоичных файлов, создаваемых другими производителям, на предмет их способности выступать в роли подключаемых оснасток.

--- Сборки.NET --- Позднее связывание

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

На первый взгляд увидеть выгоду от позднего связывания не просто. Действительно, если есть возможность выполнить раннее связывание с объектом (например, добавить ссылку на сборку и разместить тип с помощью ключевого слова new), следует обязательно так и поступать. Одна из наиболее веских причин состоит в том, что раннее связывание позволяет выявлять ошибки во время компиляции, а не во время выполнения. Тем не менее, позднее связывание тоже играет важную роль в любом создаваемом расширяемом приложении.

Класс System.Activator

Класс System. Activator (определенный в сборке mscorlib.dll) играет ключевую роль в процессе позднего связывания в.NET. В текущем примере интересует пока что только его метод Activator.CreateInstance() , который позволят создавать экземпляр подлежащего позднему связыванию типа. Этот метод имеет несколько перегруженных версий и потому обеспечивает довольно высокую гибкость. В самой простой версии CreateInstance() принимает действительный объект Type, описывающий сущность, которая должна размещаться в памяти на лету.

Чтобы увидеть, что имеется в виду, давайте создадим новый проект типа Console Application, импортируем в него пространства имен System.I0 и System.Reflection с помощью ключевого слова using и затем изменим класс Program, как-показано ниже:

Using System; using System.Reflection; using System.IO; namespace ConsoleApplication1 { class Program { static void Main() { Assembly ass = null; try { ass = Assembly.Load("fontinfo"); } catch (FileNotFoundException ex) { Console.WriteLine(ex.Message); } if (ass != null) CreateBinding(ass); Console.ReadLine(); } static void CreateBinding(Assembly a) { try { Type color1 = a.GetType("FontColor"); // Используем позднее связывание object obj = Activator.CreateInstance(color1); Console.WriteLine("Объект создан!"); } catch (Exception ex) { Console.WriteLine(ex.Message); } } } }

Прежде чем запускать данное приложение, необходимо вручную скопировать сборку fontinfo.dll в подкаталог bin\Debug внутри каталога этого нового приложения с помощью проводника Windows. Дело в том, что здесь вызывается метод Assembly.Load(), а это значит, что CLR-среда будет зондировать только папку клиента (при желании можно было бы воспользоваться методом Assembly.LoadFrom() и указывать полный путь к сборке, но в данном случае в этом нет никакой необходимости).

I. Основные принципы ООП

1. Инкапсуляция – принцип ООП, который заключается в объединении в единое целое данных и алгоритмов их обработки.

Данные объектав ООП называются полями объекта , а алгоритмы, т. е. действия над данными объекта,называются методами объекта , которые оформляются в виде подпрограмм.

2. Наследование – принцип ООП, который заключается в свойстве объектов порождать своих потомков.

Объект-потомок автоматически наследует от родителя все поля и методы, может дополнять объекты новыми полями и заменять или дополнять методы родителя.

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

Полиморфизм методов выражается в наличии у предка и потомка методов с одинаковыми именами, имеющих разную реализацию.

II. Структура объявления объектного типа

<имя_типа>=OBJECT

<имя_поля>:<тип_поля>;

<методы>;

После определения типа объекта должны следовать описания всех методов, перечисленных в объектном типе, которые представляют собой тексты процедур и функций. Отличие от обычного описания заключается в том, что имя подпрограммы , являющейся методом объекта, указываемое в её заголовке, состоит из 2 частей:

<имя_типа>.<имя_подпрограммы>

III. Свойство наследования

родительским типом , от которого наследуются все поля и методы. Для того, чтобы произошло наследование, в определение объектного типа после OBJECT в скобках необходимо указать имя родительского типа.

Правила наследования.

1. В определении типа наследника не должно быть полей, совпадающих по именам с полями родителя; имена методов у наследников и родителя могут совпадать – в этом проявляется свойство полиморфизма .

2. При построении объектного типа наследника, в первую очередь, наследуются поля родителя, затем добавляются поля наследника; после полей наследуются все методы родителя, затем добавляются методы наследника, если их имена не совпадают с именами методов родителя; если совпадения существуют, то это означает, что методы наследника являются полиморфными методами и замещают одноименные методы предка.

Свойство полиморфизма.

Свойство полиморфизма заключается в том, что в определении типа наследника могут быть методы, одноименные с именами методов родителя.

Правила наследования при полиморфизме:

· одноименные методы наследника заменяют методы родителя;

· методы наследника, не совпадающие по именам с методами родителя, добавляются после методов родителя.

IV. Виртуальные методы

Методы объекта являются статическими методами , если компилятор размещает их и разрешает все ссылки на них во время компиляции и компоновки. Процесс, с помощью которого вызовы статических методов однозначно разрешаются компилятором во время компиляции, называется ранним связыванием .

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

Виртуальные методы и таблица виртуальных методов (ТВМ).

Позднее связывание реализуется с помощью виртуальных методов , признаком которых является наличие ключевого слова VIRTUAL после заголовка метода в определении объектного типа.

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

Конструктор.

метод-конструктор

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

Конструктором может быть только процедура, в заголовке которой вместо служебного слова PROCEDURE необходимо написать CONSTRUCTOR.

Конструктор не может быть виртуальным.

После выполнения метода-конструктора при вызове виртуальных методов их адреса определяются из ТВМ.

Деструктор.

Деструктор служит для очистки памяти и удаления динамически размещенного объекта. Обычно в деструкторе указываются завершающие действия с объектом. В заголовке деструктора служебное слово PROCEDURE заменяется на DESTRUCTOR Для одного и того же типа объекта можно определить несколько деструкторов. Деструкторы можно наследовать, они могут быть статическими или виртуальными.

V. Пример объектного типа

1. Постановка задачи: реализовать движение точки на экране.

2. Математическая модель: каждая точка на экране характеризуется координатами x, y и состоянием v –видима/невидима.

3. Объявление объектного типа точка:

POINT=object

X, Y: integer; {координаты}

V: boolean; {признак видимости: TRUE-видима; FALSE-невидима}

function GET_X: integer; { получение координаты Х }

function GET_Y: integer; { получение координаты Y }

function GET_V: boolean; { получение признака видимости }

procedure INIT(X0, Y0: integer); { задание координат }

procedure TURN_ON; { рисование точки – получение видимой точки }

procedure TURN_OFF; { стирание точки – получение невидимой точки}

procedure MOVE(XN, YN: integer); { перемещение точки }

4. Определение методов объекта POINT:

function POINT.GET_X; { получение координаты Х }

GET_X:=X;

function POINT.GET_Y; { получение координаты Y }

GET_Y:=Y;

function POINT.GET_V; { получение признака видимости }

GET_V:=V;

procedure POINT.INIT; { задание координат }

V:=false;

procedure POINT.TURN_ON; { получение видимой точки }

if not V then

PutPixel(X,Y,GetColor); { GetColor возвращает текущий цвет }

V:=true;

procedure POINT.TURN_OFF; { получение невидимой точки – стирание точки }

if V then

PutPixel(X,Y,GetBkColor); { GetBkColor возвращает фоновый цвет }

V:=false;

procedure POINT.MOVE; { перемещение точки }

F: boolean;

if F then

TURN_OFF;

if F then

TURN_ON;

Свойство наследования.

Кроме точки, можно задать другие объекты, описывающие геометрические фигуры, например, объект типа окружность , который определяется радиусом, координатой центра, признаком видимости , и с ним возможны такие же действия, как с точкой: получение видимой окружности; получение невидимой окружности; перемещение окружности (изменение координат центра). Чтобы этот объект привязать к координатной сетке экрана, необходимо определить объектный тип PLACE (место ), который будет иметь данные, общие для всех геометрических фигур, – координаты привязки объекта к экрану X и Y :

PLACE=object

X, Y: integer;

function GET_X: integer;

function GET_Y: integer;

Каждый новый объектный тип может строиться на основе ранее определенного типа, называемого родительским типом , от которого наследуются все поля и методы. Чтобы произошло наследование, в объявлении объектного типа после OBJECT в скобках указывается имя родительского типа.

Используя тип PLACE как родительский, можно определить объектный тип POINT таким образом:

POINT=object(PLACE)

V: boolean;

procedure INIT(X0, Y0: integer);

function GET_V:boolean;

procedure TURN_ON;

procedure TURN_OFF;

6. Свойство полиморфизма заключается в том, что в объявлении типа наследника могут быть методы, одноименные с именами методов родителя.

Объявление типа окружность c использованием в качестве родителя типа POINT будет следующим:

CIRCL=object(POINT)

R: integer;

procedure INIT(X0, Y0, R0: integer); { задание окружности }

procedure TURN_ON; { получение видимой окружности }

procedure TURN_OFF; { получение невидимой окружности }

procedure MOVE(XN, YN: integer); { перемещение окружности}

function GET_R: integer; { получение радиуса }

Определение методов с использованием наследования при полиморфизме:

procedure CIRCL.INIT;

X:=X0; Y:=Y0;

V:=false;

procedure CIRCL.TURN_ON;

V:=true;

CIRCLE(X,Y,R);

procedure CIRCL.TURN_OFF;

C: byte;

C:=GetColor;

SetColor(GetBkColor); { установка цвета рисования }

Circle(X,Y,R);

V:=false;

SetColor(C);

procedure CIRCL.MOVE;

F: boolean;

if F then

TURN_OFF;

if F then

TURN_ON;

function CIRCL.GET_R;

GET_R:=R;

В результате текст процедуры CIRCL.MOVE совпадает с текстом процедуры POINT.MOVE , но машинные коды у этих процедур будут разные; CIRCL.MOVE при своем исполнении обращается к адресам процедур CIRCL.TURN_ON и CIRCL.TURN_OFF , а процедура POINT.MOVE – к адресам процедур POINT.TURN_ON и POINT.TURN_OFF .

Раннее и позднее связывание методов.

Ранее рассмотренные статические методы компилятор размещает и разрешает все ссылки на них во время компиляции и компоновки (раннее связывание ).

Для примера, описанного ранее, в объекте CIRCL можно наследовать метод MOVE у объекта POINT. Методы TURN_ON и TURN_OFF должны быть объявлены виртуальными, чтобы с ними произошло позднее связывание.

Компилятор оставляет после компиляции неразрешенными ссылки к тем методам, которые объявлены виртуальными. Для описанного ранее примера в методе MOVE неразрешенными ссылками будут адреса методов TURN_ON, TURN_OFF , т. е. процедура MOVE будет не готова к исполнению после компиляции, т. к. ее машинный код полностью не определен.

Во время выполнения программы до исполнения виртуальных методов любого объекта должен выполниться метод-конструктор для данного экземпляра объекта, в который компилятор вставляет действия по связи неразрешенных ссылок с ТВМ.

Объявления объектных типов PLACE, POINT и CIRCL с использованием конструктора и виртуальных методов:

PLACE=object

X,Y: integer;

function GET_X: integer;

function GET_Y: integer;

POINT=object(PLACE)

V: boolean;

constructor INIT(X0, Y0: integer);

function GET_V: boolean;

procedure TURN_ON; virtual;

procedure TURN_OFF;virtual;

procedure MOVE(XN, YN: integer);

CIRCL=object(POINT)

R: integer;

constructor INIT(X0, Y0, R0: integer);

function GET_R: integer;

procedure TURN_ON; virtual;

procedure TURN_OFF; virtual;

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


ВИРТУАЛЬНЫЕ ФУНКЦИИ______________________________________________________________ 1

Раннее и позднее связывание. Динамический полиморфизм ___________________________________ 1

Виртуальные функции___________________________________________________________________ 1 Виртуальные деструкторы _______________________________________________________________ 4 Абстрактные классы и чисто виртуальные функции___________________________________________ 5

ВИРТУАЛЬНЫЕ ФУНКЦИИ

Раннее и позднее связывание. Динамический полиморфизм

В C++ полиморфизм поддерживается двумя способами.

Во-первых, при компиляции он поддерживается посредством перегрузки функций и операторов. Такой вид полиморфизма называетсястатическим полиморфизмом , поскольку он реализуется еще до выпол-

нения программы, путем раннего связывания идентификаторов функций с физическими адресамина стадии компиляции и компоновки.

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

Виртуальная функция – это функция, вызов которой (и выполняемые при этом действия) зависит от типа объекта, для которого она вызвана. Объект определяет, какую функцию нужно вызвать уже во время выполнения программы. Этот вид полиморфизма называется динамическим полиморфизмом .

Основой динамического полиморфизма является предоставляемая C++ возможность определить указатель на базовый класс, который реально будет указывать не только на объект этого класса, но и на любой объект производного класса. Эта возможность возникает благодаря наследованию, поскольку объект производного класса всегда является объектом базового класса. Во время компиляции еще не известно, объект какого класса захочет создать пользователь, располагая указателем на объект базового класса. Такой указатель связывается со своим объектом только во время выполнения программы, то есть динамически. Класс, содержащий хоть одну виртуальную функцию, называетсяполиморфным .

Для каждого полиморфного типа данных компилятор создает таблицу виртуальных функций и встраивает в каждый объект такого класса скрытый указатель на эту таблицу. Она содержит адреса виртуальных функций соответствующего объекта. Имя указателя на таблицу виртуальных функций и название таблицы зависят от реализации в конкретном компиляторе. Например, в Visual C++ 6.0 этот указатель имеет имя vfptr , а таблица называетсяvftable (от английского Virtual Function Table). Компилятор автоматически встраивает в начало конструктора полиморфного класса фрагмент кода, который инициализирует указатель на таблицу виртуальных функций. Если вызывается виртуальная функция, код, сгенерированный компилятором, находит указатель на таблицу виртуальных функций, затем просматривает эту таблицу и извлекает из нее адрес соответствующей функции. После этого производится переход на указанный адрес и вызов функции.

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

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

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

Виртуальные функции

Функции, у которых известен интерфейс вызова (то есть прототип), но реализация не может быть задана в общем случае, а может быть определена только для конкретных случаев, называются виртуальными (термин, означающий, что функция может быть переопределена в производном классе).

Виртуальные функции – это функции, которые гарантируют, что будет вызвана правильная функция для объекта безотносительно к тому, какое выражение используется для осуществления вызова.

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

class Coord

Базовый класс координат

// базовый класс координат

protected:

// защищённые члены класса

double x , y ;

// координаты

public:

// открытые члены класса

Coord () { x = 0 ; y = 0 ; }

// конструктор базового класса

void Input () ;

// объявляет невиртуальную функцию

virtual void Print () ;

// объявляет виртуальную функцию

void Coord:: Input ()

// позволяет вводить координаты с клавиатуры

cout<<"\tx=";

// вводит значение x с клавиатуры

cout<<"\ty=";

// вводит значение y с клавиатуры

void Coord:: Print ()

// выводит значения координат на экран

cout<<"\tx="<

Производный класс точки

class Dot: publicCoord

// наследник класса координат

char name ;

// имя точки

public:

// открытые члены класса

Dot (ch ar N) : Coord () { name = N ; }

// вызывает конструктор базового класса

void Input () ;

void Print () ;

void Dot:: Input ()

// позволяет вводить координаты точки с клавиатуры

char S ="Введите координаты точки ";

CharToOem (S , S) ;

cout<

Coord:: Input () ;

void Dot:: Print()

// выводит значения координат точки на экран

char S ="Координаты точки ";

CharToOem (S , S) ;

// преобразует символы строки в кириллицу

cout<

// выводит на экран заголовок и имя точки

Coord:: Print () ;

// вызывает функцию базового класса

class Vec: publicCoord

Производный класс вектора

// наследник класса координат

char name [ 3 ] ;

// имя вектора

public:

// открытые члены класса

Vec (char * pName) : Coord () { strncpy (name , pName , 3) ; name [ 2 ] = "\0" ; }

void Input () ;

// переопределяет невиртуальную функцию

void Print () ;

// переопределяет виртуальную функцию

void Vec:: Input()

// позволяет вводить проекции вектора с клавиатуры

Лекция 9 Виртуальные функции 3

char S ="Введите проекции вектора "; // объявляет и инициализирует строку приглашения

CharToOem (S , S) ;

// преобразует символы строки в кириллицу

cout<

// выводит на экран приглашение и имя вектора

Coord:: Input () ;

// вызывает функцию базового класса

void Vec:: Print ()

// выводит значения проекций вектора на экран

char S = "Проекции вектора ";

// объявляет и инициализирует строку заголовка

CharToOem (S , S) ;

// преобразует символы строки в кириллицу

cout<

// выводит на экран заголовок и имя вектора

Coord:: Print () ;

// вызывает функцию базового класса

В приведённом примере объявлен базовый класс Coord и два производных классаDot иVec . ФункцияPrint () в производных классах является виртуальной, так как она объявлена виртуальной в базовом классеCoord . ФункцияPrint () в производных классахDot иVec переопределяет функцию базового класса. Если производный класс не предоставляет переопределенной реализации функцииPrint () , используется реализация по умолчанию из базового класса.

Функция Input () объявлена невиртуальной в базовом классеCoord и переопределена в производных классахDot иVec .

void main ()

Coord* pC = new Coord () ;

// объявляет указатель на координаты и выделяет память

Dot* pD = new Dot ("D") ;

// объявляет указатель на точку и выделяет память

Vec* pV = new Vec ("V") ;

// объявляет указатель на вектор и выделяет память

pC->Input () ;

pC->Print () ;

// вызывает виртуальную функцию Coord:: Print ()

// указатель на координаты получает адрес объекта типа точки

pC->Input () ;

// вызывает невиртуальную функцию Coord:: Input ()

pC->Print () ;

// вызывает виртуальную функцию Dot:: Print ()

// указатель на координаты получает адрес объекта типа вектора

pC->Input () ;

// вызывает невиртуальную функцию Coord:: Input ()

pC->Print () ;

// вызывает виртуальную функцию Vec:: Print ()

В приведённом примере указатель на координаты pC поочерёдно принимает значения адреса объектов координат, точки и вектора. Несмотря на то, что тип указателяpC не изменяется, он вызывает различныевиртуальные функции в зависимости от своего значения.

При использовании указателя на базовый класс, который реально указывает на объект производного класса, вызывается невиртуальная функция базового класса.

Необходимо отметить, что операция присвоения pC = pD , в которая использует операнды различных типов (Coord* иDot* ) без преобразования, возможна только для указателя на базовый класс в левой части. Обратная операция присвоенияpD = pC недопустима и вызывает ошибку синтаксиса.

При выполнении программа выводит на экран:

Координаты точки D:

Проекции вектора V:

При вызове функции с помощью указателей и ссылок, применяются следующее правила:

вызов виртуальной функции разрешается в соответствии с типом объекта, адрес которого хранит указатель или ссылка;

вызов невиртуальной функции разрешается в соответствии с типом указателя или ссылки.

Виртуальные функции вызываются только для объектов, принадлежащих некоторому классу. Поэтому

нельзя объявить глобальную или статическую функцию виртуальной. Ключевое слово virtual может