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

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

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

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

922

}

Тестируя программу, мы применили ее к нескольким текстам. Первым стал короткий рассказ Германа Мелвилла “Bartleby”. Здесь иллюстрируется составной запрос AndQuery, для которого подходящие слова расположены в соседних строках. (Отметим, что слова, заключенные между символами косой черты, предполагаются набранными курсивом.)

Введите запрос. Пожалуйста, разделяйте все его элементы пробелами. Запрос (или весь сеанс) завершается точкой ( . ).

==> John && Jacob && Astor

john ( 3 ) lines match jacob ( 3 ) lines match

john && jacob ( 3 ) lines match astor ( 3 ) lines match

john && jacob && astor ( 5 ) lines match

Requested query: john && jacob && astor

( 34 ) All who know me consider me an eminently /safe/ man. The late John Jacob

( 35 ) Astor, a personage little given to poethic enthusiasm, had no hesitation in

( 38 ) my profession by the late John Jacob Astor, a name which, I admit I love to

( 40 ) bullion. I will freely add that I was not insensible to the late John Jacob

( 41 ) Astor's good opinion.

Следующий запрос, в котором тестируются скобки и составные операторы, обращен к тексту новеллы “Heart of Darkness” Джозефа Конрада:

==> horror || ( absurd && mystery ) || ( North && Pole )

horror ( 5 ) lines match absurd ( 8 ) lines match mystery ( 12 ) lines match

( absurd && mystery ) ( 1 ) lines match

horror || ( absurd && mystery ) ( 6 ) lines match north ( 2 ) lines match

pole ( 7 ) lines match

( north && pole ) ( 1 ) lines match

horror || ( absurd && mystery ) || ( north && pole ) ( 7 ) lines match

Requested query: horror || ( absurd && mystery ) || ( north && pole ) ( 257 ) up I will go there.' The North Pole was one of these

( 952 ) horros. The heavy pole had skinned his poor nose

( 3055 ) some lightless region of subtle horrors, where pure, ( 3673 ) " 'The horror! The horror!'

( 3913 ) the whispered cry, 'The horror! The horror! '

( 3957 ) absurd mysteries not fit for a human being to behold. ( 4088 ) wind. 'The horror! The horror!'

Последний запрос был обращен к отрывку из романа Генри Джеймса “Portrait of a Lady”. В нем иллюстрируется составной запрос в применении к большому текстовому файлу:

==> clever && trick || devious

clever ( 46 ) lines match trick ( 12 ) lines match

clever && trick ( 2 ) lines match devious ( 1 ) lines match

clever && trick || devious ( 3 ) lines match

Requested query: clever && trick || devious

( 13914 ) clever trick she had guessed. Isabel, as she herself grew older

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

923

( 13935 ) lost the desire to know this lady's clever trick. If she had ( 14974 ) desultory, so devious, so much the reverse of processional. There were

Упражнение 17.23

Реализованная нами обработка запроса пользователя обладает одним недостатком: она не применяет к каждому слову те же предварительные фильтры, что и программа, строящая вектор позиций (см. разделы 6.9 и 6.10). Например, пользователь, который хочет найти слово “maps”, обнаружит, что в нашем представлении текста распознается только “map”,

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

Упражнение 17.24

Поисковую систему можно было бы усовершенствовать, добавив еще одну разновидность запроса И”, которую мы назовем InclusiveAndQuery и будем обозначать символом &. Строка текста удовлетворяет условиям запроса, если в ней находятся оба указанных слова, пусть даже не рядом. Например, строка

We were her pride of ten, she named us

удовлетворяет запросу:

pride & ten

но не:

pride && ten

Поддержите запрос InclusiveAndQuery.

Упражнение 17.25

Представленная ниже реализация функции display_solution() может выводить только в стандартный вывод. Более правильно было бы позволить пользователю самому задавать поток ostream, в который надо направить вывод. Модифицируйте display_solution() так, чтобы ostream можно было задавать. Какие еще изменения необходимо внести в определение класса UserQuery?

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

924

void TextQuery:: display_solution()

{

cout << "\n"

<<"Requested query: "

<<*query << "\n\n";

const set<short,less<short>,allocator> *solution = query- >solution();

if ( ! solution->size() ) { cout << "\n\t"

<<"Sorry, no matching lines were found in text.\n"

<<endl;

}

set<short>::const_iterator it = solution->begin(), end_it = solution->end();

for ( ; it != end_it; ++it ) { int line = *it;

// пронумеруем строки с 1 ...

cout << "( " << line+1 << " ) "

<< (*lines_of_text)[line] << '\n';

}

cout << endl;

}

Упражнение 17.26

Нашему классу TextQuery не хватает возможности принимать аргументы, заданные пользователем в командной строке.

(a)Предложите синтаксис командной строки для нашей поисковой системы.

(b)Добавьте в класс необходимые данные и функции-члены.

(c)Предложите средства для работы с командной строкой (см. пример в разделе 7.8).

Упражнение 17.27

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

(a)Реализуйте поддержку, необходимую для представления запроса AndQuery в виде одной строки, например “Motion Picture Screen Cartoonists”.

