Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих.pdf
Скачиваний:
183
Добавлен:
01.05.2014
Размер:
3.97 Mб
Скачать

(c)class C3 : public C2 { public:

C3( int val )

: C2( val ), _object_count( val ) {}

// ...

};

(d)class C4 : public ConcreteBase { public:

C4( int val )

:ConcreteBase ( _id+val ) {}

//...

};

Упражнение 17.11

В первоначальном определении языка C++ порядок следования инициализаторов в списке инициализации членов определял порядок вызова конструкторов. Принцип, который действует сейчас, был принят в 1986 году. Как вы думаете, почему была изменена исходная спецификация?

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

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

void Query::display( Query *pb )

{

set<short> *ps = pb- >solutions();

// ...

display();

(или указателя, или ссылки на объект), для которого она вызвана:

}

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

class Query { public:

virtual ostream& print( ostream* = cout ) const;

// ...

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

};

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

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

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.)

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

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

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

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

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

ostream&

AndQuery::print( ostream &os ) const

{

_lop->print( os ); os << " && "; _rop->print( os );

AndQuery эта функция могла бы выглядеть так:

}

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

class Query { public:

virtual ostream& print( ostream &os=cout ) const {}

//...

еекак пустую функцию, а потом сделаем чисто виртуальной:

};

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

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

//только в определении класса

print() приведет к ошибке компиляции:

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?"

3 Увы! Правые скобки не распознаются, пока OrQuery не выведет все ассоциированное с ним частичное решение.

ostream&

NameQuery::

print( ostream &os ) const

{

if ( _lparen ) print_lparen( _lparen,

os );

os << _name;

if ( _rparen ) print_rparen( _rparen,

os );

return os;

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

}

class NameQuery : public Query { public:

virtual ostream& print( ostream &os ) const;

//...

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

};

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

class NotQuery : public Query { public:

virtual ostream& print( ostream &os ) const;

// ...

это нужно.) Вот объявление и реализация print() в NotQuery:

};

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 практически

class AndQuery : public Query { public:

virtual ostream& print( ostream &os ) const;

// ...

дублируют друг друга. Поэтому приведем их только для AndQuery:

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

cout << "Был сформулирован запрос ";

Query *pq = retrieveQuery();

в поток класса ostream или любого другого, производного от него:

pq->print( cout );

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

Query *pq = retrieveQuery();

cout << "В ответ на запрос

"

<< *pq

помощью оператора вывода из библиотеки iostream:

<< " получены следующие результаты:\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;

Query *pquery = retrieveQuery();

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

cout << *pquery << endl;