调试的概念
在编写代码的过程中,相信大家肯定遇到过这样的情况:代码能够编译通过,没有语法错误,但是运行结果却不对,反复检查了很多遍,依然不知道哪里出了问题。这个时候,就需要调试程序了。
所谓调试(Debug),就是让代码一步一步慢慢执行,跟踪程序的运行过程。比如,可以让程序停在某个地方,查看当前所有变量的值,或者内存中的数据;也可以让程序一次只执行一条或者几条语句,看看程序到底执行了哪些代码。
在调试的过程中,我们可以监控程序的每一个细节,包括变量的值、函数的调用过程、内存中的数据、线程的调度等,从而发现隐藏的错误或者低效的代码。
编译器可以发现程序的语法错误,调试可以发现程序的逻辑错误。所谓逻辑错误,是指代码思路或者设计上的缺陷。
对于初学者来说,学习调试也可以增加编程的功力,它能让我们更加了解自己的程序,比如变量是什么时候赋值的、内存是什么时候分配的,从而弥补学习的纰漏。
调试器
调试需要借助专业的辅助软件-调试器。主流的C/C++调试器有下面几种:
Remote Debugger、WinDbg、LLDB、GDB。
设置断点,开始调试
默认情况下,程序不会进入调试模式,代码会瞬间从开头执行到末尾。要想观察程序的内部细节,就得让程序在某个地方停下来,我们可以在这个地方设置断点。
所谓断点(BreakPoint),可以理解为障碍物,人遇到障碍物不能行走,程序遇到断点就暂停执行。
调用堆栈可以看到当前函数的调用关系。
断点窗口可以看到当前设置的所有断点。
即时窗口可以让我们临时运行一段代码。
输出窗口用来显示程序的运行过程,给出错误信息和警告信息。
自动窗口会显示当前代码行和上一行码行中所使用到的变量。
局部变量窗口会显示当前函数中的所有局部变量。
线程和模块窗口暂时无需理会。
断点的真正含义
严格来说,调试器遇到断点时会把程序暂时挂起,让程序进入一种特殊的状态-中断状态,这种状态下操作系统不会终止程序的执行,也不会清除与程序相关的元素,比如变量、函数等,它们在内存中的位置不会发生变化。
关键是,处于中断状态下的程序允许用户查看和修改它的运行状态,比如查看和修改变量的值、查看和修改内存中的数据、查看函数调用关系等,这就是调试的奥秘。
继续执行程序
点击“运行”按钮或者按F5键即可跳过断点,让程序恢复正常状态,继续执行后面的代码,直到程序结束或者遇到下一个断点。
在调试过程中,按照上面的方法可以设置多个断点,程序在执行过程中每次遇到断点都会暂停。
删除断点
如果不希望程序暂停,可以删除断点。删除断点也很简单,在原有断点处再次单击鼠标即可,也可以将光标定位到要删除断点的代码行,再次按F9键,或者在右键菜单中删除。
代替暂停语句
在VS下,程序运行结束后不会自动暂停(一闪而退),要手动添加暂停语句system("pause"); ,如果大家觉得麻烦,也可以在代码最后插入断点,强制程序暂停。
查看和修改变量的值
设置了断点,就可以观察程序的运行情况了,其中很重要的一点就是查看相关变量的值,这足以发现大部分逻辑错误。
添加监视
如果你希望长时间观测某个变量,还可以将该变量添加到监视窗口。
添加监视之后,每次变量的值被改变都会反映到该窗口中,无需再将鼠标移动到变量上方查看其值。尤其是当程序稍大时,往往需要同时观测多个变量的值,添加监视的方式就会显得非常方便。
单步调试(逐语句调试和逐过程调试)
单步调试,就是让代码一步一步地执行。
逐过程调试(F10)和逐语句调试(F11)都可以用来进行单步调试,但是它们有所区别:
逐过程调试(F10)在遇到函数时,会把函数从整体上看做一条语句,不会进入函数内部;
逐语句调试(F11)在遇到函数时,认为函数由多条语句构成,会进入函数内部。
修改代码运行位置
调试器还允许我们直接跳过一段代码,不去执行它们。
随意修改程序运行位置是非常危险的行为,假设我们定义了一个指针,在第N行代码中让它指向了一个数组,如果我们在修改程序运行位置的时候跳过了第N行代码,并且后面也使用到了该指针,那么就极有可能导致程序崩溃。
即时窗口的使用
“即时窗口”是VS提供的一项非常强大的功能,在调试模式下,我们可以在即时窗口中输入C语言代码并立即运行。
在即时窗口中可以使用代码中的变量,可以输出变量或表达式的值(无需使用printf()函数),也可以修改变量的值。
即时窗口本质上是一个命令解释器,它负责解释我们输入的代码,再由VS中的对应模块执行,最后将输出结果呈现到即时窗口。
需要注意的是,在即时窗口中不能定义新的变量,因为程序运行时 Windows 已经为它分配好了只够刚好使用的内存,定义变量是需要额外分配内存的,所以调试器不允许在程序运行的过程中定义变量,因为这可能会导致不可预知的后果。
调用函数
在即时窗口中除了可以使用代码中的变量,也可以调用代码中的函数。
查看、修改运行时的内存
类型名 | 变量类型 | 内存查看窗口中应选择的数据格式 |
short | 16位整型 | 2字节整数 |
int | 32位整型 | 4字节整数 |
long | 32位整型 | 4字节整数 |
long long | 64位整型 | 8字节整数 |
float | 32位单精度浮点数 | 32位浮点 |
double | 64位双精度浮点数 | 64位浮点 |
assert断言函数
assert 函数的用法很简单,我们只要传入一个表达式即可,它会计算我们传入的表达式的结果,如果为真,则不会有任何操作,但是如果我们传入的表达式的计算结果为假,它就会像 stderr (标准错误输出)打印一条错误信息,然后调用 abort 函数直接终止程序的运行。
错误,是代码编写途中的缺陷导致的,是程序运行中可以用 if 语句检测出来的。而异常在我们的 C 语言中,一般也可以使用 if 语句判断,并进行对应的处理。而 assert 是用来判断我们程序中的代码级别的错误的。像用户输入错误,调用其他函数库错误,这些问题我们都是可以用 if 语句检测处理的。另一方面,使用 if 语句的程序对用户更友好。
在发布版程序中,所有的 assert 语句都会失效,所以不要使用会改变环境的语句作为断言函数的参数,这可能导致实际运行中出现问题。
调试信息的输出
我们可以用一个 Windows 操作系统提供的函数-OutputDebugString,在调试器的调试窗口上输出我们自己的调试信息。这个函数非常常用,它可以向调试输出窗口输出信息(无需设置断点,执行就会输出调试信息),并且一般只在绑定了调试器的情况下才会生效,否则会被 Windows 直接忽略。
首先,这个函数在 windows.h 中被定义,所以我们需要包含 windows.h 这个头文件才能使用 OutputDebugString 函数。这个函数的使用方式非常的简单,它只有简单的一个参数——我们要输出的调试信息。但是有一点值得注意:准确来说 OutputDebugString 并不是一个函数,他是一个宏。