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

Штерн В. - Основы C++. Методы программной инженерии - 2003

.pdf
Скачиваний:
267
Добавлен:
13.08.2013
Размер:
28.32 Mб
Скачать

640

Часть III • Программирование с агрегированием и наследованием

Обратите внимание, что такая структура вовсе не пропагандируется. Просто стоит ознакомиться с этим типом использования наследования. Факторы, которые следует принять во внимание, рассматривая компромиссные варианты,— это ко­ личество классов, которые следует реализовать, защита от неверного использо­ вания объектов и предотврандение распространения общей информации между классами в приложении.

Итоги

В данной главе наследование сравнивалось с другими методами программиро­ вания, например агрегацией и общими связями между классами.

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

Для иллюстрации структур рассмотрено также использование диаграмм UML на примерах. Диаграммы полезны, поскольку позволяют проектировщикам видеть всю картину в целом при обсуждении связей между классами. В примерах исполь­ зовались только основные конструкции UML. Язык UML в целом очень сложен. Вопрос о том, нужно ли энергично взяться за изучение UML или следует вначале сосредоточиться на глубоком освоении C+ + , достаточно спорный.

Поскольку эта книга посвящена языку C+ + , а не объектно-ориентированному анализу и проектированию, навыкам в C++ отдается большее предпочтение. Именно от вашего умения написать на C++ программу, которая передает обязан­ ности серверным классам, зависит качество и удобство сопровождения ПО.

Члс/mb IV

10,асширенное использование C++

] ^ \ ^ последней части книги обсуждается расширенное использование языка

Жв1|1^С+ + : виртуальные функции, абстрактные классы, расширенные пере-

^4u!l-^^^ груженные операции, шаблоны, исключительные ситуации, специаль­ ные типы и идентификация информации времени выполнения.

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

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

В главе 16 "Расширенное использование перегрузки операций" обсуждается расширенное использование перегрузки операций: унарные операторы, операторы, возвращаюш^ие элемент массива по индексу, операторы вызова функции и опера­ торы ввода/вывода. Как и в других случаях использования перегруженных опера­ торов, эти операторы создают великолепный синтаксис в юшентской программе. Во всем остальном вклад синтаксиса оператора в качество программ на языке C + + ограничен.

В главе 17 "Шаблоны: еш,е один инструмент проектирования" представлен один из методов языка C + + для проектирования повторного использования: обобщен­ ные шаблоны. Синтаксис определений шаблонов достаточно сложный. Их влия­ ние на размер объектной программы и на ее выполнение часто является вредным. Начинающие программисты на C + + должны соблюдать ограничения при постро­ ении своих собственных шаблонных классов.

Однако шаблонные классы, предоставляемые библиотекой стандартных шаб­ лонов C + + (Standard Template Library — STL), спроектированы очень хорошо и должны использоваться, если возможно, для сложных структур данных. Такие классы библиотеки шаблонов представляют исключительный пример проектиро­ вания и повторного использования программ.

В главе 18 "Программирование исключительных ситуаций" рассматривается обработка исключительных ситуаций — еще одного метода C + + . Это очень инте­ ресная область программирования. Возможно, следует попробовать использовать исключительные ситуации в ограниченных размерах. Скорее всего, потом вы сможете оценить, насколько полезна подобная методика в конкретной ситуации. Здесь также обсуждаются специальные типы и идентификация объектов времени выполнения.

Глава 19 "Подведение итогов" является обзором. В ней изложено то, о чем обычно говорится во введении. Мы надеемся, что читатель заинтересуется ис пользованием этого замечательного языка и сможет продуктивно его прим*^^'

%Г^а6(^jj

иртуальные функции и прочее расширенное

использование наследования

Темы данной главы

^Преобразования между несвязанными классами

^Преобразования между классами, связанными наследованием

^Виртуальные функции: еще одно новая идея

•^ Множественное наследование: несколько базовых классов

^Итоги

]^\^ предыдущей главе обсуждалась нотация UML для представления

ш~!5^-Связей "клиент-сервер" и рассматривались методы реализации этих

^^^^^^ связей в программах на C+ + .

Наиболее общей является связь включения (композиция или агрегация).

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

