- •И. А. Андрианов, д. В. Кочкин, с. Ю. Ржеуцкая
- •Учебное пособие
- •Оглавление
- •1. Основы языка 8
- •1.2.2 Простые типы данных 13
- •6. Паттерны проектирования 159
- •7. Антипаттерны 211
- •2. Работа с памятью 73
- •3. Основы объектно-ориентированного программирования 87
- •4.Обработка исключений 114
- •5. Шаблонные функции и классы. Библиотека стандартных шаблонов 130
- •9. Методы отладки и оптимизации кода 242
- •1. Основы языка
- •1.1.2 Понятие проекта
- •1.2 Простые типы данных
- •1.2.1 Понятие типа
- •1.2.2 Простые типы данных
- •1.2.3 Внутреннее представление простых типов
- •1.2.4 Ключевое слово typedef. Тип size_t
- •1.3 Константы и переменные
- •1.3.1 Литералы
- •1. Числовые константы:
- •2. Символьные константы:
- •1.3.2 Переменные
- •1.3.3 Описание переменных
- •1.4. Выражения. Преобразование типов
- •1.4.1 Операнды и операции
- •1.4.2 Приоритет операций
- •1.4.3 Преобразование типов
- •1.5 Ветвления и циклы
- •1.5.2 Циклы
- •1.6 Массивы, строки
- •1.6.1 Основные понятия
- •1.6.2 Встроенные массивы
- •1.6.3 Cтроки. Обработка строк с завершающим нулём
- •1.7 Указатели и ссылки. Связь указателей и массивов. Библиотека cstring
- •1.7.1 Понятия указателя и ссылки
- •1.7.2 Связь между массивами и указателями
- •1.7.3 Библиотека cstring
- •1.8 Использование типов vector и string
- •1.8.1 Шаблонный класс vector
- •1.8.2 Класс string
- •1.9 Структуры и объединения. Битовые поля
- •1.10.1 Понятие функции
- •1.10.2 Описание функции и прототип функции
- •1.11 Параметры функции. Способы передачи параметров
- •1.11.1 Параметры функции и глобальные переменные
- •1.11.2 Способы передачи параметров в функцию
- •1.11.3 Передача массивов в функцию
- •1.11.4 Параметры-константы
- •1.11.5 Значения параметров по умолчанию
- •1.12.1 Указатель на функцию
- •1.12.2 Функции с переменным числом параметров
- •1.12.3 Перегрузка функций
- •1.12.4 Встроенные (inline) функции
- •1.13 Рекурсивные функции
- •1.14 Пространства имён
- •1.15 Директивы препроцессора. Макросы
- •2. Работа с памятью
- •2.1 Управление выделением и освобождением памяти
- •2.1.1 Статическое и динамическое выделение памяти
- •2.1.2 Способы динамического выделения и освобождения памяти
- •2.2 Динамические структуры данных
- •2.2.1 Основные понятия
- •2.2.2 Примеры реализации динамических структур на основе указателей
- •3. Основы объектно-ориентированного программирования
- •3.1 Основные понятия ооп
- •3.2.1 Описание класса
- •3.2.2 Область видимости элементов класса. Инкапсуляция
- •3.2.3 Первые примеры
- •3.3. Конструкторы и деструкторы.
- •3.4 Указатель this
- •3.5 Перегрузка операций
- •3.6 Дружественные функции и классы
- •3.7 Статические элементы класса
- •3.8 Наследование и полиморфизм
- •3.8.1. Основные понятия
- •3.8.2 Одиночное наследование
- •3.8.3 Множественное наследование
- •3.8.4 Конструкторы и деструкторы классов-потомков
- •3.9. Полиморфизм при наследовании классов
- •3.9.1 Механизмы раннего и позднего связывания
- •3.9.2 Абстрактные классы
- •4.Обработка исключений
- •4.1 Основные понятия
- •4.2 Перехват исключений
- •4.3 Поиск обработчика исключений. Раскрутка стека.
- •4.4 Повторное возбуждение исключений
- •4.5 "Аппаратные" и "программные" исключения
- •4.6 Стандартные классы исключений
- •4.7 Спецификация исключений, возбуждаемых функцией
- •4.8 Исключения в конструкторах при наследовании
- •4.9. Исключения в деструкторах
- •5. Шаблонные функции и классы. Библиотека стандартных шаблонов
- •5.1 Шаблонные функции
- •5.2 Шаблонные классы
- •5.3 Специализация шаблонов
- •5.4 Шаблонные параметры шаблонов
- •5.6 Использование шаблонов для вычислений на этапе компиляции
- •5.5 Разработка шаблонных классов с настраиваемой функциональностью
- •5.7 Библиотека стандартных шаблонов (stl) – основные понятия
- •5.8 Последовательные контейнеры. Итераторы
- •5.9. Адаптеры контейнеров
- •5.10 Ассоциативные контейнеры
- •5.11 Алгоритмы
- •6. Паттерны проектирования
- •6.1 Порождающие шаблоны
- •6.2 Структурные шаблоны
- •6.3 Шаблоны поведения
- •6.4 Шаблон "фабричный метод" (Factory method)
- •6.5 Шаблон "одиночка" (Singleton)
- •6.6 Шаблон "итератор" (Iterator)
- •6.7 Шаблон "наблюдатель" (Observer)
- •6.8 Шаблон "пул объектов" (Object pool)
- •6.9 Шаблон "команда" (Command)
- •6. 10 Шаблон "посетитель" (Visitor)
- •6.11 Дополнительные задания
- •6.11.1 Шаблон Iterator
- •6.11.2 Шаблон Observer
- •6.11.3 Шаблоны Command и Observer
- •6.11.5 Шаблон Visitor
- •6.11.5 Разработка класса − контейнера
- •6.11.6 Оценка производительности кода
- •7. Антипаттерны
- •7.1 Программирование методом копирования и вставки (Copy-Paste Programming)
- •7.2 Спагетти-код (Spaghetti code)
- •7.3 Магические числа (Magic numbers)
- •7.4 Бездумное комментирование
- •7.5 Жесткое кодирование (Hard code)
- •7.6 Мягкое кодирование (Soft code)
- •7.7 Золотой молоток (Golden hammer)
- •7.8 Слепая вера (Blind faith)
- •7.9 Ненужная сложность (Accidental complexity)
- •7.10 Божественный объект (God Object)
- •7.11 Лодочный якорь (Boat anchor)
- •7.12 Поток лавы (Lava flow)
- •7.13 Изобретение велосипеда (Reinventing the wheel)
- •7.14 Программирование перебором (Programming by permutation)
- •8.1 Выведение типов
- •8.2 Списки инициализации
- •8.3 Улучшение процесса инициализации объектов
- •8.4 Цикл for по коллекции
- •8.5 Лямбда-функции
- •8.6 Константа нулевого указателя nullptr
- •8.7 "Умные" указатели
- •9. Методы отладки и оптимизации кода
- •9.1 Отладка кода
- •9.1.1 Основные этапы отладки
- •9.1.2 Инструменты и приёмы отладки
- •9.2 Оптимизация кода
- •9.2.1 Рекомендации по выполнению оптимизации
- •9.2.2 Методики оптимизации кода
- •Заключение
- •Библиографический список
5.9. Адаптеры контейнеров
Адаптеры контейнеров представляют собой шаблонные классы, которые используют для внутреннего хранения элементов другие контейнеры, адаптируя их интерфейсы под другой интерфейс. В пункте 5.2 приводился пример реализации класса PasArray − его вполне можно назвать адаптером контейнера std::vector. В библиотеке STL имеется три адаптера контейнеров − std::stack, std::queue и std::priority_queue.
Класс std::stack реализует абстрактный тип данных "стек", в котором элементы организованы по принципу LIFO (last in − first out, то есть "последним пришёл − первым вышел"). Элементы могут вставляться и удаляться только с одного конца контейнера, который обычно называется вершиной стека. По умолчанию для хранения данных используется класс std::deque (но при желании его можно изменить, например, на std::vector).
Класс std::stack имеет следующие основные методы:
• push − вставляет элемент на вершину стека
• pop − удаляет элемент с вершины стека
• top − возвращает значение элемента на вершине стека
• size − возвращает количество элементов в стеке
• empty − возвращает true, если стек пуст
Все перечисленные операции имеют константную (в среднем) временную сложность.
Рассмотрим примеры решения задач с использованием стека. Первая задача звучит так: вводится некоторое количество целых чисел. Количество чисел заранее неизвестно, окончание ввода обозначается числом ноль. Требуется вывести эти числа в обратном порядке.
// Пример 5.13 - разворот последовательности чисел
#include <iostream>
#include <stack>
int main() {
std::stack<int> st;
for(;;) {
int x;
std::cin >> x;
if (x == 0) break;
st.push(x);
}
while (!st.empty()) {
std::cout << st.top() << " ";
st.pop();
}
}
Теперь решим более интересную задачу. Вводится N целых чисел, не превышающих по модулю 109. Требуется для каждого из них вывести ближайшее к нему слева число меньше его (а если такого числа нет, вывести прочерк). Например, если на вход поступили числа 1 3 2 5 2 4, то ответом будет "- 1 1 2 1 2".
Данная задача решается очевидным образом с вычислительной сложностью O(N2). Однако, с помощью стека данную задачу можно решить за O(N). Будем последовательно помещать входные числа в стек. Заметим следующее: если очередное прочитанное число меньше или равно числу на вершине стека, то число на вершине стека нам никогда более не понадобится (ведь оно никогда не сможет больше оказаться в ответе). Поэтому число на вершине можно удалить. Операцию удаления из стека производим до тех пор, пока на вершине не окажется число меньше текущего − это как раз и будет очередной ответ. Решение:
// Пример 5.14 - поиск для каждого числа ближайшего слева к нему
// числа строго меньше его
#include <iostream>
#include <stack>
const int INF = 1E9+1; // "бесконечность"
int main() {
int n;
std::cin >> n;
std::stack<int> st;
st.push(-INF); // "барьерный" элемент
for (int i = 0; i < n; i++) {
int x;
std::cin >> x;
while (x <= st.top())
st.pop();
if (st.top() == -INF)
std::cout << "- ";
else
std::cout << st.top() << " ";
st.push(x);
}
}
Класс std::queue реализует абстрактный тип данных "очередь", где элементы организованы по принципу FIFO (first in − first out, "первым пришёл − первым вышел"). Элементы вставляются в конец очереди (back), а удаляются из начала очереди (front). По умолчанию для хранения данных используется класс std::deque.
Класс std::queue имеет следующие основные методы:
• push − вставляет элемент в конец очереди
• pop − удаляет элемент из начала очереди
• front − возвращает значение первого элемента очереди (тот, который будет удалён при следующем вызове pop)
• back − возвращает значение первого последнего элемента очереди (тот, который был вставлен последним вызовом push)
• size − возвращает количество элементов
• empty − возвращает true, если очередь пуста
Рассмотрим пример решения задачи с использованием очередей. Требуется вывести в порядке возрастания первые N чисел (где N не превышает 1000), в разложение которых на простые множители входят только числа 2, 3, 5 (https://www.mccme.ru/free-books/shen/shen-progbook.pdf). Например, для N = 10 ответом будет 1 2 3 4 5 6 8 9 10 12.
Для решения этой задачи создадим три очереди q2, q3 и q5, в которых будут храниться элементы в 2, 3 и 5 раз больше напечатанных, но ещё не напечатанные. На каждом шаге цикла будет делать следующее. Пусть x = min (q2.front(), q3.front(), q5.front()). Выведем x в ответ. Добавим x∙2 в очередь q2, x∙3 − в q3, x∙5 − в q5. Удалим x из тех очередей, где он является первым элементом (заметим, что x может присутствовать в более чем одной очереди).
Для обоснования правильности данного алгоритма заметим, что в каждой очереди элементы будут располагаться в порядке возрастания. Сложность алгоритма, очевидно, линейная. Решение:
// Пример 5.15 - вывод чисел, в разложении которых
// на простые множители встречаются только 2, 3, 5
#include <iostream>
#include <queue>
int main() {
int n;
std::cin >> n;
std::queue<int> q2, q3, q5;
std::cout << 1;
q2.push(2); q3.push(3); q5.push(5);
for (int i = 2; i <= n; i++) {
int x=std::min(q2.front(),std::min(q3.front(),q5.front()));
std::cout << " " << x;
q2.push(x * 2); q3.push(x * 3); q5.push(x * 5);
if (q2.front() == x) q2.pop();
if (q3.front() == x) q3.pop();
if (q5.front() == x) q5.pop();
}
}
Класс std::priority_queue реализует абстрактный тип данных "очередь с приоритетами", в котором первый элемент всегда является наибольшим среди всех элементов контейнера. По умолчанию для хранения данных используется класс std::vector.
Класс std::priority_queue содержит следующие основные методы:
• push − вставляет элемент в очередь
• pop − удаляет верхний (максимальный) элемент из очереди
• top − возвращает верхний (максимальный) элемент
• size − возвращает количество элементов
• empty − возвращает true, если очередь пуста
Методы push и pop имеют логарифмическую вычислительную сложность, остальные перечисленные методы − константную.
Часто бывает нужно, чтобы верхним элементом приоритетной очереди был не максимальный, а минимальный элемент. Для изменения способа сравнения элементов предназначен третий параметр данного шаблонного класса. Изменить порядок на противоположный можно с помощью функционального объекта std::greater:
std::priority_queue<int, std::vector<int>, std::greater<int> > q;
В качестве примера ещё раз решим предыдущую задачу, но теперь вместо трёх обычных очередей будем использовать одну очередь с приоритетами. Решение получается даже проще предыдущего:.
// Пример 5.16 - решение задачи из примера 5.15
// с помощью приоритетной очереди
#include <iostream>
#include <queue>
#include <functional>
int main() {
int n;
std::cin >> n;
std::priority_queue<int, std::vector<int>, std::greater<int> > q;
int x = 0;
q.push(1);
for (int i = 1; i <= n; i++) {
while (q.top() == x) // удаляем дубликаты
q.pop();
x = q.top();
q.pop();
std::cout << x << " ";
q.push(x * 2); q.push(x * 3); q.push(x * 5);
}
}
Заметим, что данное решение имеет вычислительную сложность O(N∙log(N)), что несколько хуже предыдущего решения.
