Сделай Сам Свою Работу на 5

Виртуальные функции в базовом и производном классах

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

void Query::display( Query *pb ) { set<short> *ps = pb->solutions(); // ... display();

}

Статический тип pb – это Query*. При обращении к невиртуальному члену solutions() вызывается функция-член класса Query. Невиртуальная функция display() вызывается через неявный указатель this. Статическим типом указателя this также является Query*, поэтому вызвана будет функция-член класса Query.

Чтобы объявить функцию виртуальной, нужно добавить ключевое слово virtual:

class Query { public: virtual ostream& print( ostream* = cout ) const; // ...

};

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

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

NameQuery nq( "lilacs" );   // правильно: но nq "усечено" до подобъекта Query

Query qobject = nq;

Инициализация qobject переменной nq абсолютно законна: теперь qobject равняется подобъекту nq, который соответствует базовому классу Query, однако qobject не является объектом NameQuery. Часть nq, принадлежащая NameQuery, “усечена” перед инициализацией qobject, поскольку она не помещается в область памяти, отведенную под объект Query. Для поддержки этой парадигмы приходится использовать указатели и ссылки, но не сами объекты:

void print ( Query object, const Query *pointer, const Query &reference ) { // до момента выполнения невозможно определить, // какой экземпляр print() вызывается pointer->print(); reference.print();   // всегда вызывается Query::print() object.print(); }   int main() { NameQuery firebird( "firebird" ); print( firebird, &firebird, firebird );

}



В данном примере оба обращения через указатель pointer и ссылку reference разрешаются своим динамическим типом; в обоих случаях вызывается NameQuery::print(). Обращение же через объект object всегда приводит к вызову Query::print(). (Пример программы, в которой используется эффект “усечения”, приведен в разделе 18.6.2.)

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

Виртуальный ввод/вывод

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

ostream& print( ostream &os = cout ) const;

Функцию print() следует объявить виртуальной, поскольку ее реализации зависят от типа, но нам нужно вызывать ее через указатель типа Query*. Например, для класса AndQuery эта функция могла бы выглядеть так:

ostream& AndQuery::print( ostream &os ) const { _lop->print( os ); os << " && "; _rop->print( os );

}

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

class Query { public: virtual ostream& print( ostream &os=cout ) const {} // ...

};

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

// ошибка: ключевое слово virtual может появляться // только в определении класса

virtual ostream& Query::print( ostream& ) const { ... }

Правильный вариант не должен включать слово virtual.

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

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

 

fiery && bird || shyly

 

пользователь ищет вхождения пары слов

 

fiery bird

 

или одного слова

 

shyly

 

С другой стороны, запрос

 

fiery && ( bird || hair )

 

найдет все вхождения любой из пар

 

fiery bird

 

или

 

fiery hair

 

Если наши реализации print() не будут показывать скобки в исходном запросе, то для пользователя они окажутся почти бесполезными. Чтобы сохранить эту информацию, введем в наш абстрактный базовый класс Query два нестатических члена, а также функции доступа к ним (подобное расширение класса – естественная часть эволюции иерархии):

class Query { public: // ...   // установить _lparen и _rparen void lparen( short lp ) { _lparen = lp; } void rparen( short rp ) { _rparen = rp; }   // получить значения_lparen и _rparen short lparen() { return _lparen; } short rparen() { return _rparen; }   // напечатать левую и правую скобки void print_lparen( short cnt, ostream& os ) const; void print_rparen( short cnt, ostream& os ) const;   protected:   // счетчики левых и правых скобок short _lparen; short _rparen; // ...

};

_lparen – это количество левых, а _rparen – правых скобок, которое должно быть выведено при распечатке объекта. (В разделе 17.7 мы покажем, как вычисляются такие величины и как происходит присваивание обоим членам.) Вот пример обработки запроса с большим числом скобок:

 

==> ( untamed || ( fiery || ( shyly ) ) )

evaluate word: untamed

_lparen: 1

_rparen: 0

