3.6 – Использование встроенного отладчика: пошаговое выполнение

Добавлено 18 апреля 2021 в 19:17

Когда вы запускаете свою программу, выполнение начинается с верхней части функции main, а затем продолжается последовательно инструкция за инструкцией, пока программа не завершится. В любой момент времени, пока ваша программа выполняется, она отслеживает множество вещей: значение используемых вами переменных, какие функции были вызваны (поэтому при возвращении из этих функций программа будет знать, куда вернуться), и текущая точка выполнения в программе (чтобы она знала, какую инструкцию выполнить следующей). Вся эта отслеживаемая информация называется состоянием программы (или, для краткости, просто состоянием).

В предыдущих уроках мы исследовали различные способы изменения вашего кода для облегчения отладки, включая печать диагностической информации или использование логгера. Эти простые методы предназначены для проверки состояния программы во время ее выполнения. Хотя они могут быть эффективными при правильном использовании, у них все же есть недостатки: они требуют изменения вашего кода, что требует времени и может привести к появлению новых ошибок, и они загромождают ваш код, затрудняя понимание существующего кода.

За методами, которые мы показывали до сих пор, стоит неустановленное предположение: как только мы запустим код, он будет работать до завершения (только с паузой для приема входных данных) без возможности вмешаться и проверить результаты программы в любой момент времени, какой мы захотим.

Однако что, если бы мы смогли убрать это предположение? К счастью, мы можем. Большинство современных IDE поставляются со встроенным инструментом, называемым отладчиком, который предназначен именно для этого.

Отладчик

Отладчик – это компьютерная программа, которая позволяет программисту контролировать выполнение программы и проверять ее состояние во время выполнения программы. Например, программист может использовать отладчик для выполнения программы построчно, по ходу проверяя значения переменных. Сравнивая фактические значения переменных с ожидаемыми или наблюдая за ходом выполнения кода, отладчик может существенно помочь в отслеживании семантических (логических) ошибок.

Возможности отладчика двойственные: способность точно контролировать выполнение программы и возможность просматривать (и при желании изменять) состояние программы.

Ранние отладчики, такие как gdb, были отдельными программами с интерфейсами командной строки, где программисту приходилось вводить загадочные команды, чтобы заставить их работать. Более поздние отладчики (такие как ранние версии Turbo Debugger от Borland) всё еще были автономными, но поставлялись со своими собственными «графическими» интерфейсами для облегчения работы с ними. Большинство современных IDE, доступных в наши дни, имеют встроенный отладчик, то есть отладчик использует тот же интерфейс, что и редактор кода, поэтому вы можете отлаживать программу, используя ту же среду, которую вы используете для написания кода (вместо того, чтобы переключаться между программами).

Практически все современные отладчики содержат один и тот же стандартный набор базовых функций, однако единообразия в том, как организованы меню для доступа к этим функциям, нет, и еще меньше единообразия в сочетаниях клавиш. Хотя в наших примерах будут использоваться скриншоты Microsoft Visual Studio (и мы расскажем, как делать всё в Code::Blocks), у вас не должно возникнуть проблем с выяснением того, как получить доступ к каждой обсуждаемой нами функции, независимо от того, в какой среде разработки вы находитесь, даже если вашу IDE, мы не рассматриваем явно.

Остаток этой главы будет посвящен изучению того, как использовать отладчик.

Совет


Не пренебрегайте изучением, как пользоваться отладчиком. По мере того, как ваши программы становятся более сложными, количество времени, которое вы тратите на обучение эффективному использованию встроенного отладчика, будет бледнеть по сравнению с количеством времени, которое вы сэкономите на поиске и устранении проблем.

Предупреждение


Прежде чем продолжить этот урок (и последующие уроки, связанные с использованием отладчика), убедитесь, что ваш проект скомпилирован, используя конфигурацию отладочной сборки (для получения дополнительной информации смотрите «0.9 – Настройка компилятора: конфигурации сборки»).

