Объектно-ориентированное программирование на C++


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


Вернемся к примеру: для изображения и удаления окружности недостаточно родительских методов Point.Show() и Point.Hide(). Поэтому потребовалось написать новые, перекрывающие их методы Circle.Show() и Circle.Hide(). Теперь сравните метод Circle.MoveTo() и Point.MoveTo().

Кто видит разницу?

Разницы, по крайней мере, внешней, нет. А что произойдет, если мы не будем перегружать этот метод?

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

В большинстве традиционных языков программирования, включая и С, используется только раннее связывание. В противоположность этому, в случае позднего связывания адрес процедуры не связывается с обращением к ней до того момента, пока обращение не произойдет фактически, т.е. во время выполнения программы. Возвращаясь к нашему примеру. Если мы вызовем Point.MoveTo(), то будет вызываться Point.Show() и Point.Hide(). Только из-за этого мы вынуждены заменять Point.MoveTo() на внешне почти полностью аналогичную функцию Circle.MoveTo().

Логика компилятора очень проста, сначала он ищет метод с таким именем, определенным внутри данного класса. Если метод с таким именем не определен внутри класса, то компилятор обращается к базовому классу и ищет его там. Если найдет, то подставит в точки вызова адрес метода из родительского класса. Если не найдет, то поднимается все выше.

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

Рассмотренные выше проблемы обусловлены разрешением ссылок на методы во время компиляции. Способ решения этой проблемы - разрешить такие ссылки во время выполнения, динамически.

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

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

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

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

Различие между вызовом статического метода и вызовом виртуального метода - это различие между решением сейчас и отложенным, задержанным решением. Когда мы кодируем вызов статического метода, то, по-существу, говорим компилятору: "Ты знаешь, чего я хочу. Делай вызов". Вызов виртуального метода, с другой стороны, подобен тому, что мы говорим компилятору: "Ты не знаешь, чего я хочу - пока. Когда придет время запроси образец".

Давайте подумаем над этой метафорой в терминах нашей проблемы с MoveTo(). Вызов Circle.MoveTo() может идти только в одно место: ближайшую реализацию MoveTo() вверх по иерархии объектов. В нашем случае он вызовет Point.MoveTo(), поскольку это ближайшая реализация. Это решение будет осуществлено во время компиляции. Но когда MoveTo() вызовет Show(), то это другая история. Каждый класс фигур имеет свою собственную реализацию метода Show(), поэтому то, какая реализация Show() вызывается в MoveTo(), должно полностью зависеть от того, какой объект вызвал MoveTo().

Это и является причиной того, почему вызов Show() внутри MoveTo() должен быть задержанным решением: при компиляции кода для MoveTo() мы не можем решить, вызов какого Show() сделать. Информация недоступна во время компиляции, поэтому решение следует отложить до времени выполнения, когда можно запросить тип объекта вызывающего MoveTo().

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

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

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



Содержание раздела