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

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

  // ошибка: int Diffident::_mumble скрыто
  _mumble = -1;
 }
 Некоторые компиляторы помечают это как ошибку типизации. Для доступа к члену базового класса, имя которого скрыто в производном, необходимо квалифицировать имя члена базового класса именем самого этого класса с помощью оператора разрешения области видимости. Так выглядит правильная реализация функции-члена turn_eyes_down():
 void
 Shy::
 turn_eyes_down()
 {
  // ...
  _mumble = "excuse me"; // правильно
 
  // правильно: имя члена базового класса квалифицировано
  Diffident::_mumble = -1;
 }
 Функции-члены базового и производного классов не составляют множество перегруженных функций:
 class Diffident {
 public:
  void mumble( int softness );
  // ...
 };
 
 class Shy : public Diffident {
 public:
  // скрывает видимость функции-члена Diffident::_mumble,
  // а не перегружает ее
  void mumble( string whatYaSay );
  void print( int soft, string words );
  // ...
 };
 Вызов функции-члена базового класса из производного в этом случае приводит к ошибке компиляции:
 Shy simon;
 
 // правильно: Shy::mumble( string )
 simon.mumble( "pardon me" );
 
 // ошибка: ожидался первый аргумент типа string
 // Diffident::mumble( int ) невидима
 simon.mumble( 2 );
 Хотя к членам базового класса можно обращаться напрямую, они сохраняют область видимости класса, в котором определены. А чтобы функции перегружали друг друга, они должны находиться в одной и той же области видимости. Если бы это было не так, следующие два экземпляра невиртуальной функции-члена turn_aside()
 class Diffident {
 public:
  void turn_aside( );
  // ...
 };
 
 class Shy : public Diffident {
 public:
  // скрывает видимость
  // Diffident::turn_aside()
  void turn_aside();
 
  // ...
 };
 привели бы к ошибке повторного определения, так как их сигнатуры одинаковы. Однако запись правильна, поскольку каждая функция находится в области видимости того класса, в котором определена.
 А если нам действительно нужен набор перегруженных функций-членов базового и производного классов? Написать в производном классе небольшую встроенную заглушку для вызова экземпляра из базового? Это возможно:
 class Shy : public Diffident {
 public:
  // один из способов реализовать множество перегруженных
  // членов базового и производного классов
  void mumble( string whatYaSay );
  void mumble( int softness ) {
  Diffident::mumble( softness ); }
  // ...
 };
 Но в стандартном C++ тот же результат достигается посредством using-объявления:
 class Shy : public Diffident {
 public:
  // в стандартном C++ using-объявление
  // создает множество перегруженных
  // членов базового и производного классов
  void mumble( string whatYaSay );
  using Diffident::mumble;
 
  // ...
 };
 По сути дела, using-объявление вводит каждый именованный член базового класса в область видимости производного. Поэтому такой член теперь входит в множество перегруженных функций, ассоциированных с именем функции-члена производного класса. (В ее using-объявлении нельзя указать список параметров, только имя. Это означает, что если некоторая функция уже перегружена в базовом классе, то в область видимости производного класса попадут все перегруженные экземпляры и, следовательно, добавить только одну из них невозможно.)
 Обратим внимание на степень доступности защищенных членов базового класса. Когда мы пишем:
 class Query {
 public:
  const vector* locations() { return &_loc; }
  // ...
 protected:
  vector _loc;
  // ...
 };
 то имеем в виду, что класс, производный от Query, может напрямую обратиться к члену _loc, тогда как во всей остальной программе для этого необходимо пользоваться открытой функцией доступа. Однако объект производного класса имеет доступ только к защищенному члену _loc входящего в него подобъекта, относящегося к базовому классу. Объект производного класса неспособен обратиться к защищенным членам другого независимого объекта базового класса:
 bool
 NameQuery::
 compare( const Query *pquery )
 {
  // правильно: защищенный член подобъекта Query
  int myMatches = _loc.size();
 
  // ошибка: нет прав доступа к защищенному члену
  // независимого объекта Query
  int itsMatches = pquery->_loc.size();
 
  return myMatches == itsMatches;
 }
 У объекта NameQuery есть доступ к защищенным членам только одного объекта Query – подобъекта самого себя. Прямое обращение к ним из производного класса осуществляется через неявный указатель this (см. раздел 13.4). Первая реакция на ошибку компиляции – переписать функцию compare() с использованием открытой функции-члена location():
 bool
 NameQuery::
 compare( const Query *pquery )
 {
  // правильно: защищенный член подобъекта Query
  int myMatches = _loc.size();
 
  // правильно: используется открытый метод доступа
  int itsMatches = pquery->locations()->size();
 
  return myMatches == itsMatches;
 }
 Однако проблема заключается в неправильном проектировании. Поскольку _loc – это член базового класса Query, то место compare() среди членов базового, а не производного класса. Во многих случаях подобные проблемы могут быть решены путем переноса некоторой операции в тот класс, где находится недоступный член, как в приведенном примере.
 Этот вид ограничения доступа не распространяется на доступ изнутри класса к другим объектам того же класса:
 bool
 NameQuery::
 compare( const NameQuery *pname )
 {
  int myMatches = _loc.size(); // правильно
  int itsMatches = name->_loc.size(); // тоже правильно
 
  return myMatches == itsMatches;
 }
 Производный класс может напрямую обращаться к защищенным членам базового в других объектах того же класса, что и он сам, равно как и к защищенным и закрытым членам других объектов своего класса.
 Рассмотрим инициализацию указателя на базовый Query адресом объекта производного NameQuery:
 Query *pb = new NameQuery( "sprite" );
 При вызове виртуальной функции, определенной в базовом классе Query, например:
 pb->eval(); // вызывается NameQuery::eval()
 вызывается функция из NameQuery. За исключением вызова виртуальной функции, объявленной в Query и переопределенной в NameQuery, другого способа напрямую добраться до членов класса NameQuery через указатель pb не существует:
 если в Query и NameQuery объявлены некоторые невиртуальные функции-члены с одинаковым именем, то через pb всегда вызывается экземпляр из Query;
 если в Query и NameQuery объявлены одноименные члены, то через pb обращение происходит к члену класса Query;
 если в NameQuery имеется виртуальная функция, отсутствующая в Query, скажем suffix(), то попытка вызвать ее через pb приводит к ошибке компиляции:
 // ошибка: suffix() - не член класса Query
 pb->suffix();
 Обращение к члену или невиртуальной функции-члену класса NameQuery через pb тоже вызывает ошибку компиляции:
 // ошибка: _name - не член класса Query
 pb->_name;
 Квалификация имени члена в этом случае не помогает:
 // ошибка: у класса Query нет базового класса NameQuery
 pb->NameQuery::_name;
 В C++ с помощью указателя на базовый класс можно работать только с данными и функциями-членами, включая виртуальные, которые объявлены (или унаследованы) в самом этом классе, независимо от того, какой фактический объект адресуется указателем. Объявление функции-члена виртуальной откладывает решение вопроса о том, какой экземпляр функции вызвать, до выяснения (во время выполнения программы) фактического типа объекта, адресуемого pb.
 Такой подход может показаться недостаточно гибким, но у него есть два весомых преимущества:
 поиск виртуальной функции-члена во время выполнения никогда не закончится неудачно из-за того, что фактический тип класса не существует. В таком случае программа просто не смогла бы откомпилироваться;
 механизм виртуализации можно оптимизировать. Часто вызов такой функции оказывается не дороже, чем косвенный вызов функции по указателю (детально этот вопрос рассмотрен в [LIPPMAN96a]).
 В базовом классе Query определен статический член _text_file:
 static vector *_text_file;
 Создается ли при порождении класса NameQuery второй экземпляр _text_file, уникальный именно для него? Нет. Все объекты производного класса ссылаются на тот же самый, единственный разделяемый статический член. Сколько бы ни было производных классов, существует лишь один экземпляр _text_file. Можно обратиться к нему через объект производного класса с помощью синтаксиса доступа:
 nameQueryObject._text_file; // правильно
 Наконец, если производный класс хочет получить доступ к закрытым членам своего базового класса напрямую, то он должен быть объявлен другом базового:
 class Query {
  friend class NameQuery;
 public:
  // ...
 };
 Теперь объект NameQuery может обращаться не только к закрытым членам своего подобъекта, соответствующего базовому классу, но и к закрытым и защищенным членам любых объектов Query.
 А если мы произведем от NameQuery класс StringQuery? Он будет поддерживать сокращенную форму запроса AndQuery, и вместо
 
 beautiful && fiery && bird
 
 можно будет написать:
 
 "beautiful fiery bird"
 
 Унаследует ли StringQuery от класса NameQuery дружественные отношения с Query? Нет. Отношение дружественности не наследуется. Производный класс не становится другом класса, который объявил своим другом один из базовых. Если производному классу требуется стать другом одного или более классов, то эти классы должны предоставить ему соответствующие права явно. Например, у класса StringQuery нет никаких специальных прав доступа по отношению к Query. Если расширенный доступ необходим, то Query должен разрешить его явно.
 Упражнение 17.6
 Даны следующие определения базового и производных классов:
 class Base {
 public:
  foo( int );
  // ...
 protected:
  int _bar;
  double _foo_bar;
 };
 
 class Derived : public Base {
 public:
  foo( string );
  bool bar( Base *pb );
  void foobar();
  // ...
 protected:
  string _bar;
 };
 Исправьте ошибки в каждом из следующих фрагментов кода:
 Derived d; d.foo( 1024 );
  (b) void Derived::foobar() { _bar = 1024; }
 (c) bool Derived::bar( Base *pb )
  { return _foo_bar == pb->_foo_bar; }
 17.4. Конструирование базового и производного классов
 Напомним, что объект производного класса состоит из одного или более подобъектов, соответствующих базовым классам, и части, относящейся к самому производному. Например, NameQuery состоит из подобъекта Query и объекта-члена string. Для иллюстрации поведения конструктора производного класса введем еще один член встроенного типа:
 class NameQuery : public Query {
 public:
  // ...
 protected:
  bool _present;
  string _name;
 };
 Если _present установлен в false, то слово _name в тексте отсутствует.
 Рассмотрим случай, когда в NameQuery конструктор не определен. Тогда при определении объекта этого класса
 NameQuery nq;
 по очереди вызывается конструктор по умолчанию Query, а затем конструктор по умолчанию класса string (ассоциированный с объектом _name). Член _present остается неинициализированным, что потенциально может служить источником ошибок. Чтобы инициализировать его, можно так определить конструктор по умолчанию для класса NameQuery:
 inline NameQuery::NameQuery() { _present = false; }
 Теперь при определении nq вызываются три конструктора по умолчанию: для базового класса Query, для класса string при инициализации члена _name и для класса NameQuery.
 А как передать аргумент конструктору базового класса Query? Ответить на этот вопрос можно, рассуждая по аналогии.
 Для передачи одного или более аргументов конструктору объекта-члена мы используем список инициализации членов (здесь можно также задать начальные значения членам, не являющимся объектами классов; подробности см. в разделе 14.5):
 inline NameQuery::
 NameQuery( const string &name )
  : _name( name ), _present( false )
 {}
 Для передачи одного или более аргументов конструктору базового класса также разрешается использовать список инициализации членов. В следующем примере мы передаем конструктору string аргумент name, а конструктору базового класса Query – объект, адресованный указателем ploc:
 inline NameQuery::
 NameQuery( const string &name,
  vector *ploc )
  : _name( name ), Query( *ploc ), _present( true )
 {}
 Хотя Query помещен в список инициализации вторым, его конструктор всегда вызывается раньше конструктора для _name. Порядок их вызова следующий:
 Конструктор базового класса. Если базовых классов несколько, то конструкторы вызываются в порядке их следования в списке базовых классов, а не в порядке появления в списке инициализации. (О множественном наследовании в этой связи мы поговорим в главе 18.)
 Конструктор объекта-члена. Если в классе есть несколько таких членов, то конструкторы вызываются в порядке их объявления в классе, а не в порядке появления в списке инициализации (подробнее см. раздел 14.5).
 Конструктор производного класса.
 Конструктор производного класса должен стремиться передать значение члена базового класса подходящему конструктору того же класса, а не присваивать его напрямую. В противном случае реализации двух классов становятся сильно связанными и тогда изменить или расширить реализацию базового будет затруднительно. (Ответственность разработчика базового класса ограничивается предоставлением подходящего множества конструкторов.)
 В оставшейся части этого раздела мы последовательно изучим конструктор базового класса и конструкторы четырех производных от него, а после этого рассмотрим альтернативный дизайн иерархии классов Query, чтобы познакомиться с иерархиями глубиной больше двух. В конце раздела речь пойдет о деструкторах классов.
 17.4.1. Конструктор базового класса
 В нашем базовом классе объявлено два нестатических члена: _solution и _loc:
 class Query {
 public:
  // ...
 protected:
  set *_solution;
  vector _loc;
  // ...
 };
 Конструктор Query по умолчанию должен явно инициализировать только член _solution. Для инициализации _loc автоматически вызывается конструктор класса vector. Вот реализация нашего конструктора:
 inline Query::Query(): _solution( 0 ) {}
 В Query нам понадобится еще один конструктор, принимающий ссылку на вектор позиций:
 inline
 Query::
 Query( const vector< locaton > &loc )
  : _solution( 0 ), _loc( loc )
 {}
 Он вызывается только из конструктора NameQuery, когда объект этого класса используется для представления указанного в запросе слова. В таком случае передается предварительно подготовленный для него вектор позиций. Остальные три производных класса вычисляют свои векторы позиций в соответствующей функции-члене eval(). (В следующем подразделе мы покажем, как это делается. Реализации функций-членов eval() приведены в разделе 17.5.)
 Какой уровень доступа обеспечить для конструкторов? Мы не хотим объявлять их открытыми, так как предполагается, что Query будет существовать в программе только в виде подобъекта в составе объектов производных от него классов. Поэтому мы объявим конструктор не открытым, а защищенным:
 class Query {
 public:
  // ...
 protected:
  Query();
  // ...
 };
 Ко второму конструктору класса Query предъявляются еще более жесткие требования: он не только должен конструировать Query в виде подобъекта производного класса, но этот производный класс должен к тому же быть NameQuery. Можно объявить конструктор закрытым, а NameQuery сделать другом класса Query. (В предыдущем разделе мы говорили, что производный класс может получить доступ только к открытым и защищенным членам базового. Поэтому любая попытка вызвать второй конструктор из классов AndQuery, OrQuery или NotQuery приведет к ошибке компиляции.)
 class Query {
 public:
  // ...
 protected:
  Query();
  // ...
 private:
  explicit Query( const vector& );
 };
 (Необходимость второго конструктора спорна; вероятно, правильнее заполнить _loc в функции eval() класса NameQuery. Однако принятый подход в большей степени отвечает нашей цели проиллюстрировать использование конструктора базового класса.)
 17.4.2. Конструктор производного класса
 В классе NameQuery также определены два конструктора. Они объявлены открытыми, поскольку ожидается, что в приложении будут создаваться объекты этого класса:
 class NameQuery : public Query {
 public:
  explicit NameQuery( const string& );
  NameQuery( const string&, const vector* );
  // ...
 protected:
  // ...
 };
 Конструктор с одним параметром принимает в качестве аргумента строку. Она передается конструктору объекта типа string, который вызывается для инициализации члена _name. Конструктор по умолчанию базового класса Query вызывается неявно:
 inline
 NameQuery::
 NameQuery( const string &name )
  // Query::Query() вызывается неявно
  : _name( name )
 {}
 Конструктор с двумя параметрами также принимает строку в качестве одного из них. Второй его параметр – это указатель на вектор позиций. Он передается закрытому конструктору базового класса Query. (Обратите внимание, что _present нам больше не нужен, и мы исключили его из числа членов NameQuery.)
 inline
 NameQuery::
 NameQuery( const string &name, vector *ploc )
  : _name( name ), Query( *ploc )
 {}
 Конструкторы можно использовать так:
 string title( "Alice" );
 NameQuery *pname;
 
 // проверим, встречается ли "Alice" в отображении слов
 // если да, получить ассоциированный с ним вектор позиций
 
 if ( vector *ploc = retrieve_location( title ))
  pname = new NameQuery( title, ploc );
 else pname = new NameQuery( title );
 В каждом из классов NotQuery, OrQuery и AndQuery определено по одному конструктору, каждый из которых вызывает конструктор базового класса неявно:
 inline NotQuery::
 NotQuery( Query *op = 0 ) : _op( op ) {}
 
 inline OrQuery::
 OrQuery( Query *lop = 0, Query *rop = 0 )
  : _lop( lop ), _rop( rop )

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

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