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

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

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

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

842

#include "TextQuery.h"

int main()

{

TextQuery tq;

tq.build_up_text(); tq.query_text();

}

build_text_map() это слегка видоизмененная функция-член doit() из главы 6. Ее основная задача построить отображение для хранения позиций всех значимых слов текста. (Если помните, мы не храним семантически нейтральные слова типа союзов if, and, but и т.д. Кроме того, мы заменяем заглавные буквы на строчные и устраняем суффиксы, обозначающие множественное число: например, testifies преобразуется в testify, а marches в march.) С каждым словом ассоциируется вектор позиций, в котором хранятся номера строки и колонки каждого вхождения слова в текст.

query_text() принимает запросы пользователя и преобразует их во внутреннюю форму на основе иерархии классов Query с одиночным наследованием и динамическим связыванием. Внутреннее представление запроса применяется к отображению слов на вектор позиций, построенному в build_text_map(). Ответом на запрос будет множество строк текстового файла, удовлетворяющих заданному критерию:

Enter a query - please separate each item by a space.

Terminate query (or session) with a dot( . ).

==> fiery && ( bird || shyly )

fiery ( 1 ) lines match bird ( 1 ) lines match shyly ( 1 ) lines match

( bird || shyly ) ( 2 ) lines match

fiery && ( bird || shyly ) ( 1 ) lines match Requested query: fiery && ( bird || shyly )

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

Внашей системе мы выбрали следующий язык запросов:

одиночное слово, например Alice или untamed. Выводятся все строки, в которых оно встречается, причем каждой строке предшествует ее номер, заключенный в скобки. (Строки печатаются в порядке возрастания номеров). Например:

==> daddy

daddy ( 3 ) lines match

Requested query: daddy

( 1 ) Alice Emma has long flowing red hair. Her Daddy says

( 4 ) magical but untamed. "Daddy, shush, there is no such thing," ( 6 ) Shyly, she asks, "I mean, Daddy, is there?"

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

843

∙ запрос НЕ”, формулируемый с помощью оператора !. Выводятся все строки, где не встречается указанное слово. Например, так формулируется отрицание запроса 1:

==> ! daddy

daddy ( 3 ) lines match

! daddy ( 3 ) lines match Requested query: ! daddy

( 2 ) when the wind blows through her hair, it looks almost alive,

( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her, ( 5 ) she tells him, at the same time wanting him to tell her more.

запрос ИЛИ”, формулируемый с помощью оператора ||. Выводятся все строки, в которых встречается хотя бы одно из двух указанных слов:

==> fiery || untamed

fiery ( 1 ) lines match untamed ( 1 ) lines match

fiery || untamed ( 2 ) lines match Requested query: fiery || untamed

( 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,"

запрос И”, формулируемый с помощью оператора &&. Выводятся все строки, где оба указанных слова встречаются, причем располагаются рядом. Сюда входит и случай, когда одно слово является последним в строке, а другое первым в следующей:

==> untamed && Daddy

untamed ( 1 ) lines match daddy ( 3 ) lines match

untamed && daddy ( 1 ) lines match

Requested query: untamed && daddy

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

Эти элементы можно комбинировать:

fiery && bird || shyly

Однако обработка производится слева направо, и все элементы имеют одинаковые приоритеты. Поэтому наш составной запрос интерпретируется как fiery bird ИЛИ shyly, а не как fiery bird ИЛИ fiery shyly:

==> fiery && bird || shyly

fiery ( 1 ) lines match bird ( 1 ) lines match

fiery && bird ( 1 ) lines match shyly ( 1 ) lines match

fiery && bird || shyly ( 2 ) lines match

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

844

 

 

 

 

Requested query: fiery && bird || shyly

 

( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her, ( 6 ) Shyly, she asks, "I mean, Daddy, is there?"

Чтобы можно было группировать части запроса, наша система должна поддерживать скобки. Например:

fiery && (bird || shyly)

выдает все вхождения fiery bird или fiery shyly1. Результат исполнения этого запроса приведен в начале данного раздела. Кроме того, система не должна многократно отображать одну и ту же строку.

Примечание [O.A.5]: Нумера ция сносок сбита.

17.1. Определение иерархии классов

В этой главе мы построим иерархию классов для представления запроса пользователя.

NameQuery

// Shakespeare

NotQuery

// ! Shakespeare

OrQuery

// Shakespeare || Marlowe

Сначала реализуем каждую операцию в виде отдельного класса:

AndQuery

// William && Shakespeare

В каждом классе определим функцию-член eval(), которая выполняет соответствующую операцию. К примеру, для NameQuery она возвращает вектор позиций, содержащий координаты (номера строки и колонки) начала каждого вхождения слова (см. раздел 6.8); для OrQuery строит объединение векторов позиций обоих своих операндов и т.д.

Таким образом, запрос

untamed || fiery

состоит из объекта класса OrQuery, который содержит два объекта NameQuery в качестве операндов. Для простых запросов этого достаточно, но при обработке составных запросов

типа

Alice || Emma && Weeks

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

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

845

возникает проблема. Данный запрос состоит из двух подзапросов: объекта OrQuery,

содержащего объекты NameQuery для представления слов Alice и Emma, и

объекта

AndQuery

OrQuery

NameQuery ("Alice")

NameQuery ("Emma")

AndQuery. Правым операндом AndQuery является объект NameQuery для слова Weeks.

NameQuery ("Weeks")

Но левый операнд это объект OrQuery, предшествующий оператору &&. На его месте мог бы быть объект NotQuery или другой объект AndQuery. Как же следует представить операнд, если он может принадлежать к типу любого из четырех классов? Эта проблема имеет две стороны:

необходимо уметь объявлять тип операнда в классах OrQuery, AndQuery и NotQuery так, чтобы с его помощью можно было представить тип любого из четырех классов запросов;

какое бы решение мы ни выбрали в предыдущем случае, мы должны иметь

возможность вызывать соответствующий классу каждого операнда вариант функции-члена eval().

Решение, не согласующееся с объектной ориентированностью, состоит в том, чтобы определить тип операнда как объединение и включить дискриминант, показывающий

// не объектно-ориентированное решение union op_type {

//объединение не может содержать объекты классов с

//ассоциированными конструкторами

NotQuery *nq; OrQuery *oq; AndQuery *aq; string *word;

};

enum opTypes {

Not_query=1, O_query, And_query, Name_query

};

class AndQuery { public:

// ...

private:

/*

*opTypes хранит информацию о фактических типах операндов запроса

*op_type - это сами операнды

*/

op_type _lop, _rop;

opTypes _lop_type, _rop_type;

текущий тип операнда:

};

Хранить указатели на объекты можно и с помощью типа void*:

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

846

class AndQuery { public:

// ...

private:

void * _lop, _rop;

opTypes _lop_type, _rop_type;

};

Нам все равно нужен дискриминант, поскольку напрямую использовать объект, адресуемый указателем типа void*, нельзя, равно как невозможно определить тип такого объекта по указателю. (Мы не рекомендуем применять описанное решение в C++, хотя в языке C это весьма распространенный подход.)

Основной недостаток рассмотренных решений состоит в том, что ответственность за определение типа возлагается на программиста. Например, в случае решения, основанного на void*-указателях, операцию eval() для объекта AndQuery можно

void AndQuery:: eval()

{

//не объектно-ориентированный подход

//ответственность за разрешение типа ложится на программиста

//определить фактический тип левого операнда

switch( _lop_type ) { case And_query:

AndQuery *paq = static_cast<AndQuery*>(_lop); paq->eval();

break; case Or_query:

OrQuery *pqq = static_cast<OrQuery*>(_lop); poq->eval();

break; case Not_query:

NotQuery *pnotq = static_cast<NotQuery*>(_lop); pnotq->eval();

break; case Name_query:

AndQuery *pnmq = static_cast<NameQuery*>(_lop); pnmq->eval();

break;

}

// то же для правого операнда

реализовать так:

}

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

Объектно-ориентированное программирование предлагает альтернативное решение, в котором работа по разрешению типов перекладывается с программиста на компилятор. Например, так выглядит код операции eval() для класса AndQuery в случае применения объектно-ориентированного подхода (eval() объявлена виртуальной):

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

847

//объектно-ориентированное решение

//ответственность за разрешение типов перекладывается на компилятор

//примечание: теперь _lop и _rop - объекты типа класса

//их определения будут приведены ниже

void AndQuery:: eval()

{

_lop->eval(); _rop->eval();

}

Если потребуется добавить или исключить какие-либо типы, эту часть программы не придется ни переписывать, ни перекомпилировать.

17.1.1. Объектно-ориентированное проектирование

Из чего складывается объектно-ориентированное проектирование четырех рассмотренных выше видов запросов? Как решаются проблемы их внутреннего представления?

С помощью наследования можно определить взаимосвязи между независимыми классами запросов. Для этого мы вводим в рассмотрение абстрактный класс Query, который будет служить для них базовым (соответственно сами эти классы будут считаться производными). Абстрактный класс можно представить себе как неполный, который становится более или менее завершенным, когда из него порождаются производные классы, – в нашем случае AndQuery, OrQuery, NotQuery и NameQuery.

В нашем абстрактном классе Query определены данные и функции-члены, общие для всех четырех типов запроса. При порождении из Query производного класса, скажем AndQuery, мы выделяем уникальные характеристики каждого вида запроса. К примеру, NameQuery это специальный вид Query, в котором операндом всегда является строка. Мы будем называть NameQuery производным и говорить, что Query является его базовым классом. (То же самое относится и к классам, представляющим другие типы запросов.) Производный класс наследует данные и функции-члены базового и может обращаться к ним непосредственно, как к собственным членам.

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

_rop->eval();

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

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

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

848

Когда мы говорим о полиморфизме в языке C++, то имеем в виду главным образом

способность указателя или ссылки на базовый класс адресовать любой из производных от

// pquery может адресовать любой из классов, производных от Query void eval( const Query *pquery )

{

pquery->eval();

него. Если определить обычную функцию eval() следующим образом:

}

int main()

{

AndQuery aq; NotQuery notq;

OrQuery *oq = new OrQuery; NameQuery nq( "Botticelli" );

//правильно: любой производный от Query класс

//компилятор автоматически преобразует в базовый класс

eval( &aq ); eval( &notq ); eval( oq ); eval( &nq );

то мы вправе вызывать ее, передавая адрес объекта любого из четырех типов запросов:

}

В то же время попытка передать eval() адрес объекта класса, не являющегося

int main()

{

string name( "Scooby-Doo" );

// ошибка: тип string не является производным от Query eval( &name );

производным от Query, вызовет ошибку компиляции:

}

Внутри eval() выполнение инструкции вида

pquery->eval();

должно вызывать нужную виртуальную функцию-член eval() в зависимости от фактического класса объекта, адресуемого указателем pquery. В примере выше pquery последовательно адресует объекты AndQuery, NotQuery, OrQuery и NameQuery. В каждой

точке вызова определяется фактический тип класса объекта и вызывается подходящий экземпляр eval().

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

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

849

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

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

//полиморфизма нет int *pi;

//нет поддержанного языком полиморфизма void *pvi;

//pquery может адресовать объект любого производного от Query класса

обязательно приводит к полиморфизму. Рассмотрим такие объявления:

Query *pquery;

В C++ полиморфизм существует только в пределах отдельных иерархий классов. Указатели типа void* можно назвать полиморфными, но в языке их поддержка не предусмотрена. Такими указателями программист должен управлять самостоятельно, с помощью явных приведений типов и той или иной формы дискриминанта, показывающего, объект какого типа в данный момент адресуется. (Можно сказать, что это второсортныеполиморфные объекты.)

Язык C++ обеспечивает поддержку полиморфизма следующими способами:

∙ путем неявного преобразования указателя или ссылки на производный класс к указателю или ссылке на открытый базовый:

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

через механизм виртуальных функций:

pquery->eval();

с помощью операторов dynamic_cast и typeid (они подробно обсуждаются в

if ( NameQuery *pnq =

разделе 19.1):

dynamic_cast< NameQuery* >( pquery )) ...

Проблему представления запроса мы решим, определив каждый операнд в классах

AndQuery, NotQuery и OrQuery как указатель на тип Query*. Например:

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

850

class AndQuery { public:

// ...

private:

Query *_lop;

Query *_rop;

};

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

_rop->eval();

На рис. 17.1 показана иерархия наследования, состоящая из абстрактного класса Query и четырех производных от него классов. Как этот рисунок транслируется в код программы на C++?

Query

AndQuery

OrQuery

NotQuery

NameQuery

Рис. 17.1. Иерархия классов Query

Вразделе 2.4 мы рассматривали реализацию иерархии классов IntArray.

Синтаксическая структура определения иерархии, изображенной на рис. 17.1,

class Query { ... };

class AndQuery

: public Query { ... };

class OrQuery

: public Query { ... };

class NotQuery

: public Query { ... };

аналогична:

class NameQuery : public Query { ... };

Наследование задается с помощью списка базовых классов. В случае одиночного наследования этот список имеет вид:

: уровень-доступа базовый-класс

где уровень-доступа это одно из ключевых слов public, protected, private (смысл защищенного и закрытого наследования мы обсудим в разделе 18.3), а базовый-класс имя ранее определенного класса. Например, Query является открытым базовым классом для любого из четырех классов запросов.

Класс, встречающийся в списке базовых, должен быть предварительно определен. Следующего опережающего объявления Query недостаточно для того, чтобы он мог выступать в роли базового:

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

851

// ошибка: Query должен быть определен class Query;

class NameQuery : piblic Query { ... };

Опережающее объявление производного класса должно включать только его имя, но не список базовых классов. Поэтому следующее опережающее объявление класса NameQuery

//ошибка: опережающее объявление не должно

//включать списка базовых классов

приводит к ошибке компиляции: class NameQuery : public Query;

//опережающее объявление как производного,

//так и обычного класса содержит только имя класса

class Query;

Правильный вариант в данном случае выглядит так: class NameQuery;

Главное различие между базовыми классами Query и IntArray (см. раздел 2.4) состоит в том, что Query не представляет никакого реального объекта в нашем приложении.

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

определение абстрактного базового класса типа Query и одиночное открытое наследование ему.

Упражнение 17.1

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

книга

аудио-книга

аудиокассета

детская кукла

видеокассета

видеоигра для приставки SEGA

книга с подневной оплатой

видеоигра для приставки SONY

специальные правила выдачи и возврата. Организуйте их в иерархию наследования:

книга на компакт-диске

видеоигра для приставки Nintendo

Упражнение 17.2

Выберите или придумайте собственную абстракцию, содержащую семейство типов. Организуйте типы в иерархию наследования:

(a) Форматы графических файлов (gif, tiff, jpeg, bmp и т.д.)

(b) Геометрические примитивы (прямоугольник, круг, сфера, конус и т.д.)