(b)Реализуйте поддержку для ответа на запрос на основе вхождения слов не в строку, а в предложение.

(c)Реализуйте подсистему хранения истории, с помощью которой пользователь мог бы ссылаться на предыдущий запрос по номеру, возможно, комбинируя его с новым запросом.

(d)Вместо того чтобы показывать счетчик найденных и все найденные строки,

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

==> John && Jacob && Astor

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

925

(1)john ( 3 ) lines match

(2)jacob ( 3 ) lines match

(3)john && jacob ( 3 ) lines match

(4)astor ( 3 ) lines match

(5)john && jacob && astor ( 5 ) lines match

//Новая возможность: пусть пользователь укажет, какой запрос выводить

//пользователь вводит число

==> вывести? 3

//Затем система спрашивает, сколько строк выводить

//при нажатии клавиши Enter выводятся все строки,

//но пользователь может также ввести номер одной строки или диапазон

ð сколько (Enter выводит все, иначе введите номер строки или диапазон)

1-3

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

926

18

18. Множественное и виртуальное

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

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

18.1. Готовим сцену

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

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

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

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

что в текущей версии своего инструмента мы решили воспользоваться каркасом приложений Open Inventor для C++ (см. [WERNECKE94]), но с помощью подтипизации расширили его, создав собственные абстракции нужных нам классов. Например, Open Inventor располагает тремя встроенными источниками освещения, производными от

class SoSpotLight : public SoLight { ... } class SoPointLight : public SoLight { ... }

абстрактного базового класса SoLight:

class SoDirectionalLight : public SoLight { ... }

Префикс So служит для того, чтобы дать уникальные имена сущностям, которые в области компьютерной графики весьма распространены (данный каркас приложений

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

927

проектировался еще до появления пространств имен). Точечный источник (point light) – это источник света, излучающий, как солнце, во всех направлениях. Направленный источник (directional light) – источник света, излучающий в одном направлении. Прожектор (spotlight) – источник, испускающий узконаправленный конический пучок, как обычный театральный прожектор.

По умолчанию Open Inventor осуществляет рендеринг графа сцены на экране с помощью библиотеки OpenGL (см. [NEIDER93]). Для интерактивного отображения этого достаточно, но почти все изображения, сгенерированные для киноиндустрии, сделаны с помощью средства RenderMan (см. [UPSTILL90]). Чтобы добавить поддержку такого алгоритма рендеринга мы, в частности, должны реализовать собственные специальные

class RiSpotLight : public SoSpotLight { ... } class RiPointLight : public SoPointLight { ... }

подтипы источников освещения:

class RiDirectionalLight : public SoDirectionalLight { ... }

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

В RenderMan направленный источник и прожектор поддерживают отбрасывание тени (поэтому мы называем их источниками освещения, дающими тень, – SCLS), а точечный нет. Общий алгоритм требует, чтобы мы обошли все источники освещения на сцене и составили карту теней для каждого включенного SCLS. Проблема в том, что источники освещения хранятся в графе сцены как полиморфные объекты класса SoLight. Хотя мы можем инкапсулировать общие данные и необходимые операции в класс SCLS, непонятно, как включить его в существующую иерархию классов Open Inventor.

В поддереве с корнем SoLight в иерархии Open Inventor нет такого класса, из которого можно было бы произвести с помощью одиночного наследования класс SCLS так, чтобы в дальнейшем уже от него произвести SdRiSpotLight и SdRiDirectionalLight. Если не пользоваться множественным наследованием, лучшее, что можно сделать, – это сравнить член класса SCLS с каждым возможным типом SCLS-источника и вызвать

SoLight *plight = next_scene_light();

if ( RiDirectionalLight *pdilite = dynamic_cast<RiDirectionalLight*>( plight ))

pdilite->scls.cast_shadow_map();

else

if ( RiSpotLight *pslite = dynamic_cast<RiSpotLight*>( plight ))

pslite->scls.cast_shadow_map();

соответствующую операцию:

// и так далее

(Оператор dynamic_cast это часть механизма идентификации типов во время выполнения (RTTI). Он позволяет опросить тип объекта, адресованного полиморфным указателем или ссылкой. Подробно RTTI будет обсуждаться в главе 19.)

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

928

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

рис. 18.1).

SoNode

 

SCLS

SoLight

 

 

SoPointLight

SoSpotLight

SoDirectionalLight

RPointLight

RSpotLight

RDirectionalLight

class RiDirectionalLight :

public SoDirectionalLight, public SCLS { ... };

class RiSpotLight :

public SoSpotLight, public SCLS { ... };

// ...

SoLight *plight = next_scene_light();

if ( SCLS *pscls = dynamic_cast<SCLS*>(plight))

Рис. 18.1. Множественное наследование источников освещения pscls->cast_shadow_map();

Это решение несовершенно. Если бы у нас был доступ к исходным текстам Open Inventor, то можно было бы избежать множественного наследования, добавив к SoLight член-

