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

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

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

570

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

 

publBO;

// OK, если вDerived стал public

 

protB=0;

// OK, если вDerived стал protected

 

} } ;

 

class Client {

 

public: ClientO

// объекты производного ибазового классов

{

Derived d; Base b;

d.publDO;

// часть public класса Derived: OK

d.publBO;

// OK, если вDerived стала public

/ /

d.privD = d.protD = 0;

// отличная отpublic часть Derived: не ОК

/ /

d.privB=d.protB-0;

// отличная отpublic часть Base вDerived: не ОК

b.publBO;

// часть public Base объекта Base: OK

 

} } ;

 

int

mainO

// создает объект, выполняет программу

{

Client c;

return 0;

 

}

 

 

Благодаря наследованию private можно сделать архитектуру программы

 

весьма запутанной, закрыть доступ к одним компонентам и открыть к другим.

 

Получится головоломка, которую вы можете сгордостью показать своим коллегам

 

и спросить: "Отгадайте,что здесьделается".

 

Н а самом деле С + + позволяет не только управлять правами доступа, но

 

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

 

В производных

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

класса незакрытыми, установив их, кпримеру, врежим protected или public.

Режим наследования по умолчанию

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

Листинг 13.12. Пример использования для классов режима наследования по умолчанию

class Base {

// доступен только из Base

 

private: int privB;

 

protected: int protB;

// доступен из Base и Derived

 

public: void publB()

// доступен из Base и Derived

 

{ privB =0; protB = 0;} }

// OKдля доступа ксобственным данным

class Derived : Base {

// поумолчанию private

 

private: int privD;

 

 

protected: int protD;

 

 

public: void publD()

// ОКдля доступа

 

{ privD =0; protD = 0;protB - 0; } }

int mainO

// объект производного класса

{

Derived d;

//

d.publDO;

// ОКдля доступа кчасти public класса Derived

d.publBO;

// не ОКдля доступа кчасти public класса Base

 

return 0;

 

 

}

 

Глава 13 • Подобные классы и их интерпретация

571

По умолчанию режим наследования для производного класса C++ является закрытым. Если забыть указать режим, то компилятор предполагает, что приме­ няется режим наследования private. Листинг 13.12 показывает пример скелета программы, где программист забыл обозначить режим наследования. В результате клиент не может обращаться к общедоступному методу publB(), унаследованному целевым классом Derived из класса Base.

Все это не так просто, как кажется. Режим по умолчанию при наследовании будет закрытым для производного класса, создаваемого не только с помощью клю­ чевого слова class. Вспомните, что ключевые слова class и struct обозначают одно и то же, за исключением назначаемых по умолчанию прав доступа к элемен­ там данных и функциям. Для класса он будет private, а для структуры — public. В остальном они одинаковы. Можно определять функции-члены в структуре, со­ здавать для них перегруженные функции и назначать аргументы по умолчанию, использовать конструкторы и деструкторы, элементы данных других классов (и структур), списки инициализации и все то, что отличает объектно-ориентиро­ ванное программирование от процедурного. Вы можете наследовать из структуры и сделать так, чтобы структура наследовала из класса или класс из структуры. Все это законно в C+ + . Только для производного класса, определенного с помощью ключевого слова struct, режимом по умолчанию будет public, а не private.

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

Листинг 13.13. Пример использования режима наследования по умолчанию для структур

