3.4 – Базовые тактики отладки

Добавлено 17 апреля 2021 в 13:14

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

Тактика отладки 1. Закомментирование кода

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

Рассмотрим следующий код:

int main()
{
    getNames();      // просим пользователя ввести несколько имен
    doMaintenance(); // делаем что-то случайное
    sortNames();     // сортируем их в алфавитном порядке
    printNames();    // выводим отсортированный список имен
 
    return 0;
}

Предположим, эта программа должна печатать имена, вводимые пользователем, в алфавитном порядке, но она печатает их в обратном алфавитном порядке. В чем проблема? getNames неправильно вводит имена? Может sortNames сортирует их в обратном порядке? Или printNames печатает их в обратном порядке? Это может быть что угодно. Но мы можем подозревать, что doMaintenance() не имеет ничего общего с проблемой, поэтому давайте закомментируем ее.

int main()
{
    getNames();      // просим пользователя ввести несколько имен
//    doMaintenance(); // делаем что-то случайное
    sortNames();     // сортируем их в алфавитном порядке
    printNames();    // выводим отсортированный список имен
 
    return 0;
}

Если проблема исчезнет, значит, проблема должна быть вызвана doMaintenance, и мы должны сосредоточить свое внимание на ней.

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

Не забывайте, какие функции вы закомментировали, чтобы их можно было раскомментировать позже!

Тактика отладки 2. Проверка выполнения кода

Другая проблема, часто встречающаяся в более сложных программах, заключается в том, что программа вызывает функцию слишком много или слишком мало раз (в том числе и совсем ни разу).

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

Совет


При печати информации для отладки используйте std::cerr вместо std::cout. Одна из причин этого заключается в том, что std::cout может буферизоваться, что означает, что может быть пауза между тем, когда вы запрашиваете std::cout вывести информацию, и тем, когда это действительно происходит. Если вы выводите с помощью std::cout, а затем ваша программа сразу же выходит из строя, std::cout может еще не успеть вывести необходимую информацию. Это может ввести вас в заблуждение относительно того, в чем проблема. С другой стороны, std::cerr не буферизуется, что означает, что всё, что вы ему отправляете, будет выводиться немедленно. Это помогает гарантировать, что все выходные отладочные данные появятся как можно скорее (за счет некоторой потери производительности, которая при отладке обычно нам не важна).

Рассмотрим следующую простую программу, которая работает некорректно:

#include <iostream>
 
int getValue()
{
	return 4;
}
 
int main()
{
    std::cout << getValue;
 
    return 0;
}

Хотя мы ожидаем, что эта программа напечатает значение 4, на самом деле она будет печатать на разных машинах разные значения. На машине автора было напечатано:

00101424

Давайте добавим к этим функциям несколько отладочных инструкций:

#include <iostream>
 
int getValue()
{
std::cerr << "getValue() called\n";
	return 4;
}
 
int main()
{
std::cerr << "main() called\n";
    std::cout << getValue;
 
    return 0;
}

Совет


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

Теперь при выполнении эти функции выводят свои имена, указывая, что они были вызваны:

main() called
00101424

Теперь мы видим, что функция getValue никогда не вызывалась. Должно быть, какая-то проблема с кодом, вызывающим функцию. Давайте подробнее рассмотрим эту строку:

std::cout << getValue;

Ой, смотрите, мы забыли скобки при вызове функции. Должно быть так:

#include <iostream>
 
int getValue()
{
std::cerr << "getValue() called\n";
	return 4;
}
 
int main()
{
std::cerr << "main() called\n";
    std::cout << getValue(); // здесь добавлены круглые скобки
 
    return 0;
}

Теперь программа даст правильный результат

main() called
getValue() called
4

И мы можем удалить временные отладочные инструкции.

Тактика отладки 3. Печать значений

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

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

Рассмотрим следующую программу, которая должна складывать два числа, но работает некорректно:

#include <iostream>
 
int add(int x, int y)
{
	return x + y;
}
 
