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

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

  // ...
 }
 Если эта инструкция используется внутри вложенных циклов или инструкций switch, она завершает выполнение того внутреннего блока, в котором находится. Цикл или switch, включающий тот цикл или switch, из которого мы вышли с помощью break, продолжает выполняться. Например:
 white ( cin >> inBuf )
 {
  switch( inBuf[ 0 ] ) {
  case '-':
  for ( int ix = 1; ix < inBuf.size(); ++ix ) {
  if ( inBuf[ ix ] == ' ' )
  break; // #1
  // ...
  // ...
  }
  break; // #2
  case '+':
  // ...
  }
 }
 Инструкция break, помеченная // #1, завершает выполнение цикла for внутри ветви case '-' блока switch, но не сам switch. Аналогично break // #2 завершает выполнение блока switch, но не цикла while, в который тот входит.
 5.9. Инструкция continue
 Инструкция continue завершает текущую итерацию цикла и передает управление на вычисление условия, после чего цикл может продолжиться. В отличие от инструкции break, завершающей выполнение всего цикла, инструкция continue завершает выполнение только текущей итерации. Например, следующий фрагмент программы читает из входного потока по одному слову. Если слово начинается с символа подчеркивания, оно обрабатывается, в противном случае программа переходит к новому слову.
 while ( cin >> inBuf ) {
  if ( inBuf[0] '= '_' )
  continue; // завершение итерации
 
  // обработка слова ...
 }
 Инструкция continue может быть использована только внутри цикла.
 5.10. Инструкция goto
 Инструкция goto обеспечивает безусловный переход к другой инструкции внутри той же функции, поэтому современная практика программирования выступает против ее применения.
 Синтаксис goto следующий:
 goto метка;
 где метка – определенный пользователем идентификатор. Метка ставится перед инструкцией, на которую можно перейти с помощью goto, и должна заканчиваться двоеточием. Нельзя ставить метку непосредственно перед закрывающей фигурной скобкой. Если же это необходимо, их следует разделить пустой инструкцией:
 end: ; // пустая инструкция
 }
 Переход через инструкцию объявления в том же блоке с помощью goto невозможен. Например, данная функция вызывает ошибку компиляции:
 int oops_in_error() {
  // mumble ...
  goto end;
  // ошибка: переход через объявление
  int ix = 10;
  // ... код, использующий ix
 end: ;
 }
 Правильная реализация функции помещает объявление ix и использующие его инструкции во вложенный блок:
 int oops_in_error() {
  // mumble ...
  goto end;
 
  {
  // правильно: объявление во вложенном блоке
  int ix = 10;
  // ... код, использующий ix
  }
 end: ;
 }
 Причина такого ограничения та же, что и для объявлений внутри блока switch: компилятор должен гарантировать, что для объявленного объекта конструктор и деструктор либо выполняются вместе, либо ни один из них не выполняется. Это и достигается заключением объявления во вложенный блок.
 Переход назад через объявление, однако, не считается ошибкой. Почему? Перепрыгнуть через инициализацию объекта нельзя, но проинициализировать один и тот же объект несколько раз вполне допустимо, хотя это может привести к снижению эффективности. Например:
 // переход назад через объявление не считается ошибкой.
 void
 mumble ( int max_size )
 {
  begin:
  int sz = get_size();
  if ( sz <= 0 ) {
  // выдать предупреждение ...
  goto end;
  }
  else
  if ( sz > max_size )
  // получить новое значение sz
  goto begin;
 { // правильно: переход через целый блок
  int ia = new int[ sz ];
  doit( ia, sz ) ;
  delete [] ia;
  }
  end:
  ;
 }
 Использование инструкции goto резко критикуется во всех современных языках программирования. Ее применение приводит к тому, что ход выполнения программы становится трудно понять и, следовательно, такую программу трудно модифицировать. В большинстве случаев goto можно заменить на инструкции if или циклы. Если вы все-таки решили использовать goto, не перескакивайте через большой фрагмент кода, чтобы можно было легко найти начало и конец вашего перехода.
 5.11. Пример связанного списка
 Мы завершали главы 3 и 4 примерами для введения читателя в механизм классов С++. В конце этого раздела мы покажем, как разработать класс, представляющий собой односвязный список. (В главе 6 мы рассмотрим двусвязный список, являющийся частью стандартной библиотеки.) Если вы в первый раз читаете эту книгу, то можете пропустить данный раздел и вернуться к нему после чтения главы 13. (Для усвоения этого материала нужно представлять себе механизм классов С++, конструкторы, деструкторы и т.д. Если вы плохо знаете классы, но все же хотите продолжить чтение данного раздела, мы рекомендуем прочесть пункты 2.3 и 3.15.
 Список представляет собой последовательность элементов, каждый из которых содержит значение некоторого типа и адрес следующего элемента (причем для последнего из них адрес может быть нулевым). К любой такой последовательности всегда можно добавить еще один элемент (хотя реальная попытка подобного добавления может закончиться неудачно, если отведенная программе свободная память исчерпана). Список, в котором нет ни одного элемента, называется пустым.
 Какие операции должен поддерживать список? Добавление (insert), удаление (remove) и поиск (find) определенных элементов. Кроме того, можно запрашивать размер списка (size), распечатывать его содержимое (display), проверять равенство двух списков. Мы покажем также, как инвертировать (reverse) и сцеплять (concatenate) списки.
 Простейшая реализация операции size() перебирает все элементы, подсчитывая их количество. Более сложная реализация сохраняет размер как член данных; она намного эффективнее, однако требует некоторого усложнения операций insert() и remove() для поддержки размера в актуальном состоянии.
 Мы выбрали второй вариант реализации функции size() и храним размер списка в члене данных. Мы предполагаем, что пользователи будут достаточно часто применять эту операцию, поэтому ее необходимо реализовать как можно более эффективно.
  (Одним из преимуществ отделения открытого интерфейса от скрытой реализации является то, что если наше предположение окажется неверным, мы сможем переписать реализацию, сохранив открытый интерфейс – в данном случае тип возвращаемого значения и набор параметров функции size() – и программы, использующие эту функцию, не нужно будет модифицировать.)
 Операция insert() в общем случае принимает два параметра: указатель на один из элементов списка и новое значение, которое вставляется после указанного элемента. Например, для списка
 
 1 1 2 3 8
 
 вызов
 mylist.insert (pointer_to_3, 5);
 изменит наш список так:
 
 1 1 2 3 5 8
 
 Чтобы обеспечить подобную возможность, нам необходимо дать пользователю способ получения адреса определенного элемента. Одним из способов может быть использование функции find() – нахождение элемента с определенным значением:
 pointer_to_3 = mylist.find( 3 );
 find() принимает в качестве параметра значение из списка. Если элемент с таким значением найден, то возвращается его адрес, иначе find() возвращает 0.
 Может быть два специальных случая вставки элемента: в начало и в конец списка. Для этого требуется только задание значения:
 insert_front( value );
 1nsert_end( value );
 Предусмотрим следующие операции удаления элемента с заданным значением, первого элемента и всех элементов списка:
 remove( value );
 remove_front();
 remove_all();
 Функция display() распечатывает размер списка и все его элементы. Пустой список можно представить в виде:
 
 (0)( )
 
 а список из семи элементов как:
 
  (7) ( 0 1 1 2 3 5 8 )
 
 reverse() меняет порядок элементов на противоположный. После вызова
 mylist.reverse();
 предыдущий список выглядит таким образом:
 
  (7) ( 8 5 3 2 1 1 0 )
 
 Конкатенация добавляет элементы второго списка в конец первого. Например, для двух списков:
 
 (4)( 0 1 1 2 ) // listl
 (4)( 2 3 5 8 ) // list2
 
 операция
 listl.concat( list2 );
 превращает list1 в
 
 (8) ( 0 1 1 2 2 3 5 8 )
 
 Чтобы сделать из этого списка последовательность чисел Фибоначчи, мы можем воспользоваться функцией remove():
 listl.remove( 2 );
 Мы определили поведение нашего списка, теперь можно приступать к реализации. Пусть список (list) и элемент списка (list_item) будут представлены двумя разными классами. (Ограничимся теми элементами, которые способны хранить только целые значения. Отсюда названия наших классов – ilist и ilist_item.)
 Наш список содержит следующие члены: _at_front – адрес первого элемента, _at_end – адрес последнего элемента и _size – количество элементов. При определении объекта типа ilist все три члена должны быть инициализированы 0. Это обеспечивается конструктором по умолчанию:
 class ilist_item;
 
 class ilist {
 public:
  // конструктор по умолчанию
  ilist() : _at_front( 0 ),
  _at_end( 0 ), _size( 0 ) {}
 
  // ...
 private:
  ilist_item *_at_front;
  ilist_item *_at_end;
  int _size;
 };
 Теперь мы можем определять объекты типа ilist, например:
 ilist mylist;
 но пока ничего больше. Добавим возможность запрашивать размер списка. Включим объявление функции size() в открытый интерфейс списка и определим эту функцию так:
 inline int ilist::size() { return _size; }
 Теперь мы можем использовать:
 int size = mylist.size();
 Пока не будем позволять присваивать один список другому и инициализировать один список другим (впоследствии мы реализуем и это, причем такие изменения не потребуют модификации пользовательских программ). Объявим копирующий конструктор и копирующий оператор присваивания в закрытой части определения списка без их реализации. Теперь определение класса ilist выглядит таким образом:
 class ilist {
 public:
  // определения не показаны
  ilist();
  int size();
  // ...
 private:
  // запрещаем инициализацию
  // и присваивание одного списка другому
 ilist( const ilist& );
 ilist& operator=( const ilist& );
 
  // данные-члены без изменения
 };
 Обе строки следующей программы вызовут ошибки компиляции, потому что функция main() не может обращаться к закрытым членам класса ilist:
 int main()
 {
  ilist yourlist( mylist ); // ошибка
  mylist = mylist; // ошибка
 }
 Следующий шаг – вставка элемента, для представления которого мы выбрали отдельный класс:
 class ilist_item {
 public:
  // ...
 private:
  int _value;
  ilist_item *_next;
 };
 Член _value хранит значение, а _next – адрес следующего элемента или 0.
 Конструктор ilist_item требует задания значения и необязательного параметра – адреса существующего объекта ilist_item. Если этот адрес задан, то создаваемый объект ilist_item будет помещен в список после указанного. Например, для списка
 
 0 1 1 2 5
 
 вызов конструктора
 ilist_item ( 3, pointer_to_2 );
 модифицирует последовательность так:
 
 0 1 1 2 3 5
 
 Вот реализация ilist_item. (Напомним, что второй параметр конструктора является необязательным. Если пользователь не задал второй аргумент при вызове конструктора, по умолчанию употребляется 0. Значение по умолчанию указывается в объявлении функции, а не в ее определении; это поясняется в главе 7.)
 class ilist_item {
 public:
  ilist_item( int value, ilist_-item *item_to_link_to = 0 );
  // ...
 };
 
 inline
 ilist_item::
 ilist_item( int value, ilist_item *item )
  : _value( value )
 {
  if ( item )
  _next = 0;
  else {
  _next = item->_next;
  item->_next = this;
 }
 Операция insert() в общем случае работает с двумя параметрами – значением и адресом элемента, после которого производится вставка. Наш первый вариант реализации имеет два недочета. Сможете ли вы их найти?
 inline void
 ilist::
 insert( ilist_item *ptr, int value )
 {
  new ilist_item( value, ptr );
  ++_size;
 }
 Одна из проблем заключается в том, что указатель не проверяется на нулевое значение. Мы обязаны распознать и обработать такую ситуацию, иначе это приведет к краху программы во время исполнения. Как реагировать на нулевой указатель? Можно аварийно закончить выполнение, вызвав стандартную функцию abort(), объявленную в заголовочном файле cstdlib:
 #include
 // ...
 if ( ! ptr )
  abort();
 Кроме того, можно использовать макрос assert(). Это также приведет к аварийному завершению, но с выводом диагностического сообщения:
 #include
 // ...
 assert( ptr != 0 );
 Третья возможность – возбудить исключение:
 if ( ! ptr )
  throw "Panic: ilist::insert(): ptr == O";
 В общем случае желательно избегать аварийного завершения программы: в такой ситуации мы заставляем пользователя беспомощно сидеть и ждать, пока служба поддержки обнаружит и исправит ошибку.
 Если мы не можем продолжать выполнение там, где обнаружена ошибка, лучшим решением будет возбуждение исключения: оно передает управление вызвавшей программе в надежде, что та сумеет выйти из положения.
 Мы же поступим совсем другим способом: рассмотрим передачу нулевого указателя как запрос на вставку элемента перед первым в списке:
 if ( ! ptr )
  insert_front( value );
 Второй изъян в нашей версии можно назвать философским. Мы реализовали size() и _size как пробный вариант, который может впоследствии измениться. Если мы преобразуем функции size() таким образом, что она будет просто пересчитывать элементы списка, член _size перестанет быть нужным. Написав:
 ++_size;
 мы тесно связали реализацию insert() с текущей конструкцией алгоритма пересчета элементов списка. Если мы изменим алгоритм, нам придется переписывать эту функцию, как и insert_front(), insert_end() и все операции удаления из списка. Вместо того чтобы распространять детали текущей реализации на разные функции класса, лучше инкапсулировать их в паре:
 inline void ilist::bump_up_size() { ++_size; }
 inline void ilist::bump_down_size() { --_size; }
 Поскольку мы объявили эти функции встроенными, эффективность не пострадала. Вот окончательный вариант insert():
 inline void
 ilist::
 insert( ilist_item *ptr, int value )
 if ( !ptr )
  insert_front( value );
 else {
  bump_up_size();
  new ilist_item( value, ptr );
  }
 }
 Реализация функций insert_front() и insert_end() достаточно очевидна. В каждой из них мы должны предусмотреть случай, когда список пуст.
 inline void
 ilist::
 insert_front( int value )
 {
  ilist_item *ptr = new ilist_item( value );
 
  if ( !_at_front )
  _at_front = _at_end = ptr;
  else {
  ptr->next( _at_front );
  _at_front = ptr;
  }
  bump_up_size();
 }
 
 inl-ine void
 ilist::
 insert_end( int value )
 {
  if ( !_at_end )
  _at_end = _at_front = new ilist_item( value );
  else _at_end = new ilist_item( value, _at_end );
 
  bump_up_s-ize();
 }
 find() ищет значение в списке. Если элемент с указанным значением найден, возвращается его адрес, иначе find() возвращает 0. Реализация find()выглядит так:
 ilist_item*
 ilist::
 find( int value )
 {
  ilist_item *ptr = _at_front;
  while ( ptr )
  {
  if ( ptr->value() == value )
  break;
  ptr = ptr->next();
  }
  return ptr;
 }
 Функцию find() можно использовать следующим образом:
 ilist_item *ptr = mylist.find( 8 );
 mylist.insert( ptr, some_value );
 или в более компактной записи:

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

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