Выбрать главу

// pquery может адресовать любой из классов, производных от Query

void eval( const Query *pquery )

{

pquery-eval();

}

то мы вправе вызывать ее, передавая адрес объекта любого из четырех типов запросов:

int main()

{

AndQuery aq;

NotQuery notq;

OrQuery *oq = new OrQuery;

NameQuery nq( "Botticelli" );

// правильно: любой производный от Query класс

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

eval( &aq );

eval( &notq );

eval( oq );

eval( &nq );

}

В то же время попытка передать eval() адрес объекта класса, не являющегося производным от Query, вызовет ошибку компиляции:

int main()

{

string name( " Scooby-Doo" );

// ошибка: тип string не является производным от Query

eval( &name );

}

Внутри eval() выполнение инструкции вида

pquery-eval();

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

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

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

Хотя для полиморфной манипуляции объектом требуется, чтобы доступ к нему осуществлялся с помощью указателя или ссылки, сам по себе факт их использования не обязательно приводит к полиморфизму. Рассмотрим такие объявления:

// полиморфизма нет

int *pi;

// нет поддержанного языком полиморфизма

void *pvi;

// pquery может адресовать объект любого производного от Query класса

Query *pquery;

В C++ полиморфизм существует только в пределах отдельных иерархий классов. Указатели типа void* можно назвать полиморфными, но в языке их поддержка не предусмотрена. Такими указателями программист должен управлять самостоятельно, с помощью явных приведений типов и той или иной формы дискриминанта, показывающего, объект какого типа в данный момент адресуется. (Можно сказать, что это "второсортные" полиморфные объекты.)

Язык C++ обеспечивает поддержку полиморфизма следующими способами:

1. путем неявного преобразования указателя или ссылки на производный класс к указателю или ссылке на открытый базовый:

Query *pquery = new NameQuery( " Class" );

1. через механизм виртуальных функций:

pquery-eval();

с помощью операторов dynamic_cast и typeid (они подробно обсуждаются в разделе 19.1):

if ( NameQuery *pnq =

dynamic_cast NameQuery* ( pquery )) ...

Проблему представления запроса мы решим, определив каждый операнд в классах AndQuery, NotQuery и OrQuery как указатель на тип Query*. Например:

class AndQuery {

public:

// ...

private:

Query *_lop;

Query *_rop;

};

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

_rop-eval();

На рис. 17.1 показана иерархия наследования, состоящая из абстрактного класса Query и четырех производных от него классов. Как этот рисунок транслируется в код программы на C++?

Query AndQuery OrQuery NotQuery NameQuery

Рис. 17.1. Иерархия классов Query