- •И. А. Андрианов, д. В. Кочкин, с. Ю. Ржеуцкая
- •Учебное пособие
- •Оглавление
- •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 Методики оптимизации кода
- •Заключение
- •Библиографический список
4.Обработка исключений
4.1 Основные понятия
Представим себе, что нам для каких-то целей потребовалось написать функцию, возвращающую первую строку файла с заданным именем. В простейшем варианте реализация функции может выглядеть так:
// Пример 4.1 - функция для получения первой строки файла
#include <fstream>
std::string firstLine(const std::string &fileName) {
std::ifstream inp(fileName);
std::string res;
std::getline(inp, res);
return res;
}
Однако, что делать, если в процессе работы функции что-то пошло не так, и она не может корректно выполнить свою работу? Например, файл с таким именем не существует, у пользователя недостаточно прав доступа к этому файлу, и т.п. Рассмотрим несколько способов, которыми функция может сигнализировать об этом:
1). Отсутствие какой-либо проверки возникновения ошибочных ситуаций. В промышленном программировании такой способ нежелателен, поскольку он может привести к неопределенному поведению ("undefined behavior"), при этом дальнейший результат работы программы непредсказуем. Однако, всё же к данному способу иногда прибегают в целях повышения производительности (даже некоторые функции стандартной библиотеки могут вызывать неопределённое поведение при передаче некорректных параметров).
2). Запись сообщения об ошибке в стандартный поток ошибок или в специальный файл журнала (так называемый "лог-файл") и аварийное завершение работы программы. В таком варианте функция из предыдущего примера будет выглядеть так:
// Пример 4.2 - функция для получения первой строки файла
// с аварийным завершением программы при ошибке
#include <fstream>
#include <string>
#include <cstdlib>
std::string firstLine(const std::string &fileName) {
std::ifstream inp(fileName);
if (!inp.is_open()) {
// Запись сообщения и аварийное завершение
std::cerr << "Не удалось открыть файл "
<< fileName << std::endl;
exit(1);
}
std::string res;
std::getline(inp, res);
return res;
}
Данный подход встречается, например, в программах, работающих в неинтерактивном режиме (системных службах и т.п.). Однако, во многих случаях этот вариант недостаточно гибок (особенно для интерактивных приложений). Представим, что имя файла, которое передаётся в нашу функцию, вводится пользователем. Если пользователь случайно ошибся при вводе, то было бы логично не завершать программу совсем, а сообщить пользователю об ошибке и позволить ввести имя файла ещё раз.
3). Возврат определённого значения по умолчанию. Например, самый первый вариант нашей функции firstLine именно это и делает: если файл открыть не удалось, будет возвращена пустая строка.
В некоторых случаях такой подход оправдан. Однако, его стоит использовать с осторожностью, так как он может привести к сложно обнаруживаемым ошибкам. Например, если пользователь случайно ошибётся при вводе имени файла, то из результата функции будет невозможно определить, что произошло − то ли не удалось открыть файл, то ли первая строка в файле оказалась пустой.
4). Функция может вернуть код ошибки (иногда используется термин "код возврата"). Это довольно типичный способ для языка C (заметим, что механизм исключительных ситуаций появился только в языке C++ − см. следующий пункт). Приведём вариант нашей функции, возвращающей код ошибки:
// Пример 4.3 - функция для получения первой строки файла
// с возвратом кода ошибки
std::string firstLine(const std::string &fileName, int *pErrCode) {
std::ifstream inp(fileName);
std::string res;
if (!inp.is_open()) {
*pErrCode = 1;
} else {
std::getline(inp, res);
*pErrCode = 0;
}
return res;
}
В функцию был добавлен дополнительный параметр pErrCode − указатель на переменную, куда функция поместит код ошибки. Если ошибки нет, то код будет равен нулю. Пример вызова этой функции тогда может выглядеть так:
int errCode;
std::string s = firstLine("aaa.txt", &errCode);
if (errCode == 0) {
std::cout << s;
} else {
std::cerr << "Не удалось открыть файл";
exit(1);
}
У данного подхода также есть существенные недостатки. При вызове функции можно забыть (или полениться) проверить код ошибки − как итог, получим неопределённое поведение. С другой стороны, если тщательно проверять все коды возврата, то в программе добавится большое число операторов if, её размер значительно вырастет, а читаемость ухудшится.
Дополнительно заметим, что язык C++ унаследовал от языка C большое количество стандартных библиотечных функций. Многие из них возвращают код ошибки не с помощью дополнительного параметра, а записывают его в специальную глобальную переменную errno. Следующий пример демонстрирует вычисление квадратного корня с помощью стандартной функции sqrt. Если вводимое число меньше нуля, то корень не может быть корректно вычислен − выведем сообщение об ошибке. Функция strerror позволяет получить краткое описание ошибки по его коду.
// Пример 4.4 - использование errno и функции strerror
#include <cstdio>
#include <cmath>
#include <cstring>
#include <cerrno>
int main() {
double x;
std::cin >> x;
double r = sqrt(x);
if (errno != 0) {
// выводим сообщение об ошибке
fprintf(stderr, strerror(errno));
} else {
printf("%0.3f", sqrt(x));
}
}
Примечание. Можно предположить, что использование глобальной переменной errno должно вызывать проблемы в многопоточном режиме. В некоторых старых компиляторах это так. Однако, согласно стандарту C++11, errno является уже не глобальной переменной, а специальным макросом, который гарантирует корректную работу в условиях многопоточности, и в современных компиляторах таких проблем быть не должно.
5). Генерация (возбуждение, выбрасывание) исключения. Данный способ появился в языке C++. Именно этот способ рекомендуется использовать при написании своего кода, и о нём пойдёт речь в оставшейся части главы.
Если в функции произошла аномальная ситуация, то та часть программы, которая её обнаружила, может сгенерировать исключение. Исключение может представлять собой значение почти любого типа. Чаще всего исключение является объектом какого-то класса (или структуры), хотя может быть и значением простого типа (например, int). Для возбуждения исключения используется ключевое слово throw. Перепишем нашу функцию firstLine в таком варианте:
// Пример 4.5 - функция для получения первой строки файла
// с возбуждением исключения
class FileNotOpenedException{};
std::string firstLine(const std::string &fileName) {
std::ifstream inp(fileName);
if (!inp.is_open())
throw FileNotOpenedException();
std::string res = "123";
std::getline(inp, res);
return res;
}
В случае, если файл по какой-то причине открыть не удалось, функция возбудит исключение − объект класса FileNotOpenedException. Заметим, что в нашем примере этот класс даже не содержит ни одного члена − сам тип уже даёт информацию о том, какое именно исключение произошло.
Дальнейшая работа программы зависит от того, существует ли подходящий оператор (блок) try-catch, способный обработать данное исключение. Если подходящего блока try-catch найти не удалось, то происходит аварийное завершение работы программы, перед этим этом в поток ошибок будет выдана информация о типе возникшего исключения.
Примечание. Заметим, что деструкторы объектов-локальных переменных при этом вызваны не будут. Корректное уничтожение локальных объектов в процессе раскрутки стека (см. следующий пункт) происходят лишь при наличии подходящего блока try-catch.