Если вместо этого вы компилируете свой проект, используя конфигурацию релиза, функциональные возможности отладчика могут работать некорректно (например, когда вы попытаетесь начать выполнять свою программу пошагово, он просто запустит программу).

Для пользователей Code::Blocks


Если вы используете Code::Blocks, ваш отладчик может быть настроен правильно, а может и нет. Давайте, проверим.

Сначала перейдите в меню Settings (Настройки) → Debugger (Отладчик). Затем откройте дерево отладчика GDB/CDB (GDB/CDB debugger) слева и выберите Default (По умолчанию). Должно открыться диалоговое окно, которое выглядит примерно так:

Рисунок 1 Настройки отладчика в Code::Blocks
Рисунок 1 – Настройки отладчика в Code::Blocks

Если вы видите большую красную полосу на месте Executable path (Путь к исполняемому файлу), то вам нужно найти отладчик. Для этого нажмите кнопку справа от поля Executable path. Затем найдите файл gdb32.exe в вашей системе – мой был в C:\Program Files (x86)\CodeBlocks\ MinGW\bin\gdb32.exe. Затем нажмите ОК.

Для пользователей Code::Blocks


Были сообщения о том, что встроенный отладчик Code::Blocks (GDB) может иметь проблемы с распознаванием некоторых путей к файлам, содержащих пробелы или неанглийские символы. Если во время прохождения этих уроков отладчик у вас работает со сбоями, то это может быть причиной.

Пошаговое выполнение

Мы собираемся начать изучение отладчиков с изучения некоторых инструментов отладки, которые позволяют нам контролировать выполнение программы.

Пошаговое выполнение – это название набора связанных функций отладчика, которые позволяют нам выполнять (поэтапно) наш код инструкцию за инструкцией.

Есть несколько пошаговых команд, которые мы рассмотрим по очереди.

Шаг с заходом (step into)

Команда шаг с заходом (step into) выполняет следующую инструкцию в обычном пути выполнения программы, а затем приостанавливает выполнение программы, чтобы мы могли проверить состояние программы с помощью отладчика. Если выполняемый оператор содержит вызов функции, шаг с заходом заставляет программу перескакивать в начало вызываемой функции, где она приостанавливается.

Давайте посмотрим на очень простую программу:

#include <iostream>
 
void printValue(int value)
{
    std::cout << value << '\n';
}
 
int main()
{
    printValue(5);
 
    return 0;
}

Давайте отладим эту программу, используя команду шаг с заходом.

Сначала найдите и затем выполните команду отладки шаг с заходом один раз.

Для пользователей Visual Studio


В Visual Studio к команде шаг с заходом (step into) можно получить доступ через меню Отладка (Debug) → Шаг с заходом (Step Into), или нажав клавишу F11.

Для пользователей Code::Blocks


В Code::Blocks к команде шага с заходом можно получить доступ через меню Debug (Отладка) → Step into (Шаг с заходом), или нажав Shift + F7.

Для других компиляторов


Если вы используете другую среду IDE, вы, вероятно, найдете команду step into в меню Debug (Отладка) или Run (Выполнить).

Когда ваша программа не запущена и вы выполняете первую команду отладки, вы можете увидеть, что происходит несколько вещей:

  • При необходимости программа перекомпилируется.
  • Программа начинает выполняться. Поскольку наше приложение является консольной программой, должно открыться окно консоли. Оно будет пустым, потому что мы еще ничего не выводили.
  • В вашей IDE могут открываться некоторые диагностические окна, которые могут иметь такие названия, как «Diagnostic Tools» (средства диагностики), «Call Stack» (стек вызовов) и «Watch» (наблюдение). Некоторые из них мы рассмотрим позже – пока вы можете их игнорировать.

Поскольку мы выполнили шаг с заходом, теперь вы должны увидеть какой-то маркер слева от открывающей скобки функции main (строка 9). В Visual Studio этот маркер представляет собой желтую стрелку (Code::Blocks использует желтый треугольник). Если вы используете другую IDE, вы должны увидеть что-то, служащее той же цели.

