Главная страница
Навигация по странице:

  • 5.11.1. Обобщенный список

  • Язык программирования C++. Вводный курс. С для начинающих


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница22 из 93
    1   ...   18   19   20   21   22   23   24   25   ...   93

    234
    запретили его использование. Лучше уж полностью лишить пользователя какой-либо операции, чем допустить возможные ошибки. (В разделе 14.5 объясняется, почему действия копирующего конструктора по умолчанию в подобных случаях неверны.) Вот реализация конструктора, использующая функцию insert_end():
    }
    Оператор присваивания должен сначала вызвать remove_all(), а затем с помощью insert_end()
    вставить все элементы второго списка. Поскольку эта операция повторяется в обеих функциях, вынесем ее в отдельную функцию insert_all():
    } после чего копирующий конструктор и оператор присваивания можно реализовать так:
    }
    Теперь осталось обеспечить пользователя возможностью путешествовать по списку, например с помощью доступа к члену _at_front: ilist_item *ilist::front() { return _at_front(); }
    После этого можно применить ilist_item::next(), как мы делали в функциях-членах: ilist::ilist( const ilist &rhs )
    { ilist_item *pt = rhs._at_front; while ( pt ) { insert_end( pt->value() ); pt = pt->next();
    } void ilist::insert_all ( const ilist &rhs )
    { ilist_item *pt = rhs._at_front; while ( pt ) { insert_end( pt->value() ); pt = pt->next();
    } inline ilist::ilist( const ilist &rhs )
    : _at_front( 0 ), _at_end( 0 )
    { insert_all ( rhs ); } inline ilist& ilist::operator=( const ilist &rhs ) { remove_all(); insert_all( rhs ); return *this;

    С++ для начинающих
    235
    }
    Хотя это решает проблему, лучше поступить иначе: реализовать общую концепцию прохода по элементам контейнера. В данном разделе мы расскажем об использовании цикла такого вида: do_something( iter->value() );
    (В разделе 2.8 мы уже касались понятия итератора. В главах 6 и 12 будут рассмотрены итераторы для имеющихся в стандартной библиотеке контейнерных типов и обобщенных алгоритмов.)
    Наш итератор представляет собой несколько больше, чем просто указатель. Он должен уметь запоминать текущий элемент, возвращать следующий и определять, когда все элементы кончились. По умолчанию итератор инициализируется значением _at_front, однако пользователь может задать в качестве начального любой элемент списка. next_iter()
    возвращает следующий элемент или 0, если элементов больше нет. Для реализации пришлось ввести дополнительный член класса:
    }; init_iter()
    выглядит так:
    } next_iter()
    перемещает указатель _current на следующий элемент и возвращает его адрес, если элементы не кончились. В противном случае он возвращает 0 и устанавливает
    _current в 0. Его реализацию можно представить следующим образом: ilist_item *pt = mylist.front(); while ( pt ) { do_something( pt->value() ); pt = pt->next(); for ( ilist_item *iter = mylist.init_iter(); iter; iter = mylist.next_iter() ) class ilist { public:
    // ... init_iter( ilist_item *it = 0 ); private:
    //... ilist_item *_current; inline ilist_item* ilist::init_iter( i1ist_item *it )
    { return _current = it ? it : _at_front;

    С++ для начинающих
    236
    }
    Если элемент, на который указывает _current, удален, могут возникнуть проблемы. Их преодолевают модификацией кода функций remove() и remove_front(): они должны проверять значение _current. Если он указывает на удаляемый элемент, функции изменят его так, чтобы он адресовал следующий элемент либо был равен 0, когда удаляемый элемент – последний в списке или список стал пустым. Модифицированная remove_front() выглядит так:
    }
    Вот модифицированный фрагмент кода remove():
    _current = prev->next();
    Что произойдет, если элемент будет вставлен перед тем, на который указывает _current?
    Значение _current не изменяется. Пользователь должен начать проход по списку с помощью вызова init_iter(), чтобы новый элемент попал в число перебираемых. При инициализации списка другим и при присваивании значение _current не копируется, а сбрасывается в 0.
    Тестовая программа для проверки работы копирующего конструктора и оператора присваивания выглядит так:: inline ilist_item* ilist:: next_iter()
    { ilist_item *next = _current
    ? _current = _current->next()
    : _current; return next; inline void ilist::remove_front()
    { if ( _at_front ) { ilist_item *ptr = _at_front;
    _at_front = _at_front->next();
    // _current не должен указывать на удаленный элемент if ( _current == ptr )
    _current = _at_front; bump_down_size(); delete ptr;
    } while ( plist ) { if ( plist->value() == value )
    { prev->next( plist->next() ); if ( _current
    == plist )

    С++ для начинающих
    237
    }
    Результат работы программы:
    Применение init_iter() и next_iter() для обхода всех элементов списка:
    9 8 7 6 5 4 3 2 1 0 0 1 2 3 4 5 6 7 8 9
    Применение копирующего конструктора
    9 8 7 6 5 4 3 2 1 0 0 1 2 3 4 5 6 7 8 9
    Применение копирующего оператора присваивания
    9 8 7 6 5 4 3 2 1 0 0 1 2 3 4 5 6 7 8 9
    5.11.1.
    Обобщенный список
    Наш класс ilist имеет серьезный недостаток: он может хранить элементы только целого типа. Если бы он мог содержать элементы любого типа – как встроенного, так и определенного пользователем, – то его область применения была бы гораздо шире.
    Модифицировать ilist для поддержки произвольных типов данных позволяет механизм шаблонов (см. главу 16).
    #include
    #include "ilist.h" int main()
    { ilist mylist; for ( int ix = 0; ix < 10; ++ix ) { mylist.insert_front( ix ); mylist.insert_end( ix );
    } cout << "\n" << "
    Применение init_iter() и next_iter() "
    << "
    для обхода всех элементов списка:\n"; ilist_item *iter; for ( iter = mylist.init_iter(); iter; iter = mylist.next_iter() ) cout << iter->value() << " "; cout << "\n" << "
    Применение копирующего конструктора\n"; ilist mylist2( mylist ); mylist.remove_all(); for ( iter = mylist2.init_iter(); iter; iter = mylist2.next_iter() ) cout << iter->value() << " "; cout << "\n" << "
    Применение копирующего оператора присваивания\n"; mylist = mylist2; for ( iter = mylist.init_iter(); iter; iter = mylist.next_iter() ) cout << iter->value() << " "; cout << "\n";

    С++ для начинающих
    238
    При использовании шаблона вместо параметра подставляется реальный тип данных.
    Например: list< string > slist; создает экземпляр списка, способного содержать объекты типа string, а list< int > ilist; создает список, в точности повторяющий наш ilist. С помощью шаблона класса можно обеспечить поддержку произвольных типов данных одним экземпляром кода.
    Рассмотрим последовательность действий, уделив особое внимание классу list_item.
    Определение шаблона класса начинается ключевым словом template, затем следует список параметров в угловых скобках. Параметр представляет собой идентификатор, перед которым стоит ключевое слово class или typename. Например: class list_item;
    Эта инструкция объявляет list_item шаблоном класса с единственным параметром- типом. Следующее объявление эквивалентно предыдущему: class list_item;
    Ключевые слова class и typename имеют одинаковое значение, можно использовать любое из них. Более удобное для запоминания typename появилось в стандарте С++ сравнительно недавно и поддерживается еще не всеми компиляторами. Поскольку наши тексты были написаны до появления этого ключевого слова, в них употребляется class.
    Шаблон класса list_item выглядит так: template template

    С++ для начинающих
    239
    };
    Все упоминания типа int в определении класса ilist_item заменены на параметр elemType
    . Когда мы пишем: list_item *ptr = new list_item( 3.14 ); компилятор подставляет double вместо elemType и создает экземпляр list_item, поддерживающий данный тип.
    Аналогичным образом модифицируем класс ilist в шаблон класса list: template class list_item { public: list_item( elemType value, list_item *item = 0 )
    : _value( value ) { if ( !item )
    _next = 0; else {
    _next = item->_next; item->_next = this;
    }
    } elemType value() { return _value; } list_item* next() { return _next; } void next( list_item *link ) { _next = link; } void value( elemType new_value ) { _value = new_value; } private: elemType _value; list_item *_next;

    С++ для начинающих
    240
    };
    Объекты шаблона класса list используются точно так же, как и объекты класса ilist.
    Основное преимущество шаблона в том, что он обеспечивает поддержку произвольных типов данных с помощью единственного определения.
    (Шаблоны являются важной составной частью концепции программирования на С++. В главе 6 мы рассмотрим набор классов контейнерных типов, предоставляемых стандартной библиотекой С++. Неудивительно, что она содержит шаблон класса, реализующего операции со списками, равно как и шаблон класса, поддерживающего векторы; мы рассматривали их в главах 2 и 3.)
    Наличие класса списка в стандартной библиотеке представляет некоторую проблему. Мы выбрали для нашей реализации название list, но, к сожалению, стандартный класс также носит это название. Теперь мы не можем использовать в программе одновременно оба класса. Конечно, проблему решит переименование нашего шаблона, однако во многих случаях эта возможность отсутствует.
    Более общее решение состоит в использовании механизма пространства имен, который позволяет разработчику библиотеки заключить все свои имена в некоторое поименованное пространство и таким образом избежать конфликта с именами из глобального пространства. Применяя нотацию квалифицированного доступа, мы можем template class list { public: list()
    : _at_front( 0 ), _at_end( 0 ), _current( 0 ),
    _size( 0 ) {}
    1ist( const list& ); list& operator=( const list& );

    list() { remove_all(); } void insert ( list_item *ptr, elemType value ); void insert_end( elemType value ); void insert_front( elemType value ); void insert_all( const list &rhs ); int remove( elemType value ); void remove_front(); void remove_all(); list_item *find( elemType value ); list_item *next_iter(); list_item* init_iter( list_item *it ); void disp1ay( ostream &os = cout ); void concat( const list& ); void reverse (); int size() { return _size; } private: void bump_up_size() { ++_size; } void bump_down_size() { --_size; } list_item *_at_front;
    1ist_item *_at_end; list_item *_current; int _size;

    С++ для начинающих
    241
    употреблять эти имена в программах. Стандартная библиотека С++ помещает свои имена в пространство std. Мы тоже поместим наш код в собственное пространство:
    }
    Для использования такого класса в пользовательской программе необходимо написать следующее:
    // ...
    (Пространства имен описываются в разделах 8.5 и 8.6.)
    Упражнение 5.16
    Мы не определили деструктор для ilist_item, хотя класс содержит указатель на динамическую область памяти. Причина заключается в том, что класс не выделяет память для объекта, адресуемого указателем _next, и, следовательно, не несет ответственности за ее освобождение. Начинающий программист мог бы допустить ошибку, вызвав деструктор для ilist_item:
    }
    Посмотрите на функции remove_all() и remove_front() и объясните, почему наличие такого деструктора является ошибочным.
    Упражнение 5.17
    Наш класс ilist не поддерживает следующие операции: namespace Primer_Third_Edition
    { template class list_item{ ... }; template class list{ ... };
    // ...
    // наш заголовочный файл
    #include "list.h"
    // сделаем наши определения видимыми в программе using namespace Primer_Third_Edition;
    // теперь можно использовать наш класс list list< int > ilist; ilist_item::ilist_item()
    { delete _next; void ilist::remove_end();

    С++ для начинающих
    242
    void ilist::remove( ilist_item* );
    Как вы думаете, почему мы их не включили? Реализуйте их.
    Упражнение 5.18
    Модифицируйте функцию find() так, чтобы вторым параметром она принимала адрес элемента, с которого нужно начинать поиск. Если этот параметр не задан, поиск начинается с первого элемента. (Поскольку мы добавляем второй параметр, имеющий значение по умолчанию, открытый интерфейс данной функции не меняется. Программы, использующие предыдущую версию find(), будут работать без модификации.)
    };
    Упражнение 5.19
    Используя новую версию find(), напишите функцию count(), которая подсчитывает количество вхождений элементов с заданным значением. Подготовьте тестовую программу.
    Упражнение 5.20
    Модифицируйте insert(int value) так, чтобы она возвращала указатель на вставленный объект ilist_item.
    Упражнение 5.21
    Используя модифицированную версию insert(), напишите функцию: int elem_cnt ); где array_of_value указывает на массив значений, который нужно вставить в ilist, elem_cnt
    – на размер этого массива, а begin – на элемент, после которого производится вставка. Например, если есть ilist:
    (3)( 0 1 21 ) и массив: int ia[] = { 1, 2, 3, 5, 8, 13 }; вызов этой новой функции class ilist { public:
    // ... ilist_item* find( int value, ilist_item *start_at = 0 );
    // ... void ilist:: insert( ilist_item *begin, int *array_of_value,

    С++ для начинающих
    243
    mylist.insert( it, ia, 6 ); изменит список таким образом:
    (9) ( 0 1 1 2 3 5 8 13 21 )
    Упражнение 5.22
    Функции concat() и reverse() модифицируют оригинальный список. Это не всегда желательно. Напишите аналогичную пару функций, которые создают новый объект ilist
    : ilist ilist::concat_copy( const ilist &rhs ); ilist_item *it = mylist.find( 1 ); ilist ilist::reverse_copy();

    С++ для начинающих
    244
    6.
    Абстрактные контейнерные типы
    В этой главе мы продолжим рассмотрение типов данных, начатое в главе 3, представим дополнительную информацию о классах vector и string и познакомимся с другими контейнерными типами, входящими в состав стандартной библиотеки С++. Мы также расскажем об операторах и выражениях, упомянутых в главе 4, сосредоточив внимание на тех операциях, которые поддерживаются объектами контейнерных типов.
    Последовательный контейнер содержит упорядоченный набор элементов одного типа.
    Можно выделить два основных типа контейнеров – вектор (vector) и список (list).
    (Третий последовательный контейнер – двусторонняя очередь (deque) – обеспечивает ту же функциональность, что и vector, но особенно эффективно реализует операции вставки и удаления первого элемента. deque следует применять, например, при реализации очереди, из которой извлекается только первый элемент. Все сказанное ниже относительно вектора применимо также и к deque.)
    Ассоциативный контейнер эффективно реализует операции проверки существования и извлечения элемента. Два основных ассоциативных контейнера – это отображение
    (map) и множество (set). map состоит из пар ключ/значение, причем ключ используется для поиска элемента, а значение содержит хранимую информацию.
    Телефонный справочник хорошо иллюстрирует понятие отображения: ключом является фамилия и имя абонента, а значением – его телефонный номер.
    Элемент контейнера set содержит только ключ, поэтому set эффективно реализует операцию проверки его существования. Этот контейнер можно применить, например, при реализации системы текстового поиска для хранения списка так называемых стоп-слов – слов, не используемых при поиске, таких, как и, или, не, так и тому подобных.
    Программа обработки текста считывает каждое слово и проверяет, есть ли оно в указанном списке. Если нет, то слово добавляется в базу данных.
    В контейнерах map и set не может быть дубликатов – повторяющихся ключей. Для поддержки дубликатов существуют контейнеры multimap и multiset. Например, multimap можно использовать при реализации такого телефонного справочника, в котором содержится несколько номеров одного абонента.
    В последующих разделах мы детально рассмотрим контейнерные типы и разработаем небольшую программу текстового поиска.
    6.1.
    Система текстового поиска
    В систему текстового поиска входят текстовый файл, указанный пользователем, и средство для задания запроса, состоящего из слов и, возможно, логических операторов.
    Если одно или несколько слов запроса найдены, печатается количество их вхождений. По желанию пользователя печатаются предложения, содержащие найденные слова.

    С++ для начинающих
    245
    Например, если нужно найти все вхождения словосочетаний Civil War и Civil
    Rights
    , запрос может выглядеть таким образом
    9
    :
    Civil && ( War || Rights )
    Результат запроса:
    Civil: 12 вхождений
    War: 48 вхождений
    Rights: 1 вхождение
    Civil && War: 1 вхождение
    Civil && Rights: 1 вхождение
    (8) Civility, of course, is not to be confused with
    Civil Rights, nor should it lead to Civil War
    Здесь (8) представляет собой номер предложения в тексте. Наша система должна печатать фразы, содержащие найденные слова, в порядке возрастания их номеров (т.е. предложение номер 7 будет напечатано раньше предложения номер 9), не повторяя одну и ту же несколько раз.
    Наша программа должна уметь:

    запросить имя текстового файла, а затем открыть и прочитать этот файл;

    организовать внутреннее представление этого файла так, чтобы впоследствии соотнести найденное слово с предложением, в котором оно встретилось, и определить порядковый номер этого слова ;

    понимать определенный язык запросов. В нашем случае он включает следующие операторы:
    &&
    два слова непосредственно следуют одно за другим в строке
    ||
    одно или оба слова встречаются в строке
    !
    слово не встречается в строке
    ()
    группировка слов в запросе
    Используя этот язык, можно написать:
    Lincoln чтобы найти все предложения, включающие слово Lincoln, или
    9 Замечание. Для упрощения программы мы требуем, чтобы каждое слово было отделено пробелом от скобок и логических операторов. Таким образом, запросы вида
    (War || Rights)
    Civil&&(War||Rights) не будут поняты нашей системой. Хотя удобство пользователей не должно приноситься в жертву простоте реализации, мы считаем, что в данном случае можно смириться с таким ограничением.

    С++ для начинающих
    1   ...   18   19   20   21   22   23   24   25   ...   93


    написать администратору сайта