中断模式下的调试

  调试技术的剩余内容在中断模式下工作。可以通过几种方式进入这种模式,这些方式都会以某种方式暂停程序的执行。

  1. 进入中断模式

  进入中断模式的最简单方式是在运行应用程序时,单击IDE中的 全部中断 按钮。这个 全部中断 按钮在 调试 工具栏上,应把该工具栏添加到VS默认显示的工具栏中。为此,右击工具栏区域,然后选择 调试,这个工具栏 如图 7-6 所示

图7-6图 7-6

  在这个工具栏上,前 3 个按钮可以手工控制中断。在 图 7-6 上,它们显示为灰色,因为在程序没有运行时,它们是不能工作的。在后面的章节需要其他按钮时,再介绍它们。

  运行一个应用程序时,工具栏就 如图 7-7 所示

图7-7图 7-7

  现在,就可以使用之前显示为灰色的 3 个按钮了。它们可以:

   ● 暂停应用程序的执行,进入中断模式

   ● 完全停止应用程序的执行(不进入中断模式,而是退出应用程序)

   ● 重新启动应用程序

  暂停应用程序是进入中断模式的最简单方式,但这并不能更好地控制停止程序运行的位置。我们可能会停止在应用程序正常暂停的地方,例如,要求用户输入信息。还可以在长时间的操作或循环过程中进入中断模式,但停止的位置可能相当随机。一般情况下,最好使用断点。

   断点

  断点是源代码中自动进入中断模式的一个标记。它们可以配置为:

   ● 在遇到断点时,立即进入中断模式

   ● 在遇到断点时,如果布尔表达式的值为 true,就进入中断模式

   ● 遇到某断点一定的次数后,进入中断模式

   ● 在遇到断点时,如果自从上次遇到断点以来变量的值发生了变化,就进入中断模式

   ● 把文本输出到 输出 窗口中,或者执行一个宏(参见 “跟踪点”小节)

  注意 ⚠️,上述功能仅能用于调试程序。如果编译发布程序,将忽略所有断点。

  添加断点有几种方法。要添加简单断点,当遇到该断点所在的代码行时,就中断执行,可以单击该代码行左边的灰色区域。其他方法包括:右击该行代码行,选择 断点 | 插入断点 菜单项;或者按下 F9 键。

  断点在代码行的旁边显示为一个红色圆圈,而该行代码也突出显示,如图 7-8 所示

