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

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

  bool dirty_bit;
 };
 
 #endif
 Array_Sort включает дополнительный член – dirty_bit. Если он установлен в true, то не гарантируется, что массив по-прежнему отсортирован. Предоставляется также ряд вспомогательных функций доступа: is_dirty() возвращает значение dirty_bit; set_bit() устанавливает dirty_bit в true; clear_bit() сбрасывает dirty_bit в false; check_bit() пересортировывает массив, если dirty_bit равно true, после чего сбрасывает его в false. Все операции, которые потенциально могут перевести массив в неотсортированное состояние, вызывают set_bit().
 При каждом обращении к шаблону Array необходимо указывать полный список параметров.
 Array::print( os );
 вызывает функцию-член print() базового класса Array, конкретизированного одновременно с Array_Sort. Например:
 Array_Sort sas;
 конкретизирует типом string оба шаблона: Array_Sort и Array.
 cout << sas;
 конкретизирует оператор вывода из класса Array, конкретизированного типом string, затем этому оператору передается строка sas. Внутри оператора вывода инструкция
 ar.print( os );
 приводит к вызову виртуального экземпляра print() класса Array_Sort, конкретизированного типом string. Сначала вызывается check_bit(), а затем статически вызывается функция-член print() класса Array, конкретизированного тем же типом. (Напомним, что под статическим вызовом понимается разрешение функции на этапе компиляции и – при необходимости – ее подстановка в место вызова.) Виртуальная функция обычно вызывается динамически в зависимости от фактического типа объекта, адресуемого ar. Механизм виртуализации подавляется, если она вызывается явно с помощью оператора разрешения области видимости, как в Array::print(). Это повышает эффективность в случае, когда мы явно вызываем экземпляр виртуальной функции базового класса из экземпляра той же функции в производном, например в print() из класса Array_Sort (см. раздел 17.5).
 Функции-члены, определенные вне тела класса, помещены в файл Array_S.C. Объявление может показаться слишком сложным из-за синтаксиса шаблона. Но, если не считать списков параметров, оно такое же, как и для обычных классов:
 template
 Array_Sort::
 Array_Sort( const Array_Sort &as )
  : Array( as )
 {
  // замечание: as.check_bit() не работает!
  // ---- объяснение см. ниже ...
  if ( as.is_dirty() )
  sort( 0, Array::_size-1 );
  clear_bit();
 }
 Каждое использование имени шаблона в качестве спецификатора типа должно быть квалифицировано полным списком параметров. Следует писать:
 template
 Array_Sort::
 Array_Sort( const Array_Sort &as )
 а не
 template
 Array_Sort::
 Array_Sort( // ошибка: это не спецификатор типа
 поскольку второе вхождение Array_Sort синтаксически является именем функции, а не спецификатором типа.
 Есть две причины, по которым правильна такая запись:
 if ( as.is_dirty() )
  sort( 0, _size );
 а не просто
 as.check_bit();
 Первая причина связана с типизацией: check_bit() – это неконстантная функция-член, которая модифицирует объект класса. В качестве аргумента передается ссылка на константный объект. Применение check_bit() к аргументу as нарушает его константность и потому воспринимается компилятором как ошибка.
 Вторая причина: копирующий конструктор рассматривает массив, ассоциированный с as, только для того, чтобы выяснить, нуждается ли вновь созданный объект класса Array_Sort в сортировке. Напомним, однако, что член dirty_bit нового объекта еще не инициализирован. К началу выполнения тела конструктора Array_Sort инициализированы только члены ia и _size, унаследованные от класса Array. Этот конструктор должен с помощью clear_bit() задать начальные значения дополнительных членов и, вызвав sort(), обеспечить специальное поведение подтипа. Конструктор Array_Sort можно было бы инициализировать и по-другому:
 // альтернативная реализация
 template
 Array_Sort::
 Array_Sort( const Array_Sort &as )
  : Array( as )
 {
  dirty_bit = as.dirty_bit;
  clear_bit();
 }
 Ниже приведена реализация функции-члена grow().1 Наша стратегия состоит в том, чтобы воспользоваться имеющейся в базовом классе Array реализацией для выделения дополнительной памяти, а затем пересортировать элементы и сбросить dirty_bit:
 template
 void Array_Sort::grow()
 {
  Array::grow();
  sort( 0, Array::_size-1 );
  clear_bit();
 }
 Так выглядит реализация двоичного поиска в функции-члене find() класса Array_Sort:
 template
 int Array_Sort::find( const Type &val )
 {
  int low = 0;
  int high = Array::_size-1;
  check_bit();
  while ( low <= high ) {
  int mid = ( low + high )/2;
 
  if ( val == ia[ mid ] )
  return mid;
 
  if ( val < ia[ mid ] )
  high = mid-1;
  else low = mid+1;
  }
  return -1;
 }
 Протестируем нашу реализацию класса Array_Sort с помощью функции try_array(). Показанная ниже программа тестирует шаблон этого класса для конкретизаций типами int и string:
 #include "Array_S.C"
 #include "try_array.C"
 #include
 
 main()
 {
  static int ia[ 10 ] = { 12,7,14,9,128,17,6,3,27,5 };
  static string sa[ 7 ] = {
  "Eeyore", "Pooh", "Tigger",
  "Piglet", "Owl", "Gopher", "Heffalump"
  };
 
  Array_Sort iA( ia,10 );
  Array_Sort SA( sa,7 );
 
  cout << "конкретизация класса Array_Sort"
  << endl;
  try_array( iA );
 
  cout << "конкретизация класса Array_Sort"
  << endl;
  try_array( SA );
 
  return 0;
 }
 При конкретизации типом string после компиляции и запуска программа печатает следующий текст (обратите внимание, что попытка вывести элемент с индексом -1 заканчивается крахом):
 
 конкретизация класса Array_Sort
 
 try_array: начальные значения массива
 ( 7 )< Eeyore, Gopher, Heffalump, Owl, Piglet, Pooh
  Tigger >
 
 try_array: после присваиваний
 ( 7 )< Eeyore, Gopher, Owl, Piglet, Pooh, Pooh
  Pooh >
 
 try_array: почленная инициализация
 ( 7 )< Eeyore, Gopher, Owl, Piglet, Pooh, Pooh
  Pooh >
 
 try_array: после почленного копирования
 ( 7 )< Eeyore, Piglet, Owl, Piglet, Pooh, Pooh
  Pooh >
 try_array: после вызова grow
 ( 7 )< , , , , Eeyore, Owl
  Piglet, Piglet, Pooh, Pooh, Pooh >
 
 искомое значение: Tigger возвращенный индекс: -1
 Memory fault (coredump)
 
 После почленного копирования массив не отсортирован, поскольку виртуальная функция вызывалась через объект, а не через указатель или ссылку. Как было сказано в разделе 17.5, в таком случае вызывается экземпляр функции из класса именно этого объекта, а не того подтипа, который может находиться в переменной. Поэтому функция sort() никогда не будет вызвана через объект Array. (Разумеется, мы реализовали такое поведение только в целях демонстрации.)
 18.6.3. Класс массива с множественным наследованием
 Определим отсортированный массив с контролем выхода за границы. Для этого можно применить множественное наследование от Array_RC и Array_Sort. Вот как выглядит наша реализация (напомним еще раз, что мы ограничились тремя конструкторами и оператором взятия индекса). Определение находится в заголовочном файле Array_RC_S.h:
 #ifndef ARRAY_RC_S_H
 #define ARRAY_RC_S_H
 
 #include "Array_S.C"
 #include "Array_RC.C"
 
 template
 class Array_RC_S : public Array_RC,
  public Array_Sort
 {
 public:
  Array_RC_S( int sz = Array::ArraySize )
  : Array( sz )
  { clear_bit(); }
 
  Array_RC_S( const Array_RC_S &rca )
  : Array( rca )
  { sort( 0,Array::_size-1 ); clear_bit(); }
 
  Array_RC_S( const Type* arr, int sz )
  : Array( arr, sz )
  { sort( 0,Array::_size-1 ); clear_bit(); }
 
  Type& operator[]( int index )
  {
  set_bit();
  return Array_RC::operator[]( index );
  }
 };
 
 #endif
 Этот класс наследует две реализации каждой интерфейсной функции Array: из Array_Sort и из виртуального базового класса Array через Array_RC (за исключением оператора взятия индекса, для которого из обоих базовых классов наследуется замещенный экземпляр). При невиртуальном наследовании вызов find() был бы помечен компилятором как неоднозначный, поскольку он не знает, какой из унаследованных экземпляров мы имели в виду. В нашем случае замещенным в Array_Sort экземплярам отдается предпочтение по сравнению с экземплярами, унаследованными из виртуального базового класса через Array_RC (см. раздел 18.5.4). Таким образом, при виртуальном наследовании неквалифицированный вызов find() разрешается в пользу экземпляра, унаследованного из класса Array_Sort.
 Оператор взятия индекса переопределен в классах Array_RC и Array_Sort, и обе реализации имеют равный приоритет. Поэтому внутри Array_RC_S неквалифицированное обращение к оператору взятия индекса неоднозначно. Класс Array_RC_S должен предоставить собственную реализацию, иначе пользователи не смогут напрямую применять такой оператор к объектам этого класса. Но какова семантика его вызова в Array_RC_S? При учете отсортированности массива он должен установить в true унаследованный член dirty_bit. А чтобы учесть наследование от класса с контролем выхода за границы массива – проверить указанный индекс. После этого можно возвращать элемент массива с данным индексом. Последние два шага выполняет унаследованный из Array_RC оператор взятия индекса. При обращении
 return Array_RC::operator[]( index );
 он вызывается явно, и механизм виртуализации не применяется. Поскольку это встроенная функция, то при статическом вызове компилятор подставляет ее код в место вызова.
 Теперь протестируем нашу реализацию с помощью функции try_array(), передавая ей по очереди классы, конкретизированные из шаблона Array_RC_S типами int и string:
 #include "Array_RC_S.h"
 #include "try_array.C"
 #include
 
 int main()
 {
  static int ia[ 10 ] = { 12,7,14,9,128,17,6,3,27,5 };
  static string sa[ 7 ] = {
  "Eeyore", "Pooh", "Tigger",
  "Piglet", "Owl", "Gopher", "Heffalump"
  };
  Array_RC_S iA( ia,10 );
  Array_RC_S SA( sa,7 );
 
  cout << "конкретизация класса Array_RC_S"
  << endl;
  try_array( iA );
 
  cout << "конкретизация класса Array_RC_S"
  << endl;
  try_array( SA );
 
  return 0;
 }
 Вот что печатает программа для класса, конкретизированного типом string (теперь ошибка выхода за границы массива перехватывается):
 
 конкретизация класса Array_Sort
 
 try_array: начальные значения массива
 ( 7 )< Eeyore, Gopher, Heffalump, Owl, Piglet, Pooh
  Tigger >
 
 try_array: после присваиваний
 ( 7 )< Eeyore, Gopher, Owl, Piglet, Pooh, Pooh
  Pooh >
 
 try_array: почленная инициализация
 ( 7 )< Eeyore, Gopher, Owl, Piglet, Pooh, Pooh
  Pooh >
 
 try_array: после почленного копирования
 ( 7 )< Eeyore, Piglet, Owl, Piglet, Pooh, Pooh
  Pooh >
 
 try_array: после вызова grow
 ( 7 )< , , , , Eeyore, Owl
  Piglet, Piglet, Pooh, Pooh, Pooh >
 
 искомое значение: Tigger возвращенный индекс: -1
 Assertion failed: ix >= 0 && ix < size
 
 Представленная в этой главе реализация иерархии класса Array иллюстрирует применение множественного и виртуального наследования. Детально проектирование класса массива описано в [NACKMAN94]. Однако, как правило, достаточно класса vector из стандартной библиотеки.
 Упражнение 18.16
 Добавьте в Array функцию-член spy(). Она запоминает операции, примененные к объекту класса: число доступов по индексу; количество вызовов каждого члена; какой элемент искали с помощью find() и сколько было успешных поисков. Поясните свои проектные решения. Модифицируйте все подтипы Array так, чтобы spy() можно было использовать и для них тоже.
 Упражнение 18.17
 Стандартный библиотечный класс map (отображение) называют еще ассоциативным массивом, поскольку он поддерживает индексирование значением ключа. Как вы думаете, является ли ассоциативный массив кандидатом на роль подтипа нашего класса Array? Почему?
 Упражнение 18.18
 Перепишите иерархию Array, пользуясь контейнерными классами из стандартной библиотеки и применяя обобщенные алгоритмы.
 
 19
 19. Применение наследования в C++
 При использовании наследования указатель или ссылка на тип базового класса способен адресовать объект любого производного от него класса. Возможность манипулировать такими указателями или ссылками независимо от фактического типа адресуемого объекта называется полиморфизмом. В этой главе мы рассмотрим три функции языка, обеспечивающие специальную поддержку полиморфизма. Сначала мы познакомимся с идентификацией типов во время выполнения (RTTI – Run-time Type Identification), которая позволяет программе узнать истинный производный тип объекта, адресованного ссылкой или указателем на тип базового класса. Затем расскажем о влиянии наследования на обработку исключений: покажем, как можно определять их в виде иерархии классов и как обработчики для типа базового класса могут перехватывать исключения производных типов. В конце главы мы вернемся к правилам разрешения перегрузки функций и посмотрим, как наследование влияет на то, какие преобразования типов можно применять к аргументам функции, и на выбор наилучшей из устоявших.
 19.1. Идентификация типов во время выполнения
 RTTI позволяет программам, которые манипулируют объектами через указатели или ссылки на базовые классы, получить истинный производный тип адресуемого объекта. Для поддержки RTTI в языке C++ есть два оператора:
 оператор dynamic_cast поддерживает преобразования типов во время выполнения, обеспечивая безопасную навигацию по иерархии классов. Он позволяет трансформировать указатель на базовый класс в указатель на производный от него, а также преобразовать l-значение, ссылающееся на базовый класс, в ссылку на производный, но только в том случае, если это завершится успешно;
 оператор typeid позволяет получить фактический производный тип объекта, адресованного указателем или ссылкой.
 Однако для получения информации о типе производного класса операнд любого из операторов dynamic_cast или typeid должен иметь тип класса, в котором есть хотя бы одна виртуальная функция. Таким образом, операторы RTTI – это события времени выполнения для классов с виртуальными функциями и события времени компиляции для всех остальных типов. В данном разделе мы более подробно познакомимся с их возможностями.
 Использование RTTI оказывается необходимым при реализации таких приложений, как отладчики или объектные базы данных, когда тип объектов, которыми манипулирует программа, становится известен только во время выполнения путем исследования RTTI-информации, хранящейся вместе с типами объектов. Однако лучше пользоваться статической системой типов C++, поскольку она безопаснее и эффективнее.
 19.1.1. Оператор dynamic_cast
 Оператор dynamic_cast можно применять для преобразования указателя, ссылающегося на объект типа класса в указатель на тип класса из той же иерархии. Его также используют для трансформации l-значения объекта типа класса в ссылку на тип класса из той же иерархии. Приведение типов с помощью оператора dynamic_cast, в отличие от других имеющихся в C++ способов, осуществляется во время выполнения программы. Если указатель или l-значение не могут быть преобразованы в целевой тип, то dynamic_cast завершается неудачно. В случае приведения типа указателя признаком неудачи служит возврат нулевого значения. Если же l-значение нельзя трансформировать в ссылочный тип, возбуждается исключение. Ниже мы приведем примеры неудачного выполнения этого оператора.
 Прежде чем перейти к более детальному рассмотрению dynamic_cast, посмотрим, зачем его нужно применять. Предположим, что в программе используется библиотека классов для представления различных категорий служащих компании. Входящие в иерархию классы поддерживают функции-члены для вычисления зарплаты:
 class employee {
 public:
  virtual int salary();
 };
 
 class manager : public employee {
 public:
  int salary();
 };
 
 class programmer : public employee {
 public:
  int salary();
 };
 
 void company::payroll( employee *pe ) {
  // используется pe->salary()
 }
 В компании есть разные категории служащих. Параметром функции-члена payroll() класса company является указатель на объект employee, который может адресовать один из типов manager или programmer. Поскольку payroll() обращается к виртуальной функции-члену salary(), то вызывается подходящая замещающая функция, определенная в классе manager или programmer, в зависимости от того, какой объект адресован указателем.
 Допустим, класс employee перестал удовлетворять нашим потребностям, и мы хотим его модифицировать, добавив еще одну функцию-член bonus(), используемую совместно с salary() при расчете платежной ведомости. Для этого нужно включить новую функцию-член в классы, составляющие иерархию employee:
 class employee {
 public:
  virtual int salary(); // зарплата
  virtual int bonus(); // премия
 };
 
 class manager : public employee {
 public:
  int salary();
 };
 
 class programmer : public employee {
 public:
  int salary();
  int bonus();
 };
 
 void company::payroll( employee *pe ) {
  // используется pe->salary() и pe->bonus()
 }
 Если параметр pe функции payroll() указывает на объект типа manager, то вызывается виртуальная функция-член bonus() из базового класса employee, поскольку в классе manager она не замещена. Если же pe указывает на объект типа programmer, то вызывается виртуальная функция-член bonus() из класса programmer.
 После добавления новых виртуальных функций в иерархию классов придется перекомпилировать все функции-члены. Добавить bonus() можно, если у нас есть доступ к исходным текстам функций-членов в классах employee, manager и programmer. Однако если иерархия была получена от независимого поставщика, то не исключено, что в нашем распоряжении имеются только заголовочные файлы, описывающие интерфейс библиотечных классов и объектные файлы с их реализацией, а исходные тексты функций-членов недоступны. В таком случае перекомпиляция всей иерархии невозможна.
 Если мы хотим расширить функциональность библиотеки классов, не добавляя новые виртуальные функции-члены, можно воспользоваться оператором dynamic_cast.
 Этот оператор применяется для получения указателя на производный класс, чтобы иметь возможность работать с теми его элементами, которые по-другому не доступны. Предположим, что мы расширяем библиотеку за счет добавления новой функции-члена bonus() в класс programmer. Ее объявление можно включить в определение programmer, находящееся в заголовочном файле, а саму функцию определить в одном из своих исходных файлов:
 class employee {
 public:
  virtual int salary();
 };
 
 class manager : public employee {
 public:
  int salary();
 };
 
 class programmer : public employee {
 public:
  int salary();
  int bonus();
 };
 Напомним, что payroll() принимает в качестве параметра указатель на базовый класс employee. Мы можем применить оператор dynamic_cast для получения указателя на производный programmer и воспользоваться им для вызова функции-члена bonus():

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

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