文章目录
- 语法和语义错误
- 调试(Debugging)
- 调试过程
- 调试策略
- 常用的调试策略
- 更多调试策略
- 使用集成调试器
- 步进(Stepping)
语法和语义错误
语法错误
编写根据 C++ 语言的语法无效的语句时,会发生语法错误。例如缺少分号、使用未声明的变量、括号或大括号不匹配等。
编译器通常会捕获语法错误并生成警告或错误。
语义错误
当语句在语法上有效,但没有执行时,就会发生语义错误。
有时这些会导致程序崩溃,例如:
#include <iostream>
int main()
{
int a { 10 };
int b { 0 };
//1.在除以零的情况下
std::cout << a << " / " << b << " = " << a / b << '\n';
//2.没有提供初始化设定项
int x; // no initializer provided
std::cout << x << '\n';
return 0;
}
现代编译器在检测某些类型的常见语义错误(例如,使用未初始化的变量)方面做得越来越好。
但是,在大多数情况下,编译器将无法捕获大多数此类问题,因为编译器旨在强制执行语法,而不是意图。
调试(Debugging)
调试过程
调试的一般方法,确定问题后,调试问题通常包括五个步骤:
- 找到根本原因
- 了解问题
- 确定修复
- 修复问题
- 复检
示例:
#include <iostream>
int add(int x, int y) // 这个函数应该执行加法
{
return x - y; // 但使用了错误的运算符
}
int main()
{
std::cout << "5 + 3 = " << add(5, 3) << '\n'; // 结果应该是8,但是2
return 0;
}
按照调试问题通常包括五个步骤进行调试:
- 找到根本原因:在第 10 行,我们可以看到我们正在传递参数(5 和 3)的文本,因此没有出错的余地。由于函数 add 的输入是正确的,但输出不正确,因此很明显,函数添加一定产生了错误的值。
- 了解问题:在这种情况下,很明显为什么会生成错误的值 ,因为我们使用了错误的运算符。
- 确定修复:我们只需将 operator
-
更改为 operator+
。 - 修复问题 :将 operator- 更改为 operator+ 并确保程序重新编译。
- 复检:实现更改后,重新运行程序将指示我们的程序现在生成正确的值 8。
此示例微不足道,但说明了诊断任何程序时将经历的基本过程。
调试策略
在调试程序时,在大多数情况下,您的绝大多数时间将花费在试图找到错误的实际位置上。
发现问题后,其余步骤(修复问题并验证问题是否已解决)相比之下通常是微不足道的。
两种找错误实际位置方式:
通过代码检查发现问题
int main()
{
getNames(); // 要求用户输入一堆名字
sortNames(); // 把它们按字母顺序排序
printNames(); // 打印已排序的名称列表
return 0;
}
如果希望此程序按字母顺序打印名称,但它以相反的顺序打印它们,则问题可能出在 sortNames 函数中。如果可以将问题范围缩小到特定函数,则只需查看代码即可发现问题。
通过运行程序来查找问题
观察程序运行时的行为,并尝试从中诊断问题。
这种方法可以概括为:
1、弄清楚如何重现问题;
2、运行程序并收集信息以缩小问题所在;
3、重复上述步骤,直到找到问题;
常用的调试策略
调试策略 #1:注释掉代码
如果程序表现出错误行为,减少必须搜索的代码量的一种方法是注释掉一些代码并查看问题是否仍然存在。
int main()
{
getNames();
// doMaintenance();
sortNames();
printNames();
return 0;
}
调试策略 #2:验证代码流
在更复杂的程序中常见的另一个问题是程序调用函数的次数过多或过少(包括根本不调用)。
在这种情况下,将语句放在函数的顶部以打印函数的名称会很有帮助。这样,当程序运行时,您可以看到正在调用哪些函数。
#include <iostream>
int getValue()
{
return 4;
}
int main()
{
std::cout << getValue << '\n';
return 0;
}
尽管我们希望此程序打印值 4,但它应该打印值1。
这时,添加临时调试语句,可以确认 getValue()韩式是否被调用。
#include <iostream>
int getValue()
{
std::cerr << "getValue() called\n";
return 4;
}
int main()
{
std::cerr << "main() called\n";
std::cout << getValue << '\n';
return 0;
}
现在我们可以看到函数 getValue 从未被调用过。
修改后:
#include <iostream>
int getValue()
{
std::cerr << "getValue() called\n";
return 4;
}
int main()
{
std::cerr << "main() called\n";
std::cout << getValue() << '\n';
return 0;
}
现在,这将产生正确的输出:
调试策略 #3:打印值
对于某些类型的错误,程序可能会计算或传递错误的值。
我们还可以输出变量(包括参数)或表达式的值,以确保它们是正确的。
int add(int x, int y)
{
std::cerr << "add() called (x=" << x << ", y=" << y << ")\n";
return x + y;
}
int main()
{
int z{ add(x, y) };
std::cerr << "main::z = " << z << '\n';
printResult(z);
return 0;
}
打印语句进行调试劣势
- 调试语句会使代码混乱。
- 调试语句使程序的输出混乱。
- 调试语句需要修改代码以添加和删除代码,这可能会引入新的错误。
- 调试语句完成后必须将其删除,这使得它们不可重用。
更多调试策略
对调试代码进行条件化
完成调试语句后,需要删除它们,或者注释掉它们。然后,如果您稍后再次想要它们,则必须重新添加它们,或者取消注释它们。
#include <iostream>
#define ENABLE_DEBUG // 注释掉以关闭调试
int getUserInput()
{
#ifdef ENABLE_DEBUG
std::cerr << "getUserInput() called\n";
#endif
std::cout << "Enter a number: ";
int x{};
std::cin >> x;
return x;
}
int main()
{
#ifdef ENABLE_DEBUG
std::cerr << "main() called\n";
#endif
int x{ getUserInput() };
std::cout << "You entered: " << x << '\n';
return 0;
}
使用日志
日志
是已发生事件的连续记录,通常带有时间戳。
生成日志的过程称为日志记录。通常,日志会写入磁盘上的文件(称为日志文件),以便以后可以查看。
日志文件的优点:
1、由于写入日志文件的信息与程序的输出是分开的,因此可以避免因混合正常输出和调试输出而导致的混乱。
2、日志文件也可以很容易地发送给其他人进行诊断。
使用集成调试器
步进(Stepping)
调试器
调试器
是一种计算机程序,它允许程序员控制另一个程序的执行方式,并在该程序运行时检查程序状态。
例如,程序员可以使用调试器逐行执行程序,并在此过程中检查变量的值。通过将变量的实际值与预期值进行比较,或通过代码观察执行路径,调试器可以极大地帮助跟踪语义(逻辑)错误。
调试器
背后的功能是双重的:精确控制程序执行的能力,以及查看(并根据需要修改)程序状态的能力。
在VSCode中配置调试器
-
请按 Ctrl+Shift+P 并选择“C/C++:添加调试配置;
- 然后选择“C/C++:g++ 生成和调试活动文件,这应该创建并打开配置文件;
-
将“stopAtEntry”更改为 true: