Опубликован: 07.03.2015 | Уровень: для всех | Доступ: свободно | ВУЗ: Компания ALT Linux
Лекция 10:

Объектно-ориентированное программирование

10.7 Элементы стандартной библиотеки C++

10.7.1 Базовые понятия

Стандартная библиотека C++ — это общий набор шаблонов классов и алгоритмов, позволяющий программистам легко реализовывать стандартные структуры данных, такие как очереди, списки и стеки.

В библиотеке выделяют пять основных компонентов:

  • Контейнер (container) — хранение набора объектов в памяти.
  • Итератор (iterator) — обеспечение средств доступа к содержимому контейнера.
  • Алгоритм (algorithm) — определение вычислительной процедуры.
  • Адаптер (adaptor) — адаптация компонентов для обеспечения различного интерфейса.
  • Функциональный объект (functor) — сокрытие функции в объекте для использования другими компонентами.

10.7.2 Контейнеры

Будем считать, что стандартная библиотека реализует следующие контейнеры:

  • vector — линейный массив (особенно эффективен при добавлении элементов в конец);
  • list — двусвязаный список (более эффективен при вставке и перестановке элементов);
  • set — ассоциативный массив уникальных ключей (математический тип множество);
  • multiset — ассоциативный массив с возможностью дублирования ключей;
  • bitset — массив, обеспечивающий компактное хранение заданного количества битов;
  • map — ассоциативный массив с уникальными ключами и значениями (хорош при поиске по ключу);
  • multimap — ассоциативный массив с возможностью дублирования ключей и значений;
  • stack — структура данных типа стек;
  • queue — структура данных типа очередь;
  • priorityqueue — очередь с приоритетами;
  • deque — очередь с двухсторонним доступом.

Например, стек целых чисел можно объявить так:

stack<int> s;

С чуть большими затратами можно заставить работать контейнеры с собственным типом данных.

Основные методы, которые присутствуют почти во всех коллекциях стандартной библиотеки, приведены ниже.

  • empty — определяет, является ли коллекция пустой.
  • size — определяет размер коллекции.
  • begin, end— указывают на начало и конец коллекции.
  • rbegin, rend — то же, но для желающих пройти коллекцию от конца к началу.
  • clear — удаляет все элементы коллекции (если в коллекции сохранены указатели, нужно ещё удалить все элементы вручную вызовом delete).
  • erase — удаляет элемент или несколько элементов из коллекции.
  • capacity — вместимость коллекции определяет реальный размер — то есть размер буфера коллекции, а не то, сколько в нём хранится элементов. Когда вы создаёте коллекцию, то выделяется некоторое количество памяти. Как только размер буфера оказывается меньшим, чем размер, необходимый для хранения всех элементов коллекции, происходит выделение памяти для нового буфера, а все элементы старого копируются в новый буфер.
  • insert — вставка элемента в произвольном месте последовательности.

Есть также и необязательные операции: front, back, push_front, push_back, pop_front, pop_back, и оператор [].

10.7.3 Итераторы

Итератор (iterator) согласно Википедии — объект, позволяющий программисту перебирать все элементы коллекции без учёта особенностей её реализации. В простейшем случае итератором в низкоуровневых языках является указатель.

В C++ есть несколько разных типов итераторов (табл. 10.4):

Таблица 10.4. Типы итераторов
Итератор Описание
input_iterator(для чтения) Читают значения с движением вперёд. Могут быть инкрементированы, сравнены и разыменованы.
output_iterator(для записи) Пишут значения с движением вперёд. Могут быть инкрементированы и разыменованы
forward_iterator(однонаправленные) Читают или пишут значения с движением вперёд. Комбинируют функциональность предыдущих двух типов с возможностью сохранять значение итератора
bidirectional_iteratorдвунаправленные) Читают и пишут значения с движением вперёд или назад. Похожи на однонаправленные, но их также можно инкрементировать и декрементировать
random_iterator(с произвольным доступом) Читают и пишут значения с произвольным доступом. Самые мощные итераторы, сочетающие функциональность двунаправленных итераторов и возможность выполнения арифметики указателей и сравнений указателей
reverse_iterator(обратные) Или итераторы с произвольным доступом, или двунаправленные, движущиеся в обратном направлении.

Итераторы обычно используются парами: один для указания текущей позиции, а второй для обозначения конца коллекции элементов. Итераторы создаются объектом-контейнером, используя стандартные методы begin() и end(). Функция begin() возвращает указатель на первый элемент, а end() — на воображаемый несуществующий элемент, следующий за последним.

К элементам контейнера — например, vector — можно обращаться и по номерам, как к элементам классического массива — и с помощью итераторов:

