3-x 第三章概要与测验

章节回顾

语法错误syntax error是指编写的语句不符合C++语言语法规则时出现的错误,编译器会捕获此类错误。

语义错误semantic error是指语法正确的语句未能实现程序员预期功能的情况。

查找并消除程序错误的过程称为调试debugging

可采用五步流程进行调试:

  1. 找出根本原因。
  2. 理解问题本质。
  3. 确定修复方案。
  4. 实施修复措施。
  5. 重新测试验证。

在调试过程中,发现错误通常是最困难的部分。

静态分析工具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。

作者注
鉴于目前所涵盖的内容有限,很难找到简单程序中存在不易察觉的调试问题的良好示例。各位读者是否有任何建议?

posted @ 2026-02-12 21:32  游翔  阅读(0)  评论(0)    收藏  举报