Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf570 |
Часть 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; |
|
|
} |
|
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;
}
Глава 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++ так, чтобы можно было перенести обязанности с клиента на сервер.
В результате клиент будет выражать смысл вычислений, а не их детали. Этот общий принцип сослужит вам хорошую службу.
