- •И. А. Андрианов, д. В. Кочкин, с. Ю. Ржеуцкая
- •Учебное пособие
- •Оглавление
- •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 Методики оптимизации кода
- •Заключение
- •Библиографический список
8.6 Константа нулевого указателя nullptr
До введения стандарта C++11 для обозначения неинициализированного указателя использовалась числовая константа 0 или макрос NULL, имевший значение ((void*)0). Такой подход вызывает проблемы, связанные с перегрузкой функции, так как числовая константа 0 может интерпретироваться человеком и как число, и как неинициализированный указатель. Однако, при использовании механизма перегрузки функций компилятор выберет функцию с целочисленным параметром.
// Пример 8.23 - проблема при использовании нуля в качестве
// неинициализированного указателя
void f1(int i) { std::cout << "void f1(int i)" << std::endl; }
void f1(int* i) { std::cout << "void f1(int* i)" << std::endl; }
int main() {
f1(0);
}
Результат работы программы следующий:
void f1(int i)
Аналогичный код в С++11 не приведет к проблемам, так как для обозначения нулевого указателя будет использоваться константа nullptr.
// Пример 8.24 - перегрузка функций и nullptr.
void f1(int i) { std::cout << "void f1(int i)" << std::endl; }
void f1(int* i) { std::cout << "void f1(int* i)" << std::endl; }
int main() {
f1(0);
f1(nullptr);
}
Результат работы программы следующий:
void f1(int i)
void f1(int* i)
Константа nullptr имеет тип std::nullptr_t и может быть неявно преобразована к нулевому указателю или к значению false логического типа. Неявное преобразование nullptr к целочисленному типу приведет к ошибке.
// Пример 8.25 - неявное приведение nullptr
int *pi = nullptr;
int *pi2 = 0;
if(pi == pi2)
std::cout << "nullptr == 0" <<std::endl;
bool b = nullptr;
if(!b)
std::cout << "b == false" << std::endl;
Результат работы программы следующий:
nullptr == 0
b == false
8.7 "Умные" указатели
Основной проблемой при работе с обычными указателями являются утечки памяти, которых сложно избежать в больших проектах. Утечки памяти могут возникать по разным причинам. Например, программист может просто забыть вызвать оператор delete. Во-вторых, утечка может произойти при возникновении исключения. Хотя в процессе раскрутки стека уничтожится локальная переменная-указатель, но динамическая память останется не освобождённой. В третьих, в больших программах может иметься несколько указателей на один и тот же ресурс. При этом бывает достаточно сложно понять, в какой момент времени на ресурс больше никто не указывает, и его пора освободить.
"Умные" указатели (smart pointers) предназначены для устранения данных проблем. Умный указатель представляет собой объект, с которым можно работать как с обычным указателем. При этом он обеспечивает автоматическое освобождение памяти и может также предоставлять некоторую дополнительную функциональность.
В C++ имеется несколько видов умных указателей: std::auto_ptr, std::unique_ptr, std::shared_ptr, std::weak_ptr. Все они объявлены в заголовочном файле memory.
Указатель std::auto_ptr появился в языке C++ довольно давно. Он имеет некоторые известные проблемы, и в настоящее время объявлен устаревшим. Вместо него рекомендуется использовать std::unique_ptr.
Указатель std::unique_ptr, появившийся в C++11, реализует концепцию единоличного владения объектом: на один объект указывает только один экземпляр unique_ptr. При уничтожении экземпляра unique_ptr в его деструкторе происходит и уничтожение управляемого объекта.
В отличие от auto_ptr, указатель unique_ptr не может быть скопирован или изменён через операцию присвоения. Например, следующий код вызовет ошибку компиляции:
std::unique_ptr<int> p(new int(1));
std::unique_ptr<int> q;
q = p; // ошибка компиляции
Заметим, что если бы вместо unique_ptr использовался auto_ptr, то этот код бы скомпилировался. Однако, после присваивания указатель p стал бы недействителен, и попытка обращения к нему вызвала бы аварийное завершение программы.
Если мы для каких-то целей хотим передать владение объектом другому указателю, то следует использовать функцию std::move:
q = std::move(p);
Ещё одним отличием unique_ptr от auto_ptr является способность работать с массивами. Однако, польза от этого довольно сомнительна − в большинстве случаев проще воспользоваться классом std::vector.
Некоторые полезные методы класса std::unique_ptr:
• get − возвращает сохранённый указатель на объект
• release − возвращает сохранённый указатель на объект и заменяет его на nullptr (заметим, что объект при этом не уничтожается)
• reset − уничтожает текущий объект и вступает во владение другим объектом
Наиболее востребованным типом умного указателя является указатель std::shared_ptr. Он реализует концепцию множественного владения, когда на один и тем же объект могут ссылаться несколько указателей. Для этого в shared_ptr реализован подсчёт ссылок на объект. Объект уничтожается тогда, когда счётчик ссылок на него станет равным нулю.
Следующий пример кода демонстрирует использование shared_ptr. Для создания объектов в нём используется специальная шаблонная функция std::make_shared. Чтобы видеть, в каком порядке происходит создание и уничтожение объектов, создана структура c именем Demo, в конструкторе и деструкторе которой на консоль выводятся диагностические сообщения.
// Пример 8.26 - использование указателей shared_ptr
#include <iostream>
#include <memory>
struct Demo {
int x;
Demo(int x) : x(x) {
std::cout << "In constructor, x = " << x << "\n";
}
~Demo() {
std::cout << "In destructor, x = " << x << "\n";
}
};
int main() {
std::shared_ptr<Demo> p = std::make_shared<Demo>(1);
std::shared_ptr<Demo> q = std::make_shared<Demo>(2);
q = p;
p.reset();
q = std::make_shared<Demo>(3);
}
Результат работы этой программы:
In constructor, x = 1
In constructor, x = 2
In destructor, x = 2
In constructor, x = 3
In destructor, x = 1
In destructor, x = 3
Указатель std::weak_ptr содержит "слабую" ссылку на объект, управляемый указателями shared_ptr. Типичный пример применения − реализация некоторой стратегии кэширования. Допустим, в кэше хранятся указатели на объекты. Мы хотим дать возможность где-то из другого места программы удалять некоторые из этих объектов для освобождения памяти. Однако, если в кэше будут использоваться "сильные" указатели типа shared_ptr, то они не позволят выполнять удаление объектов. Как раз для этой цели удобно использовать weak_ptr.
Некоторые полезные методы класса std::weak_ptr:
• expired − возвращает true, если объект, на который ссылается указатель, уничтожен
• lock − возвращает указатель shared_ptr на данный объект (weak_ptr не позволяет работать с объектом напрямую)
• use_count − возвращает количество указателей shared_ptr, которые ссылаются на данный объект
Проиллюстрируем на простом примере работу с weak_ptr:
// Пример 8.27 - использование указателей weak_ptr
// создадим новый объект типа int
std::shared_ptr<int> p = std::make_shared<int>(5);
// получим "слабый" указатель на объект
std::weak_ptr<int> w = p;
// изменим значение объекта через временный "сильный" указатель
std::shared_ptr<int> tmp = w.lock();
*tmp = 8;
tmp.reset();
p.reset(); // уничтожим объект
if (w.expired())
std::cout << "Объект был уничтожен\n";
Ещё одним возможным применением weak_ptr является решение проблемы циклических ссылок. Представим себе два каких-то объекта. Пусть поле типа shared_ptr внутри первого объекта ссылается на второй объект, а аналогичное поле внутри второго объекта − на первый. Поскольку счётчик ссылок для обоих объектов равен хотя бы единице, то их деструкторы не будут вызваны никогда. Для решения этой проблемы в одном из объектов можно заменить shared_ptr на weak_ptr.
В завершение данного подраздела приведём пример реализации шаблонного класса Stack − упрощенного аналога стандартного класса std::stack. Для хранения данных будем использовать структуру данных "связный список" с использованием "умных" указателей. Класс будет иметь следующие методы: push − вставка элемента, pop − удаление элемента, top − возвращение значения с вершины стека, empty − проверка стека на пустоту.
// Пример 8.28 - реализация шаблонного класса Stack
// с помощью умных указателей
#include <iostream>
#include <memory>
template <class T>
class Stack {
struct Elem {
T value;
std::shared_ptr<Elem> next;
Elem(const T &value, std::shared_ptr<Elem> next = nullptr)
: value(value), next(next) {}
};
std::shared_ptr<Elem> _top;
public:
bool empty() const { return _top == nullptr; }
void push(const T &value) {
_top = std::make_shared<Elem>(value, _top);
}
void pop() { _top = _top->next; }
T& top() { return _top->value; }
};
int main() {
Stack<int> s;
for (int i = 1; i <= 5; i++)
s.push(i);
while (!s.empty()) {
std::cout << s.top() << " ";
s.pop();
}
}
В данном разделе были рассмотрены некоторые нововведения стандарта С++11. Данный перечень не является полным, для детального изучения последних изменений следует обратиться к стандарту либо специализированной литературе.
