// 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( ¬q );
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