- •И. А. Андрианов, д. В. Кочкин, с. Ю. Ржеуцкая
- •Учебное пособие
- •Оглавление
- •1. Основы языка 8
- •1.2.2 Простые типы данных 13
- •2. Работа с памятью 73
- •3. Основы объектно-ориентированного программирования 87
- •4.Обработка исключений 114
- •5. Шаблонные функции и классы. Библиотека стандартных шаблонов 130
- •6. Паттерны проектирования 159
- •7. Антипаттерны 211
- •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.5 Разработка шаблонных классов с настраиваемой функциональностью
- •5.6 Использование шаблонов для вычислений на этапе компиляции
- •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 Методики оптимизации кода
- •Заключение
- •Библиографический список
4.3 Поиск обработчика исключений. Раскрутка стека.
Пусть в какой-то точке программы был вызван оператор throw. Рассмотрим подробнее, что будет происходить далее. После вызова throw нормальная последовательность исполнения операторов программы прерывается, и выполняется раскрутка стека вызовов до первого обработчика исключений подходящего типа, после чего управление передаётся обработчику.
Поясним подробнее, что именно происходит при раскрутке стека. Пусть функция f1 вызвала функцию f2, функция f2 вызвала функцию f3, а в ней произошло исключение. Допустим, что подходящий обработчик исключения этого типа нашёлся только в функции f1. Перед тем, как передать управление этому обработчику, будут корректно удалены из стека все локальные переменные и параметры функции f3, затем − локальные переменные и параметры функции f2, и лишь после этого управление будет передано обработчику.
Словосочетание "корректно удалить" в том числе означает, что если локальная переменная или параметр функции является объектом, то для него будет вызван деструктор. Таким образом, гарантируется, что если локальный объект был полностью построен (его конструктор завершился успешно), то при раскрутке стека будет вызван его деструктор, то есть утечки ресурсов не произойдёт. Это очень важный момент, поясним его на примере функции firstLine (см. пример 4.5).
В этой функции создаётся локальный объект inp класса std::ifstream. Если где-то в функции произойдёт исключение, то для объекта inp будет гарантированно вызван деструктор, то есть не произойдёт утечки памяти и, возможно, других ресурсов (например, если исключение случится внутри функции getline, то открытый файл будет закрыт в деструкторе).
Рассмотрим ещё один момент: если в одном операторе try имеется сразу несколько подходящих блоков catch, то который из них будет использован? Обычно такая ситуация встречается при наследовании классов исключений. В этом случае выбирается первый подходящий блок catch. При этом нужно быть аккуратным, чтобы не допустить ошибку: блок catch с производным типом должен располагаться перед блоком catch с родительским типом.
Рассмотрим пример. Представьте себе, что вы разрабатываете библиотеку для выполнения арифметических операций, например, над простыми дробями. Пусть у вас есть класс AriphmeticException (арифметическая ошибка), а от него наследуется класс DivideByZeroException (деление на ноль). Следующий код содержит ошибку:
try { тело блока try }
catch(AriphmeticException) {std::cerr <<"Арифметическая ошибка"; }
catch(DivideByZeroException) { std::cerr << "Деление на 0"; }
Здесь второй обработчик не выполнится никогда. Дело в том, что если произойдёт исключение типа DivideByZeroException, то всё равно выполнится первый обработчик, так как родительский тип совместим с дочерним. Чтобы пример стал работать правильно, необходимо поменять блоки catch местами.
4.4 Повторное возбуждение исключений
Оператор throw без параметров, находящийся внутри обработчика исключения, используется для повторного возбуждения того же самого исключения, которое было поймано в текущем блоке catch. Он применяется в том случае, когда исключение не может быть полностью обработано в данном блоке catch, и его обработка будет закончена где-то в другом месте. Чаще всего оператор try c блоком catch, содержащим повторное возбуждение исключения, используется для освобождения захваченных ресурсов перед тем, как продолжить раскрутку стека.
Рассмотрим пример. Пусть дан текстовый файл, содержащий последовательность целых чисел. Вначале в файле записано количество чисел N, а затем − сами числа, разделенные пробелами и/или переводами строк. Требуется написать функцию readIntegers, которая считывает все числа в массив и возвращает его в качестве результата. Если количество чисел окажется меньше, чем N, или в файле встретится не число, то функция должна возбудить исключение WrongFileFormatException:
// Пример 4.7 - повторное возбуждение исключения
class FileNotOpenedException{};
class WrongFileFormatException{};
int* readIntegers(const std::string &fileName) {
std::ifstream inp(fileName);
if (!inp.is_open()) throw FileNotOpenedException(fileName);
int n;
inp >> n;
if (!inp.good()) throw WrongFileFormatException();
int* a = new int [n];
try{
for (int i = 0; i < n; i++) {
inp >> a[i];
if (!inp.good()) throw WrongFileFormatException();
}
} catch(...) {
delete[] a;
throw; // повторное возбуждение исключения
}
return a;
}
Если при вводе чисел произойдёт исключение любого типа, то управление передастся блоку catch, в котором произойдёт освобождение памяти, выделенной под массив. Заметим, что автоматически эта память не освободилась бы: из стека удалилась бы лишь локальная переменная-указатель a, но захваченный в куче блок памяти так и остался бы занятым.
Мы предотвратили утечку памяти, но функция всё-таки не смогла выполнить свою работу − прочитать N чисел из файла. Поэтому повторно возбуждаем исключение, чтобы оно могло быть поймано другим обработчиком (например, находящимся в функции main).
Заметим, что в хорошо написанной программе повторное возбуждение исключений используется редко. Авторы рекомендуют по возможности всегда помещать код освобождения ресурсов в деструкторы классов. В этом случае нет необходимости в лишних блоках try-catch, а также нет опасности забыть выполнить освобождение ресурсов. Например, вышеописанную функцию правильней было бы написать так, чтобы она возвращала не массив, а вектор. В случае исключения деструктор вектора будет вызван автоматически. Приведём улучшенный вариант данной функции с использованием вектора вместо массива:
// Пример 4.8 - улучшенный вариант функции readIntegers
std::vector<int> readIntegers(const std::string &fileName) {
std::ifstream inp(fileName);
if (!inp.is_open()) throw FileNotOpenedException(fileName);
int n;
inp >> n;
if (!inp.good()) throw WrongFileFormatException();
std::vector<int> a(n);
for (int i = 0; i < n; i++) {
inp >> a[i];
if (!inp.good()) throw WrongFileFormatException();
}
return a;
}
Как видим, код получился более коротким и понятным, он не содержит лишних блоков try-catch, а выделение и освобождение памяти скрыто в реализации класса std::vector.
