3-x 第三章概要与测验
章节回顾
语法错误syntax error是指编写的语句不符合C++语言语法规则时出现的错误,编译器会捕获此类错误。
语义错误semantic error是指语法正确的语句未能实现程序员预期功能的情况。
查找并消除程序错误的过程称为调试debugging。
可采用五步流程进行调试:
- 找出根本原因。
- 理解问题本质。
- 确定修复方案。
- 实施修复措施。
- 重新测试验证。
在调试过程中,发现错误通常是最困难的部分。
静态分析工具Static analysis tools是用于分析代码并查找可能表明代码存在问题的语义问题的工具。
能够可靠地重现问题是调试的第一步,也是最重要的一步。
我们可以使用多种策略来帮助发现问题:
- 注释掉代码。
- 使用输出语句验证代码流程。
- 打印值。
使用打印语句调试时,请选用std::cerr而非std::cout。但更优方案是避免依赖打印语句进行调试。
日志文件log file用于记录程序运行中的事件,向日志文件写入信息的操作称为日志记录logging。
在不改变行为的前提下重构代码的过程称为重构refactoring,通常用于提升程序的组织性、模块化或性能表现。
单元测试Unit testing是一种软件测试方法,通过测试源代码的小单元来验证其正确性。
防御性编程Defensive programming是一种技术,程序员试图预见软件可能被滥用的所有方式。这些滥用行为通常可被检测并缓解。
程序中追踪的所有信息(变量值、已调用的函数、当前执行点)均属于程序状态program state。
调试器debugger是允许程序员控制程序执行方式并检查运行中程序状态的工具。集成调试器integrated debugger是指与代码编辑器集成的调试工具。
单步执行Stepping调试是一组相关功能的统称,可实现逐条语句执行代码。
步入Step into:执行程序正常执行路径中的下一条语句后暂停。若该语句包含函数调用,则跳转至被调用函数的顶部。
步过Step over:执行程序正常执行路径中的下一条语句后暂停。若该语句包含函数调用,则执行函数并在函数执行完毕后将控制权交还给您。
步出Step out执行当前函数中所有剩余代码,待函数返回后将控制权交还给您。
运行至光标Run to cursor执行程序直至到达鼠标光标选定的位置。
继续Continue执行程序直至程序终止或遇到断点。开始Start与继续功能相同,只是从程序开头开始执行。
断点breakpoint是一种特殊标记,指示调试器在到达该标记时暂停程序执行。
设置下一个语句set next statement命令可将执行点切换至其他语句(非正式称为跳转)。此功能可用于向前跳转以跳过本应执行的代码段,或向后跳转使已执行内容重新运行。
监视变量Watching a variable功能可在调试模式下实时查看变量值。监视窗口watch window支持检查变量或表达式的数值。
调用栈call stack是记录所有已执行函数的列表,这些函数共同构成了当前执行点。调用栈窗口call stack window作为调试器组件,专门展示该调用栈信息。
测验时间
问题 #1
以下程序本应执行两个数字的加法运算,但运行结果不正确。
请使用集成调试器逐步执行该程序,并观察变量 x 的值。根据获取的信息,修正下列程序:
#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;
}
显示答案
此处的主要问题在于主函数的第二行——readNumber函数的返回值未被赋值给任何变量,因此被丢弃。次要问题是readNumber函数使用了参数调用,而它本应采用局部变量形式。
#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。根据调试过程中发现的问题,修正以下程序:
#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,导致出现除以零的情况,从而引发未定义行为。
#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.2节——算术运算符中对此进行更详细的讨论。
问题 #3
当执行点位于第 4 行时,下列程序的调用堆栈呈现何种形态?本练习仅需列出函数名称,无需标注返回点的行号。
我们在第 3.9 课——使用集成调试器:调用栈中讨论过调用堆栈。
#include <iostream>
void d()
{ // here
}
void c()
{
}
void b()
{
c();
d();
}
void a()
{
b();
}
int main()
{
a();
return 0;
}
显示解答
d
b
a
main
问题 #4
额外加分题:以下程序本应执行两个数字的加法运算,但运行不正确。
使用集成调试器逐步执行该程序。输入参数为 8 和 4。根据调试过程中发现的问题,修正以下程序:
#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时,它同样未被存储为4,而是被存储为52。因此readNumber()函数返回的结果是56和52,而非预期的8和4。
解决方案是将第6行的数据类型从char改为int。
作者注
鉴于目前所涵盖的内容有限,很难找到简单程序中存在不易察觉的调试问题的良好示例。各位读者是否有任何建议?

浙公网安备 33010602011771号