章节目录

第3章总结

本节阅读量:

章节回顾

语法错误是编写了不符合 C++ 语法规则的语句时发生的错误。编译器会捕获这些错误。

当语句在语法上是有效的,但没有实现程序员预期的功能时,就会发生语义错误。

从程序中查找和删除错误的过程称为调试。

我们可以使用五步过程来进行调试:

  1. 寻找错误原因
  2. 理解问题
  3. 决定如何修复
  4. 修复问题
  5. 重新测试

查找错误通常是调试中最困难的部分。

静态分析工具是分析代码并查找可能指示代码存在问题的语义问题的工具。

能够可靠地重现问题是调试中的第一步,也是最重要的一步。

我们可以使用许多策略来帮助发现问题:

  1. 注释掉代码。
  2. 使用输出语句验证代码流程。
  3. 打印值。

使用 print 语句时,请使用 std::cerr 而不是 std::cout。但更好的做法是,避免通过打印语句进行调试。

日志文件是记录程序中发生的事件的文件。将信息写入日志文件的过程称为日志记录。

在不改变代码行为的情况下重新组织代码结构的过程称为重构。这通常是为了使程序更具组织性、更加模块化或提升性能。

单元测试是一种软件测试方法,通过该方法测试源代码的小单元,以确定它们是否正确。

防御性编程是一种技术,程序员试图预见软件可能被滥用的所有方式。这些误用通常可以被检测到并加以缓解。

程序中跟踪的所有信息(变量值、已调用的函数、当前执行点)都是程序状态的一部分。

调试器是一种工具,它允许程序员控制程序的执行方式,并在程序运行时检查程序状态。集成调试器是集成到代码编辑器中的调试器。

单步调试是一组相关调试功能的名称,这些功能允许您逐条语句地执行代码。

Step-into 会执行程序路径中的下一条语句,然后暂停执行。如果该语句包含函数调用,则会跳转到被调用函数的顶部。

Step-over 会执行程序路径中的下一条语句,然后暂停执行。如果该语句包含函数调用,Step-over 会直接执行完该函数,并在函数执行后将控制权返回给您。

Step-out 会执行当前函数中的所有剩余代码,然后在函数返回时将控制权返回给您。

Run-to-cursor 会执行程序,直到到达光标所选择的语句。

“继续"按钮会运行程序,直到程序终止或命中断点。Start 与 continue 功能相同,只是它从程序的开头开始执行。

断点是一个特殊的标记,它告诉调试器在执行到达断点时暂停程序的执行。

监视变量允许您在程序以调试模式执行时检查变量的值。监视窗口允许您检查变量或表达式的值。

调用堆栈是为到达当前执行点而依次调用的所有活动函数的列表。调用堆栈窗口是用于显示调用堆栈的调试器窗口。


习题

问题 1

下面这个程序本应把两个数字相加,但运行结果不正确。

请使用集成调试器单步执行这个程序,并监视变量x的值。根据你观察到的信息,修正下面的程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

int readNumber(int x)
{
	std::cout << "Please enter a number: ";
	std::cin >> x;
	return x;
}

void writeAnswer(int x)
{
	std::cout << "The sum is: " << x << '\n';
}

int main()
{
	int x {};
	readNumber(x);
	x = x + readNumber(x);
	writeAnswer(x);

	return 0;
}
查看答案

主要问题出在函数main的第二行——readNumber的返回值没有被赋给任何变量,因此被丢弃了。一个小问题是,readNumber本应使用一个局部变量,却被写成了接受形参。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

int readNumber()
{
	std::cout << "Please enter a number: ";
	int x {};
	std::cin >> x;
	return x;
}

void writeAnswer(int x)
{
	std::cout << "The sum is: " << x << '\n';
}

int main()
{
	int x { readNumber() };
	x = x + readNumber();
	writeAnswer(x);

	return 0;
}

问题 2

下面这个程序本应把两个数字相除,但运行结果不正确。

请使用集成调试器单步执行这个程序。输入时分别输入8和4。根据你观察到的信息,修正下面的程序:

 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
#include <iostream>

int readNumber()
{
	std::cout << "Please enter a number: ";
	int x {};
	std::cin >> x;
	return x;
}

void writeAnswer(int x)
{
	std::cout << "The quotient is: " << x << '\n';
}

int main()
{
	int x{ };
	int y{ };
	x = readNumber();
	x = readNumber();
	writeAnswer(x/y);

	return 0;
}
查看答案

问题在于对 readNumber 的第二次调用把返回值误赋给了x,而不是给y,结果导致除以0,从而产生未定义的行为。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

int readNumber()
{
	std::cout << "Please enter a number: ";
	int x {};
	std::cin >> x;
	return x;
}

void writeAnswer(int x)
{
	std::cout << "The quotient is: " << x << '\n';
}

int main()
{
	int x{ readNumber() };
	int y{ readNumber() };
	writeAnswer(x/y);

	return 0;
}

你可能已经注意到,当第二个输入不能整除第一个输入时,这个程序似乎会得出错误答案。两个整数相除时,C++会丢弃商的小数部分。我们会在「6.1 算术运算符」中更详细地讨论这一点。

问题 3

在下面的程序中,当执行点位于第4行时,调用栈是什么样子的?本题只需要写函数名,不需要写表示返回位置的函数名。

调用栈在「3.8 调用堆栈」中讨论过。

 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
#include <iostream>

void d()
{ // 此处
}

void c()
{
}

void b()
{
	c();
	d();
}

void a()
{
	b();
}

int main()
{
	a();

	return 0;
}
查看答案

从上到下依次为:

d
b
a
main

问题 4

附加题:下面这个程序本应把两个数字相加,但运行结果不正确。

请使用集成调试器单步执行这个程序。输入时分别输入8和4。根据你观察到的信息,修正下面的程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

int readNumber()
{
    std::cout << "Please enter a number: ";
    char x{};
    std::cin >> x;

    return x;
}

void writeAnswer(int x)
{
    std::cout << "The sum is: " << x << '\n';
}

int main()
{
    int x { readNumber() };
    int y { readNumber() };
    writeAnswer(x + y);

    return 0;
}
查看答案

问题出在第6行的char数据类型上。当我们输入数字8时,它并不是以值8存储的,而是存储为56;输入数字4时,存储为52。因此readNumber()返回的是56和52,而不是期望的8和4。

解决办法是把第6行的数据类型从char改为int。


3.9 在问题变成问题前进行避免

上一节

4.0 基本数据类型简介

下一节