С++ для начинающих


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


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

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 в данной точке выполнения программы.


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