Шаг 30.
Виртуальные функции. Статическое и динамическое связывание

    На этом шаге мы рассмотрим организацию доступа к методам дочерних классовч ерез указатели на базовые классы.

    Вернемся к упомянутому на предыдущем шаге примеру с фигурой в виде базового класса с названием figure. Пусть в этом классе определена компонентная функция void show(). Так как внешний вид фигуры в базовом классе еще не определен, то в каждый из производных классов нужно включить свою функцию void show() для формирования изображения на экране. Если оставаться в рамках проиллюстрированного в примере с классами base и dir механизма, то доступ к функции show() производного класса возможен только с помощью явного указания области видимости:

            имя_производного_класса::show() 
либо с использованием имени конкретного объекта:
           имя_объекта_производного_класса.show()

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

    Большую гибкость (особенно при использовании уже готовых библиотек классов) обеспечивает позднее (отложенное), или динамическое связывание, которое предоставляется механизмом виртуальных функций. Любая нестатическая функция базового класса может быть сделана виртуальной, если в ее объявлении использовать спецификатор virtual. Прежде чем объяснить преимущества динамического связывания, приведем пример. Опишем в базовом классе виртуальную функцию и введем два производных класса, где определим функции с такими же прототипами, но без спецификатора virtual. В следующей программе в базовом классе base определена виртуальная функция void vfun(int). В производных классах dir1, dir2 эта функция подменяется (override), т.е. определена по-другому:

//OOР30_1.СРР - виртуальная функция в базовом классе.
#include <iostream.h>
#include <conio.h>
struct base {
  virtual void vfun(int i) 
   { cout << "\nbase::i = " << i; }
};
struct dir1: public base { 
  void vfun (int i) 
    { cout << "\ndir1::i = " << i; }
};
struct dir2: public base { 
  void vfun (int i) 
    { cout << "\ndir2::i = " << i; }
};
void main(void)
{ 
  base B, *bp = &B; 
  dir1 D1, *dp1 = &D1; 
  dir2 D2, *dp2 = &D2;
  bp->vfun(1);  // Печатает: base::i = 1
  dp1->vfun(2); // Печатает: dir1::i = 2 
  dp2->vfun(3); // Печатает: dir2::i = 3 
  bp = &D1; 
  bp->vfun(4);  // Печатает: dir1::i = 4 
  bp = &D2; 
  bp->vfun(5);  // Печатает: dir2::i = 5
}
Текст этой программы можно взять здесь.

    Результат выполнения программы:

     base::i  =  1
     dir1::i   =  2
     dir2::i   =  3
     dir1::i   =  4
     dir2::i   =  5

    В примере надо обратить внимание на доступ к функциям vfun() через указатель на базовый класс. Когда bp принимает значение адреса объекта класса base, то вызывается функция из базового класса. Затем bp последовательно присваиваются значения ссылок на объекты производных классов &D1, &D2, и выбор соответствующего экземпляра функции vfun() каждый раз определяется именно объектом. Таким образом, интерпретация каждого вызова виртуальной функции через указатель на базовый класс зависит от значения этого указателя, т.е. от типа объекта, для которого выполняется вызов. В противоположность этому интерпретация вызова через указатель невиртуальной функции зависит только от типа указателя (это было показано в предыдущем примере с функцией fun()).

    На следующем шаге мы закончим рассмотрение виртуальных функций.




Предыдущий шаг Содержание Следующий шаг