图7-8图 7-8

  使用 Breakpoints 窗口还可以查看文件中的断点信息(前面介绍过启用该窗口的方法)。在 Breakpoints 窗口中,可以禁用断点(删除描述信息左边的记号;禁用的断点用未填充的红色圆圈来表示),删除断点,编辑断点的属性。还可以为断点添加标签,这是分组选定断点的一种便捷方式。可以在 Labels 列中查看标签,以及按标签筛选 Breakpoints 窗口中的项。

  这个窗口中显示的 条件命中次数 菜单项,就可以编辑它们。

  选择 条件 将弹出一个对话框。在该对话框中可以键入任意布尔表达式,该表达式可以包含在断点位置仍在作用域内的任何变量。例如,可以配置一个断点,输入表达式 maxVal > 4,选择 Is true 选项,则在遇到这个断点,且 maxVal 的值大于 4 时,就会触发该断点。还可以检查这个表达式时否有变化,仅当发生变化时,才会出发断点(例如,如果在遇到断点时,maxVal 的值从 2 改为 6,就会触发该断点)。

  选择 命中次数 将弹出另一个对话框。在这个对话框中可以指定在遇到断点多少次后才触发该断点。该对话框中的下拉列表提供了如下选项:

   ● 总是中断

   ● 中断,条件是命中次数等于

   ● 中断,条件时命中次数几倍于

   ● 中断,条件是命中次数大于或等于

  所选的选项与选项旁边的文本框中输入的值共同确定断点的行为。这个计数在比较长的循环中很有用,例如,在执行了前 5000 次循环后需要中断。如果不这么做,中断并再启动 5000 次是很痛苦的。

  带有附加属性集(例如,条件或遇到断点的次数)的断点在显示时与普通断点略有区别。已配置的断点不是显示一个简单的红色圆圈,而在红色的圆圈中有一个白色加好。这是很有用的,因为它允许很快辨认出哪个断点总是进入中断模式,哪个断点仅在某种情况下才进入中断模式。

  进入中断模式的其他方式

  进入中断模式还有两种方式。一种是在抛出一个未处理的异常时选择进入该模式。这种方式在本章后面讨论到错误处理时论述。另一种方式是在生成一个判定语句时中断。

  判定语句是可以用用户定义的消息中断应用程序的指令。它们常常用于应用程序的开发过程,作为测试程序时否能平滑运行的一种方式。例如,在应用程序的某一处要求给定的变量值小于 10,此时就可以使用一个判定语句,确定它是否为 true,如果不是,就中断程序的执行。当遇到判定语句时,可以选择 终止,中断应用程序的执行,也可以选择 重试,进入中断模式,还可以选择 忽略,让应用程序像往常一样继续执行。

  与前面的调试输出函数一样,判定函数也有两个版本:

   ● Debug.Assert()

   ● Trace.Assert()

  其调试版本也是仅用于编译调试程序。

  这两个函数带 3 个参数。第一个参数是一个布尔值,其值为 false 会触发判定语句。第二、三个参数是两个字符串,分别把信息写到弹出的对话框和 输出 窗口中。上面的示例需要一个函数调用,如下所示:

  1. Debug.Assert(myVar < 10, "myVar is 10 or greater.", "Assertion occurred in Main().");

  判定语句通常在应用程序的早期使用比较有效。可以分发应用程序的一个发布程序,其中包含 Trace.Assert() 函数,以了解应用程序的运行情况。如果触发了判定语句,用户就会收到通知,把这些消息传递给开发人员。这样,即使开发人员不知道错误是如何发生的,也可以改正这个错误。

  例如,在第一个字符串中提供有关错误的简短描述,在第二个字符串中提供下一步该如何操作的指示:

  1. Trace.Assert(myVar < 10, "Variable out of bounds.",
  2. "Please contact vendor with the error code KCW001.");

  如果出发了这个判定语句,用户就会看到 如图 7-9 所示的对话框。

  诚然,这并不是最友好的对话框,因为它包含了许多令人感到迷惑的信息,但如果用户给开发人员发送了错误的屏幕图,开发人员就可以很快找出问题所在。

  下一个要论述的主题是应用程序中断,以及进入中断模式后,我们可以做什么。一般情况下,进入中断模式的目的是找出代码中的错误(或确信程序工作正常)。一旦进入中断模式,就可以使用各种技巧分析代码,并分析应用程序在暂停时的状态。