Рисунок 2 Маркер строки, которая будет выполнена следующей, в Visual Studio
Рисунок 2 – Маркер строки, которая будет выполнена следующей, в Visual Studio

Этот маркер стрелки указывает, что указанная строка будет выполнена следующей. В этом случае отладчик сообщает нам, что следующая выполняемая строка – это открывающая скобка функции main (строка 9).

Выполните шаг с заходом (используя соответствующую команду для вашей IDE, как описано выше), чтобы выполнить открывающую скобку, и стрелка переместится к следующей инструкции (строка 10).

Рисунок 3 Положение маркера после выполнения открывающей фигурной скобки
Рисунок 3 – Положение маркера после выполнения открывающей фигурной скобки

Это означает, что следующая строка, которая будет выполнена, – это вызов функции printValue.

Выполните шаг с заходом снова. Поскольку эта инструкция содержит вызов функции printValue, мы переходим к функции, и стрелка переместится в верхнюю часть тела printValue (строка 4).

Рисунок 4 Положение маркера после выполнения захода в функцию printValue
Рисунок 4 – Положение маркера после выполнения захода в функцию printValue

Выполните шаг с заходом снова, чтобы выполнить открывающую скобку функции printValue, которая переместит стрелку на строку 5.

Рисунок 5 Положение маркера после выполнения следующего шага с заходом
Рисунок 5 – Положение маркера после выполнения следующего шага с заходом

Выполните шаг с заходом снова, который выполнит инструкцию std::cout << value и переместит стрелку в строку 6.

Предупреждение


Поскольку operator<< реализован как функция, ваша IDE может вместо этого перейти к реализации operator<<.

Если это произойдет, вы увидите, как ваша IDE откроет новый исходный файл, а стрелка-маркер переместится в начало функции с именем operator<< (это часть стандартной библиотеки). Закройте только что открытый исходный файл, затем найдите и выполните команду отладки шаг с выходом (step out) (если вам нужна помощь, инструкции приведены ниже в разделе «Шаг с выходом (step out)»).

Теперь, поскольку std::cout << value выполнено, теперь мы должны увидеть, что в окне консоли появилось значение 5.

Выполните шаг с заходом снова, чтобы выполнить закрывающую скобку функции printValue. На этом этапе printValue завершает выполнение, и управление возвращается в main.

Вы заметите, что стрелка снова указывает на printValue!

Рисунок 6 Положение маркера после завершения функции printValue
Рисунок 6 – Положение маркера после завершения функции printValue

Хотя вы можете подумать, что отладчик намеревается снова вызвать printValue, на самом деле он просто сообщает вам, что он возвращается из вызова функции.

Выполните шаг с заходом еще три раза. На этом этапе мы выполнили все строки в нашей программе, поэтому мы закончили. Некоторые отладчики на этом этапе автоматически завершают сеанс отладки, некоторые – нет. Если ваш отладчик этого не делает, вам может потребоваться найти в меню команду Stop Debugging (остановить отладку) (в Visual Studio это находится в разделе Отладка (Debug) → Остановить отладку (Stop Debugging)).

Обратите внимание, что Остановить отладку для завершения сеанса отладки можно в любой момент процесса отладки.

Поздравляем, теперь вы пошагово выполнили программу и наблюдали за выполнением каждой строки!

Шаг с обходом (step over)

Как и шаг с заходом, команда шаг с обходом (step over) выполняет следующую инструкцию в обычном пути выполнения программы. Однако в то время как шаг с заходом будет входить в вызовы функций и выполнять их построчно, шаг с обходом выполнит всю функцию без остановки и вернет вам управление после выполнения этой функции.

Для пользователей Visual Studio


В Visual Studio к команде шаг с обходом (step over) можно получить доступ через меню Отладка (Debug) → Шаг с обходом (Step Over), или нажав клавишу F10.

Для пользователей Code::Blocks