Необъектный подход Правильный (объектный) подход
#include <iostream>
#include <vector>
using namespace std;
main ( )
{
	vector<int> a;
	//добавляем элементы
	a.push_back ( 1 );
	a.push_back ( 4 );
	a.push_back ( 8 );
	for ( in t y=0;y<a.size ( ); y++)
	{
		//выводим 1 4 8
		cout<<a [ y]<<"";
	}
}
#include <iostream>
#include <vector>
using namespace std;
main ( )
{
	vector <int> a;
	vector <int >::iterator it;
	//добавляем элементы
	a.push_back ( 1 );
	a.push_back ( 4 );
	a.push_back ( 8 );
	for ( it=a.begin ( ); it !=a.end ( ); it ++)
	{
		//выводим 1 4 8
		cout<<*it <<" ";
	}
}

В отличие от счётчика цикла, итератор обеспечивает доступ к элементу, а не только его перебор. В отличие от операции индексации, итератор "не портится" при добавлении в контейнер новых элементов. Кроме того, индексация иногда вообще неприменима — например, в коллекциях с медленным произвольным доступом, таких как списки (list).

Рассмотрим, как использовать контейнеры на примере класса vector:

#include <iostream>
#include <vector >
#include <algorithm>
using namespace std;
main ( )
{
	//Объявляем вектор из целых чисел
	vector <int> k;
	//Добавляем элементы в конец вектора
	k.push_back ( 22 );
	k.push_back ( 11 );
	k.push_back ( 4 );
	k.push_back ( 100 );
	vector <int >::iterator p;
	cout << "Вывод неотсортированного вектора:\n ";
	for ( p = k.begin ( ); p<k.end ( ); p++) {
	cout << *p << " ";
	}
	//Сортировка вектора.
	sort ( k.begin ( ), k.end ( ) );
	cout << " \nВывод отсортированного вектора:\n ";
	for ( p = k.begin ( ); p<k.end ( ); p++)
	{
	cout << *p << " ";
	}
	cout << endl;
}

Как видно, пример сначала заполняет вектор целых чисел четырьмя значениями, затем поэлементно выводит содержимое вектора на экран, сортирует с использованием алгоритма sort, а затем снова выводит на экран. Вывод программы выглядит следующим образом:

Вывод неотсортированного вектора:
22 11 4 100
Вывод отсортированного вектора:
4 11 22 100

10.7.4 Алгоритмы

В состав стандартной библиотеки входит группа функций, выполняющих некоторые стандартные действия, например поиск, преобразование, сортировку, поштучный перебор элементов, копирование и т. д. Они называются алгоритмами. Параметрами для алгоритмов, как правило, служат итераторы. Алгоритму нет никакого дела до типа переданного ему итератора, лишь бы итератор подпадал под определённую категорию. Так, если параметром алгоритма должен быть однонаправленный итератор, то подставляемый итератор должен быть либо однонаправленным, либо двунаправленным, либо итератором произвольного доступа.

Например, алгоритм простого поиска find просматривает элементы подряд, пока нужный не будет найден. Для такой процедуры вполне достаточно итератора ввода. С другой стороны, алгоритм более быстрого бинарного поиска binary_search должен иметь возможность переходить к любому элементу последовательности, и поэтому требует итератора с произвольным доступом.

Многие алгоритмы получают в качестве параметра различные функции. Эти функции используются для сравнения элементов, их преобразования и т. д. Однако вызов функции по указателю — ресурсоёмкая операция. Вместо указателя на функцию можно передать объект любого класса с перегруженным оператором вызова функции operator(). Одно из преимуществ обращения к переопределённому оператору вызова функции вместо собственно её вызова заключается в том, что переопределённый оператор может быть реализован в классе как встроенная функция, т. е. код оператора будет подставлен вместо вызова.

Рассмотрим пример алгоритма find, который находит первое вхождение заданного значения в коллекцию. Алгоритм принимает в качестве аргументов пару итераторов и искомое значение; соответственно возвращается итератор, указывающий на первое вхождение заданного значения. Благодаря универсальности механизма итераторов, алгоритм будет работать со структурой любого типа, в том числе и с обычными массивами языка C. Например, чтобы найти первое вхождение числа 7 в массив целых, требуется выполнить следующий код, использующий в качестве итераторов обычные указатели:

int data[100];
...
int * where;
where = find ( data, data +100, 7 );

Поиск первого значения в целочисленном векторе выглядит приблизительно так же:

vector <int> a;
...
vector <int >::iterator where;
where = find ( a.begin ( ), a.end ( ), 7 );
Сергей Радыгин
Сергей Радыгин

Символы кириллицы выводит некорректно. Как сделать чтобы выводился читабельный текст на русском языке?

Тип приложения - не Qt,

Qt Creator 4.5.0 основан на Qt 5.10.0. Win7.

 

Юрий Герко
Юрий Герко

Кому удалось собрать пример из раздела 13.2 Компоновка (Layouts)? Если создавать проект по изложенному алгоритму, автоматически не создается  файл mainwindow.cpp. Если создавать этот файл вручную и добавлять в проект, сборка не получается - компилятор сообщает об отсутствии класса MainWindow. Как правильно выполнить пример?