章节目录

基本调试策略

本节阅读量:

上节课中,探索了查找问题的策略,运行程序并猜测问题位置。本课程,将介绍一些其它基本策略,用于实际进行猜测,收集信息以帮助定位问题。


调试策略#1:注释代码

从一个简单的程序开始。如果程序表现出错误行为,减少必须搜索的代码量的一种方法是注释掉一些代码,并查看问题是否仍然存在。如果问题保持不变,则注释掉的代码则可能不是问题的根因。

考虑以下代码:

1
2
3
4
5
6
7
8
9
int main()
{
    getNames(); // 让用户输入一些名称
    doMaintenance(); // 做一下其他事情
    sortNames(); // 排序名称
    printNames(); // 打印排序后的名称

    return 0;
}

假设这个程序应按顺序打印用户输入的名称,但它未按顺序打印。问题出在哪里?getNames输入的名称是否不正确?sortNames是否排序正确?printNames是否正常打印它们?可能是任何一种原因。但可能doMaintenance() 与该问题无关,所以先将其注释掉。

1
2
3
4
5
6
7
8
9
int main()
{
    getNames(); // 让用户输入一些名称
//    doMaintenance(); // 做一下其他事情
    sortNames(); // 排序名称
    printNames(); // 打印排序后的名称

    return 0;
}

有三种可能的结果:

  1. 如果问题消失了,那么doMaintenance一定是问题的根源。
  2. 如果问题没有改变(这更可能),可假设doMaintenance没有错,且可以从排查中排除整个函数。这不能解决实际问题是调用doMaintenance之前还是之后,但减少了随后检查的代码量。
  3. 如果注释掉doMaintenance导致问题演变为其他相关问题(例如,程序停止打印名称),则doMaintenance很可能正在做一些其他代码依赖的有用的事情。在这种情况下,无法判断问题是在doMainternance中还是在其他地方,因此取消注释doMaintentenance并尝试其他方法。

调试策略#2:验证代码流程

更复杂的程序中,另一常见问题是程序调用函数次数太多或太少(包括根本不调用)。

这种情况下,将语句放在函数的顶部来打印函数名称会很有帮助。程序运行时,可看到调用了哪些函数。

请考虑以下无法正常工作的简单程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <iostream>

int getValue()
{
	return 4;
}

int main()
{
    std::cout << getValue << '\n';

    return 0;
}

禁用“将警告视为错误”,才能编译。

尽管希望程序打印值4,但它可能打印值:

1
1

在Visual Studio(可能还有其他一些编译器)上,它可能会打印以下内容:

1
00101424

为函数添加调试语句:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>

int getValue()
{
std::cerr << "getValue() called\n";
	return 4;
}

int main()
{
std::cerr << "main() called\n";
    std::cout << getValue << '\n';

    return 0;
}

函数执行时,输出函数名称表示被调用:

1
2
main() called
1

可以看到未调用函数getValue。调用函数处有问题。仔细看这行:

1
    std::cout << getValue << '\n';

在函数调用中忘记了括号。应该是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>

int getValue()
{
std::cerr << "getValue() called\n";
	return 4;
}

int main()
{
std::cerr << "main() called\n";
    std::cout << getValue() << '\n'; // 添加括号

    return 0;
}

正确的输出

1
2
3
main() called
getValue() called
4

删除临时调试语句。


调试策略#3:打印值

对于某些类型的错误,程序可能计算或传递错误的值。

还可以输出变量(包括参数)或表达式的值,以检查是否正确。

考虑以下程序,该程序将两个数字相加,但不能正常工作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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;
}

下面是该程序的一些输出:

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

你发现错误了吗?即使在这个简短的程序中,它也很难被发现。让我们添加一些代码来调试变量的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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;
}

下面是对应的输出:

1
2
3
4
5
6
7
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#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;
}

现在,我们将获得输出:

1
2
3
4
5
6
7
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不知何故获得了参数y的值5。我们一定传递了错误的参数。果然:

1
	int z{ add(x, 5) };

就是这样。我们传递了文本5,而不是变量y的值作为参数。这很简单就可修复,然后我们可以删除调试语句。


再举一个例子

该程序与前一个程序非常相似,但也不能正常工作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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;
}

如果运行此代码并看到以下内容:

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

嗯,有点不对劲。但在哪里呢?

让我们对该代码进行一些调试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#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;
}

现在,让我们使用相同的输入再次运行该程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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,但main的x得到的是值3。在用户输入的位置和将该值分配给main的变量x的位置之间,肯定出现了问题。让我们通过向函数getUserInput添加一些调试代码来确保程序从用户处获得正确的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#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;
}

输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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

通过这一额外的调试行,我们可以看到用户输入被正确地接收到getUserInput的变量x中。然而,main的变量x得到了错误的值。问题一定在这两点之间。唯一的罪魁祸首是函数getUserInput的返回值。让我们更仔细地看一下那一行。

1
	return --x;

嗯,这很奇怪。那是什么–x之前的符号?我们还没有在这些教程中介绍这一点,所以如果您不知道它的含义,请不要担心。但即使不知道它意味着什么,通过调试工作,您也可以合理地确定这行代码有问题——因此,很可能是这个“——”符号导致了问题。

由于我们确实希望getUserInput仅返回x的值,因此让我们删除“–”并查看发生了什么:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#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;
}

现在输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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. 在解决问题之后,需要移除调试代码,这些代码完全不可重用

我们可以做得更好。我们将在以后的课程中探索如何。


3.2 调试策略

上一节

3.4 更多调试策略

下一节