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

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

  // elemType заменяется на int
  Array ia(array_size);
 
  // elemType заменяется на double
  Array da(array_size);
 
  // elemType заменяется на char
  Array ca(array_size);
 
  int ix;
 
  for ( ix = 0; ix < array_size; ++ix ) {
  ia[ix] = ix;
  da[ix] = ix * 1.75;
  ca[ix] = ix + 'a';
  }
 
  for ( ix = 0; ix < array_size; ++ix )
  cout << "[ " << ix << " ] ia: " << ia[ix]
  << "\tca: " << ca[ix]
  << "\tda: " << da[ix] << endl;
 
  return 0;
 }
 Здесь определены три экземпляра класса Array:
 Array ia(array_size);
 Array da(array_size);
 Array ca(array_size);
 Что делает компилятор, встретив такое объявление? Подставляет текст шаблона Array, заменяя параметр elemType на тот тип, который указан в каждом конкретном случае. Следовательно, объявления членов приобретают в первом случае такой вид:
 // Array ia(array_size);
 int _size;
 int *_ia;
 Заметим, что это в точности соответствует определению массива IntArray.
 Для оставшихся двух случаев мы получим следующий код:
 // Array da(array_size);
 int _size;
 double *_ia;
 
 // Array ca(array_size);
 int _size;
 char *_ia;
 Что происходит с функциями-членами? В них тоже тип-параметр elemType заменяется на реальный тип, однако компилятор не конкретизирует те функции, которые не вызываются в каком-либо месте программы. (Подробнее об этом в разделе 16.8.)
 При выполнении программа этого примера выдаст следующий результат:
 
 [ 0 ] ia: 0 ca: a da: 0
 [ 1 ] ia: 1 ca: b da: 1.75
 [ 2 ] ia: 2 ca: c da: 3.5
 [ 3 ] ia: 3 ca: d da: 5.25
 
 Механизм шаблонов можно использовать и в наследуемых классах. Вот как выглядит определение шаблона класса ArrayRC:
 #include
 #include "Array.h"
 
 template
 class ArrayRC : public Array {
 public:
  ArrayRC( int sz = DefaultArraySize )
  : Array( sz ) {}
  ArrayRC( const ArrayRC& r )
  : Array( r ) {}
  ArrayRC( const elemType *ar, int sz )
  : Array( ar, sz ) {}
 
  elemType& ArrayRC::operator[]( int ix )
  {
  assert( ix >= 0 && ix < Array::_size );
  return _ia[ ix ];
  }
 private:
 // ...
 };
 Подстановка реальных параметров вместо типа-параметра elemType происходит как в базовом, так и в производном классах. Определение
 ArrayRC ia_rc(10);
 ведет себя точно так же, как определение IntArrayRC из предыдущего раздела. Изменим пример использования из предыдущего раздела. Прежде всего, чтобы оператор
 // функцию swap() тоже следует сделать шаблоном
 swap( ia1, 1, ia1.size() );
 был допустимым, нам потребуется представить функцию swap() в виде шаблона.
 #include "Array.h"
 
 template
 inline void
 swap( Array &array, int i, int j )
 {
  elemType tmp = array[ i ];
  array[ i ] = array[ j ];
  array[ j ] = tmp;
 }
 При каждом вызове swap() генерируется подходящая конкретизация, которая зависит от типа массива. Вот как выглядит программа, использующая шаблоны Array и ArrayRC:
 #include
 #include "Array.h"
 #include "ArrayRC.h"
 
 template
 inline void
 swap( Array &array, int i, int j )
 {
  elemType tmp = array[ i ];
  array[ i ] = array[ j ];
  array[ j ] = tmp;
 }
 
 int main()
 {
  Array ia1;
  ArrayRC ia2;
 
  cout << "swap() with Array ia1" << endl;
  int size = ia1.size();
  swap( ia1, 1, size );
 
  cout << "swap() with ArrayRC ia2" << endl;
  size = ia2.size();
  swap( ia2, 1, size );
 
  return 0;
 }
 Упражнение 2.13
 Пусть мы имеем следующие объявления типов:
 template class Array;
 enum Status { ... };
 typedef string *Pstring;
 Есть ли ошибки в приведенных ниже описаниях объектов?
 (a) Array< int*& > pri(1024);
 (b) Array< Array > aai(1024);
 (c) Array< complex< double > > acd(1024);
 (d) Array< Status > as(1024);
 (e) Array< Pstring > aps(1024);
 Упражнение 2.14
 Перепишите следующее определение, сделав из него шаблон класса:
 class example1 {
 public:
  example1 (double min, double max);
  example1 (const double *array, int size);
 
  double& operator[] (int index);
  bool operator== (const example1&) const;
 
  bool insert (const double*, int);
  bool insert (double);
 
  double min (double) const { return _min; };
  double max (double) const { return _max; };
 
  void min (double);
  void max (double);
 
  int count (double value) const;
 
 private:
  int size;
  double *parray;
  double _min;
  double _max;
 }
 Упражнение 2.15
 Имеется следующий шаблон класса:
 template class Example2 {
 public:
  explicit Example2 (elemType val=0) : _val(val) {};
 
  bool min(elemType value) { return _val < value; }
  void value(elemType new_val) { _val = new_val; }
  void print (ostream &os) { os << _val; }
 
 private:
  elemType _val;
 }
 
 template
 ostream& operator<<(ostream &os,const Example2 &ex)
  { ex.print(os); return os; }
 Какие действия вызывают следующие инструкции?
 (a) Example2*> ex1;
 (b) ex1.min (&ex1);
 (c) Example2 sa(1024),sb;
 (d) sa = sb;
 (e) Example2 exs("Walden");
 (f) cout << "exs: " << exs << endl;
 Упражнение 2.16
 Пример из предыдущего упражнения накладывает определенные ограничения на типы данных, которые могут быть подставлены вместо elemType. Так, параметр конструктора имеет по умолчанию значение 0:
 explicit Example2 (elemType val=0) : _val(val) {};
 Однако не все типы могут быть инициализированы нулем (например, тип string), поэтому определение объекта
 Example2 exs("Walden");
 является правильным, а
 Example2 exs2;
 приведет к синтаксической ошибке. Также ошибочным будет вызов функции min(), если для данного типа не определена операция меньше. С++ не позволяет задать ограничения для типов, подставляемых в шаблоны. Как вы думаете, было бы полезным иметь такую возможность? Если да, попробуйте придумать синтаксис задания ограничений и перепишите в нем определение класса Example2. Если нет, поясните почему.
 Упражнение 2.17
 Как было показано в предыдущем упражнении, попытка использовать шаблон Example2 с типом, для которого не определена операция меньше, приведет к синтаксической ошибке. Однако ошибка проявится только тогда, когда в тексте компилируемой программы действительно встретится вызов функции min(), в противном случае компиляция пройдет успешно. Как вы считаете, оправдано ли такое поведение? Не лучше ли предупредить об ошибке сразу, при обработке описания шаблона? Поясните свое мнение.
 2.6. Использование исключений
 Исключениями называют аномальные ситуации, возникающие во время исполнения программы: невозможность открыть нужный файл или получить необходимое количество памяти, использование выходящего за границы индекса для какого-либо массива. Обработка такого рода исключений, как правило, плохо интегрируется в основной алгоритм программы, и программисты вынуждены изобретать разные способы корректной обработки исключения, стараясь в то же время не слишком усложнить программу добавлением всевозможных проверок и дополнительных ветвей алгоритма.
 С++ предоставляет стандартный способ реакции на исключения. Благодаря вынесению в отдельную часть программы кода, ответственного за проверку и обработку ошибок, значительно облегчается восприятие текста программы и сокращается ее размер. Единый синтаксис и стиль обработки исключений можно, тем не менее, приспособить к самым разнообразным нуждам и запросам.
 Механизм исключений делится на две основные части:
 точка программы, в которой произошло исключение. Определение того факта, что при выполнении возникла какая-либо ошибка, влечет за собой возбуждение исключения. Для этого в С++ предусмотрен специальный оператор throw. Возбуждение исключения в случае невозможности открыть некоторый файл выглядит следующим образом:
 if ( !infile ) {
  string errMsg("Невозможно открыть файл: ");
  errMsg += fileName;
  throw errMsg;
 }
 Место программы, в котором исключение обрабатывается. При возбуждении исключения нормальное выполнение программы приостанавливается и управление передается обработчику исключения. Поиск нужного обработчика часто включает в себя раскрутку так называемого стека вызовов программы. После обработки исключения выполнение программы возобновляется, но не с того места, где произошло исключение, а с точки, следующей за обработчиком. Для определения обработчика исключения в С++ используется ключевое слово catch. Вот как может выглядеть обработчик для примера из предыдущего абзаца:
 catch (string exceptionMsg) {
  log_message (exceptionMsg);
  return false;
 }
 Каждый catch-обработчик ассоциирован с исключениями, возникающими в блоке операторов, который непосредственно предшествует обработчику и помечен ключевым словом try. Одному try-блоку могут соответствовать несколько catch-предложений, каждое из которых относится к определенному виду исключений. Приведем пример:
 int* stats (const int *ia, int size)
 {
  int *pstats = new int [4];
  try {
  pstats[0] = sum_it (ia,size);
  pstats[1] = min_val (ia,size);
  pstats[2] = max_val (ia,size);
  }
  catch (string exceptionMsg) {
  // код обработчика
  }
  catch (const statsException &statsExcp) {
  // код обработчика
  }
 
  pstats [3] = pstats[0] / size;
  do_something (pstats);
 
  return pstats;
 }
 В данном примере в теле функции stats() три оператора заключены в try-блок, а четыре – нет. Из этих четырех операторов два способны возбудить исключения.
 1) int *pstats = new int [4];
 Выполнение оператора new может окончиться неудачей. Стандартная библиотека С++ предусматривает возбуждение исключения bad_alloc в случае невозможности выделить нужное количество памяти. Поскольку в примере не предусмотрен обработчик исключения bad_alloc, при его возбуждении выполнение программы закончится аварийно.
 2) do_something (pstats);
 Мы не знаем реализации функции do_something(). Любая инструкция этой функции, или функции, вызванной из этой функции, или функции, вызванной из функции, вызванной этой функцией, и так далее, потенциально может возбудить исключение. Если в реализации функции do_something и вызываемых из нее предусмотрен обработчик такого исключения, то выполнение stats() продолжится обычным образом. Если же такого обработчика нет, выполнение программы аварийно завершится.
 Необходимо заметить, что, хотя оператор
 pstats [3] = pstats[0] / size;
 может привести к делению на ноль, в стандартной библиотеке не предусмотрен такой тип исключения.
 Обратимся теперь к инструкциям, объединенным в try-блок. Если в одной из вызываемых в этом блоке функций – sum_it(), min_val() или max_val() –произойдет исключение, управление будет передано на обработчик, следующий за try-блоком и перехватывающий именно это исключение. Ни инструкция, возбудившая исключение, ни следующие за ней инструкции в try-блоке выполнены не будут. Представим себе, что при вызове функции sum_it() возбуждено исключение:
 throw string ("Ошибка: adump27832");
 Выполнение функции sum_it() прервется, операторы, следующие в try-блоке за вызовом этой функции, также не будут выполнены, и pstats[0] не будет инициализирована. Вместо этого возбуждается исключительное состояние и исследуются два catch-обработчика. В нашем случае выполняется catch с параметром типа string:
 catch (string exceptionMsg) {
  // код обработчика
 }
 После выполнения управление будет передано инструкции, следующей за последним catch-обработчиком, относящимся к данному try-блоку. В нашем случае это
 pstats [3] = pstats[0] / size;
 (Конечно, обработчик сам может возбуждать исключения, в том числе – того же типа. В такой ситуации будет продолжено выполнение catch-предложений, определенных в программе, вызвавшей функцию stats().)
 Вот пример:
 catch (string exceptionMsg) {
  // код обработчика
  cerr << "stats(): исключение: "
  << exceptionMsg
  << endl;
  delete [] pstats;
  return 0;
 }
 В таком случае выполнение вернется в функцию, вызвавшую stats(). Будем считать, что разработчик программы предусмотрел проверку возвращаемого функцией stats() значения и корректную реакцию на нулевое значение.
 Функция stats() умеет реагировать на два типа исключений: string и statsException. Исключение любого другого типа игнорируется, и управление передается в вызвавшую функцию, а если и в ней не найдется обработчика, – то в функцию более высокого уровня, и так до функции main().При отсутствии обработчика и там, программа аварийно завершится.
 Возможно задание специального обработчика, который реагирует на любой тип исключения. Синтаксис его таков:
 catch (...) {
  // обрабатывает любое исключение,
  // однако ему недоступен объект, переданный
  // в обработчик в инструкции throw
 }
 (Детально обработка исключительных ситуаций рассматривается в главах 11 и 19.)
 Упражнение 2.18
 Какие ошибочные ситуации могут возникнуть во время выполнения следующей функции:
 int *alloc_and_init (string file_name)
 {
  ifstream infile (file_name)
  int elem_cnt;
  infile >> elem_cnt;
  int *pi = allocate_array(elem_cnt);
 
  int elem;
  int index=0;
  while (cin >> elem)
  pi[index++] = elem;
 
  sort_array(pi,elem_cnt);
  register_data(pi);
 
  return pi;
 }
 Упражнение 2.19
 В предыдущем примере вызываемые функции allocate_array(), sort_array() и register_data() могут возбуждать исключения типов noMem, int и string соответственно. Перепишите функцию alloc_and_init(), вставив соответствующие блоки try и catch для обработки этих исключений. Пусть обработчики просто выводят в cerr сообщение об ошибке.
 Упражнение 2.20
 Усовершенствуйте функцию alloc_and_init() так, чтобы она сама возбуждала исключение в случае возникновения всех возможных ошибок (это могут быть исключения, относящиеся к вызываемым функциям allocate_array(), sort_array() и register_data() и какими-то еще операторами внутри функции alloc_and_init()). Пусть это исключение имеет тип string и строка, передаваемая обработчику, содержит описание ошибки.
 2.7. Использование пространства имен
 Предположим, что мы хотим предоставить в общее пользование наш класс Array, разработанный в предыдущих примерах. Однако не мы одни занимались этой проблемой; возможно, кем-то где-то, скажем, в одном из подразделений компании Intel был создан одноименный класс. Из-за того что имена этих классов совпадают, потенциальные пользователи не могут задействовать оба класса одновременно, они должны выбрать один из них. Эта проблема решается добавлением к имени класса некоторой строки, идентифицирующей его разработчиков, скажем,
 class Cplusplus_Primer_Third_Edition_Array { ... };
 Конечно, это тоже не гарантирует уникальность имени, но с большой вероятностью избавит пользователя от данной проблемы. Как, однако, неудобно пользоваться столь длинными именами!
 Стандарт С++ предлагает для решения проблемы совпадения имен механизм, называемый пространством имен. Каждый производитель программного обеспечения может заключить свои классы, функции и другие объекты в свое собственное пространство имен. Вот как выглядит, например, объявление нашего класса Array:
 namespace Cplusplus_Primer_3E {
  template class Array { ... };
 }
 Ключевое слово namespace задает пространство имен, определяющее видимость нашего класса и названное в данном случае Cplusplus_Primer_3E. Предположим, что у нас есть классы от других разработчиков, помещенные в другие пространства имен:
 namespace IBM_Canada_Laboratory {
  template class Array { ... };
  class Matrix { ... };
 }
 
 namespace Disney_Feature_Animation {
  class Point { ... };
  template class Array { ... };
 }

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

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