Чаще используется связь ассоциация. Если клиентский класс содержит указа­ тель или ссылку на объект серверного класса, он реализует общую ассоциацию между классами. Объект-сервер может совместно использоваться другими кли­ ентскими объектами.

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

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

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

644 Часть IV # Расширенное использование С^^-

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

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

Выберите подходяш^ий вариант — наименьшую степень видимости, при кото­ рой все еиде поддерживаются клиентские требования. Программист C + + должен помнить о выборе одной из трех альтернатив реализации ассоциации. Нотация UML в проекте одна и та же для этих трех методов. Часто проектировш,ики не знают, какой метод лучше подходит в том или ином случае. Они просто объявляют, что объекты связаны между собой. Следовательно, правильный выбор должен сделать программист. C+ + .

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

В этой главе обсуждается расширенное использование наследования с вир­ туальными функциями и абстрактными классами. Цель применения наследова­ ния — облегчить труд программисту клиентской части за счет выбора оптимальной структуры клиентской программы.

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

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

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

Рассмотрим вопросы, связанные с использованием объектов одного класса, в котором предполагается наличие объектов другого класса. Правила языка C+ + для такой подстановки с использованием наследования отличаются от правил для несвязанных объектов. Мы попытаемся объяснить вам, в каком направлении следует развивать свою интуицию.

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

Глава 15 • Виртуальные функции и использование наследования

| 645 |

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

Преобразования несвязанных классов

Как утверждалось ранее, C + + стремится поддерживать концепцию строгого контроля за типами. Желательно, чтобы этот принцип современного програм­ мирования был воспринят как естественный и привлекательный. Если в соответ­ ствии с контекстом программы ожидается объект конкретного типа, то было бы синтаксически неверно использовать вместо него объект другого типа.

Это правило применимо в следуюш^их контекстах:

Выражения и присваивания

Параметры функций (включая указатели и ссылки)

Объекты, используемые как цели сообпдений

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

Приведем небольшой пример, демонстрируюидий все три контекста, в кото­ рых C + + поддерживает строгий контроль типов. Суш,ествуют два класса (Base и Other), которые не связаны наследованием. Функция-член Base: :set() предпо­ лагает параметр целого типа, функция-член Other: :setOther() — параметр типа Вазе, а функция-член Other: :getOther() — указатель на объект Base. Для про­ стоты не были включены примеры с параметрами-ссылками, но Все, что будет сказано об указателях, также относится и к ссылкам.

class

Base {

/ / первый класс

int

х;

 

public:

// модификатор

void set(int а)

{ X = а; }

// средство доступа

int showO const

{return х; }

};

class Other {

 

// второй класс

int z ;

 

 

public:

b)

// изменение цели

void setOther(Base

{ z = b.showO; }

*b) const

// изменение параметра

void getOther(Base

{ b->set(z); }

 

 

} ;

 

 

I 646 I

Часть IV # Расширенное использование С^-^-

В следующей клиентской функции main() определяются три объекта для мани­ пулирования, по одному типа Base и типа Other и один числового типа. Во второй строке представлен параметр правильного типа и верная цель сообщения. Третья строка также правильная и тривиальная. Операнды выражения совместимы между собой, а цель соответствует типу значения.

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

Следуюидие два оператора также правильные. Имена сообщений в них (setOther() и getOther()) совпадают с именами функций-членов, описанных в целевом классе (класс Other), а параметры сообщений имеют правильный тип (класс в четвертой строке или класс Base в пятой строке). Все другие операторы в клиентской про­ грамме неправильные и превращены в комментарии. Рассмотрим каждый оператор.

int

mainO

 

// создание объектов

{ Other а; Base b; int x;

b.set(10) ;

// OK: правильные типы параметра и цели

X = 5 + 7.0;

// OK: правильный тип для выражений и lvalue

a.setOther(b) ;

//OK: правильный тип цели, параметра

a.getOther(&b) ; //OK: правильный тип цели, параметра

// b = 5 + 7;

//не ОК: неопределен operator = (int)

// X = b + 7;

// неOK: отсутствует оператор или преобразование для int

// b.set(a);

// неOK: объект как числовой параметр

// a.set0ther(5);

// неОК: невозможно преобразовать число в объект

/ /

a.getOther(&a)

// не ОК: отсутствует преобразование Other* вBase*

/ /

b.getOther(&b)

// не ОК: неверный тип цели, неэлемент

/ /

x.getOther(&b)

// неОК: число в качестве цели сообщения

return 0; }