evaluate Or

_lparen: 0

_rparen: 0

 

evaluate word: fiery

_lparen: 1

_rparen: 0

 

evaluate 0r

_lparen: 0

_rparen: 0

 

evaluate word: shyly

_lparen: 1

_rparen: 0

 

evaluate right parens:

_rparen: 3

 

( untamed ( 1 ) lines match

( fiery ( 1 ) lines match

( shyly ( 1 ) lines match

( fiery || (shyly ( 2 ) lines match3

( untamed || ( fiery || ( shyly ))) ( 3 ) lines match

 

Requested query: ( untamed || ( fiery || ( shyly ) ) )

( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her,

( 4 ) magical but untamed. "Daddy, shush, there is no such thing,"

( 6 ) Shyly, she asks, "I mean, Daddy, is there?"

 

Реализация print() для класса NameQuery:

ostream& NameQuery:: print( ostream &os ) const { if ( _lparen ) print_lparen( _lparen, os );   os << _name;   if ( _rparen ) print_rparen( _rparen, os );   return os;

}

А так выглядит объявление:

class NameQuery : public Query { public: virtual ostream& print( ostream &os ) const; // ...

};

Чтобы реализация виртуальной функции в производном классе замещала реализацию из базового, прототипы функций обязаны совпадать. Например, если бы мы опустили слово const или объявили еще один параметр, то реализация print() в NameQuery не заместила бы реализацию из базового класса. Возвращаемые значения также должны быть одинаковыми за одним исключением: значение, возвращенное реализацией в производном классе, может принадлежать к типу класса, который открыто наследует классу значения, возвращаемого реализацией в базовом классе. Если бы реализация из базового класса возвращала значение типа Query*, то реализация из производного могла бы возвращать NameQuery*. (Позже при работе с функцией clone() мы покажем, зачем это нужно.) Вот объявление и реализация print() в NotQuery:

class NotQuery : public Query { public: virtual ostream& print( ostream &os ) const; // ...

};

ostream& NotQuery:: print( ostream &os ) const { os << " ! ";   if ( _lparen ) print_lparen( _lparen, os );   _op->print( os );   if ( _rparen ) print_rparen( _rparen, os );   return os;

}

Разумеется, вызов print() через _op – виртуальный.

Объявления и реализации этой функции в классах AndQuery и OrQuery практически дублируют друг друга. Поэтому приведем их только для AndQuery:

class AndQuery : public Query { public: virtual ostream& print( ostream &os ) const; // ...

};

ostream& AndQuery:: print( ostream &os ) const { if ( _lparen ) print_lparen( _lparen, os );   _lop->print( os ); os << " && "; _rop->print( os );   if ( _rparen ) print_rparen( _rparen, os );   return os;

}

Такая реализация виртуальной функции print() позволяет вывести любой подтип Query в поток класса ostream или любого другого, производного от него:

cout << "Был сформулирован запрос "; Query *pq = retrieveQuery();

pq->print( cout );

Однако такой возможности недостаточно. Еще нужно уметь распечатывать любой производный от Query тип, который уже есть или может появиться в будущем, с помощью оператора вывода из библиотеки iostream:

Query *pq = retrieveQuery(); cout << "В ответ на запрос " << *pq

<< " получены следующие результаты:\n";

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

inline ostream& operator<<( ostream &os, const Query &q ) { // виртуальный вызов print() return q.print( os );

}

Строки

AndQuery query; // сформулировать запрос ...

cout << query << endl;

вызывают наш оператор вывода в ostream, который в свою очередь вызывает

q.print( os )

где q привязано к объекту query класса AndQuery, а os – к cout. Если бы вместо этого мы написали:

NameQuery query2( "Salinger" );

cout << query2 << endl;

то была бы вызвана реализация print() из класса NameQuery. Обращение

Query *pquery = retrieveQuery();

cout << *pquery << endl;

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



©2015- 2019 stydopedia.ru Все материалы защищены законодательством РФ.