- •И. А. Андрианов, д. В. Кочкин, с. Ю. Ржеуцкая
- •Учебное пособие
- •Оглавление
- •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 Методики оптимизации кода
- •Заключение
- •Библиографический список
1.15 Директивы препроцессора. Макросы
В пункте 1.1 было приведено определение термина "препроцессинг". В данном разделе мы рассмотрим использование директив препроцессора более подробно.
Выше мы уже сталкивались с директивой #include, она позволяет вставить содержимое указанного файла в текущее место. Имя файла может задаваться двумя способами − в угловых скобках (#include<имя_файла>) и в кавычках (#include"имя_файла"). В первом варианте поиск файла выполняется в папках, содержащих файлы стандартной библиотеки, во втором − в текущей папке и папках, указанных в командной строке компилятора.
Директива #define предназначена для определения макросов:
#define имя_макроса текст
Простейший макрос представляет собой замену имени макроса на указанный текст. Например, следующая директива:
#define EPS 1E-9
заменит в исходном файле слово EPS на текст 1E-9.
Существует набор предопределённых макросов препроцессора: например, __FILE__ заменяется на имя текущего исходного файла, __LINE__ − на номер текущей строки, __TIMESTAMP__ − на текущую дату/время и др.
С помощью директивы #define также можно создавать параметризованные макросы. Например, макрос SUM для нахождения суммы двух своих параметров выглядит так:
#define SUM(a, b) ((a) + (b))
Его использование напоминает вызов функции:
int x = SUM(2, 3);
Обратите внимание на круглые скобки вокруг параметров, а также вокруг всего выражения. Если их не написать, то результат в некоторых случаях может оказаться неверным, например:
#define SUM(a, b) a + b
int x = 5 * sum(2, 3);
В результате в переменной x окажется значение 13, а вовсе не 25, потому что выражение 5 * sum(2, 3) на стадии препроцессинга преобразуется в 5 * 2 + 3.
При написании макросов можно использовать макрооператоры # и ##. Символ #, стоящий перед названием параметра макроса, превращает значение параметра в строку (заключает его в кавычки). Например, для целей отладки может быть полезен макрос, позволяющий вывести на экран заданное выражение и его значение:
#define DEBUG_PRINT(x) std::cout << #x << " = " << (x) << std::endl;
int a = 3;
DEBUG_PRINT(a * 8 + 5);
В результате будет выведено a * 8 + 5 = 29
Символ ## склеивает две лексемы вместе, применяется довольно редко.
При использовании макросов стоит быть осторожным − даже правильное применение скобок не даёт полной гарантии от ошибок. Для примера рассмотрим код:
#define SQR(x) ((x) * (x))
int x = 1;
int y = SQR(x++);
Вероятно, в третьей строчке ожидалось получить результат y=1, x=2. На самом же деле получится x = 3, y = 2. Также к недостаткам макросов можно отнести то, что они затрудняют отладку программы. Мы рекомендуем не злоупотреблять макросами в своём коде. Тем не менее, в некоторых случаях макросы полезны. Например, для целей отладки может пригодиться вот такой макрос:
#define ASSERT(c) \
if (!(c)) { \
std::cerr << "\nAssertion failed in " << __FILE__ \
<< "(line " << __LINE__ << ") : " << #c << std::endl; \
exit(1); \
}
Если передать ему ложное условие, например ASSERT(2 * 2 != 4), то он выведет нечто вроде "Assertion failed in test.cpp(line 12) : 2 * 2 != 4" и завершит работу программы.
Следующая группа директив препроцессора − директивы условной компиляции #if, #ifdef, #ifndef, #elsif, #else, #endif. С помощью директив условной компиляции можно компилировать или пропускать компиляцию частей кода в зависимости от выполнения тех или иных условий. Типичный пример: если определён макрос DEBUG, то код для отладочной печати компилируется, в противном случае − пропускается:
#define DEBUG
#ifdef DEBUG
std::cerr << "a = " << a << std::endl;
#endif
Если закомментировать первую строчку, то отладочная печать исчезнет.
Условная компиляция широко применяется для настройки программы под конкретную платформу, а также для включения/отключения возможностей, которые будет поддерживать программа (типичный пример − сборка большинства приложений для операционных систем семейства Unix/Linux).
Важным частным случаем применения директив условной компиляции является предотвращение двойного подключения заголовочных файлов при компиляции. Поясним, в чём суть проблемы. Допустим, в проекте имеются файлы header1.h, header2.h и main.cpp. Пусть в файле header2.h имеется директива #include <header1.h>. В файле main.cpp стоят две директивы − #include <header1.h> и #include <header2.h>. Тогда в файл main.cpp содержимое файла header1.h вставится дважды − один раз напрямую и один раз косвенно через файл header2.h. Это, скорей всего, вызовет ошибку компиляции. Для устранения проблемы можно добавить в файл header1.h следующие строчки:
# ifndef HEADER1_H
# define HEADER1_H
...(остальной код заголовочного файла)...
# endif
Заметим, что это же самое можно сделать и проще, поместив в заголовочный файл директиву #pragma once
Директива #pragma предназначена для передачи компилятору различных инструкций. Набор допустимых инструкций зависит от конкретного компилятора. Исключением является упомянутая выше #pragma once, которая поддерживается почти всеми распространёнными компиляторами (тем не менее, и она не входит в стандарт языка).
Пример применения директивы #pragma для компилятора MS Visual C++ для установки размера стека равным 16 мегабайт:
#pragma comment(linker, "/STACK:16777216")