void printResult(int z)
{
	std::cout << "The answer is: " << z << '\n';
}
 
int getUserInput()
{
	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
	return x;
}
 
int main()
{
	int x{ getUserInput() };
	int y{ getUserInput() };
 
	std::cout << x << " + " << y << '\n';
 
	int z{ add(x, 5) };
	printResult(z);
 
	return 0;
}

Вот результаты работы этой программы:

Enter a number: 4
Enter a number: 3
4 + 3
The answer is: 9

Это не правильно. Вы видите ошибку? Даже в этой короткой программе ее сложно заметить. Давайте добавим код для отладки наших значений:

#include <iostream>
 
int add(int x, int y)
{
	return x + y;
}
 
void printResult(int z)
{
	std::cout << "The answer is: " << z << '\n';
}
 
int getUserInput()
{
	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
	return x;
}
 
int main()
{
	int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
	int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';
 
	std::cout << x << " + " << y << '\n';
 
	int z{ add(x, 5) };
std::cerr << "main::z = " << z << '\n';
	printResult(z);
 
	return 0;
}

Вот результат приведенной выше программы:

Enter a number: 4
main::x = 4
Enter a number: 3
main::y = 3
4 + 3
main::z = 9
The answer is: 9

Переменные x и y получают правильные значения, а переменная z – нет. Проблема должна быть между этими двумя точками, что делает функцию add ключевым подозреваемым.

Давайте изменим функцию add:

#include <iostream>
 
int add(int x, int y)
{
std::cerr << "add() called (x=" << x <<", y=" << y << ")\n";
	return x + y;
}
 
void printResult(int z)
{
	std::cout << "The answer is: " << z << '\n';
}
 
int getUserInput()
{
	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
	return x;
}
 
int main()
{
	int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
	int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';
 
	std::cout << x << " + " << y << '\n';
 
	int z{ add(x, 5) };
std::cerr << "main::z = " << z << '\n';
	printResult(z);
 
	return 0;
}

Теперь мы получим вывод:

Enter a number: 4
main::x = 4
Enter a number: 3
main::y = 3
add() called (x=4, y=5)
main::z = 9
The answer is: 9

Переменная y имела значение 3, но каким-то образом наша функция add получила значение 5 для параметра y. Мы, должно быть, передали неправильный аргумент. Конечно же:

int z{ add(x, 5) };

Вот оно. Мы передали в качестве аргумента литерал 5 вместо значения переменной y. Это просто исправить, а затем мы можем удалить отладочные инструкции.

Еще один пример

Эта программа очень похожа на предыдущую, но работает не так, как должна:

#include <iostream>
 
int add(int x, int y)
{
	return x + y;
}
 
void printResult(int z)
{
	std::cout << "The answer is: " << z << '\n';
}
 
int getUserInput()
{
	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
	return --x;
}
 
int main()
{
	int x{ getUserInput() };
	int y{ getUserInput() };
 
	int z { add(x, y) };
	printResult(z);
 
	return 0;
}

Если мы запустим этот код и увидим следующее:

Enter a number: 4
Enter a number: 3
The answer is: 5

Хммм, что-то не так. Но где?

Давайте изменим этот код с помощью отладочных инструкций:

#include <iostream>
 
int add(int x, int y)
{
std::cerr << "add() called (x=" << x << ", y=" << y << ")\n";
	return x + y;
}
 
void printResult(int z)
{
std::cerr << "printResult() called (z=" << z << ")\n";
	std::cout << "The answer is: " << z << '\n';
}
 
int getUserInput()
{
std::cerr << "getUserInput() called\n";
	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
	return --x;
}
 
int main()
{
std::cerr << "main() called\n";
	int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
	int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';
 
	int z{ add(x, y) };
std::cerr << "main::z = " << z << '\n';
	printResult(z);
 
	return 0;
}

Теперь давайте снова запустим программу с теми же входными данными:

main() called
getUserInput() called
Enter a number: 4
main::x = 3
getUserInput() called
Enter a number: 3
main::y = 2
add() called (x=3, y=2)
main::z = 5
printResult() called (z=5)
The answer is: 5

Теперь мы сразу видим, что что-то идет не так: пользователь вводит значение 4, но x в main получает значение 3. Что-то должно быть не так между тем, где пользователь вводит входные данные, и тем, где это значение присваивается переменной x в main. Давайте убедимся, что программа получает правильное значение от пользователя, добавив отладочный код в функцию getUserInput:

#include <iostream>
 
int add(int x, int y)
{
std::cerr << "add() called (x=" << x << ", y=" << y << ")\n";
	return x + y;
}
 
void printResult(int z)
{
std::cerr << "printResult() called (z=" << z << ")\n";
	std::cout << "The answer is: " << z << '\n';
}
 
int getUserInput()
{
std::cerr << "getUserInput() called\n";
	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
std::cerr << "getUserInput::x = " << x << '\n'; // добавлена дополнительная строка отладки
	return --x;
}
 
int main()
{
std::cerr << "main() called\n";
	int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
	int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';
 
	int z{ add(x, y) };
std::cerr << "main::z = " << z << '\n';
	printResult(z);
 
	return 0;
}

И вывод:

main() called
getUserInput() called
Enter a number: 4
getUserInput::x = 4
main::x = 3
getUserInput() called
Enter a number: 3
getUserInput::x = 3
main::y = 2
add() called (x=3, y=2)
main::z = 5
printResult() called (z=5)
The answer is: 5

С помощью этой дополнительной строки отладки мы видим, что пользовательские входные данные правильно принимаются в переменную x в getUserInput. И всё же почему-то переменная x в main получает неправильное значение. Проблема должна быть между этими двумя точками. Остался единственный виновник – это значение, возвращаемое функцией getUserInput. Давайте посмотрим на эту строку более внимательно.

return --x;

Хммм, это странно. Что это за символы -- перед x? Мы еще не рассматривали их в этих уроках, поэтому не волнуйтесь, если вы не понимаете, что это значит. Но, даже не зная, что это означает, благодаря вашим усилиям по отладке вы можете быть достаточно уверены, что именно эта строка неисправна – и, следовательно, вполне вероятно, что это символы -- вызывают проблему.

Поскольку мы хотим, чтобы getUserInput возвращала только значение x, давайте удалим -- и посмотрим, что произойдет:

#include <iostream>
 
int add(int x, int y)
{
std::cerr << "add() called (x=" << x << ", y=" << y << ")\n";
	return x + y;
}
 
void printResult(int z)
{
std::cerr << "printResult() called (z=" << z << ")\n";
	std::cout << "The answer is: " << z << '\n';
}
 
int getUserInput()
{
std::cerr << "getUserInput() called\n";
	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
std::cerr << "getUserInput::x = " << x << '\n';
	return x; // удалены -- перед x
}
 
int main()
{
std::cerr << "main() called\n";
	int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
	int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';
 
	int z{ add(x, y) };
std::cerr << "main::z = " << z << '\n';
	printResult(z);
 
	return 0;
}

И вывод:

main() called
getUserInput() called
Enter a number: 4
getUserInput::x = 4
main::x = 4
getUserInput() called
Enter a number: 3
getUserInput::x = 3
main::y = 3
add() called (x=4, y=3)
main::z = 7
printResult() called (z=7)
The answer is: 7

Программа теперь работает правильно. Даже не понимая, что делает --, мы смогли определить конкретную строку кода, вызывающую проблему, а затем исправить ее.

Почему использование инструкций печати для отладки – не самый лучший метод

Хотя добавление отладочных инструкций в программы для диагностических целей является распространенным рудиментарным и рабочим методом (особенно когда отладчик по какой-то причине недоступен), это способ не так хорош по ряду причин:

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

Мы можем лучше. Мы узнаем, как это сделать, в будущих уроках.

Теги

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

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

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