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

  • Таблица 6.1. Размер и емкость для различных типов данных Тип данных Размер в байтах Емкость после первой вставки

  • Таблица 6.2. Время в секундах для вставки 10 000 000 элементов Тип данных List Vector

  • Таблица 6.4. Время в секундах для вставки 10 000 элементов при различной емкости* Емкость Время в секундах

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


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница23 из 93
    1   ...   19   20   21   22   23   24   25   26   ...   93
    246
    ! Lincoln для поиска фраз, не содержащих такого слова, или же
    ( Abe || Abraham ) && Lincoln для поиска тех предложений, где есть словосочетания Abe Lincoln или Abraham Lincoln.
    Представим две версии нашей системы. В этой главе мы решим проблему чтения и хранения текстового файла в отображении, где ключом является слово, а значением – номер строки и позиции в строке. Мы обеспечим поиск по одному слову. (В главе 17 мы реализуем полную систему поиска, поддерживающую все указанные выше операторы языка запросов с помощью класса Query.) .
    Возьмем шесть строчек из неопубликованного детского рассказа Стена Липпмана (Stan
    Lippman)10:
    Рис. 2.
    Alice Emma has long flowing red hair. Her Daddy says when the wind blows through her hair, it looks almost alive, like a fiery bird in flight. A beautiful fiery bird, he tells her, magical but untamed. "Daddy, shush, there is no such thing," she tells him, at the same time wanting him to tell her more.
    Shyly, she asks, "I mean. Daddy, is there?"
    После считывания текста его внутреннее представление выглядит так (процесс считывания включает ввод очередной строки, разбиение ее на слова, исключение знаков препинания, замену прописных букв строчными, минимальная поддержка работы с суффиксами и исключение таких слов, как and, a, the): alice ((0,0)) alive ((1,10)) almost ((1,9)) ask ((5,2)) beautiful ((2,7)) bird ((2,3),(2,9)) blow ((1,3)) daddy ((0,8),(3,3),(5,5)) emma ((0,1)) fiery ((2,2),(2,8)) flight ((2,5)) flowing ((0,4)) hair ((0,6),(1,6)) has ((0,2)) like ((2,0)) long ((0,3)) look ((1,8)) magical ((3,0)) mean ((5,4)) more ((4,12)) red ((0,5)) same ((4,5)) say ((0,9)) she ((4,0),(5,1)) shush ((3,4)) shyly ((5,0)) such ((3,8)) tell ((2,11),(4,1),(4,10))
    10 Иллюстрация Елены Дрискилл (Elena Driskill).

    С++ для начинающих
    247
    there ((3,5),(5,7)) thing ((3,9)) through ((1,4)) time ((4,6)) untamed ((3,2)) wanting ((4,7)) wind ((1,2))
    Ниже приводится пример работы программы, которая будет реализована в данном разделе (то, что задает пользователь, выделено курсивом): please enter file name: alice_emma enter a word against which to search the text. to quit, enter a single character ==> alice alice occurs 1 time:
    ( line 1 ) Alice Emma has long flowing red hair. Her Daddy says enter a word against which to search the text. to quit, enter a single character ==> daddy daddy occurs 3 times:
    ( line 1 ) Alice Emma has long flow-ing red hair. Her Daddy says
    ( line 4 ) magical but untamed. "Daddy, shush, there is no such thing,"
    ( line 6 ) Shyly, she asks, "I mean, Daddy, is there?" enter a word against which to search the text. to quit, enter a single character ==> phoenix
    Sorry. There are no entries for phoenix. enter a word against which to search the text. to quit, enter a single character ==> .
    Ok, bye!
    Для того чтобы реализация была достаточно простой, необходимо детально рассмотреть стандартные контейнерные типы и тип string, представленный в главе 3.
    6.2.
    Вектор или список?
    Первая задача, которую должна решить наша программа, – это считывание из файла заранее неизвестного количества слов. Слова хранятся в объектах типа string.
    Возникает вопрос: в каком контейнере мы будем хранить слова – в последовательном или ассоциативном?
    С одной стороны, мы должны обеспечить возможность поиска слова и, в случае успеха, извлечь относящуюся к нему информацию. Отображение map является самым удобным для этого классом.
    Но сначала нам нужно просто сохранить слова для предварительной обработки – исключения знаков препинания, суффиксов и т.п. Для этой цели последовательный контейнер подходит гораздо больше. Что же нам использовать: вектор или список?
    Если вы уже писали программы на С или на С++ прежних версий, для вас, скорее всего, решающим фактором является возможность заранее узнать количество элементов. Если это количество известно на этапе компиляции, вы используете массив, в противном случае – список, выделяя память под очередной его элемент.
    Однако это правило неприменимо к стандартным контейнерам: и vector, и deque допускают динамическое изменение размера. Выбор одного из этих трех классов должен

    С++ для начинающих
    248
    зависеть от способов, с помощью которых элементы добавляются в контейнер и извлекаются из него.
    Вектор представляет собой область памяти, где элементы хранятся друг за другом. Для этого типа произвольный доступ (возможность извлечь, например, элемент 5, затем 15, затем 7 и т.д.) можно реализовать очень эффективно, поскольку каждый из них находится на некотором фиксированном расстоянии от начала. Однако вставка, кроме случая добавления в конец, крайне неэффективна: операция вставки в середину вектора потребует перемещения всего, что следует за вставляемым. Особенно это сказывается на больших векторах. (Класс deque устроен аналогично, однако операции вставки и удаления самого первого элемента работают в нем быстрее; это достигается двухуровневым представлением контейнера, при котором один уровень представляет собой реальное размещение элементов, а второй уровень адресует первый и последний из них.)
    Список располагается в памяти произвольным образом. Каждый элемент содержит указатели на предыдущий и следующий, что позволяет перемещаться по списку вперед и назад. Вставка и удаление реализованы эффективно: изменяются только указатели. С другой стороны, произвольный доступ поддерживается плохо: чтобы прийти к определенному элементу, придется посетить все предшествующие. Кроме того, в отличие от вектора, дополнительно расходуется память под два указателя на каждый элемент списка.
    Вот некоторые критерии для выбора одного из последовательных контейнеров:

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

    если количество элементов известно заранее, также предпочтительнее вектор;

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

    если нам не нужна возможность вставлять и удалять элементы в начало контейнера, вектор предпочтительнее, чем deque.
    Как быть, если нам нужна возможность и произвольного доступа, и произвольного добавления/удаления элементов? Приходится выбирать: тратить время на поиск элемента или на его перемещение в случае вставки/удаления. В общем случае мы должны исходить из того, какую основную задачу решает приложение: поиск или добавление элементов?
    (Для выбора подхода может потребоваться измерение производительности для обоих типов контейнеров.) Если ни один из стандартных контейнеров не удовлетворяет нас, может быть, стоит разработать свою собственную, более сложную, структуру данных.
    Какой из контейнеров выбрать, если мы не знаем количества его элементов (он будет динамически расти) и у нас нет необходимости ни в произвольном доступе, ни в добавлении элементов в середину? Что в таком случае более эффективно: список или вектор? (Мы отложим ответ на этот вопрос до следующего раздела.)
    Список растет очень просто: добавление каждого нового элемента приводит к тому, что указатели на предыдущий и следующий для тех элементов, между которыми вставляется новый, меняют свои значения. В новом элементе таким указателям присваиваются значения адресов соседних элементов. Список использует только тот объем памяти, который нужен для имеющегося количества элементов. Накладными расходами являются два указателя в каждом элементе и необходимость использования указателя для получения значения элемента.
    Внутреннее представление вектора и управление занимаемой им памятью более сложны.
    Мы рассмотрим это в следующем разделе.

    С++ для начинающих
    249
    Упражнение 6.1
    Что лучше выбрать в следующих примерах: вектор, список или двустороннюю очередь?
    Или ни один из контейнеров не является предпочтительным?
    1. Неизвестное заранее количество слов считывается из файла для генерации случайных предложений.
    2. Считывается известное количество слов, которые вставляются в контейнер в алфавитном порядке.
    3. Считывается неизвестное количество слов. Слова добавляются в конец контейнера, а удаляются всегда из начала.
    4. Считывается неизвестное количество целых чисел. Числа сортируются и печатаются.
    6.3.
    Как растет вектор?
    Вектор может расти динамически. Как это происходит? Он должен выделить область памяти, достаточную для хранения всех элементов, скопировать в эту область все старые элементы и освободить ту память, в которой они содержались раньше. Если при этом элементы вектора являются объектами класса, то для каждого из них при таком копировании вызываются конструктор и деструктор. Поскольку у списка нет необходимости в таких дополнительных действиях при добавлении новых элементов, кажется очевидным, что ему проще поддерживать динамический рост контейнера.
    Однако на практике это не так. Давайте посмотрим почему.
    Вектор может запрашивать память не под каждый новый элемент. Вместо этого она запрашивается с некоторым запасом, так что после очередного выделения вектор может поместить в себя некоторое количество элементов, не обращаясь за ней снова. (Каков размер этого запаса, зависит от реализации.) На практике такое свойство вектора обеспечивает значительное увеличение его эффективности, особенно для небольших объектов. Давайте рассмотрим некоторые примеры из реализации стандартной библиотеки С++ от компании Rogue Wave. Однако сначала определим разницу между размером и емкостью контейнера.
    Емкость – это максимальное количество элементов, которое может вместить контейнер без дополнительного выделения памяти. (Емкостью обладают только те контейнеры, в которых элементы хранятся в непрерывной области памяти, – vector, deque и string.
    Для контейнера list это понятие не определено.) Емкость может быть получена с помощью функции capacity(). Размер – это реальное количество элементов, хранящихся в данный момент в контейнере. Размер можно получить с помощью функции size()
    . Например:

    С++ для начинающих
    250
    }
    В реализации Rogue Wave и размер, и емкость ivec сразу после определения равны 0.
    После вставки первого элемента размер становится равным 1, а емкость – 256. Это значит, что до первого дополнительного выделения памяти в ivec можно вставить 256 элементов. При добавлении 256-го элемента вектор должен увеличиться: выделить память объемом в два раза больше текущей емкости, скопировать в нее старые элементы и освободить прежнюю память. Обратите внимание: чем больше и сложнее тип данных элементов, тем менее эффективен вектор в сравнении со списком. В таблице 6.1 показана зависимость начальной емкости вектора от используемого типа данных.
    Таблица 6.1. Размер и емкость для различных типов данных
    Тип данных
    Размер
    в байтах
    Емкость
    после
    первой вставки
    int
    4 256 double
    8 128 простой класс #1 12 85 string
    12 85 большой простой класс
    8000 1 большой сложный класс
    8000 1
    Итак, в реализации Rogue Wave при первой вставке выделяется точно или примерно 1024 байта. После каждого дополнительного выделения памяти емкость удваивается. Для типа данных, имеющего большой размер, емкость мала, и увеличение памяти с копированием старых элементов происходит часто, вызывая потерю эффективности. (Говоря о сложных классах, мы имеем в виду класс, обладающий копирующим конструктором и операцией присваивания.) В таблице 6.2 показано время в секундах, необходимое для вставки десяти миллионов элементов разного типа в список и в вектор. Таблица 6.3 показывает время, требуемое для вставки 10 000 элементов (вставка элементов большего размера оказалась слишком медленной).
    Таблица 6.2. Время в секундах для вставки 10 000 000 элементов
    Тип данных
    List
    Vector
    int
    10.38 3.76
    #include
    #include int main()
    { vector< int > ivec; cout << "ivec: размер: " << ivec.size()
    << " емкость: " << ivec.capacity() << endl; for ( int ix = 0; -ix < 24; ++ix ) { ivec.push_back( ix ); cout << "ivec: размер: " << ivec.size()
    << " емкость: " << ivec.capacity() << endl;
    }

    С++ для начинающих
    251
    double
    10.72 3.95 простой класс
    12.31 5.89 string
    14.42 11.80
    Таблица 6.3. Время в секундах для вставки 10 000 элементов
    Тип данных
    List
    Vector
    большой простой класс
    0.36 2.23 большой сложный класс
    2.37 6.70
    Отсюда следует, что вектор лучше подходит для типов данных малого размера, нежели список, и наоборот. Эта разница объясняется необходимостью выделения памяти и копирования в нее старых элементов. Однако размер данных – не единственный фактор, влияющий на эффективность. Сложность типа данных также ухудшает результат.
    Почему?
    Вставка элемента как в список, так и в вектор, требует вызова копирующего конструктора, если он определен. (Копирующий конструктор инициализирует один объект значением другого. В разделе 2.2 приводится начальная информация, а в разделе
    14.5 о таких конструкторах рассказывается подробно). Это и объясняет различие в поведении простых и сложных объектов при вставке в контейнер. Объекты простого класса вставляются побитовым копированием (биты одного объекта пересылаются в биты другого), а для строк и сложных классов это производится вызовом копирующего конструктора.
    Вектор должен вызывать их для каждого элемента при перераспределении памяти. Более того, освобождение памяти требует работы деструкторов для всех элементов (понятие деструктора вводится в разделе 2.2). Чем чаще происходит перераспределение памяти, тем больше времени тратится на эти дополнительные вызовы конструкторов и деструкторов.
    Конечно, одним из решений может быть переход от вектора к списку, когда эффективность вектора становится слишком низкой. Другое, более предпочтительное решение состоит в том, чтобы хранить в векторе не объекты сложного класса, а указатели на них. Такая замена позволяет уменьшить затраты времени на 10 000 вставок с 6.70 секунд до 0.82 секунды. Почему? Емкость возросла с 1 до 256, что существенно снизило частоту перераспределения памяти. Кроме того, копирующий конструктор и деструктор не вызываются больше для каждого элемента при копировании прежнего содержимого вектора.
    Функция reserve() позволяет программисту явно задать емкость контейнера11.
    Например:
    11 Отметим, что deque не поддерживает операцию reserve()
    int main() { vector< string > svec; svec.reserve( 32 ); // задает емкость равной 32
    // ...

    С++ для начинающих
    252
    } svec получает емкость 32 при размере 0. Однако эксперименты показали, что любое изменение начальной емкости для вектора, у которого она по умолчанию отлична от 1, ведет к снижению производительности. Так, для векторов типа string и double увеличение емкости с помощью reserve() дало худшие показатели. С другой стороны, увеличение емкости для больших сложных типов дает значительный рост производительности, как показано в таблице 6.4.
    Таблица 6.4. Время в секундах для вставки 10 000 элементов при различной
    емкости*
    Емкость
    Время в секундах
    1 по умолчанию
    670 4,096 555 8,192 444 10,000 222
    *
    Сложный класс размером 8000 байт с конструктором копирования и деструктором
    В нашей системе текстового поиска для хранения объектов типа string мы будем использовать вектор, не меняя его емкости по умолчанию. Наши измерения показали, что производительность вектора в данном случае лучше, чем у списка. Но прежде чем приступать к реализации, посмотрим, как определяется объект контейнерного типа.
    Упражнение 6.2
    Объясните разницу между размером и емкостью контейнера. Почему понятие емкости необходимо для контейнера, содержащего элементы в непрерывной области памяти, и не нужно для списка?
    Упражнение 6.3
    Почему большие сложные объекты удобнее хранить в контейнере в виде указателей на них, а для коллекции целых чисел применение указателей снижает эффективность?
    Упражнение 6.4
    Объясните, какой из типов контейнера – вектор или список – больше подходит для приведенных примеров (во всех случаях происходит вставка неизвестного заранее числа элементов):.
    (a) Целые числа
    (b) Указатели на большие сложные объекты
    (c) Большие сложные объекты
    6.4.
    Как определить последовательный контейнер?
    Для того чтобы определить объект контейнерного типа, необходимо сначала включить соответствующий заголовочный файл:

    С++ для начинающих
    253
    #include
    Определение контейнера начинается именем его типа, за которым в угловых скобках следует тип данных его элементов
    12
    . Например: list< int > ilist;
    Переменная svec определяется как вектор, способный содержать элементы типа string, а ilist – как список с элементами типа int. Оба контейнера при таком определении пусты. Чтобы убедиться в этом, можно вызвать функцию-член empty():
    ; // что-то не так
    Простейший метод вставки элементов – использование функции-члена push_back(), которая добавляет элементы в конец контейнера. Например: svec.push_back( text_word );
    Здесь строки из стандартного ввода считываются в переменную text_word, и затем копия каждой строки добавляется в контейнер svec с помощью push_back().
    Список имеет функцию-член push_front(), которая добавляет элемент в его начало.
    Пусть есть следующий массив: int ia[ 4 ] = { 0, 1, 2, 3 };
    12 Существующие на сегодняшний день реализации не поддерживают шаблоны с параметрами по умолчанию. Второй параметр – allocator – инкапсулирует способы выделения и освобождения памяти. В С++ он имеет значение по умолчанию, и его задавать не обязательно. Стандартная реализация использует операторы new и delete.
    Применение распределителя памяти преследует две цели: упростить реализацию контейнеров путем отделения всех деталей, касающихся работы с памятью, и позволить программисту при желании реализовать собственную стратегию выделения памяти.
    Определения объектов для компилятора, не поддерживающего значения по умолчанию параметров шаблонов, выглядят следующим образом: vector< string, allocator > svec; list< int, allocator > ilist;
    #include
    #inclnde
    #include
    #include vector< string > svec; if ( svec.empty() != true ) string text_word; while ( cin >> text_word )

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


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