图7-9图 7-9

  2. 监视变量的内容

  监视变量的内容是VS帮助我们使工作变得简单的一个例子。查看变量值的最简单方式是在中断模式下,使鼠标指向源代码中的变量名,此时就会出现一个工具提示,显示该变量的信息,其中包括该变量的当前值。

  还可以高亮显示整个表达式,以相同方式得到该表达式的结果。对于比较复杂的值(例如数组),甚至可以扩展工具提示中的值,查看各个数组元素项。甚至可以把这些工具提示窗口固定到代码视图中,这对于查看特别感兴趣的变量很有帮助。固定的工具提示中添加注释,移动工具提示窗口,查看变量的最后一个值,即使应用程序并没有运行也同样如此。

  注意 ⚠️,在运行应用程序时,IDE中各个窗口的布局发生了变化。在默认情况下,运行期间会发生如下变化(变化的情况会根据具体的安装略有区别):

   ● 属性窗口和其他一些窗口会消失,其中可能包括 解决方法资源管理器 窗口

   ● 错误列表窗口会被IDE窗口底部的两个新窗口替代

   ● 新窗口中会出现几个新的选项卡

  新的屏幕布局 如图 7-10 所示。这可能与读者的显示情况不完全相同,一些选项卡和窗口可能不完全匹配。但是,这些窗口的功能(后面将讨论)是相同的,这个显示完全可以通过 视图调试 | 窗口 菜单来定制(在中断模式下),也可以在屏幕上拖动窗口,重新设定它们的位置。

  左下角的新窗口在调试时非常有用,它允许在中断模式下,密切监视应用程序的变量值。它包含 3 个选项卡,如下所示:

   ● 自动窗口 — 当前和前面的语句使用的变量( Ctrl + D, A

   ● 局部变量 — 在作用域内的所有变量( Ctrl + D, L

   ● 监视 N — 可定制的变量和表达式显示(其中 N 从 1~4,在 调试 | 窗口 | 监视 上 )

图 7-10图 7-10

  这些选项卡的工作方式或多或少有些类似,并根据它们的特定功能添加了各种附加特性。一般情况下,每个选项卡都包含一个变量列表,其中包括变量的名称、值和类型等信息。更复杂的变量(如数组)可以使用变量名左边的 +-(展开/折叠)符号进一步查看,它们的内容可以树状视图的方式显示。例如,在前面的示例中,在代码中放置了一个断点,得到的 局部变量 选项卡 如图 7-11所示,其中显示了数组变量 Integres 的展开视图。

图 7-11图 7-11

  在这个视图中,还可以编辑变量的内容。它有效地绕过了前面代码中的其他变量赋值。为此,只需在 Value 列中为要编辑的变量输入一个新值即可。也可以将这种技巧用于其他情况,例如,需要修改代码才能编辑变量值的情况。

  可通过 监视 窗口监视特定变量或涉及特定变量的表达式。要使用这个窗口,只需在 Name 列中键入变量名或表达式,就可以查看它们的结果。注意,并不是应用程序中的所有变量在任何时候都在作用域内,并在 监视 窗口中对变量做出标记。例如,图 7-12 显示了一个 监视 窗口,其中包含几个示例变量和表达式,在遇到 Maxima() 函数末尾前面的一个断点时,会显示这个 监视 窗口。

图 7-12图 7-12

  testArray 数组对于 Main() 来说是局部数组,所以在该图中没有值,而是显示了一个信息,告诉我们这个变量不在作用域内。

  要在 监视 窗口中添加变量,还可以把变量从源代码拖动到该窗口中。

  在这个窗口中可以访问变量的各种显示结果,一个优点是它们可以显示变量在断点之间的变化情况。新值显示为红色而不是黑色,所以很容易看出哪个值发生了变化。

  如前说述,要添加更多 监视 窗口,可以在中断模式下,使用 调试 | 窗口 | 监视 菜单选项打开或关闭 监视 的 4 个窗口。每个窗口都可以包含变量和表达式的一组观察结果,所以可以把相关的变量组合在一起,以便于访问。

  除了这些 监视 窗口外,VS还有一个 快速监视 窗口,它能快速提供源代码中某个变量的详细信息。要使用这个窗口,可以右击要查看的变量,选择 快速监视 菜单选项。但大多数情况下,使用标准的 监视窗口 或者像前面介绍的那样固定变量的工具提示就足够了。

  监视窗口 可以在应用程序的各个执行过程之间保留下来。如果中断应用程序,再重新运行,就不必再次添加 监视窗口 了,IDE会记住上次使用的 监视窗口

   3. 单步执行代码

  前面介绍了如何在中断模式下查看应用程序的运行情况,下面讨论如何在中断模式下使用IDE单步执行代码,查看代码的准确执行结果,人们的思维速度不会比计算机运行得更快,所以这是一个极有价值的技巧。

  VS进入中断模式后,在代码视图的左边,马上要执行的代码旁边会出现一个黄色箭头光标(如果使用断点进入中断模式,该光标最初应显示在断点的红色圆圈中),如图 7-13 所示

图 7-13图 7-13

  这显示了在进入中断模式时程序执行到的位置。在这个位置上,可以选择逐行执行。为此,使用前面看到的其他一些 调试 工具栏按钮,如图 7-14 所示

图 7-14图 7-14

  第 6、7、8 个图标控制了中断模式下的程序流。它们依次是:

   ● 逐语句 — 执行并移动到下一条要执行的语句上

   ● 逐过程 — 同上,但不进去嵌套的代码块,包括函数

   ● 跳出 — 执行到代码块的末尾处,在执行完该语句块后,重新进入中断模式

  如果要查看应用程序执行的每个操作,可以使用 逐语句 按顺序执行指令,这包括在函数中执行,如上面示例中的 Maxima()。当光标到达第 15 行,调用 Maxima() 时,单击这个图标,会使光标移动到 Maxima() 时,单击这个图标,会使光标移动到 Maxima() 函数内部的第一行代码上。而如果光标移动到第 15 行时单击 逐过程,就会使光标移动到第 16 行,不进入 Maxima() 中的代码(但仍执行这段代码)。如果单步执行到不敢兴趣的函数,可以点击 跳出,返回到调用该函数的代码。在单步执行代码时,变量的值可能会发生变化。注意观察上一节讨论的 监视 窗口,可以看到变量值的变化情况。

  通过右击代码行并选择 设置下一语句(X),或将黄色箭头拖动到不同的代码行,也可以更改接下来要执行的代码行。这有时是不可行的,例如跳过变量初始化时。但是,当跳过存在问题的代码行来查看发生的情况时,或向后移动箭头来重复执行代码时,这种方法是非常有用的。

  在存在语义错误的代码中,这些技巧也许是最有效的。可以单步执行代码,当执行到有错误的代码时,错误会像正常运行程序那样发生。或者可以修改执行代码,让语句多次执行。在这个过程中,可以监视数据,看看什么地方出错。本章后面将使用这个技巧查看示例应用程序的执行情况。

  4. 即时窗口和命令窗口

  命令窗口即时窗口 (在 调试 窗口菜单下)可以在运行应用程序的过程中执行命令。通过 命令窗口 可以手动执行VS操作(例如,菜单和工具栏操作),即时窗口 可以执行与当前正在执行的源代码不同的额外代码,以及计算表达式。

  VS中的这些窗口在内部是链接在一起的(实际上,VS的早期版本把它们当作同一个窗口)。甚至可以在它们之间切换:输入命令 immed,可以从 命令窗口 切换到 即时窗口;输入 cmd 可以从 即时窗口 切换到 命令窗口

  下面详细讨论 即时窗口,因为 命令窗口 仅适用于复杂的操作。即时窗口 最简单的用法是计算表达式,有点像 监视 窗口中的一次性使用。为此,只需键入一个表达式,并按回车键即可。接着就会显示请求的信息,如图 7-15 所示

图 7-15图 7-15

  可以在这里修改变量的内容,如图 7-16 所示

图 7-16图 7-16

  在大多数情况下,使用前面介绍的变量监视窗口更容易得到相同的效果,但这个技巧对于调整变量值和测试表达式很方便。

  5. 调用堆栈窗口

  这是最后一个要讨论的窗口,它描述了程序是如何执行到当前位置的。简言之,该窗口显示了当前函数、调用它的函数以及调用该函数的函数(即一个嵌套的函数调用列表)。调用的确切位置也被记录下来。

  在前面的示例中,在执行到 Maxima() 时进入中断模式,或者使用代码单步执行功能移动到这个函数的内部,得到 如图 7-17 所示的信息。

图 7-17图 7-17

  如果双击某一项,就会移动到相应的位置,跟踪代码执行到当前位置的过程。在第一次检测错误时,这个窗口非常有用,因为它们可以查看临近发生时的情况。对于常用函数中出现的错误,则有助于找到错误的源头。

  有时 调用堆栈 窗口会显示一些非常杂乱的信息,例如,有时因为以错误方式使用了外部函数,错误在应用程序的外部发生,就会出现这种情况。此时,这个窗口中会列出一个很长的列表,其中只有一两项是我们熟悉的。如有必要,可以右击该窗口,选择 转到源代码,来查看外部引用。