7.1 Visual Studio中的调试
前面提到,可以采用两种方式执行应用程序:调试模式或非调试模式。在VS中执行应用程序时,默认在调试模式下执行。例如,按下F5键或单击工具栏中的绿色Start按钮时,就是在调试模式下执行应用程序。要在非调试模式下执行应用程序,应选择Debug | Start Without Debugging,或按下Ctrl+F5键。
VS允许在两种配置下生成应用程序:调试(默认)和发布。使用标准工具栏中的Solution Configurations下拉框可在这两种配置之间切换。
在调试配置下生成应用程序,并在调试模式下运行程序时,并不仅是运行编写好的代码。调试程序包含应用程序的符号信息,所以IDE知道执行每行代码时发生了什么。符号信息意味着跟踪(例如)未编译代码中使用的变量名,这样它们就可以匹配已编译的机器码应用程序中现有的值,而机器码程序不包含便于人们阅读的信息。此类信息包含在.pdb文件中,这些文件位于计算机的Debug目录下。
发布配置会优化应用程序代码,所以我们不能执行以上这些操作。但发布版本运行速度较快。完成了应用程序的开发时,一般应给用户提供发布版本,因为发布版本不需要调试版本所包含的符号信息。
本节介绍调试技巧,以及如何使用它们找出并修改未按预期方式执行的那些代码,这个过程称为调试。按照这些技术的使用方法把它们分为两部分。一般情况下,可以首先中断程序的执行,再进行调试,或者注上标记,以便以后加以分析。在VS术语中,应用程序可以处于运行状态,也可以处于中断模式,即暂停正常的执行。下面首先介绍非中断模式(运行期间或正常执行)技术。
7.1.1 非中断(正常)模式下的调试
本书经常使用的一个命令是WriteLine()函数,它可以把文本输出到控制台。在开发应用程序时,这个函数可以方便地获得操作的额外反馈,例如:
WriteLine("MyFunc() Function about to be called."); MyFunc("Do something."); WriteLine("MyFunc() Function execution completed.");
这段代码说明了如何获取MyFunc()函数的额外信息。这么做完全正确,但控制台的输出结果会比较混乱。在开发其他类型的应用程序时,如桌面应用程序,没有用于输出信息的控制台。作为一种替代方法,可将文本输出到另一个位置——IDE中的Output窗口。
第2章简要介绍了Error List窗口,提到其他窗口也可以显示在这个位置。其中一个窗口就是Output窗口,在调试时这个窗口非常有用。要显示这个窗口,可以选择View | Output。在这个窗口中,可以查看与代码的编译和执行相关的信息,包括在编译过程中遇到的错误等,还可以将自定义的诊断信息直接写到这个窗口中。该窗口如图7-1所示。
图7-1
注意:使用Output窗口的下拉菜单可以选择几种模式:Build、Deployment和Debug。Build和Debug模式分别显示编译和运行期间的信息。本节提到“写入Output窗口”时,实际上是指“写入Output窗口的Debug模式视图”。
另外,还可以创建一个日志文件,在运行应用程序时,会把信息添加到该日志文件中。把信息写入日志文件所用的技巧与把文本写到Output窗口中所用的技巧相同,但需要理解如何从C#应用程序中访问文件系统。我们把这个功能放在后面的章节中加以讨论,因为目前不必了解文件访问技巧也可以完成很多工作。
1.输出调试信息
在运行期间把文本写入Output窗口是非常简单的。只要用需要的调用替代WriteLine()调用,就可以把文本写到希望的地方。此时可以使用如下两个命令:
● Debug.WriteLine()
● Trace.WriteLine()
这两个命令函数的用法几乎完全相同,但有一个重要区别:第一个命令仅在调试模式下运行,而第二个命令还可用于发布程序。实际上,Debug.WriteLine()命令甚至不能编译到可发布的程序中,在发布版本中,该命令会消失,这肯定有其优点(编译好的代码文件比较小)。
注意:Debug.WriteLine()和Trace.WriteLine()方法包含在System.Diagnostics名称空间内。using static指令只能用于静态类,例如包括WriteLine()方法的System.Console。
这两个函数的用法与Console.WriteLine()是不同的。其唯一的字符串参数用于输出消息,而不需要使用{X}语法插入变量值。这意味着必须使用+串联运算符等方式在字符串中插入变量值。它们还可以有第二个字符串参数(可选),用于显示输出文本的类别。这样,如果应用程序的不同地方输出了类似的消息,我们马上可以确定Output窗口中显示的是哪些输出信息。
这些函数的一般输出如下所示:
<category>: <message>
例如,下面的语句把MyFunc作为可选的类别参数:
Debug.WriteLine("Added 1 to i", "MyFunc");
其结果为:
MyFunc: Added 1 to i
下面的示例按这种方式输出调试信息。
试一试:把文本输出到Output窗口:Ch07Ex01\Program.cs
(1)在C:\BegVCSharp\Chapter07目录中创建一个新的控制台应用程序Ch07Ex01。
(2)修改代码,如下所示:
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; using static System.Console; namespace Ch07Ex01 { class Program { static void Main(string[] args) { int[] testArray = {4, 7, 4, 2, 7, 3, 7, 8, 3, 9, 1, 9}; int[] maxValIndices; int maxVal = Maxima(testArray, out maxValIndices); WriteLine($"Maximum value {maxVal} found at element indices:"); foreach (int index in maxValIndices) { WriteLine(index); } ReadKey(); } static int Maxima(int[] integers, out int[] indices) { Debug.WriteLine("Maximum value search started."); indices = new int[1]; int maxVal = integers[0]; indices[0] = 0; int count = 1; Debug.WriteLine(string.Format( $"Maximum value initialized to {maxVal}, at element index 0.")); for (int i = 1; i < integers.Length; i++) { Debug.WriteLine(string.Format( $"Now looking at element at index {i}.")); if (integers[i] > maxVal) { maxVal = integers[i]; count = 1; indices = new int[1]; indices[0] = i; Debug.WriteLine(string.Format( $"New maximum found. New value is {maxVal}, at element index {i}.")); } else { if (integers[i] == maxVal) { count++; int[] oldIndices = indices; indices = new int[count]; oldIndices.CopyTo(indices, 0); indices[count - 1] = i; Debug.WriteLine(string.Format( $"Duplicate maximum found at element index {i}.")); } } } Trace.WriteLine(string.Format( $"Maximum value {maxVal} found, with {count} occurrences.")); Debug.WriteLine("Maximum value search completed."); return maxVal; } } }
(3)在Debug模式下执行代码,结果如图7-2所示。
图7-2
(4)中断应用程序的执行,查看Output窗口中的内容(在Debug模式下),如下所示(有删减):
... Maximum value search started. Maximum value initialized to 4, at element index 0. Now looking at element at index 1. New maximum found. New value is 7, at element index 1. Now looking at element at index 2. Now looking at element at index 3. Now looking at element at index 4. Duplicate maximum found at element index 4. Now looking at element at index 5. Now looking at element at index 6. Duplicate maximum found at element index 6. Now looking at element at index 7. New maximum found. New value is 8, at element index 7. Now looking at element at index 8. Now looking at element at index 9. New maximum found. New value is 9, at element index 9. Now looking at element at index 10. Now looking at element at index 11. Duplicate maximum found at element index 11. Maximum value 9 found, with 2 occurrences. Maximum value search completed. The thread #### has exited with code 0 (0x0).
(5)使用标准工具栏上的下拉菜单,切换到Release模式,如图7-3所示。
图7-3
(6)再次运行程序,这次在Release模式下运行,并在执行终止时再查看一下Output窗口,结果如下所示(有删减):
... Maximum value 9 found, with 2 occurrences. The thread #### has exited with code 0 (0x0).
示例说明
这个应用程序是第6章中一个示例的扩展版本,它使用一个函数计算整数数组中的最大值。这个版本也返回一个索引数组,表示最大值在数组中的位置,以便调用代码处理这些元素。
首先在代码开头使用了一个额外的using指令:
using System.Diagnostics;
这简化了前面讨论的对函数的访问,因为它们包含在System.Diagnostics名称空间中,没有这个using语句,下面的代码:
Debug.WriteLine("Bananas");
就需要进一步限定,重新编写这行语句,如下所示:
System.Diagnostics.Debug.WriteLine("Bananas");
Main()中的代码仅初始化一个测试用的整数数组testArray,并声明了另一个整数数组maxValIndices,以存储Maxima()(执行计算的函数)的索引输出结果,接着调用这个函数。函数返回后,代码就会输出结果。
Maxima()稍复杂一些,但用到的代码大部分在前面已经看到过。在数组中进行搜索的方式与第6章的MaxVal()函数类似,但要用一条记录来存储最大值的索引。
特别需要注意用来跟踪索引的函数(而不是输出调试信息的那些代码行)。Maxima()并没有返回一个足以存储源数组中每个索引的数组(需要与源数组有相同的维数),而是返回一个正好能容纳搜索到的索引的数组。这可通过在搜索过程中连续重建不同长度的数组来实现。必须这么做,因为一旦创建好数组,就不能重新设置长度。
开始搜索时,假定源数组(integers)中的第一个元素就是最大值,而且数组中只有一个最大值。因此可以为maxVal(函数的返回值,即搜索到的最大值)和indices(out参数数组,存储搜索到的最大值的索引)设置值。maxVal被赋予integers中第一个元素的值,indices被赋予一个值0,即数组中第一个元素的索引。在变量count中存储搜索到的最大值的个数,以便跟踪indices数组。
函数的主体是一个循环,它迭代integers数组中的各个值,但忽略第一个值,因为它已经处理过这个值。每个值都与maxVal的当前值进行比较,如果maxVal更大,就忽略该值。如果当前处理的值比maxVal大,就修改maxVal和indices,以反映这种情况。如果当前处理的值与maxVal相等,就递增count,用一个新数组替代indices。这个新数组比旧indices数组多一个元素,包含搜索到的新索引。
最后一个功能的代码如下所示:
if (integers[i] == maxVal) { count++; int[] oldIndices = indices; indices = new int[count]; oldIndices.CopyTo(indices, 0); indices[count - 1] = i; Debug.WriteLine(string.Format( $"Duplicate maximum found at element index {i}.")); }
这段代码把旧indices数组备份到if代码块的oldIndices局部整型数组中。注意使用<array>. CopyTo()函数把oldIndices中的值复制到新的indices数组中。这个函数的参数是一个目标数组和一个用于复制第一个元素的索引,并把所有的值都粘贴到目标数组中。
在代码中,各个文本部分都使用Debug.WriteLine()和Trace.WriteLine()函数进行输出,这些函数使用string.Format()函数把变量值嵌套在字符串中,其方式与WriteLine()相同。这比使用+串联运算符更高效。
在Debug模式下运行应用程序时,其最终结果是一条完整记录,它记述了在循环中计算出结果所采取的步骤。在Release模式下,仅能看到计算的最终结果,因为没有调用Debug.WriteLine()函数。
2.跟踪点
另一种把信息输出到Output窗口的方法是使用跟踪点(tracepoint)。这是VS的一个功能,而不是C#的功能,但其作用与使用Debug.WriteLine()相同。它实际上是输出调试信息且不修改代码的一种方式。
为了演示跟踪点,可用它们替代上一个示例中的调试命令(请参阅本章的下载代码中的Ch07Ex01TracePoints文件)。添加跟踪点的过程如下:
(1)把光标放在要插入跟踪点的代码行上。跟踪点会在执行这行代码之前被处理。
(2)右击该行代码,选择Breakpoint | Insert Tracepoint。右击代码行旁边的红圆,选择Settings菜单项。
(3)选中Actions复选框,在Log a message部分的Message文本框中键入要输出的字符串。如果要输出变量值,应把变量名放在花括号中。
(4)单击OK按钮。在包含跟踪点的代码行的左边会出现一个红色菱形,该行代码也会突出显示为红色。
看一下添加跟踪点的对话框标题和所需要的菜单选项,显然,跟踪点是断点的一种形式(可以暂停应用程序的执行,就像断点一样)。断点一般用于更高级的调试目的,本章稍后将介绍断点。
图7-4显示了Ch07Ex01TracePoints中第32行所需的跟踪点。在删除已有的Debug.WriteLine()语句后,对代码行编号。
图7-4
还有一个窗口可用于快速查看应用程序中的跟踪点。要显示这个窗口,可从VS菜单中选择Debug | Windows | Breakpoints。这是显示断点的通用窗口(如前所述,跟踪点是断点的一种形式)。可以定制显示的内容,从这个窗口的Columns下拉框中添加When Hit列,显示与跟踪点关系更密切的信息。图7-5显示的窗口配置了该列,还显示了添加到Ch07Ex01TracePoints中的所有跟踪点。
图7-5
在调试模式下执行这个应用程序,会得到与前面完全相同的结果。在代码窗口中右击跟踪点,或者利用Breakpoints窗口,可以删除或临时禁用跟踪点。在Breakpoints窗口中,跟踪点左边的复选框指示是否启用跟踪点;禁用的跟踪点未被选中,在代码窗口中显示为菱形框,而不是实心菱形。
3.诊断输出与跟踪点
前面介绍了两种输出相同信息的方法,下面分析它们的优缺点。首先,跟踪点与Trace命令并不等价,也就是说,不能使用跟踪点在发布版本中输出信息。这是因为跟踪点并没有包含在应用程序中。跟踪点由VS处理,在应用程序的已编译版本中,跟踪点是不存在的。只有应用程序运行在VS调试器中时,跟踪点才起作用。
跟踪点的主要缺点也是其主要优点,即它们存储在VS中,因此可以在需要时便捷地添加到应用程序中,而且也非常容易删除。如果输出非常复杂的信息字符串,觉得跟踪点非常讨厌,只需单击表示其位置的红色菱形,就可以删除跟踪点。
跟踪点的一个优点是允许方便地添加额外信息,如$FUNCTION会把当前的函数名添加到输出信息中。这个信息可以用Debug和Trace命令来编写,但比较难。总之,输出调试信息的两种方法是:
● 诊断输出:总是要从应用程序中输出调试结果时使用这种方法,尤其是在要输出的字符串比较复杂,涉及几个变量或许多信息的情况下,使用该方法比较好。另外,如果要在执行发布版本的应用程序的过程中进行输出,Trace命令经常是唯一选择。
● 跟踪点:调试应用程序时,如果希望快速输出重要信息,以便消除语义错误,应使用跟踪点。
7.1.2 中断模式下的调试
本章描述的剩余调试技术在中断模式下工作。可以通过几种方式进入这种模式,这些方式都会以某种方式暂停程序的执行。
1.进入中断模式
进入中断模式的最简单方式是在运行应用程序时,单击IDE中的Pause按钮。这个Pause按钮在Debug工具栏上,你应把该工具栏添加到VS默认显示的工具栏中。为此,右击工具栏区域,然后选择Debug,这个工具栏如图7-6所示。
在这个工具栏上,前3个按钮可以手工控制中断。在图7-6上,它们显示为灰色,因为在程序没有运行时,它们是不能工作的。在后面的章节需要其他按钮时,再介绍它们。
运行一个应用程序时,工具栏如图7-7所示。
图7-6
图7-7
现在,就可以使用之前显示为灰色的3个按钮了。它们可以:
● 暂停应用程序的执行,进入中断模式
● 完全停止应用程序的执行(不进入中断模式,而是退出应用程序)
● 重新启动应用程序
暂停应用程序是进入中断模式的最简单方式,但这并不能更好地控制停止程序运行的位置。我们可能会停止在应用程序正常暂停的地方,例如,要求用户输入信息。还可以在长时间的操作或循环过程中进入中断模式,但停止的位置可能相当随机。一般情况下,最好使用断点。
断点
断点是源代码中自动进入中断模式的标记。它们可以配置为:
● 遇到断点时,立即进入中断模式
● 遇到断点时,如果布尔表达式的值为true,就进入中断模式
● 遇到某断点一定的次数后,进入中断模式
● 在遇到断点时,如果自从上次遇到断点以来变量的值发生了变化,就进入中断模式
注意,上述功能仅能用于调试程序。如果编译发布程序,将忽略所有断点。
添加断点有几种方法。要添加简单断点,当遇到该断点所在的代码行时,就中断执行,可以单击该代码行左边的灰色区域。其他方法包括:右击该代码行,选择Breakpoint | Insert Breakpoint菜单项;选择Debug | Toggle Breakpoint;或者按下F9键。
断点在代码行的旁边显示为一个红色圆圈,而该行代码也突出显示,如图7-8所示。
图7-8
使用Breakpoints窗口还可以查看文件中的断点信息(前面介绍过启用该窗口的方法)。在Breakpoints窗口中,可以禁用断点(删除描述信息左边的记号;禁用的断点用未填充的红色圆圈来表示)、删除断点、编辑断点的属性。还可以为断点添加标签,这是分组选定断点的一种便捷方式。可以在Labels列中查看标签,以及按标签过滤Breakpoints窗口中的项。
这个窗口中显示的Condition和Hit Count列是最有用的两个列。右击断点(在代码或Breakpoints窗口中),选择Condition或Hit Count菜单项,就可以编辑它们。
选择Condition将弹出一个对话框。在该对话框中可以键入任意布尔表达式,该表达式可以包含在断点位置仍在作用域内的任何变量。例如,可配置一个断点,输入表达式maxVal>4,选择Is true选项,在遇到这个断点且maxVal的值大于4时,就会触发该断点。还可以检查这个表达式是否有变化,仅当发生变化时,才会触发断点(例如,如果在遇到断点时,maxVal的值从2改为6,就会触发该断点)。
选择Hit Count将弹出另一个对话框。在这个对话框中可以指定在遇到断点多少次后才触发该断点。该对话框中的下拉列表提供了如下选项:
● 总是中断
● 在Hit Count等于多少次时中断
● 在Hit Count是某个数的倍数时中断
● 在Hit Count大于等于多少次时中断
所选的选项与在选项旁边的文本框中输入的值共同确定断点的行为。这个计数在比较长的循环中很有用,例如,在执行了前5000次循环后需要中断。如果不这么做,中断并重启5000次是很痛苦的。
进入中断模式的其他方式
进入中断模式还有两种方式。一种是在抛出一个未处理的异常时选择进入该模式。这种方式在本章后面讨论到错误处理时论述。另一种方式是在生成一条判定语句(assertion)时中断。
判定语句是可以用用户定义的消息中断应用程序的指令。它们常常用于应用程序的开发过程,作为测试程序能否平滑运行的一种方式。例如,在应用程序的某一处要求给定的变量值小于10,此时就可以使用一条判定语句,确定它是否为true,如果不是,就中断程序的执行。当遇到判定语句时,可以选择Abort,终止应用程序的执行;也可以选择Retry,进入中断模式;还可以选择Ignore,让应用程序像往常一样继续执行。
与前面的调试输出函数一样,判定函数也有两个版本:
● Debug.Assert()
● Trace.Assert()
其调试版本也是仅用于编译调试程序。
这两个函数带3个参数。第一个参数是一个布尔值,其值为false会触发判定语句。第二、第三个参数是两个字符串,分别把信息写到弹出的对话框和Output窗口中。上面的示例需要一个函数调用,如下所示:
Debug.Assert(myVar < 10, "myVar is 10 or greater.", "Assertion occurred in Main().");
判定语句通常在应用程序的早期使用比较有效。可以分发应用程序的一个发布程序,其中包含Trace.Assert()函数,以了解应用程序的运行情况。如果触发了判定语句,用户就会收到通知,把这些消息传递给开发人员。这样,即使开发人员不知道错误是如何发生的,也可以改正这个错误。
例如,在第一个字符串中提供有关错误的简短描述,在第二个字符串中提供下一步该如何操作的指示:
Trace.Assert(myVar < 10, "Variable out of bounds.", "Please contact vendor with the error code KCW001.");
如果触发了这条判定语句,用户将看到如图7-9所示的对话框。
图7-9
诚然,这并不是最友好的对话框,因为它包含了许多令人感到迷惑的信息。但如果用户给开发人员发送了错误的屏幕图,开发人员就可以很快找出问题所在。
下一个要论述的主题是应用程序中断,以及进入中断模式后,我们可以做什么。一般情况下,进入中断模式的目的是找出代码中的错误(或确信程序工作正常)。一旦进入中断模式,就可以使用各种技巧分析代码,并分析应用程序在暂停时的状态。
2.监视变量的内容
监视变量的内容是VS帮助我们使工作变得简单的一个例子。查看变量值的最简单方式是在中断模式下,使鼠标指向源代码中的变量名,此时会出现一个工具提示,显示该变量的信息,其中包括该变量的当前值。
还可以高亮显示整个表达式,以相同方式得到该表达式的结果。对于比较复杂的值(例如数组),甚至可以扩展工具提示中的值,查看各个数组元素项。
甚至可以把这些工具提示窗口固定到代码视图中,这对于查看特别感兴趣的变量很有帮助。固定的工具提示会一直显示,所以即使在停止并重启调试后,仍然可以看到它们。甚至可以在固定的工具提示中添加注释,移动工具提示窗口,查看变量的最后一个值,即使应用程序并没有运行也同样如此。
注意,在运行应用程序时,IDE中各个窗口的布局发生了变化。默认情况下,运行期间会发生如下变化(变化的情况因具体的安装而异):
● Properties窗口和其他一些窗口会消失,其中可能包括Solution Explorer窗口
● Error List窗口会被IDE窗口底部的两个新窗口替代
● 新窗口中会出现几个新的选项卡
新的屏幕布局如图7-10所示。这可能与读者的显示情况不完全相同,一些选项卡和窗口可能不完全匹配。但是,这些窗口的功能(后面将讨论)是相同的,这个显示完全可以通过View和Debug |Windows菜单来定制(在中断模式下),也可以在屏幕上拖动窗口,重新设定它们的位置。
图7-10
左下角的新窗口在调试时非常有用,它允许在中断模式下,密切监视应用程序的变量值。它包含3个选项卡,如下所示:
● Autos—— 当前和前面的语句使用的变量(Ctrl+D, A)
● Locals—— 作用域内的所有变量(Ctrl+D, L)
● Watch N—— 可定制的变量和表达式显示(其中N为1-4的值,在Debug | Windows | Watch上)
这些选项卡的工作方式或多或少有些类似,并根据它们的特定功能添加了各种附加特性。一般情况下,每个选项卡都包含一个变量列表,其中包括变量的名称、值和类型等信息。更复杂的变量(如数组)可以使用变量名左边的+和-(展开/折叠)符号进一步查看,它们的内容可以树状视图的方式显示。例如,在前面的示例中,在代码中放置了一个断点,得到的Locals选项卡如图7-11所示,其中显示了数组变量maxValIndices的展开视图。
图7-11
在这个视图中,还可以编辑变量的内容。它有效地绕过了前面代码中的其他变量赋值。为此,只需要在Value列中为要编辑的变量输入一个新值即可。也可以将这种技巧用于其他情况,例如,需要修改代码才能编辑变量值的情况。
可通过Watch窗口监视特定变量或涉及特定变量的表达式。要使用这个窗口,只需在Name列中键入变量名或表达式,就可以查看它们的结果。注意,并不是应用程序中的所有变量在任何时候都在作用域内,并在Watch窗口中对变量做出标记。例如,图7-12显示了一个Watch窗口,其中包含几个示例变量和表达式,在遇到Maxima()函数末尾前面的一个断点时,会显示这个Watch窗口。
图7-12
testArray数组对于Main()来说是局部数组,所以在该图中没有值,它是灰显的。
3.单步执行代码
前面介绍了如何在中断模式下查看应用程序的运行情况,下面讨论如何在中断模式下使用IDE单步执行代码,查看代码的准确执行结果。人们的思维速度不会比计算机运行得更快,所以这是一个极有价值的技巧。
VS进入中断模式后,在代码视图的左边,马上要执行的代码旁边会出现一个黄色箭头光标(如果使用断点进入中断模式,该光标最初应显示在断点的红色圆圈中),如图7-13所示。
图7-13
这显示了在进入中断模式时程序执行到的位置。在这个位置,可以选择逐行执行。为此,使用前面看到的其他一些Debug工具栏按钮,如图7-14所示。
图7-14
第6、第7、第8个图标控制了中断模式下的程序流。它们依次是:
● Step Into—— 执行并移动到下一条要执行的语句上
● Step Over—— 同上,但不进入嵌套的代码块,包括函数
● Step Out—— 执行到代码块的末尾处,在执行完该语句块后,重新进入中断模式
如果要查看应用程序执行的每个操作,可以使用Step Into按顺序执行指令,这包括在函数中执行,如上面示例中的Maxima()。当光标到达第17行,调用Maxima()时,单击这个图标,会使光标移动到Maxima()函数内部的第一行代码上。而如果光标移到第17行时单击Step Over,就会使光标移动到第18行,不进入Maxima()中的代码(但仍执行这段代码)。如果单步执行到不感兴趣的函数,可以单击Step Out,返回到调用该函数的代码。在单步执行代码时,变量的值可能会发生变化。注意观察上一节讨论的Watch窗口,可以看到变量值的变化情况。
通过右击代码行并选择Set Next Statement,或将黄色箭头拖到不同的代码行,也可以更改接下来要执行的代码行。这有时是不可行的,例如跳过变量初始化时。但是,当跳过存在问题的代码行来查看发生的情况时,或向后移动箭头来重复执行代码时,这种方法是非常有用的。
在存在语义错误的代码中,这些技巧也许是最有效的。可以单步执行代码,当执行到有错误的代码时,错误会像正常运行程序那样发生。或者可以修改执行代码,让语句多次执行。在这个过程中,可以监视数据,看看什么地方出错。本章后面将使用这个技巧查看示例应用程序的执行情况。
4. Immediate和Command窗口
通过Command和Immediate窗口(在Debug窗口菜单下),可以在运行应用程序的过程中执行命令。通过Command窗口可以手动执行VS操作(例如,菜单和工具栏操作), Immediate窗口可以执行与当前正在执行的源代码不同的额外代码,以及计算表达式。
VS中的这些窗口在内部是链接在一起的。甚至可以在它们之间切换:输入命令immed,可以从Command窗口切换到Immediate窗口;输入cmd可以从Immediate窗口切换到Command窗口。
下面详细讨论Immediate窗口,因为Command窗口仅适用于复杂的操作。Immediate窗口最简单的用法是计算表达式,有点像Watch窗口中的一次性使用。为此,只需要键入一个表达式,并按回车键即可。接着就会显示请求的信息,如图7-15所示。
图7-15
可以在这里修改变量的内容,如图7-16所示。
图7-16
大多数情况下,使用前面介绍的变量监视窗口更容易得到相同的效果,但这个技巧对于调整变量值和测试表达式很方便。
5. Call Stack窗口
这是最后一个要讨论的窗口,它描述了程序是如何执行到当前位置的。简言之,该窗口显示了当前函数、调用它的函数以及调用该函数的函数(即一个嵌套的函数调用列表)。调用的确切位置也被记录下来。
在前面的示例中,在执行到Maxima()时进入中断模式,或者使用代码单步执行功能移动到这个函数的内部,得到如图7-17所示的信息。
图7-17
如果双击某一项,就会移动到相应的位置,跟踪代码执行到当前位置的过程。第一次检测错误时,这个窗口非常有用,因为它们可以查看临近错误发生时的情况。对于常用函数中出现的错误,这有助于找到错误的源头。