- •И. А. Андрианов, д. В. Кочкин, с. Ю. Ржеуцкая
- •Учебное пособие
- •Оглавление
- •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 Методики оптимизации кода
- •Заключение
- •Библиографический список
9.1.2 Инструменты и приёмы отладки
Инварианты
Применение инвариантов относится не столько к отладке, сколько к вопросам написания качественного кода, содержащего минимум ошибок. Тем не менее, мы всё же решили вставить данный материал в эту главу ввиду его полезности.
Инвариантом (invariant) называется логическое выражение, которое на заданном отрезке (или в заданных точках) программы должно быть всегда истинным. Инварианты удобно использовать для обоснования корректности кода. Рассмотрим пример. Следующая функция находит наибольший общий делитель (НОД) двух неотрицательных целых чисел m и n (из них хотя бы одно не равно нулю):
// Пример 9.1.1 - поиск НОД алгоритмом Евклида
// параметры: m >= 0, n >= 0, m или n не равен нулю
int gcd(int m, int n)
{
int a = m, b = n;
while (a != 0 && b!= 0) {
if (a >= b) a %= b; else b %= a;
}
return std::max(a, b);
}
Докажем, что эта функция работает верно. Для этого рассмотрим такой инвариант: "после каждой итерации цикла while верно, что НОД(a, b) = НОД(m, n)". Если инвариант верен, то функция действительно найдёт НОД(m, n). Это следует из того, что цикл закончится тогда, когда одна из переменных станет равна нулю, а НОД(x, 0) = x. Заметим, что "зациклиться" программа не может, поскольку на каждой итерации цикла уменьшается либо a, либо b.
Осталось обосновать правильность инварианта. Для этого достаточно доказать, что НОД(a, b) = НОД(a % b, b) − оставляем это упражнение читателю.
Утверждения, макрос assert
Термин "утверждение" (assertion) очень похож на понятие инварианта. Утверждение − это условие, которое в заданной точке программы обязано быть всегда истинным. Если же оно вдруг оказывается ложным, то это говорит о наличии ошибки в программе.
Типичным примером является принадлежность аргументов функции своим областям определения, а результата − своей области значений. Например, в нашей функции gcd аргументы должны быть неотрицательными, и хотя бы один из них не равен нулю. Результат должен быть больше нуля, а также должен являться делителем чисел m и n.
Можно вставить в код функции условные операторы для проверки утверждений и возбуждать исключение, если утверждение не выполняются. Однако, такой подход замедляет работу, что не всегда приемлемо.
Хорошим вариантом является использование отладочного макроса assert из заголовочного файла cassert. Макрос assert принимает один аргумент − логическое выражение. Если выражение ложно, то в стандартный поток ошибок выводится диагностическое сообщение вида:
Assertion failed: expression, file filename, line line_number
где expression − текст логического выражения, filename − имя исходного файла, line_number − строка в исходном файле. После этого работа программы завершается.
Макрос assert можно отключить, вставив директиву #define NDEBUG перед директивой #include <cassert>. В этом случае диагностический код компилироваться не будет, и работа программы не замедлится. Типичной практикой является отключение диагностики при компиляции версии программы для передачи её конечным пользователям.
Приведём пример версии функции gcd с использованием assert:
// Пример 9.1.2 - поиск НОД алгоритмом Евклида
// параметры: m >= 0, n >= 0, m или n не равен нулю
// результат: НОД (m, n)
//#define NDEBUG
#include <cassert>
int gcd(int m, int n)
{
assert(m >= 0 && n >= 0);
assert (m != 0 || n != 0);
int a = m, b = n;
while (a != 0 && b!= 0)
{
if (a > b)
a %= b;
else
b %= a;
}
int res = std::max(a, b);
assert (res > 0);
assert (m % res == 0 && n % res == 0);
return res;
}
Пример сообщения, которое выдаётся при срабатывании assert:
Assertion failed: m >= 0 && n >= 0, file example_9_1_2.cpp, line 6
Заметим, что описанный подход широко применяется не только на стадии отладки, но ещё на стадии написания кода: зачастую он позволяет обнаружить при тестировании те ошибки, которые иначе были бы пропущены и проявились бы лишь впоследствии при эксплуатации программы.
Синтаксические ошибки
Обнаружение и исправление синтаксических ошибок обычно не представляет особых сложностей. Однако, некоторые моменты всё же стоит отметить.
Язык C++ имеет достаточно сложную грамматику, поэтому компилятор не всегда может точно определить строку с ошибкой. Если в сообщении компилятора говорится, что он обнаружил ошибку в строке 100, то на самом деле она может быть в строке 99 (или даже 98). При этом сообщение об ошибке может оказаться довольно неочевидным. Однако, при просмотре этой и соседних строк причина ошибки обычно быстро находится. Кроме того, зачастую единственная синтаксическая ошибка приводит к тому, что компилятор обнаруживает огромное количество ошибок, являющихся следствием данной. Для примера рассмотрим код:
// Пример 9.1.3 - программа с синтаксической ошибкой
#include <iostream>
int main() {
int x;
std::cin >> x;
std::cout << x * x;
}
В данной программе имеется одна ошибка − пропущена точка с запятой в конце четвёртой строки (должно быть int x;). А теперь посмотрим на сообщения компилятора:
example_9_1_3.cpp: In function 'int main()':
example_9_1_3.cpp:5:3: error: expected initializer before 'std'
std::cin >> x;
^
example_9_1_3.cpp:6:16: error: 'x' was not declared in this scope
std::cout << x * x;
^
Как видим, компилятор обнаружил синтаксическую ошибку в пятой строке, а не в четвёртой. Из сообщения "expected initializer before 'std'" не сразу очевидно, что проблема в отсутствии точки с запятой перед словом std. Кроме того, компилятор обнаружил ещё одну ошибку в шестой строке − переменная x не объявлена. Данная ошибка является следствием первой.
Обычно нет смысла продолжать компиляцию файла, если в нём уже нашлось много синтаксических ошибок, так как многие из них являются следствием предыдущих. Например, для компилятора g++ параметр -fmax-errors=2 приводит к остановке компиляции при обнаружении двух ошибок.
Предупреждения компилятора
Рекомендуем всегда включать максимальный или близкий к максимальному уровень предупреждений (warnings) компилятора (по умолчанию он обычно стоит на некотором среднем уровне). Например, в компиляторе g++ многие (но не все!) предупреждения включаются с помощью параметра командной строки -Wall. Некоторые дополнительные предупреждения активируются ключом -Wextra. Ключ -pedantic проверяет строгое соответствие программы стандарту языка C++, это полезно при написании переносимых программ.
При использовании интегрированной среды разработки эти опции обычно можно найти в настройках проекта. Например, в среде CodeBlocks нужно зайти в меню "Project − Build Options", открыть вкладку "Compiler Settings" и ниже − вкладку "Compilers Flags". При работе с Visual C++ аналогичные параметры можно найти в свойствах проекта в разделе "Configuration Properties − C/C++ − General", параметр "Warning Level".
Разумеется, предупреждения компилятора никак не помогут в отладке, если их игнорировать. Иногда возникает соблазн не обращать внимание на предупреждения, поскольку "программа ведь компилируется и работает". Это легко может привести к тому, что вы пропустите ошибку, которая не будет найдена при тестировании и проявится у конечных пользователей. Чтобы дисциплинировать себя, можно попросить компилятор обрабатывать предупреждения как ошибки: тогда при наличии предупреждений компиляция будет прервана. Например, в g++ для этого служит ключ -Werror.
Рассмотрим на примерах, как предупреждения помогают выявлять ошибки в программах. Пусть дан следующий код:
// Пример 9.1.4 - пример программы с ошибкой
#include <cstdio>
int main()
{
int x;
scanf("%d", x);
printf("%d", x * x);
}
При использовании компилятора g++ 4.9.2 с параметрами компиляции по умолчанию мы получаем исполняемый файл, при этом ничего подозрительного не обнаруживается. Однако, указав при компиляции параметр -Wall, получаем следующее сообщение:
example_9_1_4.cpp:5:22: warning: format '%d' expects argument of type 'int*', but argument 3 has type 'int' [-Wformat=]
Из этого предупреждения сразу понятно, что в пятой строчке функции scanf передаётся аргумент типа int, а ей нужен указатель на int. Исправленная строчка выглядит так:
scanf("%d", &x);
Рассмотрим ещё один пример:
// Пример 9.1.5 - ещё одна программа с ошибкой
#include <iostream>
int main()
{
int x;
std::cin >> x;
if (x = 0) std::cout << "zero\n";
}
На этот раз воспользуемся компилятором MS Visual C++. Компиляция с параметрами по умолчанию ни о чём подозрительном не сообщает. Однако, при компиляции с ключом /W4 мы получаем следующее сообщение:
example_9_1_5.cpp(5) : warning C4706: assignment within conditional expression
Из данного сообщения сразу понятна суть ошибки: вместо сравнения x==0 в программе написано присваивание x=0.
Примечание: здесь мы использовали ключ /W4, а не /Wall, так как с ключом /Wall компилятор Visual C++ находит слишком много предупреждений в стандартном заголовочном файле iostream.
Отладочные библиотеки
С некоторыми компиляторами C++ поставляются две версии стандартной библиотеки языка − основная и отладочная. Если основная версия реализована в расчёте на максимальную производительность, то отладочная содержит дополнительный код, позволяющий облегчить отладку программ. Отладочная библиотека может обнаружить, например, выход индекса за границу вектора, вызов функции с неверными аргументами и др. Для примера рассмотрим следующий код:
// Пример 9.1.6 - программа с ошибкой выхода за границу
#include <iostream>
#include <vector>
int main()
{
std::vector<int> x(10);
x[10] = 5;
}
Выражение x[10]=5; ошибочно, так как индекс должен принадлежать диапазону от 0 до 9. Откомпилируем данный код с помощью Visual C++ и запустим программу. Если при этом использовалась основная версия библиотеки, то поведение программы непредсказуемо − она может завершиться успешно, а может "вылететь" с ошибкой времени выполнения. Если использовалась отладочная библиотека, то при запуске программы будет выведено сообщение наподобие такого:
Debug Assertion Failed!
Program: C:\Windows\system32\MSVCP120D.dll
File: C:\ ... \ include\vector
Line: 1201
Expression: vector subscript out of range
Для указания компилятору Visual C++, какие версии библиотек использовать, имеются специальные ключи (/MD, /MDd, /MT, /MTd). Однако, проще всего при работе в Visual Studio использовать предустановленные конфигурации Debug и Release. Набор настроек в конфигурации Debug ориентирован на облегчение отладки, а в конфигурации Release − на максимальную производительность. Заметим, что разница в производительности может быть очень существенной. Например, замеры скорости обращения к элементу вектора показали отличие примерно в 90 раз!
Метод "разделяй и властвуй"
Одним из способов локализации места ошибки в коде является метод "разделяй и властвуй". Используется обычно в том случае, когда нет обоснованных предположений о месте ошибки. Суть метода в следующем. Выбираем некоторую точку в коде и определяем, произошла ошибка раньше этой точки или позже неё. Основная сложность заключается в том, как именно это определять − критерии очевидны далеко не всегда.
Рассмотрим для начала простой пример. Допустим, в программе последовательно вызываются функции f1 и f2, а ошибка проявляется в том, что программа аварийно завершается. Мы хотим определить, в которой из функций происходит ошибка. Закомментируем вызов функции f2. Если программа теперь завершается корректно, то ошибка, судя по всему, находится в функции f1, иначе − в f2.
Теперь рассмотрим пример посложней. Дан вектор, содержащий набор целых чисел − координаты некоторых точек на числовой оси. Требуется выполнить так называемое сжатие координат: самое маленькое число становится нулём, следующее по величине − единицей, и так далее. Реализация функции выглядит следующим образом:
// Пример 9.1.7 - функция сжатия координат (содержит ошибку)
void coord_compress(std::vector<int> &x)
{
// Часть 1. Строим отображение старых координат в новые
std::map<int, int> to;
std::vector<int> xx = x;
std::sort(xx.begin(), xx.end());
int cur = 0;
for (int v : x) // в этой строчке ошибка
if (to.count(v) == 0)
to[v] = cur++;
// Часть 2. Заменяем координаты в исходном массиве на сжатые
for (int &v : x)
v = to[v];
}
Работа функции состоит из двух этапов. На первом этапе строится отображение to из старых координат в новые. Например, для входного вектора {5, 2, 8, 5} должно получиться отображение 2→0, 5→1, 8→2. На втором этапе строится результирующий вектор − в нашем примере будет {1, 0, 2, 1}.
При проверке работы этой функции обнаружилось, что она работает неправильно. Вектор {5, 2, 8, 5} должен был преобразоваться в {1, 0, 2, 1}, а получилось {0, 1, 2, 0}. Чтобы найти ошибку, применим подход "разделяй и властвуй" − определим, в первой части функции ошибка или во второй.
Для этого посмотрим, правильно ли строится отображение to. Добавим в текст функции отладочную печать (прямо перед комментарием "// Часть 2…"):
for (auto elem : to)
std::cerr << elem.first << "->" << elem.second << " ";
std::cerr << std::endl;
Результат отладочного вывода: 2->1 5->0 8->2, то есть ошибка находится в той части функции, которая строит отображение. Посмотрев внимательно на код, понимаем, что вместо "for (int v : x)" должно быть "for (int v : xx)".
Примечание 1. Вероятно, для поиска ошибки в столь коротком коде хватило бы и метода "пристального взгляда".
Примечание 2. Интересно, что ни один из компиляторов, которыми пользовался автор, не выдал предупреждения о том, что переменная xx не используется для получения результата функции.
Логирование
Логирование − это вывод на консоль или в файл различной информации − событий входа в определённые функции и выхода из них, вывод значений входных и выходных аргументов функций, значений переменных в определённых точках программы и различных других сведений, которые могут быть полезны при отладке.
К логированию кода можно отнести две достаточно разные задачи.
Во-первых, отладочная печать. Операторы отладочного вывода добавляются в программу при поиске конкретной ошибки. После устранения ошибки эти операторы из программы удаляются. Пример использования отладочной печати приводился в предыдущем подразделе.
Во-вторых, это ведение файла журнала (так называемого "лог-файла"), содержащего историческое описание исполнения программы (когда и кем программа была запущена, какие функции, в какое время и с какими параметрами активизировались и т.п.) Для интерактивных приложений имеется сходное понятие журнал действий пользователей.
Представим ситуацию, что при работе пользователя с программой произошла ошибка. Он сообщает об ошибке в отдел поддержки. Чтобы исправить ошибку, нужно уметь её воспроизвести. Вряд ли обычный пользователь сможет вспомнить абсолютно все свои шаги, а ведь порой ошибка проявляется только при строго определённой последовательности действий. Если система логирует действия пользователя, то воспроизвести ошибку становится намного проще, и её исправление не составит проблемы.
Стоит отметить, что при наличии хорошего лога "продвинутый" пользователь зачастую и сам может понять причину ошибки, не прося помощи у разработчиков. Это характерно, например, для многих программ для операционных систем семейств Unix/Linux.
Для целей логирования можно использовать как стандартные средства ввода/вывода языка C++, так и специализированные библиотеки с дополнительными возможностями.
При использовании стандартной библиотеки C++ следует обратить внимание на важный момент: обычно при записи в файл данные буферизируются в оперативной памяти, и попадают в файл не сразу. Если вследствие аварийного завершения программы файл корректно не закроется, то данные из буфера могут пропасть.
При использовании потокового вывода для сброса содержимого буфера в файл достаточно вывести std::flush или std::endl (в последнем случае выведется также перевод строки). Заметим, что символ перевода строки ('\n'), в отличие от std::endl, не гарантирует сброс буфера в файл. Также для этой цели можно использовать C-функцию fflush. Пример:
// Пример 9.1.8 - пример вывода со сбросом буфера
#include <stdio.h>
#include <iostream>
int main()
{
// перенаправим стандартный поток вывода в файл
freopen("output.txt", "w", stdout);
std::cout << "Hello, " << std::flush;
std::cout << "world!" << std::endl;
printf("I like C++");
fflush(stdout);
}
Каждая из трёх операций вывода в данном случае гарантированно выведет свой текст в файл, даже если после неё в программе произойдёт исключение.
Использование отладчика
Как правило, любая среда программирования имеет в своём составе отладчик. Отладчик позволяет производить трассировку программы, то есть её пошаговое выполнение строка за строкой. При этом, если очередная строка является вызовом функции, то можно как выполнить её целиком, так и зайти внутрь и продолжать пошаговое выполнение уже внутри тела функции.
В ходе трассировки можно просматривать текущие значения переменных. Хорошие отладчики умеют отображать достаточно сложные данные − динамические массивы, объекты пользовательских классов и др. Зачастую бывает полезно посмотреть стек вызовов, чтобы узнать последовательность вызова функций, которая привела к текущей точке программы.
Стоит отметить, что с помощью отладчика можно не только смотреть, но и изменять значения переменных − иногда это бывает удобно.
Отладчик позволяет задавать так называемые точки останова (breakpoints) − условия, при котором выполнение программы приостанавливается. Чаще всего используются позиционные точки останова, которые привязываются к определённым строкам в программе. Как только управление доходит до такой строки, выполнение прерывается. Для таких точек останова обычно можно указать число итераций, после которого точка должна сработать, а также дополнительное условие срабатывания в виде логического выражения.
Помимо позиционных точек, некоторые отладчики поддерживают также точки останова по данным. Такая точка останова срабатывает, когда изменяется указанная область памяти. При этом неважно, в какой строке программы это произошло.
В качестве примера использования отладчика вернёмся к функции coord_compress из примера 9.1.7. Вместо добавления в программу кода отладочной печати можно было бы поставить точку останова на строку "for (int &v : x)". Тогда перед началом выполнения этого цикла сработала бы точка останова, после чего мы бы посмотрели содержимое переменной to.
Интересно, что некоторые специалисты в области программирования вообще не рекомендуют применять инструменты отладки, а для поиска ошибок советуют использовать свою голову. Это, конечно, спорное утверждение, но частично с ним можно согласиться: не следует полагаться на отладчик слишком сильно, он не сможет заменить грамотных рассуждений.
Другие инструменты отладки
При выполнении отладки можно использовать целый ряд дополнительных инструментов. Рассмотрим кратко некоторые из них.
Во-первых, это утилита для сравнения содержимого файлов. Если вы плохо помните, что именно успели изменить в большом исходном файле при отладке, то можно сравнить старую версию файла с новой и получить список различий. В Windows стандартная утилита для сравнения файлов называется fc, в Unix/Linux − diff.
Существуют инструменты, которые проверяют исходный код в каких-то аспектах тщательней, чем компилятор, и могут находить в нём подозрительные места − например, переменные без инициализации и др. Для C++ примерами таких программ являются lint и cppcheck.
Как ни странно, для целей отладки могут оказаться полезны и инструменты для профилирования выполнения программы (профилировщики). Допустим, вы реализовали эффективный алгоритм, который теоретически должен работать очень быстро. Однако, результат профилирования показал, что ваша функция выполняется многократно медленнее ожидаемого. Это может быть признаком того, что в реализации алгоритма есть ошибка.
Также при отладке можно использовать средства для поиска утечек памяти, инструменты для перехвата сетевого трафика при отладке приложений "клиент-сервер" и др.