В первом присваивании (см. ниже) компилятор ожидает значение числового типа. Вместо него используется тип, определенный программистом. Компилятор хотел бы, чтобы было определено присваивание operator=(int) с целыми пара­ метрами типа Base. В этом случае первый оператор стал бы законным. Во втором случае добавлен объект типа, определенного пользователем, и числовая пере­ менная. Такие типы несовместимы. Чтобы это выражение было допустимым, ком­ пилятор хотел бы, чтобы был определен operator+(int) для типа Base. В обоих случаях позиция С-Ы бескомпромиссная. Строгое соблюдение типа предот­ вращает появление ошибок на этапе компиляции, а не этапе выполнения.

В = 5 + 7; X = b + 7;

/ / синтаксическая ошибка

Следующие два оператора осуществляют передачу параметра. Если функция, например. Base:: set (int), ожидает параметр числового типа, то не допускается использование вместо него объекта класса, определенного программистом. Здесь мог бы помочь оператор преобразования, но он рассматривается только в следую­ щей главе. Напротив, если функция, например Other: :setOther(Base), ожидает параметр конкретного типа, определенного пользователем, то невозможно вместо него использовать числовое значение или значение некоторого другого типа, опре­ деленного пользователем. Во всех этих случаях компилятор отказывается выпол­ нять преобразование значения одного типа в значение другого типа и помечает их как ошибки на этапе компиляции.

b.set(a); a.set0ther(5);

/ / синтаксическая ошибка

Язык C++ также пытается поддерживать принцип строгого контроля типов для указателей и ссылок. Указатели и ссылки объединены вместе, поскольку правила для них одинаковы. Если у функции есть параметр, который определяется

Глава 15 • Виртуальные функции и использование наследования

647

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

a.getOther(&a) ;

/ / синтаксическая ошибка

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

Для целей сообш^ений концепция сильного контроля типов проявляется в огра­ ничении набора сообщений. Если имя сообндения, отправленного объекту, не об­ наруживается в спецификациях класса, возникает ошибка независимо от того, можно найти эту функцию в любом другом классе или нет. Для компилятора до­ статочно, что сообидение не находится в классе, к которому принадлежит цель сообш^ения. И, конечно, невозможно отправить сообш^ение числовой переменной или значению, потому что они не принадлежат ни одному классу и не могут соот­ ветствовать никаким сообш.ениям. Компилятору требуется переменная с типом, определенным пользователем, в левой части оператора выбора точки (dot selector operator).

•b.setOther(b); x. setOther(b) ;

/ / неверные типы цели

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

Other а; Base b;

// OK: совместимые типы

Base &г1 = b; Base *p1 = &b;

Base &г2 = a; Base *p2 = &a;

// несовместимые типы

Строгий и слабый контроль типов

Это идеальное решение всех проблем. Однако в языке C + + в строгих прави­ лах допускается множество исключений.

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

Некоторые компиляторы при использовании числового значения "старшего" типа, когда предполагается значение "младшего" типа, могут выдавать предупреждаюш.ее сообш^ение. Это происходит, например, при попытке сжать значение плавающего типа с двойной точностью в целое или в символьную переменную. Однако компилятор выдает лишь предупреждение, а не показывает синтаксиче­ скую ошибку. Вслед за С, C + + позволяет использовать явное приведение, чтобы показать устройству считывания желание проектировш^ика преобразовать значе­ ние одного числового типа в значение другого числового типа.

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

648 Часть IV » Расширенное использование C-t--^

С этой точки зрения, C + + (подобно С) является языком со слабым контролем типов. Здесь компилятор предполагает, что вам известно, что вы делаете. Если вам неизвестно или вы не уделяете внимания этой стороне вычислений, будем считать, что ваши вычисления действительно не зависят от точности усеченных значений.

C + + также поддерживает другие исключения из правил строгого соблюдения типов, которые несовместимы с языком С. Эти исключения вытекают из исполь­ зования специальных функций-членов и приведения типов:

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

