Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
138
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

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

882

является объектом 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 эта функция могла бы выглядеть так:

}

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

883

Необходимо объявить 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

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

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

884

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

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

885

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

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 не заместила бы реализацию из базового класса. Возвращаемые значения также должны

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

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

886

быть одинаковыми за одним исключением: значение, возвращенное реализацией в производном классе, может принадлежать к типу класса, который открыто наследует классу значения, возвращаемого реализацией в базовом классе. Если бы реализация из базового класса возвращала значение типа 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:

};

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

887

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;

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

Строки

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

888

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;

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

17.5.2. Чисто виртуальные функции

С точки зрения кодирования основная задача, стоящая перед нами в связи с поддержкой пользовательских запросов, – это реализация зависимых от типа операций для каждого из возможных операторов. Для этого мы определили четыре конкретных типа классов: AndQuery, OrQuery и т.д. Однако с точки зрения проектирования наша цель инкапсулировать обработку каждого вида запроса, спрятать за не зависящим от типа интерфейсом. Это позволит построить ядро приложения, которое не потребует изменений при добавлении или удалении типов.

Чтобы добиться этого, определим абстрактный тип класса Query. При этом мы не будем программировать разные типы пользовательских запросов, а лишь абстрактные

void doit_and_bedone( vector< Query* > *pvec )

{

vector<Query*>::iterator it = pvec->begin(), end_it = pvec->end();

for ( ; it != end_it; ++it )

{

Query *pq = *it;

cout << "обрабатывается " << *pq << endl; pq->eval();

pq->display(); delete pq;

}

операции, применимые к ним:

}

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

889

Такое определение позволяет добавлять неограниченное число типов запросов без необходимости изменять или даже перекомпилировать ядро системы, но при условии, что

открытый интерфейс нашего абстрактного базового класса Query достаточен для поддержки новых запросов.

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

Поскольку Query абстрактный класс, объекты которого в приложении не создаются, то

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

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

class Query { public:

// объявляется чисто виртуальная функция

virtual ostream& print( ostream&=cout ) const = 0; // ...

Объявляются они следующим образом:

};

Заметьте, что за объявлением функции следует присваивание нуля.

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

//В классе Query объявлены одна или несколько виртуальных функций,

//поэтому программист не может создавать независимые объекты

//класса Query

//правильно: подобъект Query в составе NameQuery

Query *pq = new NameQuery( "Nostromo" );

// ошибка: оператор new создает объект класса Query

чисто виртуальной функции с помощью механизма виртуализации.) Например:

Query *pq2 = new Query;

Абстрактный базовый класс может существовать только как подобъект в составе объекта некоторого производного от него класса. Это именно та семантика, которая нужна нам для базового Query.

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

890

17.5.3. Статический вызов виртуальной функции

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

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

//isA() вызывается динамически с помощью механизма виртуализации

//реально будет вызвана NameQuery::isA()

pquery->isA();

//isA вызывается статически во время компиляции

//реально будет вызвана Query::isA

икаждом из производных классов иерархии Query: pquery->Query::isA();

Тогда явный вызов Query::isA() разрешается на этапе компиляции в пользу реализации isA() в базовом классе Query, хотя pquery адресует объект NameQuery.

Зачем нужно отменять механизм виртуализации? Как правило, ради эффективности. В

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

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

Реализации функции print() в классах AndQuery и OrQuery совпадают во всем, кроме литеральной строки, представляющей название оператора. Реализуем только одну функцию, которую можно вызывать из данных классов. Для этого мы снова определим абстрактный базовый BinaryQuery (его наследники AndQuery и OrQuery). В нем определены два операнда и еще один член типа string для хранения значения оператора. Поскольку это абстрактный класс, объявим print() чисто виртуальной функцией:

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

891

class BinaryQuery : public Query { public:

BinaryQuery( Query *lop, Query *rop, string oper ) : _lop(lop), _rop(rop), _oper(oper) {}

~BinaryQuery() { delete _lop; delete _rop; } ostream &print( ostream&=cout, ) const = 0;

protected: Query *_lop; Query *_rop; string _oper;

};

Вот как реализована в BinaryQuery функция print(), которая будет вызываться из

inline ostream& BinaryQuery::

print( ostream &os ) const

{

if ( _lparen )

print_lparen( _lparen, os );

_lop->print( os );

os << ' ' << _oper << ' '; _rop->print( os );

if ( _rparen )

print_rparen( _rparen, os );

return os;

производных классов AndQuery и OrQuery:

}

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

С другой стороны, нужно определить в классе BinaryQuery виртуальную функцию print() и уметь вызывать ее через объекты AndQuery и OrQuery.

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

inline ostream& AndQuery::

print( ostream &os ) const

{

//правильно: подавить механизм виртуализации

//вызвать BinaryQuery::print статически BinaryQuery::print( os );

можно вызывать статически:

}