class SoLight : public SoNode { public:

void cast_shadow_map()

{if ( _scls ) _scls->cast_shadow_map(); }

//...

protected: SCLS *_scls;

};

// ...

SdSoLight *plight = next_scene_light();

указатель на SCLS и поддержку операции cast_shadow_map(): plight-> cast_shadow_map();

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

929

Самое распространенное приложение, где используется множественное (и виртуальное) наследование, – это потоковая библиотека ввода/вывода в стандартном C++. Два основных видимых пользователю класса этой библиотеки istream (для ввода) и ostream (для вывода). В число их общих атрибутов входят:

информация о форматировании (представляется ли целое число в десятичной, восьмеричной или шестнадцатеричной системе счисления, число с плавающей точкой в нотации с фиксированной точкой или в научной нотации и т.д.);

информация о состоянии (находится ли потоковый объект в нормальном или ошибочном состоянии и т.д.);

информация о параметрах локализации (отображается ли в начале даты день или месяц и т.д.);

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

Эти общие атрибуты вынесены в абстрактный базовый класс ios, для которого istream и ostream являются производными.

Класс iostream наш второй пример множественного наследования. Он предоставляет поддержку для чтения и записи в один и тот же файл; его предками являются классы istream и ostream. К сожалению, по умолчанию он также унаследует два различных экземпляра базового класса ios, а нам это не нужно.

Виртуальное наследование решает проблему наследования нескольких экземпляров базового класса, когда нужен только один разделяемый экземпляр. Упрощенная иерархия iostream изображена на рис. 18.2.

 

ios

 

 

istream

ostream

ifstream

iostream

ofstream

fstream

Рис. 18.2. Иерархия виртуального наследования iostream (упрощенная)

Еще один реальный пример виртуального и множественного наследования дают распределенные объектные вычисления. Подробное рассмотрение этой темы см. в серии статей Дугласа Шмидта (Douglas Schmidt) и Стива Виноски (Steve Vinoski) в [LIPPMAN96b].

В данной главе мы рассмотрим использование и поведение механизмов виртуального и множественного наследования. В другой нашей книге, “Inside the C++ Object Model”, описаны более сложные вопросы производительности и дизайна этого аспекта языка.

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

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

930

свои имена: Линь-Линь, Маугли или Балу. Каждое животное принадлежит к какому-то виду; скажем, Линь-Линь это гигантская панда. Виды в свою очередь входят в семейства. Так, гигантская панда член семейства медведей, хотя, как мы увидим в разделе 18.5, по этому поводу в зоологии долго велись бурные дискуссии. Каждое семейство член животного мира, в нашем случае ограниченного территорией зоопарка.

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

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

18.2. Множественное наследование

Для поддержки множественного наследования синтаксис списка базовых классов

class Bear : public ZooAnimal { ... };

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

class Panda : public Bear, public Endangered { ... };

Для каждого из перечисленных базовых классов должен быть указан уровень доступа: public, protected или private. Как и при одиночном наследовании, множественно наследовать можно только классу, определение которого уже встречалось ранее.

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

В случае множественного наследования объект производного класса содержит по одному подобъекту каждого из своих базовых (см. раздел 17.3). Например, когда мы пишем

Panda ying_yang;

то объект ying_yang будет состоять из подобъекта класса Bear (который в свою очередь содержит подобъект ZooAnimal), подобъекта Endangered и нестатических членов, объявленных в самом классе Panda, если таковые есть (см. рис. 18.3).

ZooAnimal

Endangered

Bear

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

931

Panda

Рис. 18.3. Иерархия множественного наследования класса Panda

Конструкторы базовых классов вызываются в порядке объявления в списке базовых классов. Например, для ying_yang эта последовательность такова: конструктор Bear (но поскольку класс Bear производный от ZooAnimal, то сначала вызывается конструктор ZooAnimal), затем конструктор Endangered и в самом конце конструктор Panda.

Как отмечалось в разделе 17.4, на порядок вызова не влияет ни наличие базовых классов в списке инициализации членов, ни порядок их перечисления. Иными словами, если бы

конструктор Bear вызывался неявно и потому не был бы упомянут в списке

//конструктор по умолчанию класса Bear вызывается до

//конструктора класса Endangered с двумя аргументами ...

Panda::Panda()

: Endangered( Endangered::environment,

Endangered::critical )

инициализации членов, как в следующем примере:

{ ... }

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

Порядок вызова деструкторов всегда противоположен порядку вызова конструкторов. В нашем примере деструкторы вызываются в такой последовательности: ~Panda(),

~Endangered(), ~Bear(), ~ZooAnimal().

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

Однако такую ошибку вызывает не потенциальная неоднозначность неквалифицированного доступа к одному из двух одноименных членов, а лишь попытка фактического обращения к нему (см. раздел 17.4). Например, если в обоих классах Bear и Endangered определена функция-член print(), то инструкция

ying_yang.print( cout );

приводит к ошибке компиляции, даже если у двух унаследованных функций-членов

Error: ying_yang.print( cout ) -- ambiguous, one of

Bear::print( ostream& )

разные списки параметров.