class Base {

private: int privB; protected: int protB; public: void publB()

{ privB = 0; protB = 0;

struct Derived : Base { private: int privD; protected: int protD; public: void publD()

{ privD = 0; protD = 0; protB = 0; } }

int mainO

{ Derived d d.publDO d.publBO return 0;

}

/ /

доступен только из Base

/ /

доступен из Base и Derived

/ /

доступен из Base и Derived

/ /

OK для доступа к собственным данным

/ /

по умолчанию private

/ /

OK для доступа

/ /

объект производного класса

/ /

ОК для доступа к части public класса Derived

/ /

теперь это вполне законно

Пример согласуется с правилами, применяемыми в С+Ч- по умолчанию для доступа к компонентам классов. Вспомните, что когда класс определяется с ис­ пользованием ключевого слова class, по умолчанию права доступа к компонентам класса будут private. Когда класс определяется с помощью ключевого слова struct, по умолчанию права доступа к его компонентам будут public.

Аналогично, при создании производного класса с указанием ключевого слова class режимом наследования является private. Когда класс создается из другого класса с ключевым с^ювом struct, режимом наследования будет public. Вся разница заключается в способе определения производного класса. Базовый класс может определяться с любым ключевым словом — class или struct,— это не влияет на режим наследования производного класса.

Не стоит полагаться на режимы по умолчанию.

I 572 I

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

Правила области действия и разрешение имен при наследовании

При наследовании области действия классов С + + можно рассматривать как вложенные. С этой точки зрения область действия производного класса "вклады­ вается" в область действия своего базового класса.

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

void

foo()

 

 

 

{ int х;

 

/ /

внешняя область действия: эквивалент базового класса

{

int

у;

/ /

внутренняя область действия: эквивалент производного класса

 

X = 0; }

/ /

ОК для доступа

к имени из внешней области

у = 0;

}

/ /

синтаксическая

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

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

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

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

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

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

int х;

/ /

внешняя область действия: ее скрывает класс или функция

class Base {

 

 

protected: int x;

/ /

имя базового класса скрывает глобальное имя

} ;

 

 

Глава 13 « Подобные классы и их интерпретация

573

class Derived

: public Base {

 

int x;

 

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

 

public:

 

 

 

void foo()

 

 

 

{ int x;

 

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

 

X = 0; } } ;

 

class Client {

 

 

public:

 

 

 

ClientO

 

 

 

{ Derived d;

// использование объекта d как получателя сообщения

d.fooO;

} } ;

int mainO

{ Client

с;

/ / определение объекта, выполнение программы

return

0;

}

В этом примере видно, что используются локальная переменная в функциичлене foo() класса Derived, элемент данных класса Derived, элемент данных класса Base и глобальная переменная в области действия файла. Оператор х = 0; в Derived: :foo() устанавливает локальную переменную х в значение 0. Элемент данных производного класса Derived: :х, элемент данных базового класса Base: :х и глобальное имя х скрываются этим локальным именем, так как оно определено во внутренней вложенной области действия.

Закомментируйте определение переменной х в методе fоо(). Тогда в операторе х = 0; при разрешении имен используется не локальная переменная — ее имя найдено не будет. Если имя не найдено в области действия оператора (в данном случае это компонентная функция производного класса), то компилятор просмат­ ривает область действия производного класса и ищет имя среди элементов данных и функций. В приведенном примере, если бы отсутствовала локальная перемен­ ная X в функции Derived: :foo(), использовалось бы имя Derived: :х. Оно будет устанавливаться в О оператором х = 0; в функции-члене Derived: :foo() производ­ ного класса.

Если упомянутое в функции имя не найдено и в области действия класса, то компилятор выполняет поиск в базовом классе. Первое имя, которое находит при таком поиске компилятор, будет использоваться для генерации объектного кода. Если бы обе переменные х в классе Derived отсутствовали (локальная переменная и элемент данных), то элемент данных Base: :х устанавливался бы в значение О оператором х = 0;.

Наконец, если компилятор не находит имя ни в одном из базовых классов, он осуществляет поиск в области действия файла (как глобальный объект, опреде­ ленный в области действия файла, или глобальный объект extern, объявленный в области действия файла, но определенный в другом месте). Если при таком про­ цессе имя обнаруживается, оно используется. Если нет, то генерируется синтакси­ ческая ошибка. В приведенном примере, если бы ни класс Derived, ни класс Base не использовали имя х, то в О оператором Derived: :foo() устанавливалась бы глобальная переменная х.

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

Ниже показан модифицированный пример с двумя классами — Base и Derived. Здесь заданы две функции foo(): одна — общедоступная функция-член класса Base, другая — общедоступная функция-член класса Derived. Клиент определяет

574

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

объект класса Derived и передает ему сообидение foo(). Так как производный класс DerivedO определяет функцию-член foo(), вызывается функция-член про­ изводного класса. Если класс Derived не обозначает функцию fоо(), то компиля­ тор генерирует вызов функции foo() базового класса. Функция базового класса имеет шанс только в том случае, если данное имя не используется в производном классе.

class Base { protected: int x;

public:

void foo() // имя из класса Base скрывается именем из класса Derived { X = 0; } } :

class Derived

: public Base {

public:

// имя из класса Derived скрывает имя из класса Base

void foo()

{ X = 0; } }

;

class Client { public:

ClientO

{ Derived d;

d.fooO; } } ; // вызов функции-члена Derived

int mainO

{ Client

c;

/ / создание объекта, вызов конструктора

return

0;

}

Обратите внимание, что в данном примере не вводится глобальная область действия. Если ни класс Base, ни класс Derived (и ни один из их предков) не имеет функции-члена foo(), то вызов функции d.foo() даст синтаксическую ошибку. Если бы функция foo() определялась в глобальной области действия, то вызов функции d. foo() в любом случае не вызывал бы эту глобальную функцию.

void foo()

{ int X = 0; }

Эта глобальная функция не скрывается функцией-членом foo() в классе Derived (или Base), поскольку имеет другой интерфейс. Такая функция-член вызывается с помощью целевого объекта, а глобальная — когда применяется только имя функции.

foo();

/ / вызов глобальной функции

Обсуждаемые вызовы функции имеют другую синтаксическую форму:

d.fooO;

//вызов функции-члена

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

Перегрузка и сокрытие имен

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

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

 

 

 

Глава 13 « Подобные класоы и их интерпретация

575

 

 

зрения соответствия аргументов? Генерируется синтаксическая ошибка. А что,

 

 

если функция в базовом классе подходит лучше — имеет то же имя и соответст­

 

 

вующую сигнатуру? Слишком поздно. У базового класса нет шансов.

 

 

 

К сожалению, многие программисты понимают это не до конца. Проработайте

 

 

правила вложенных областей действия и убедитесь, что вы все поняли. Из следую-

 

 

ш,его примера было исключено все лишнее, не относяш,ееся к вопросу сокрытия

 

 

имен во вложенных областях действия, и оставлен лишь небольшой фрагмент.

 

 

Листинг 13.14 показывает упрош,енную часть иерархии классов из бухгалтер­

 

 

ской программы. Здесь используются только классы Account и CheckingAccount.

Итоговые

балансы

 

В производном классе переопределяется функция-член

 

withdrawO базового класса. В клиенте определяются

объект расчетного счета

1099.8

объекты CheckingAccount, им отправляются сообш^ения,

 

 

 

 

принаддежаш.ие базовому классу (getBal() и deposit())

Рис. 13.10. Результат

программы

или производному классу (withdrawO). Результат про­

 

 

из листинга

13.14

граммы продемонстрирован на рис. 13.10.

 

Листинг 13.14. Пример иерархии наследования для классов Account

 

#include

<iostream>

 

 

 

using

namespace std;

 

 

 

class

Account {

 

/ / базовый класс

 

protected:

 

 

 

double

balance;

 

 

 

public:

 

 

 

 

Account(double initBalance = 0)

 

 

{ balance = initBalance; }

// наследование без изменений

 

double getBalO

 

 

{ return balance; }

 

 

 

void withdraw(double amount)

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

 

{ if (balance > amount)

 

 

balance -=amount; }

 

 

void deposit(double

amount)

// наследуется без изменений

 

{ balance +=amount; }

 

} ;

 

 

 

 

 

class CheckingAccount

public Account

// производный класс

 

double fee;

 

 

 

 

 

public:

 

 

 

 

CheckingAccount(double initBalance)

 

 

{ balance = initBalance; fee = 0.2;

 

 

void withdraw(double

amount)

// скрывает метод базового класса

 

{ if (balance > amount)

 

 

}

balance = balance - amount fee;" }

 

 

 

 

 

 

int mainO

 

// объект производного класса

 

{ CheckingAccount a1(1000);

 

a1.withdraw(100);

 

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

 

a1.deposit(200);

 

// метод базового класса

 

cout «

" Итоговые балансы\п";

a1.getBal() « endl;

 

cout «

" объект CheckingAccount: " «

 

return 0;

}

576

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

void

CheckingAccount::deposit

(double

amount, double fee)

{

balance = balance + amount

- fee;

}

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

a1.deposit(200,5);

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

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

До C++ мы работали со многими языками программирования, и ранее ничего подобного не происходило. Предположим, что и вы, какие бы языки вам прежде не встречались, не видели ничего похожего. Это еще один вклад C++ в технику разработки ПО.

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

всуществующем коде не возникают синтаксические ошибки.

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

Определим причины возникновения таких необычных трудностей программи­ рования. Для этого снова рассмотрим класс CheckingAccount.

class CheckingAccount ; public Account { double fee;

public:

CheckingAccount(double initBalance)

{balance = initBalance; fee = 0.2; } void withdraw(double amount)

{i f (balance > amount )

balance = balance - amount - fee; } void deposit(double amount, double fee) { balance = balance + amount - fee; }

}

/ / скрывает метод базового класса

/ / новый метод / / скрывает метод базового класса

Когда компилятор обрабатывает существующие 200 страниц клиента, вызов функции-члена depositO предполагает обращение к функции базового класса Account::depositO с одним параметром.

a1.deposit(200);

// метод базового класса?

Глава 13 • Подобные классы и их интерпретация

577

Согласно правилам разрешения имен, о которых рассказывалось выше, компи­ лятор анализирует тип получателя сообш,ения, находит объект a1, принадлежандий классу CheckingAccount, иш,ет в классе CheckingAccount функцию-член deposit(). Найдя ее, он прекраш^ает поиск по цепочке наследования. Далее следует проверка сигнатуры. Компилятор обнаруживает, что метод CheckingAccount:: deposit(), найденный в производном классе, имеет два параметра. Между тем клиент (вызываюш,ий метод базового класса) подставляет только один параметр. В результате выводится сообщение о синтаксической ошибке.

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

Хотелось бы, чтобы компилятор рассматривал эту ситуацию как перегрузку имен функций. Есть функция deposit() в базовом классе с одним параметром. Имеется функция deposit () с двумя параметрами в производном классе. Однако объект производного класса является также объектом базового класса! Он уна­ следовал функцию depositO с одним параметром. В производном классе получи­ лось две функции depositO — с одним и с двумя параметрами. Было бы неплохо, если бы компилятор использовал правила перегрузки имен функций и выбрал верную функцию — с одним параметром. Между тем, как уже отмечалось выше, когда метод базового класса скрывается методом производного класса, у метода базового класса не остается шансов. Перегрузка применяется к нескольким функ­ циям в одной области действия. "Сокрытие" имеет место для функций в разных вложенных областях действия.

О с т о р о ж н о ! C++ поддерживает перегрузку имен функций только

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

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

На рис. 13.11 показан объект производного класса с этими двумя функция­ ми — одной из базового класса, другой из производного. Вертикальная стрелка от клиента демонстрирует, что компилятор начинает поиск с производного класса.

 

класс Account

 

класс CheckingAccount

 

 

1

 

 

1

 

 

deposit(x)

 

deposit(x,y)

 

 

1

 

 

1

 

 

 

 

метод базового класса скрыт

 

 

 

 

метод производного класса

 

 

 

 

скрывает метод базового класса

часть

ZL

 

 

в объекте производного класса

базового

deposit(x)

 

 

 

 

класса

 

 

 

 

 

часть

deposit(x,y)

 

 

 

производного

 

 

 

класса

 

I.

 

 

 

объект класса \

 

 

Client code

CheckingAccount

 

 

 

 

Р и с . 13 . 1 1.

Как метод производного

класса

скрывает

 

 

мет,од базового

класса

в объекте

производного

класса

578

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

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

Вызов метода базового класса, скрытого производным классом

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

i nt

mainO

// объект производного класса

{ CheckingAccount a1(1000);

a1.withclraw(100);

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

//

a1.cieposit(200);

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

a1.Account::deposit(200);

// решение проблемы

cout «

" Итоговые балансы\п";

a1.getBal() « endl;

cout «

" объект CheckingAccount: " «

return 0; }

 

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

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

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

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

class

Account {

 

/ /

базовый класс

protected:

 

 

 

double balance;

 

 

public:

 

 

 

Account(double initBalance^O)

 

 

{

balance = initBalance;}

 

 

double getBalO

/ /

наследуется без изменений

{

return balance; }

 

 

void withdraw(double amount)

/ /

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

{

i f

(balance

> amount)

 

 

 

 

balance

-= amount; }

 

 

 

 

Глава 13 • Подобные классы и их интерпретация

579

void deposit(double amount)

 

// наследуется без изменений

{ balance +=amount }

 

// совмещение depositO

void deposit(double amount, double fee)

{ balance = balance + amount - fee; } }

;

 

 

 

class CheckingAccount : public Account

{

// производный класс

 

double fee;

 

 

 

 

 

public:

 

 

 

 

 

 

CheckingAccount(double initBalance)

 

 

 

 

 

{ balance = initBalance; fee = 0.2; }

 

// скрывает метод базового класса

void withdraw(double amount)

 

{ if (balance > amount)

 

 

 

 

 

 

balance = balance - amount - fee; } } ;

 

 

 

int mainO

 

 

 

 

 

{ CheckingAccount a1(1000);

 

/ /

объект производного

класса

a1.withdraw(100);

 

/ /

метод производного

класса

a1.deposit(200);

 

/ /

существующий код клиента

a1.

deposit(200,5);

 

/ /

новый код

клиента

 

cout

«

" Итоговые балансы\п";

 

 

 

 

 

cout

«

" объект CheckingAccount: " «

a1.getBal() «

endl;

 

return

0; }

 

 

 

 

 

Хорошее решение. Обратите внимание, что оно реализовано в виде добавления программного кода к серверному классу, а не в форме модификации клиента. В этом случае работа переносится на класс Account, и это хорошо. Однако оно требует открывать и изменять базовый, а не производный класс. Это нежелатель­ но из соображений управления конфигурацией. Чем выше класс в иерархии на­ следования, тем больше нужно заш^ищать его от изменений, так как они могут повлиять на производные классы. И наоборот, чем ниже класс в иерархии насле­ дования, тем безопаснее его открывать и изменять.

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

кэлементам данных того же класса, но не к элементам данных производного класса.

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

void CkeckingAccount::deposit(double

amount, double fee)

{ balance = balance + amount - fee -

CheckingAccount: :fee; }

Однако включение нового метода depositO в производный класс возвраш.ает

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

базового класса depositO с одним аргументом, что синтаксически некорректно.

Лучше всего разместить функцию depositO в производном классе. Для вызо­

вов функции depositO с одним допустимым параметром можно совместить функ­

цию depositO в производном классе, а не в базовом. Кроме того, производный класс — это серверный класс клиента, и такое решение состоит в переносе обя­ занностей на серверный класс.

С о в е т у е м всегда ищите способы писать программу C++ так, чтобы можно было перенести обязанности с клиента на сервер.

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

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