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

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

 {}
 
 inline AndQuery::
 AndQuery( Query *lop = 0, Query *rop = 0 )
  : _lop( lop ), _rop( rop )
 {}
 (В разделе 17.7 мы построим объекты каждого из производных классов для представления различных запросов пользователя.)
 17.4.3. Альтернативная иерархия классов
 Хотя наша иерархия классов Query представляется вполне приемлемой, она вовсе не является единственно возможной. Например, AndQuery и OrQuery связаны с бинарной операцией, поэтому они в какой-то степени дублируют друг друга. Можно вынести все данные и функции-члены, общие для них, в абстрактный базовый класс BinaryQuery. Поддерево новой иерархии Query изображено на рисунке 17.2:
 
 Query
 
 
 BinaryQuery
 
 
 AndQuery OrQuery
 
 Рис. 17.2. Альтернативная иерархия классов
 Класс BinaryQuery – это тоже абстрактный базовый класс, следовательно, его фактические объекты в приложении не появляются. Разумной реализации eval() для него предложить нельзя, поэтому чисто виртуальная функция, объявленная в Query, в классе BinaryQuery останется чисто виртуальной. (Подробнее о таких функциях мы поговорим в разделе 17.5.)
 Две функции-члена для доступа – lop() и rop(), общие для обоих классов, переносятся выше, в BinaryQuery, и определяются как нестатические встроенные. Аналогично два члена _lop и _rop, объявленные в обоих классах, также переносятся в BinaryQuery и становятся нестатическими и защищенными. Открытые конструкторы обоих производных классов объединяются в один защищенный конструктор BinaryQuery:
 class BinaryQuery : public Query {
 public:
  const Query *lop() { return _lop; }
  const Query *rop() { return _rop; }
 
 protected:
  BinaryQuery( Query *lop, Query *rop )
  : _lop( lop ), _rop( rop )
  {}
 
  Query *_lop;
  Query *_rop;
 };
 Складывается впечатление, что теперь оба производных класса должны предоставить лишь подходящие реализации eval():
 // увы! эти определения классов некорректны
 
 class OrQuery : public BinaryQuery {
 public:
  virtual void eval();
 };
 
 class AndQuery : public BinaryQuery {
 public:
  virtual void eval();
 };
 Однако в том виде, в котором мы их определили, эти классы неполны. При компиляции самих определений ошибок не возникает, но если мы попытаемся определить фактический объект:
 // ошибка: отсутствует конструктор класса AndQuery
 AndQuery proust( new NameQuery( "marcel" ),
  new NameQuery( "proust " ));
 то компилятор выдаст сообщение об ошибке: в классе AndQuery нет конструктора, готового принять два аргумента.
 Мы предположили, что AndQuery и OrQuery наследуют конструктор BinaryQuery точно так же, как они наследуют функции-члены lop() и rop(). Однако производный класс не наследует конструкторов базового. (Это могло бы привести к ошибкам, связанным с неинициализированными членами производного класса. Представьте, что будет, если в AndQuery добавить пару членов, не являющихся объектами классов: унаследованный конструктор базового класса для инициализации объекта производного AndQuery применять уже нельзя. Однако программист может этого не осознавать. Ошибка проявится не при конструировании объекта AndQuery, а позже, при его использовании. Кстати говоря, перегруженные операторы new и delete наследуются, что иногда приводит к аналогичным проблемам.)
 Каждый производный класс должен предоставлять собственный набор конструкторов. В случае классов AndQuery и OrQuery единственная цель конструкторов – обеспечить интерфейс для передачи двух своих операндов конструктору BinaryQuery. Так выглядит исправленная реализация:
 // правильно: эти определения классов корректны
 
 class OrQuery : public BinaryQuery {
 public:
  OrQuery( Query *lop, Query *rop )
  : BinaryQuery( lop, rop ) {}
 
  virtual void eval();
 };
 
 class AndQuery : public BinaryQuery {
 public:
  AndQuery( Query *lop, Query *rop )
  : BinaryQuery( lop, rop ) {}
 
  virtual void eval();
 };
 Если мы еще раз взглянем на рис. 17.2, то увидим, что BinaryQuery – непосредственный базовый класс для AndQuery и OrQuery, а Query –для BinaryQuery. Таким образом, Query не является непосредственным базовым классом для AndQuery и OrQuery.
 Конструктору производного класса разрешается напрямую вызывать только конструктор своего непосредственного предшественника в иерархии (виртуальное наследование является исключением из этого правила, да и из многих других тоже: см. раздел 18.5). Например, попытка включить конструктор Query в список инициализации членов объекта AndQuery приведет к ошибке.
 При определении объектов классов AndQuery и OrQuery теперь вызываются три конструктора: для базового Query, для непосредственного базового класса BinaryQuery и для производного AndQuery или OrQuery. (Порядок вызова конструкторов базовых классов отражает обход дерева иерархии наследования в глубину.) Дополнительный уровень иерархии, связанный с BinaryQuery, практически не влияет на производительность, поскольку мы определили его конструкторы как встроенные.
 Так как модифицированная иерархия сохраняет открытый интерфейс исходного проекта, то все эти изменения не сказываются на коде, который был написан в расчете на старую иерархию. Хотя модифицировать пользовательский код не нужно, перекомпилировать его все же придется, что может отвратить некоторых пользователей от перехода на новую версию.
 17.4.4. Отложенное обнаружение ошибок
 Начинающие программисты часто удивляются, почему некорректные определения классов AndQuery и OrQuery (в которых отсутствуют необходимые объявления конструкторов) компилируются без ошибок. Если бы мы не попытались определить фактический объект класса AndQuery, в этой модифицированной иерархии так и осталась бы ненайденная ошибка. Дело в том, что:
 если ошибка обнаруживается в точке объявления, то мы не можем продолжать компиляцию приложения, пока не исправим ее. Если же конфликтующее объявление – это часть библиотеки, для которой у нас нет исходного текста, то разрешение конфликта может оказаться нетривиальной задачей. Более того, возможно, в нашем коде никогда и не возникнет ситуации, когда эта ошибка проявляется, так что для нас она останется лишь потенциальной угрозой;
 с другой стороны, если ошибка не найдена вплоть до момента использования, то код может оказаться замусоренным ошибками, проявляющимися в самый неподходящий момент к удивлению программиста. При такой стратегии успешная компиляция говорит не об отсутствии семантических ошибок, а лишь о том, что программа не исполняет код, нарушающий семантические правила языка.
 Выдача сообщения об ошибке в точке использования – это одна из форм отложенного вычисления, распространенного метода повышения производительности программ. Он часто применяется для того, чтобы отложить потенциально дорогую операцию выделения или инициализации ресурса до момента, когда в нем возникнет реальная необходимость. Если ресурс так и не понадобится, мы сэкономим на ненужных подготовительных операциях. Если же он потребуется, но не сразу, мы растянем инициализацию программы на более длительный период.
 В C++ потенциальные ошибки “комбинирования”, связанные с перегруженными функциями, шаблонами и наследованием классов, обнаруживаются в точке использования, а не в точке объявления. (Мы полагаем, что это правильно, поскольку необходимость выявлять все возможные ошибки, которые можно допустить в результате комбинирования многочисленных компонентов, – пустая трата времени). Следовательно, для обнаружения и устранения латентных ошибок необходимо тщательно тестировать код. Подобные ошибки, возникающие при комбинировании двух или более больших компонентов, допустимы; однако в пределах одного компонента, такого, как иерархия классов Query, их быть не должно.
 17.4.5. Деструкторы
 Когда заканчивается время жизни объекта производного класса, автоматически вызываются деструкторы производного и базового классов (если они определены), а также деструкторы всех объектов-членов. Например, если имеется объект класса NameQuery:
 NameQuery nq( "hyperion" );
 то порядок вызова деструкторов следующий: сначала деструктор NameQuery, затем деструктор string для члена _name и наконец деструктор базового класса. В общем случае эта последовательность противоположна порядку вызова конструкторов.
 Вот деструкторы нашего базового Query и производных от него (все они объявлены открытыми членами соответствующих классов):
 inline Query::
 ~Query(){ delete _solution; }
 
 inline NotQuery::
 ~NotQuery(){ delete _op; }
 
 inline OrQuery::
 ~OrQuery(){ delete _lop; delete _rop; }
 
 inline AndQuery::
 ~AndQuery(){ delete _lop; delete _rop; }
 Отметим два аспекта:
 мы не предоставляем явного деструктора NameQuery, потому что никаких специальных действий по очистке его объекта предпринимать не нужно. Деструкторы базового класса и класса string для члена _name вызываются автоматически;
 в деструкторах производных классов оператор delete применяется к указателю типа Query*. Чтобы вызвать не деструктор Query, а деструктор класса того объекта, который фактически адресуется этим указателем, мы должны объявить деструктор базового Query виртуальным. (Более подробно о виртуальных функциях вообще и о виртуальных деструкторах в частности мы поговорим в следующем разделе.)
 В нашей реализации неявно подразумевалось, что память для операндов, указатели на которые имеются в объектах классов NotQuery, OrQuery и AndQuery, выделена из хипа. Именно поэтому в деструкторах мы применяли к этим указателям оператор delete. Но язык не позволяет обеспечить истинность такого предположения, так как в нем нет различий между адресами в хипе и вне его. С этой точки зрения наша реализация не застрахована от ошибок.
 В разделе 17.7 мы инкапсулируем выделение памяти и конструирование объектов иерархии Query в управляющий класс UserQuery. Это гарантирует выполнение нашего предположения. На уровне программы в целом следует перегрузить операторы new и delete для классов иерархии. Например, можно поступить следующим образом. Оператор new устанавливает в объекте флажок, говорящий, что память для него выделена из хипа. Перегруженный оператор delete проверяет этот флажок: если он есть, то память освобождается с помощью стандартного оператора delete.
 Упражнение 17.7
 Идентифицируйте конструкторы и деструкторы базового и производных классов для той иерархии, которую вы выбрали в упражнении 17.2 (раздел 17.1).
 Упражнение 17.8
 Измените реализацию класса OrQuery так, чтобы он был производным от BinaryQuery.
 Упражнение 17.9
 Найдите ошибку в следующем определении класса:
 class Object {
 public:
  virtual ~Object();
  virtual string isA();
 protected:
  string _isA;
 private:
  Object( string s ) : _isA( s ) {}
 };
 Упражнение 17.10
 Дано определение базового класса:
 class ConcreteBase {
 public:
  explicit ConcreteBase( int );
  virtual ostream& print( ostream& );
  virtual ~Base();
 
  static int object_count();
 protected:
  int _id;
  static int _object_count;
 };
 Что неправильно в следующих фрагментах:
 (a) class C1 : public ConcreteBase {
  public:
  C1( int val )
  : _id( _object_count++ ) {}
  // ...
  };
 (b) class C2 : public C1 {
  public:
  C2( int val )
  : ConcreteBase( val ), C1( val ) {}
  // ...
  };
 (c) class C3 : public C2 {
  public:
  C3( int val )
  : C2( val ), _object_count( val ) {}
  // ...
  };
 (d) class C4 : public ConcreteBase {
  public:
  C4( int val )
  : ConcreteBase ( _id+val ){}
  // ...
  };
 Упражнение 17.11
 В первоначальном определении языка C++ порядок следования инициализаторов в списке инициализации членов определял порядок вызова конструкторов. Принцип, который действует сейчас, был принят в 1986 году. Как вы думаете, почему была изменена исходная спецификация?
 17.5. Виртуальные функции в базовом и производном классах
 По умолчанию функции-члены класса не являются виртуальными. В подобных случаях при обращении вызывается функция, определенная в статическом типе объекта класса (или указателя, или ссылки на объект), для которого она вызвана:
 void Query::display( Query *pb )
 {
  set *ps = pb->solutions();
  // ...
  display();
 }
 Статический тип pb – это Query*. При обращении к невиртуальному члену solutions() вызывается функция-член класса Query. Невиртуальная функция display() вызывается через неявный указатель this. Статическим типом указателя this также является Query*, поэтому вызвана будет функция-член класса Query.
 Чтобы объявить функцию виртуальной, нужно добавить ключевое слово virtual:
 class Query {
 public:
  virtual ostream& print( ostream* = cout ) const;
  // ...
 };
 Если функция-член виртуальна, то при обращении к ней вызывается функция, определенная в динамическом типе объекта класса (или указателя, или ссылки на объект), для которого она вызвана. Однако для самих объектов класса статический и динамический тип – это одно и то же. Механизм виртуальных функций правильно работает только для указателей и ссылок на объекты.
 Таким образом, полиморфизм проявляется только тогда, когда объект производного класса адресуется косвенно, через указатель или ссылку на базовый. Использование самого объекта базового класса не сохраняет идентификацию типа производного. Рассмотрим следующий фрагмент кода:
 NameQuery nq( "lilacs" );
 
 // правильно: но nq "усечено" до подобъекта Query
 Query qobject = nq;
 Инициализация qobject переменной nq абсолютно законна: теперь qobject равняется подобъекту nq, который соответствует базовому классу Query, однако qobject не является объектом NameQuery. Часть nq, принадлежащая NameQuery, “усечена” перед инициализацией qobject, поскольку она не помещается в область памяти, отведенную под объект Query. Для поддержки этой парадигмы приходится использовать указатели и ссылки, но не сами объекты:
 void print ( Query object,
  const Query *pointer,
  const Query &reference )
 {
  // до момента выполнения невозможно определить,
  // какой экземпляр print() вызывается
  pointer->print();
  reference.print();
 
  // всегда вызывается Query::print()
  object.print();
 }
 
 int main()
 {
  NameQuery firebird( "firebird" );
  print( firebird, &firebird, firebird );
 }
 В данном примере оба обращения через указатель pointer и ссылку reference разрешаются своим динамическим типом; в обоих случаях вызывается NameQuery::print(). Обращение же через объект object всегда приводит к вызову Query::print(). (Пример программы, в которой используется эффект “усечения”, приведен в разделе 18.6.2.)
 В следующих подразделах мы продемонстрируем определение и использование виртуальных функций в разных обстоятельствах. Каждая такая функция-член будет иллюстрировать один из аспектов объектно-ориентированного проектирования.
 17.5.1. Виртуальный ввод/вывод
 Первая виртуальная операция, которую мы хотели реализовать, – это печать запроса на стандартный вывод либо в файл:
 ostream& print( ostream &os = cout ) const;
 Функцию print() следует объявить виртуальной, поскольку ее реализации зависят от типа, но нам нужно вызывать ее через указатель типа Query*. Например, для класса AndQuery эта функция могла бы выглядеть так:
 ostream&
 AndQuery::print( ostream &os ) const
 {
  _lop->print( os );
  os << " && ";
  _rop->print( os );
 }
 Необходимо объявить print() виртуальной функцией в абстрактном базовом Query, иначе мы не сможем вызвать ее для членов классов AndQury, OrQuery и NotQuery, являющихся указателями на операнды соответствующих запросов типа Query*. Однако для самого Query разумной реализации print() не существует. Поэтому мы определим ее как пустую функцию, а потом сделаем чисто виртуальной:
 class Query {
 public:
  virtual ostream& print( ostream &os=cout ) const {}
  // ...
 };
 В базовом классе, где виртуальная функция появляется в первый раз, ее объявлению должно предшествовать ключевое слово virtual. Если же ее определение находится вне этого класса, повторно употреблять virtual не следует. Так, данное определение print() приведет к ошибке компиляции:
 // ошибка: ключевое слово virtual может появляться
 // только в определении класса
 virtual ostream& Query::print( ostream& ) const { ... }
 Правильный вариант не должен включать слово virtual.
 Класс, в котором впервые появляется виртуальная функция, должен определить ее или объявить чисто виртуальной (напомним, что пока мы определили ее как пустую). В производном классе может быть либо определена собственная реализация той же функции, которая в таком случае становится активной для всех объектов этого класса, либо унаследована реализация из базового класса. Если в производном классе определена собственная реализация, то говорят, что она замещает реализацию из базового.
 Прежде чем приступать к рассмотрению реализаций print() для наших четырех производных классов, обратим внимание на употребление скобок в запросе. Например, с помощью
 
 fiery && bird || shyly
 
 пользователь ищет вхождения пары слов
 
 fiery bird
 
 или одного слова
 
 shyly
 
 С другой стороны, запрос
 
 fiery && ( bird || hair )
 
 найдет все вхождения любой из пар
 
 fiery bird
 
 или
 
 fiery hair
 
 Если наши реализации print() не будут показывать скобки в исходном запросе, то для пользователя они окажутся почти бесполезными. Чтобы сохранить эту информацию, введем в наш абстрактный базовый класс Query два нестатических члена, а также функции доступа к ним (подобное расширение класса – естественная часть эволюции иерархии):
 class Query {
 public:
  // ...
 
  // установить _lparen и _rparen
  void lparen( short lp ) { _lparen = lp; }
  void rparen( short rp ) { _rparen = rp; }
 
  // получить значения_lparen и _rparen
  short lparen() { return _lparen; }
  short rparen() { return _rparen; }
 
  // напечатать левую и правую скобки
  void print_lparen( short cnt, ostream& os ) const;
  void print_rparen( short cnt, ostream& os ) const;
 
 protected:
 
  // счетчики левых и правых скобок
  short _lparen;
  short _rparen;
  // ...
 };
 _lparen – это количество левых, а _rparen – правых скобок, которое должно быть выведено при распечатке объекта. (В разделе 17.7 мы покажем, как вычисляются такие величины и как происходит присваивание обоим членам.) Вот пример обработки запроса с большим числом скобок:
 
 ==> ( untamed || ( fiery || ( shyly ) ) )
 evaluate word: untamed
 _lparen: 1
 _rparen: 0
 evaluate Or
 _lparen: 0
 _rparen: 0
 
 evaluate word: fiery
 _lparen: 1
 _rparen: 0
 
 evaluate 0r
 _lparen: 0
 _rparen: 0
 
 evaluate word: shyly
 _lparen: 1
 _rparen: 0
 
 evaluate right parens:
 _rparen: 3
 
 ( untamed ( 1 ) lines match
 ( fiery ( 1 ) lines match
 ( shyly ( 1 ) lines match
 ( fiery || (shyly ( 2 ) lines match3
 ( untamed || ( fiery || ( shyly ))) ( 3 ) lines match
 

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

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