Операторы преобразования

Приведение указателей (или ссылок)

Эти специальные функции представляют компилятору C + + способы указания принять клиентскую программу, которая нарушает правила строгого контроля типов.

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

Предположим, что

класс

Base

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

с числовым параметром.

 

 

Base::Base(int x i n i t

= 0)

/ /

конструктор преобразования

{ X = x I n i t ; }

 

 

 

При наличии этого конструктора оператор компилируется.

а.set0ther(5);

 

/ /

неверный тип, но не синтаксическая ошибка

Компилятор интерпретирует подобное сообш^ение так:

a.set0ther(Base(5));

 

/ / ' с точки зрения компилятора

Создается временный объект класса Base, инициализированный вызовом конст­ руктора преобразований. Он используется как фактический параметр правильно­ го типа, а затем уничтожается. Следовательно, требования сильного контроля типов рассматриваются на уровне компилятора — функция получает значение нужного типа. Эти требования не удовлетворяются на уровне программиста. Программист передает в setOther() параметр неверного типа.

Обратите внимание на то, что конструктор получает значение параметра по умолчанию. Для чего это сделано? Прежде чем этот конструктор был добавлен к классу Base, в нем имелся конструктор по умолчанию, предоставляемый систе­ мой. Вы можете определить объекты класса Base без параметров. При наличии "на месте" конструктора преобразования система убирает конструктор по умолча­ нию, а определения объектов Base без параметров не становятся синтаксическими ошибками.

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

Чтобы избежать подобных трудностей, можно было бы добавить к классу Ваз. определенный программистом конструктор, задаваемый по умолчанию, которыГ; ничего не делает. В этом случае не требуется инициализация объекта в конкретно^

Глава 15 • Виртуальные функции и использование наследования

649

значение, которое в дальнейшем даже не используется. Но из-за небрежности было добавлено нулевое значение параметра по умолчанию, чтобы обеспечить компилирование существующей клиентской программы. В чем недостаток подоб­ ного решения? Предположим, что нулевое значение используется где-то еще. Между тем оно не применяется. Мы знаем, что оно не используется, но програм­ мист, осуществляющий сопровождение программы, должен будет это установить. Следовательно, усложняется чтение программы.

Итак, добавление конструктора преобразования к классу Base позволяет ском­ пилировать вызов функции-члена Other: :setOther(Base) с реальным параметром числового типа.

a.set0ther(5);

/ / то же, что и a.set0ther(Base(5));

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

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

b = 5 + 7;

/ / н е ошибка: то же, что и b = Base(5+7);

Компилятор пытается предугадать цели программиста. Одна из задач проек­ тирования на С-Ы избежать этого и позволить программисту явно указать назначение данной программы. Один из способов явного указания, что подразуме­ вается, состоит в использовании явного приведения типов. Однако, согласно пра­ вилам слабого контроля типов в C/C-I- + , для преобразования числовых типов явное приведение не требуется, а вызов конструкторов преобразований может выполняться без явного вызова. Что делать? Стандарт ISO/ANSI предлагает ком­ промисс. Если проектировщик класса чувствует, что конструктор преобразования должен вызываться только явно, зарезервированное слово explicit используется как модификатор (см. главу 10).

explicit Base::Base(int x i n i t = 0)

/ / отсутствуют неявные вызовы

{ х = x I n i t ; }

 

Использование ключевого с/юва explicit является необязательным. Если вос­ пользоваться им при проектировании класса Base, то написать программу будет трудно. Также сложнее будет написать клиентскую программу (программист клиентской части должен будет использовать явное приведение типов), но понять полученную в результате программу станет легче. Если это ключевое слово не ис­ пользовать, ухудшится но качество программы. Прийти к компромиссу сложно.

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

a.get0ther(5) ;

/ / недопустимо, если конструктор определяется явно

Обратите внимание, что неявные преобразования применяются только к пара­ метрам, которые передаются по значению. Это не нужно в отношении параметров ссылок и указателей. Добавление конструктора преобразования не вызывает компиляции вызова Other: :getOther(Base* b) с цифровым параметром.

int X = 5;

a.getOther(&x) ;

// это все еще синтаксическая ошибка

Соседние файлы в предмете Программирование на C++