<< Пред.           стр. 90 (из 121)           След. >>

Список литературы по разделу

  // ...
 };
 Еще одна причина для открытия отдельных членов заключается в том, что иногда необходимо разрешить доступ к защищенным членам закрыто унаследованного базового класса при последующем наследовании. Предположим, что пользователям нужен подтип стека PeekbackStack, который может динамически расти. Для этого классу, производному от PeekbackStack, понадобится доступ к защищенным элементам ia и _size класса IntArray:
 template
 class PeekbackStack : private IntArray {
 public:
  using intArray::size;
  // ...
 
 protected:
  using intArray::size;
  using intArray::ia;
  // ...
 };
 Производный класс может лишь вернуть унаследованному члену исходный уровень доступа, но не повысить или понизить его по сравнению с указанным в базовом классе.
 На практике множественное наследование очень часто применяется для того, чтобы унаследовать открытый интерфейс одного класса и закрытую реализацию другого. Например, в библиотеку классов Booch Components включена следующая реализация растущей очереди Queue (см. также статью Майкла Вило (Michaeel Vilot) и Грейди Буча (Grady Booch) в [LIPPMAN96b]):
 template < class item, class container >
 class Unbounded_Queue:
  private Simple_List< item >, // реализация
  public Queue< item > // интерфейс
 { ... }
 18.3.3. Защищенное наследование
 Третья форма наследования – это защищенное наследование. В таком случае все открытые члены базового класса становятся в производном классе защищенными, т.е. доступными из его дальнейших наследников, но не из любого места программы вне иерархии классов. Например, если бы нужно было унаследовать PeekbackStack от Stack, то закрытое наследование
 // увы: при этом не поддерживается дальнейшее наследование
 // PeekbackStack: все члены IntArray теперь закрыты
 class Stack : private IntArray { ... }
 было бы чересчур ограничительным, поскольку закрытие членов IntArray в классе Stack делает невозможным их последующее наследование. Для того чтобы поддержать наследование вида:
 class PeekbackStack : public Stack { ... };
 класс Stack должен наследовать IntArray защищенно:
 class Stack : protected IntArray { ... };
 18.3.4. Композиция объектов
 Есть две формы композиции объектов:
 композиция по значению, когда членом одного класса объявляется сам объект другого класса. Мы показывали это в исправленной реализации PeekbackStack;
 композиция по ссылке, когда членом одного класса является указатель или ссылка на объект другого класса.
 Композиция по значению обеспечивает автоматическое управление временем жизни объекта и семантику копирования. Кроме того, прямой доступ к объекту оказывается более эффективным. А в каких случаях следует предпочесть композицию по ссылке?
 Предположим, что мы решили с помощью композиции представить класс Endangered. Надо ли определить его объект непосредственно внутри ZooAnimal или сослаться на него с помощью указателя или ссылки? Сначала выясним, все ли объекты ZooAnimal обладают этой характеристикой, а если нет, то может ли она изменяться с течением времени (допустимо ли добавлять или удалять эту характеристику).
 Если ответ на первый вопрос положительный, то, как правило, лучше применить композицию по значению. (Как правило, но не всегда, поскольку с точки зрения эффективности включение больших объектов не оптимально, особенно когда они часто копируются. В таких случаях композиция по ссылке позволит обойтись без ненужных копирований, если применять при этом подсчет ссылок и технику, называемую копированием при записи. Увеличение эффективности, правда, достигается за счет усложнения управления объектом. Обсуждение этой техники не вошло в наш вводный курс; тем, кому это интересно, рекомендуем прочитать книгу [KOENIG97], главы 6 и 7.)
 Если же оказывается, что только некоторые объекты класса ZooAnimal обладают указанной характеристикой, то лучшим вариантом будет композиция по ссылке (скажем, в примере с зоопарком не имеет смысла включать в процветающие виды большой объект, описывающий виды вымирающие).
 Поскольку объекта Endangered может и не существовать, то представлять его надо указателем, а не ссылкой. (Предполагается, что нулевой указатель не адресует объект. Ссылка же всегда должна именовать определенный объект. В разделе 3.6 это различие объяснялось более подробно.)
 Если ответ на второй вопрос положительный, то необходимо задать функции, позволяющие вставить и удалить объект Endangered во время выполнения.
 В нашем примере лишь небольшая часть всего множества животных в зоопарке находится под угрозой вымирания. Кроме того, по крайней мере теоретически, данная характеристика не является постоянной, и, допустим, в один прекрасный день это может перестать грозить панде.
 class ZooAnimal {
 public:
  // ...
  const Endangered* Endangered() const;
  void addEndangered( Endangered* );
  void removeEndangered();
  // ...
 protected:
  Endangered *_endangered;
  // ...
 };
 Если предполагается, что наше приложение будет работать на разных платформах, то полезно инкапсулировать всю платформенно-зависимую информацию в иерархию абстрактных классов, чтобы запрограммировать платформенно-независимый интерфейс. Например, для вывода объекта ZooAnimal на дисплей UNIX-машины и ПК, можно определить иерархию классов DisplayManager:
 class DisplayManager { ... };
 class DisplayUNIX : public DisplayManager { ... };
 class DisplayPC : public DisplayManager { ... };
 Наш класс ZooAnimal не является разновидностью класса DisplayManager, но содержит экземпляр последнего посредством композиции, а не наследования. Возникает вопрос: использовать композицию по значению или по ссылке?
 Композиция по значению не может представить объект DisplayManager, с помощью которого можно будет адресовать либо объект DisplayUNIX, либо объект DisplayPC. Только ссылка или указатель на объект DisplayManager позволят нам полиморфно манипулировать его подтипами. Иначе говоря, объектно-ориентированное программирование поддерживается только композицией по ссылке (подробнее см. [LIPPMAN96a].)
 Теперь нужно решить, должен ли член класса ZooAnimal быть ссылкой или указателем на DisplayManager:
 член может быть объявлен ссылкой лишь в том случае, если при создании объекта ZooAnimal имеется реальный объект DisplayManager, который не будет изменяться по ходу выполнения программы;
 если применяется стратегия отложенного выделения памяти, когда память для объекта DisplayManager выделяется только при попытке вывести объект на дисплей, то объект следует представить указателем, инициализировав его значением 0;
 если мы хотим переключать режим вывода во время выполнения, то тоже должны представить объект указателем, который инициализирован нулем. Под переключением мы понимаем предоставление пользователю возможности выбрать один из подтипов DisplayManager в начале или в середине работы программы.
 Конечно, маловероятно, что для каждого подобъекта ZooAnimal в нашем приложении будет нужен собственный подтип DisplayManager для отображения. Скорее всего мы ограничимся статическим членом в классе ZooAnimal, указывающим на объект DisplayManager.
 Упражнение 18.6
 Объясните, в каких случаях имеет место наследование типа, а в каких – наследование реализации:
 (a) Queue : List // очередь : список
 (b) EncryptedString : String // зашифрованная строка : строка
 (c) Gif : FileFormat
 (d) Circle : Point // окружность : точка
 (e) Dqueue : Queue, List
 (f) DrawableGeom : Geom, Canvas // рисуемая фигура : фигура, холст
 Упражнение 18.7
 Замените член IntArray в реализации PeekbackStack (см. раздел 18.3.1) на класс deque из стандартной библиотеки. Напишите небольшую программу для тестирования.
 Упражнение 18.8
 Сравните композицию по ссылке с композицией по значению, приведите примеры их использования.
 18.4. Область видимости класса и наследование
 У каждого класса есть собственная область видимости, в которой определены имена членов и вложенные типы (см. разделы 13.9 и 13.10). При наследовании область видимости производного класса вкладывается в область видимости непосредственного базового. Если имя не удается разрешить в области видимости производного класса, то поиск определения продолжается в области видимости базового.
 Именно эта иерархическая вложенность областей видимости классов при наследовании и делает возможным обращение к именам членов базового класса так, как если бы они были членами производного. Рассмотрим сначала несколько примеров одиночного наследования, а затем перейдем к множественному. Предположим, есть упрощенное определение класса ZooAnimal:
 class ZooAnimal {
 public:
  ostream &print( ostream& ) const;
 
  // сделаны открытыми только ради демонстрации разных случаев
  string is_a;
  int ival;
 private:
  double dval;
 };
 и упрощенное определение производного класса Bear:
 class Bear : public ZooAnimal {
 public:
  ostream &print( ostream& ) const;
 
  // сделаны открытыми только ради демонстрации разных случаев
  string name;
  int ival;
 };
 Когда мы пишем:
 Bear bear;
 bear.is_a;
 то имя разрешается следующим образом:
 bear – это объект класса Bear. Сначала поиск имени is_a ведется в области видимости Bear. Там его нет.
 Поскольку класс Bear производный от ZooAnimal, то далее поиск is_a ведется в области видимости последнего. Обнаруживается, что имя принадлежит его члену. Разрешение закончилось успешно.
 Хотя к членам базового класса можно обращаться напрямую, как к членам производного, они сохраняют свою принадлежность к базовому классу. Как правило, не имеет значения, в каком именно классе определено имя. Но это становится важным, если в базовом и производном классах есть одноименные члены. Например, когда мы пишем:
 bear.ival;
 ival – это член класса Bear, найденный на первом шаге описанного выше процесса разрешения имени.
 Иными словами, член производного класса, имеющий то же имя, что и член базового, маскирует последний. Чтобы обратиться к члену базового класса, необходимо квалифицировать его имя с помощью оператора разрешения области видимости:
 bear.ZooAnimal::ival;
 Тем самым мы говорим компилятору, что объявление ival следует искать в области видимости класса ZooAnimal.
 Проиллюстрируем использование оператора разрешения области видимости на несколько абсурдном примере (надеемся, вы никогда не напишете чего-либо подобного в реальном коде):
 int ival;
 
 int Bear::mumble( int ival )
 {
  return ival + // обращение к параметру
  ::ival + // обращение к глобальному объекту
  ZooAnimal::ival +
  Bear::ival;
 }
 Неквалифицированное обращение к ival разрешается в пользу формального параметра. (Если бы переменная ival не была определена внутри mumble(), то имел бы место доступ к члену класса Bear. Если бы ival не была определена и в Bear, то подразумевался бы член ZooAnimal. А если бы ival не было и там, то речь шла бы о глобальном объекте.)
 Разрешение имени члена класса всегда предшествует выяснению того, является ли обращение к нему корректным. На первый взгляд, это противоречит интуиции. Например, изменим реализацию mumble():
 int dval;
 int Bear::mumble( int ival )
 {
  // ошибка: разрешается в пользу закрытого члена ZooAnimal::dval
  return ival + dval;
 }
 Можно возразить, что алгоритм разрешения должен остановиться на первом допустимом в данном контексте имени, а не на первом найденном. Однако в приведенном примере алгоритм разрешения выполняется следующим образом:
 Определено ли dval в локальной области видимости функции-члена класса Bear? Нет.
 Определено ли dval в области видимости Bear? Нет.
 Определено ли dval в области видимости ZooAnimal? Да. Обращение разрешается в пользу этого имени.
 После того как имя разрешено, компилятор проверяет, возможен ли доступ к нему. В данном случае нет: dval является закрытым членом, и прямое обращение к нему из mumble() запрещено. Правильное (и, возможно, имевшееся в виду) разрешение требует явного употребления оператора разрешения области видимости:
 return ival + ::dval; // правильно
 Почему же имя члена разрешается перед проверкой уровня доступа? Чтобы предотвратить тонкие изменения семантики программы в связи с совершенно независимым, казалось бы, изменением уровня доступа к члену. Рассмотрим, например, такой вызов:
 int dval;
 int Bear::mumble( int ival )
 {
  foo( dval );
  // ...
 }
 Если бы функция foo() была перегруженной, то перемещение члена ZooAnimal::dval из закрытой секции в защищенную вполне могло бы изменить всю последовательность вызовов внутри mumble(), а разработчик об этом даже и не подозревал бы.
 Если в базовом и производном классах есть функции-члены с одинаковыми именами и сигнатурами, то их поведение такое же, как и поведение данных-членов: член производного класса лексически скрывает в своей области видимости член базового. Для вызова члена базового класса необходимо применить оператор разрешения области видимости:
 ostream& Bear::print( ostream &os) const
 {
  // вызывается ZooAnimal::print(os)
  ZooAnimal::print( os );
 
  os << name;
  return os;
 }
 18.4.1. Область видимости класса при множественном наследовании
 Как влияет множественное наследование на алгоритм просмотра области видимости класса? Все непосредственные базовые классы просматриваются одновременно, что может приводить к неоднозначности в случае, когда в нескольких из них есть одноименные члены. Рассмотрим на нескольких примерах, как возникает неоднозначность и какие меры можно предпринять для ее устранения. Предположим, есть следующий набор классов:
 class Endangered {
 public:
  ostream& print( ostream& ) const;
  void highlight();
  // ...
 };
 
 class ZooAnimal {
 public:
  bool onExhibit() const;
  // ...
 private:
  bool highlight( int zoo_location );
  // ...
 };
 
 class Bear : public ZooAnimal {
 public:
  ostream& print( ostream& ) const;
  void dance( dance_type ) const;
  // ...
 };
 Panda объявляется производным от двух классов:
 class Panda : public Bear, public Endangered {
 public:
  void cuddle() const;
  // ...
 };
 Хотя при наследовании функций print() и highlight() из обоих базовых классов Bear и Endangered имеется потенциальная неоднозначность, сообщение об ошибке не выдается до момента явно неоднозначного обращения к любой из этих функций.
 В то время как неоднозначность двух унаследованных функций print() очевидна с первого взгляда, наличие конфликта между членами highlight() удивляет (ради этого пример и составлялся): ведь у них разные уровни доступа и разные прототипы. Более того, экземпляр из Endangered – это член непосредственного базового класса, а из ZooAnimal – член класса, стоящего на две ступеньки выше в иерархии.
 Однако все это не имеет значения (впрочем, как мы скоро увидим, может иметь, но в случае виртуального наследования). Bear наследует закрытую функцию-член highlight() из ZooAnimal; лексически она видна, хотя вызывать ее из Bear или Panda запрещено. Значит, Panda наследует два лексически видимых члена с именем highlight, поэтому любое неквалифицированное обращение к этому имени приводит к ошибке компиляции.
 Поиск имени начинается в ближайшей области видимости, объемлющей его вхождение. Например, в коде
 int main()
 {
  Panda yin_yang;
  yin_yang.dance( Bear::macarena );
 }
 ближайшей будет область видимости класса Panda, к которому принадлежит yin_yang. Если же мы напишем:
 void Panda::mumble()
 {
  dance( Bear::macarena );
  // ...
 }
 то ближайшей будет локальная область видимости функции-члена mumble(). Если объявление dance в ней имеется, то разрешение имени на этом благополучно завершится. В противном случае поиск будет продолжен в объемлющих областях видимости.
 В случае множественного наследования имитируется одновременный просмотр всех поддеревьев наследования – в нашем случае это класс Endangered и поддерево Bear/ZooAnimal. Если объявление обнаружено только в поддереве одного из базовых классов, то разрешение имени заканчивается успешно, как, например, при таком вызове dance():
 // правильно: Bear::dance()
 yin_yang.dance( Bear::macarena );
 Если же объявление найдено в двух или более поддеревьях, то обращение считается неоднозначным и компилятор выдает сообщение об ошибке. Так будет при неквалифицированном обращении к print():
 int main()
 {
  // ошибка: неоднозначность: одна из
  // Bear::print( ostream& ) const
  // Endangered::print( ostream& ) const
  Panda yin_yang;
  yin_yang.print( cout );
 }
 На уровне программы в целом для разрешения неоднозначности достаточно явно квалифицировать имя нужной функции-члена с помощью оператора разрешения области видимости:
 int main()
 {
  // правильно, но не лучшее решение
  Panda yin_yang;
  yin_yang.Bear::print( cout );
 }
 Предложенный способ неэффективен: теперь пользователь вынужден решать, каково правильное поведение класса Panda; однако лучше, если такого рода ответственность примет на себя проектировщик и класс Panda сам устранит все неоднозначности, свойственные его иерархии наследования. Простейший способ добиться этого – задать квалификацию уже в определении экземпляра в производном классе, указав тем самым требуемое поведение:
 inline void Panda::highlight() {
  Endangered::highlight();
 }
 
 inline ostream&
 Panda::print( ostream &os ) const
 {
  Bear::print( os );
  Endangered::print( os );
  return os;
 }
 Поскольку успешная компиляция производного класса, наследующего нескольким базовым, не гарантирует отсутствия скрытых неоднозначностей, мы рекомендуем при тестировании вызывать все функции-члены, даже самые тривиальные.
 Упражнение 18.9
 Дана следующая иерархия классов:
 class Base1 {
 public:
  // ...
 protected:
  int ival;
  double dval;
  char cval;
  // ...
 private:
  int *id;
  // ...
 };
 
 class Base2 {
 public:
  // ...
 protected:
  float fval;
  // ...
 private:
  double dval;
  // ...
 };
 
 class Derived : public Base1 {
 public:
  // ...
 protected:
  string sval;
  double dval;
  // ...
 };
 
 class MI : public Derived, public Base2 {
 public:
  // ...
 protected:
  int *ival;
  complex cval;
  // ...
 };
 и структура функции-члена MI::foo():
 int ival;
 double dval;
 
 void MI::
 foo( double dval )
 {
  int id;
  // ...
 }
 Какие члены видны в классе MI? Есть ли среди них такие, которые видны в нескольких базовых?
 Какие члены видны в MI::foo()?
 Упражнение 18.10
 Пользуясь иерархией классов из упражнения 18.9, укажите, какие из следующих присваиваний недопустимы внутри функции-члена MI::bar():
 void MI::
 bar()
 {
  int sval;
  // вопрос упражнения относится к коду, начинающемуся с этого места ...
 }
 
 (a) dval = 3.14159; (d) fval = 0;
 (b) cval = 'a'; (e) sval = *ival;
 (c) id = 1;
 Упражнение 18.11
 Даны иерархия классов из упражнения 18.9 и скелет функции-члена MI::foobar():
 int id;
 
 void MI::

<< Пред.           стр. 90 (из 121)           След. >>

Список литературы по разделу