- •И. А. Андрианов, д. В. Кочкин, с. Ю. Ржеуцкая
- •Учебное пособие
- •Оглавление
- •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 Методики оптимизации кода
- •Заключение
- •Библиографический список
1.13 Рекурсивные функции
С++, как и другие языки программирования, поддерживает рекурсивные функции. Рекурсия — это такой способ организации вычислительного процесса, при котором функция в ходе выполнения обращается сама к себе. Рекурсивные функции оформляются в С++, как и в других языках программирования, – каждая функция всегда содержит, по крайней мере, одну терминальную ветвь для своевременного выхода из рекурсии и одну или несколько рекурсивных ветвей, в которых и происходит обращение функции к себе самой.
В качестве простого примера дадим рекурсивное определение суммы первых n натуральных чисел. Сумма первых n натуральных чисел равна сумме первых (n – 1) натуральных чисел плюс n, а сумма первого числа равна 1. Или: Sn = Sn-1 + n; S1 = 1.
Функция, которая вычисляет сумму, пользуясь данным определением:
int sum(int n){
if (n==1) //условие выхода из рекурсии
sum=1;// терминальная ветвь
else
sum=sum(n-1)+n; // рекурсивная ветвь
};
Обратим внимание, что рекурсия как способ реализации алгоритма является альтернативой итерации (циклу). Так, приведенный пример с суммой легко реализуется при помощи цикла. При этом решение без рекурсии в большинстве случаев эффективнее рекурсивного решения, так что предыдущий пример – это только пример.
Но в некоторых случаев использование рекурсивной формы организации алгоритма позволяет реализовать компактный, наглядный программный код, работающий с приемлемой эффективностью. Классический пример – быстрая сортировка массивов Хоара, которая обычно реализуется с помощью рекурсивной функции.
Мы в качестве более интересного примера применения рекурсии рассмотрим следующую задачу. Требуется возвести число a в степень b (a и b – натуральные). Если выполнять это "в лоб", то нам потребуется выполнить b умножений: ab=a·a·a…·a (b раз). Однако, можно вычислять ab, используя следующие соображения:
Например,
-
итого всего 4 операции умножения, а не
7.
Несложно написать рекурсивную функцию, выполняющую вычисления по данной формуле:
unsigned int pow(unsigned int a, unsigned int b)
{
if (b==1) return a; //терминальная ветвь
else if (b % 2 == 0)
{
unsigned int p = pow(a,b/2);
return p*p;
}
else return pow(a,b-1)*a;
}
Конечно, для решения этой задачи вполне подходит и стандартная функция pow, но представленный вариант для целочисленных аргументов, возможно, будет эффективнее.
1.14 Пространства имён
Понятие пространства имён. Пространство имен — это декларативная область, в рамках которой определяются различные идентификаторы (имена типов, функций, переменных, и т. д.). Пространства имен используются для организации кода в виде логических групп и с целью избежания конфликтов имен, которые могут возникнуть, особенно в таких случаях, когда проект организован в виде нескольких файлов, разработанных разными программистами, или включает сторонние библиотеки.
Все идентификаторы в пределах пространства имен доступны друг другу без уточнения. За пределами пространства имен должно использоваться полное имя идентификатора, включающее имя пространства имён и имя самого идентификатора, например:
std::cout<<”Hello”<<std::endl;// endl тоже требует уточнения
std::vector<std::string> vec; // вектор с элементами типа string
Инструкция using. Можно обойтись без уточнений имён, если использовать инструкцию using. Её можно поместить в верхнюю часть CPP-файла (в области видимости файла – это обычная практика) или внутрь определения класса или функции (в области видимости этого класса или функции). Имеется два варианта инструкции using:
для отдельного идентификатора, например:
using std::string;// далее можно использовать просто string без std
или для всех идентификаторов в пространстве имен, например:
using namespace std;
Последнюю инструкцию мы уже неоднократно использовали в примерах пособия, при этом предупреждали, что это не самое лучшее решение. Действительно, в пространстве имён std объявлено огромное количество идентификаторов, поэтому нельзя исключить ситуацию, когда имя какого либо нашего объекта совпадёт с идентификатором, объявленным в std (возникнет конфликт имён). В таком случае совпадающий идентификатор из std окажется "закрытым" нашим объектом, что может впоследствии привести к трудно находимым ошибкам. Если используется всего несколько идентификаторов из std, то лучше каждый из них указать в отдельной инструкции using или в тексте программы использовать для них полные имена с указанием пространства имён.
Обратим внимание, что код в заголовочных файлах (с расширением .h) должен содержать полные имена всех идентификаторов – инструкция using в них является источником таких конфликтов имён, которые и предсказать зачастую невозможно.
Объявление пространства имён. Глобальное пространство имён и пространство имён std. Если в программе не объявляется никаких пространств имён, то считается, что всё описанные в ней объекты принадлежат глобальному пространству имён. Функция main всегда принадлежит глобальному пространству имён. Определения других функций программы рекомендуется размещать в пространствах имён, объявленных в этой программе (исключение можно сделать только для совсем небольших программ, таких как примеры данного пособия).
Объявить пространство имён можно так:
тamespace имя_пространства_имён
{программный код, содержащий объявления различных объектов
}
Имена всех объектов, описания которых содержатся внутри фигурных скобок, принадлежат объявленному пространству имён. Приведем простой пример.
// Пример 1.22 демонстрация использования пространств имён
#include <iostream>
namespace ns1{
int a=5;
void f(){std::cout<<a<<std::endl;}
}
namespace ns2{
int a=ns1::a+1;
void f(){std::cout<<a+1<<std::endl;}
}
int main(){
ns1::f(); ns2::f();
system ("pause"); return 0;
}
Пространство имен std.
Все типы и функции стандартной библиотеки C++ объявлены в пространстве имен std или в пространстве имен, вложенном в std.
Вложенные пространства имен. Пространство имен является вложенным, если оно размещено внутри другого (родительского) пространства имён. Обычное вложенное пространство имен имеет неограниченный доступ к объектам своего родительского пространства, но из родительского пространства нет неограниченного доступа к вложенному пространству имен (если оно не объявлено как встроенное), Обычные вложенные пространства имен можно использовать для сокрытия каких-либо деталей внутренней реализации, которые не нужны при использовании объектов родительского пространства имен.
Встроенные пространства имен (C++ 11). Встроенные пространства имён объявляются с использованием ключевого слова inline (inline namespace). В отличие от обычных вложенных пространств имен объекты встроенного пространства имен обрабатываются как и объекты родительского пространства имен. Эта особенность позволяет, например, выполнять поиск перегруженных функций, если имеются перегрузки в родительском и вложенном встроенном пространстве имен.