В Code::Blocks команда шаг с обходом называется Next line (следующая строка), и к ней можно получить доступ через меню Debug (Отладка) → Next line (Следующая строка), или нажав клавишу F7.

Давайте посмотрим на пример, в котором мы делаем шаг с обходом вызова функции printValue:

#include <iostream>
 
void printValue(int value)
{
    std::cout << value << '\n';
}
 
int main()
{
    printValue(5);
 
    return 0;
}

Сначала используйте команду шаг с заходом, пока маркер выполнения не окажется в строке 10:

Рисунок 7 Положение маркера перед выполнением функции printValue
Рисунок 7 – Положение маркера перед выполнением функции printValue

Теперь выполните шаг с обходом. Отладчик выполнит функцию (которая выводит значение 5 в окно консоли), а затем вернет управление вам в следующей инструкции (строка 12).

Команда шаг с обходом предоставляет удобный способ пропустить функции, если вы уверены, что они уже работают, или не заинтересованы в их отладке прямо сейчас.

Шаг с выходом (step out)

В отличие от двух других пошаговых команд, шаг с выходом (step out) не просто выполняет следующую строку кода. Вместо этого он выполняет весь оставшийся код функции, которая выполняется в настоящее время, а затем возвращает управление вам, когда будет выполнен возврат из функции.

Для пользователей Visual Studio


В Visual Studio команду шаг с выходом можно выполнить через меню Отладка (Debug) → Шаг с выходом (Step Out), или нажав комбинацию клавиш Shift + F11.

Для пользователей Code::Blocks


В Code::Blocks к команде шаг с выходом можно получить доступ через меню Debug (Отладка) → Step Out (Шаг с выходом), или нажав комбинацию клавиш Ctrl + F7.

Давайте посмотрим на пример этого, используя ту же программу, что и выше:

#include <iostream>
 
void printValue(int value)
{
    std::cout << value << '\n';
}
 
int main()
{
    printValue(5);
 
    return 0;
}

Выполните команды шаг с заходом, пока не окажетесь внутри функции printValue с маркером выполнения в строке 4.

Рисунок 8 Положение маркера в начале выполнения функции printValue
Рисунок 8 – Положение маркера в начале выполнения функции printValue

Затем выполните шаг с выходом. Вы заметите, что в окне вывода появится значение 5, и отладчик вернет вам управление после завершения функции (в строке 10).

Рисунок 9 Положение маркера после выполнения шага с выходом в функции printValue
Рисунок 9 – Положение маркера после выполнения шага с выходом в функции printValue

Эта команда наиболее полезна, когда вы случайно вошли в функцию, которую не хотите отлаживать.

Шагнули слишком далеко

При пошаговом выполнении программы вы обычно можете шагать только вперед. Очень легко случайно перешагнуть то место, которое вы хотели исследовать.

Если вы перешагнете намеченное место назначения, обычное дело – остановить отладку и снова ее перезапустить, соблюдая осторожность, чтобы на этот раз не пройти мимо цели.

Шаг назад

Некоторые отладчики (такие как Visual Studio Enterprise Edition и GDB 7.0) представили возможность пошагового выполнения, обычно называемую шагом назад или обратной отладкой. Цель шага назад – отмотать последний шаг назад, чтобы вы могли вернуть программу в предыдущее состояние. Это может быть полезно, если вы перешагнули цель, или если вы хотите повторно проверить только что выполненную инструкцию.

Реализация шага назад требует большой сложности со стороны отладчика (потому что он должен отслеживать отдельное состояние программы для каждого шага). Из-за сложности эта возможность еще не стандартизирована и зависит от отладчика. На момент написания (январь 2019 г.) ни Visual Studio Community Edition, ни последняя версия Code::Blocks не поддерживают эту возможность. Надеюсь, что в какой-то момент в будущем она попадет в эти продукты и станет доступной для более широкого использования.

Теги

C++ / CppCode::BlocksDebugIDEVisual StudioДля начинающихОбучениеОтладкаОтладчик / DebuggerПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.