在调试程序的过程中,如果程序在某个位置挂起执行(例如:中断到某个断点),通常我们希望能够通过一些工具观察程序的当前状态。其中,最重要的当属查看程序中数据的值。例如,查看某个变量的类型和值、某个寄存器的值、或某段内存的值。所以,几乎所有的调试器提供了大量用于检查和修改程序数据的工具。
表 1列出了Visual Studio所提供的用于检查和修改程序数据的工具。
工具名称 |
描述 |
“局部变量”窗口 |
用于显示对于当前上下文或范围来说位于本地的变量。 通常,这是当前正在执行的过程或函数。
调试器自动填充此窗口。 |
“自动” 窗口 |
用于显示在当前代码行和上一代码行中使用的变量。 对于C++程序,“自动”窗口还会显示函数返回值。
与“局部变量”窗口类似,“自动”窗口是由调试器自动填充的。 |
“监视”窗口 |
在“监视”窗口中可以添加要监视其值的变量。 此外,还可以添加调试器所能识别的任何有效表达式。 |
“快速监视”对话框 |
“快速监视”对话框在概念上类似于“监视”窗口,但是“快速监视”每次只能显示一个变量或表达式。
如果需要快速查看变量或表达式而不想打开“监视”窗口,则可以使用“快速监视”。 |
“寄存器”窗口 |
“寄存器”窗口用于显示寄存器内容,只有在程序正在运行或处于中断模式时“寄存器”窗口才会显示。
为了减少混乱,“寄存器”窗口将寄存器组织成组,具体情况随平台和处理器类型的不同而不同。右击“寄存器”窗口,可以看到一个包含组列表的快捷菜单,可根据需要显示或隐藏它。 |
“内存”窗口 |
使用“内存”窗口可以看到应用程序所占用的内存空间的情况。 在“监视”窗口、“快速监视”、“自动”窗口和“局部变量”窗口中都可看到内存中特定位置初变量的内容,但在“内存”窗口中可看到尺寸较大的图像。
这对于检查大片的数据(如缓冲区和大的字符串)很方便,在其他窗口中显示就不太好。 但是,“内存”窗口不仅限于显示数据,
按照定义“内存”窗口可以显示内存空间中的任何内容,无论它是数据、代码或是未分配内存中的无用随机位。 |
数据提示 |
数据提示是用于在调试过程中查看程序中的变量和对象的有关信息的最方便工具之一。
在调试器处于中断模式时,可以在当前范围内查看变量的值,方法是将鼠标指针置于源窗口中的变量上。 |
可视化工具 |
通过可视化工具可以以有意义的方式查看对象或变量的内容。 例如,可以使用
HTML 可视化工具来查看 HTML 字符串,因为这样可以解释该字符串并在浏览器中显示出来。 您可以通过数据提示、“监视”窗口、“自动”窗口、“局部变量”窗口或“快速监视”对话框来访问可视化工具。 |
表 1 Visual Studio提供的数据检查和修改工具列表
下面,让我们通过一些示例来看看如何使用这些工具来检查和修改数据。
观察变量值的变化
给定清单1所示的程序,我们希望查看变量sum值的变化,从而验证程序的逻辑是否正确。
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
int sum = 0;
for (int i = 1; i <= 10; ++i)
sum += i;
cout << "1 + 2 + 3 +...
+ 10 = " << sum << endl;
return 0;
}
清单1 观察变量值的变化代码示例
最直接的方法就是在“sum += i”所在行设置一个断点,让程序反复中断到该断点。这时,我们可以通过“局部变量”窗口、“自动”
窗口、“监视”窗口、“快速监视”对话框、或数据提示来观察变量sum值的变化。对于“局部变量”窗口、“自动”
窗口、“监视”窗口来说,每一次变量的值发生改变,Visual Studio会使用红色的字体标记这种变化。图
1~图 5显示了在i等于5的时,通过“局部变量”窗口、“自动” 窗口、“监视”窗口、“快速监视”对话框、和数据提示显示sum的值。
图 1 使用“局部变量”窗口观察变量
图 2使用“自动” 窗口观察变量
图 3 使用“监视”窗口观察变量
图 4 使用数据提示观察变量
图 5 使用“快速监视”对话框观察变量
虽然这种方法虽然比较直接,但是并不方便,因为我们不得不让程序不断地中断到“sum
+= i”所在行。清单 1所示的程序中循环只执行了10次,如果是100次或1000次,你肯定就会想有没有更好的办法了。
当然有更好的办法了。我们可以使用跟踪点来解决这个问题。在“sum +=
i”所在行,选择快捷菜单“断点”->“插入跟踪点”,就会出想如图 6所示的对话框。然后,选择“打印消息”
复选框,然后在相应的文本框中输入消息文本“i={i}, sum={sum}”({x}代表x的值)。那么,每到程序运行到跟踪点所在行时,就会在“输出”窗口中打印该消息。
图 6 使用跟踪点观察变量值的变化
有个这个跟踪点,你就不必不断地中断程序。在一次性执行完成清单 1中的for之后,“输出”窗口中就会显示如清单
2所示的结果。我们可以非常清晰地看到每次变量sum值的变化。
i=1, sum=0
i=2, sum=1
i=3, sum=3
i=4, sum=6
i=5, sum=10
i=6, sum=15
i=7, sum=21
i=8, sum=28
i=9, sum=36
i=10, sum=45
清单 2 使用跟踪点后清单 1代码在“输出”窗口中的输出结果
查看指向数组的指针
给定清单 3所示的程序,我们希望在main返回之前查看一下指针p所指数组的内容。
int main(int argc, char* argv[])
{
wchar_t* wstr = L"中国";
float* p = new float [10];
for(int i = 0; i <= 10; i++)
{
p[i] = (float)i;
}
delete[] p;
return 0;
}
清单 3 查看指向数组的指针代码示例
如果直接“局部变量”窗口、“自动” 窗口、“监视”窗口或“快速监视”对话框来查看p,Visual
Studio会显示类似于图 7的内容。从图 7中我们只能看到数组第一个元素的值。
图 7 指针p的内容
非常明显,图 7不是我们想看到的结果。那么,该怎么办呢?答案非常简单,只需要在
“监视”窗口或“快速监视”对话框中输入“p, 10”,就会看到。其中,逗号之后的整数代表所显示数组元素的数量。
图 8指针p所指数组的内容
如果您想要查看数组中的某个元素,例如第6个元素,那么只需要输入“p[5]”。如果只想显示数组中某段连续的元素,例如第3个到第6个元素,那么可以输入“(p+2),
4”即可。其中,将指针p加2是为了移到数组的第3个元素,4代表显示4个元素。
还有一种查看指针所指数组内容的办法是使用“内存”窗口。在“代码”窗口中选中指针p,将其拖放到“内存”窗口,或者在“内存”窗口的“地址”框中输入“p”后按回车键,那么就会出现看到类似如图
9所示的内容。
图 9 使用“内存”窗口查看指针所指数组的内容
由于默认情况下,“内存”窗口是以单字节十六进制整数的方式来显示数据,所以很难看懂“内存”窗口中的数据。这时,我们需要修改数据的显示方式。通过“内存”窗口的快捷菜单可以改变数据的显示方式。在这个示例中,我们选择快捷菜单项“32位浮点数”,就会出现如图
10所示的数据。这时,我们可以清楚地知道数据的真正含义。
图 10 以32位浮点数的方式在“内存”窗口中查看指针所指数组的内容
Visual Studio支持将“内存”窗口中的数据显示为1字节整数、2字节整数、3字节整数、4字节整数、32位浮点数、64位浮点数、ANSI字符、Unicode字符。许多程序开发人员在使用Visual
Studio时碰到过这样一个问题:“内存”窗口无法显示Unicode字符。其实,这是因为“内存”窗口默认情况下将数据显示为ANSI字符,只需要将显示方式设置为Unicode字符就可以正常显示Unicode字符。例如:将显示方式设置为Unicode字符后,就可以在“内存”窗口中显示清单
3中的字符串指针wstr所指的字符串了。
注意
“内存”窗口中的“地址”框不仅接受数字值,而且接受计算结果为地址的表达式。在激活活动计算功能的情况下,“内存”窗口将“地址”表达式视为活动表达式,“地址”框中将显示表达式,程序执行时将对该表达式进行重新计算。在禁用活动计算功能的情况下,“地址”表达式只会计算一次,“地址”框中始终显示表达式的结果。点击“地址”框右边的按钮,或者选择快捷菜单上单击“自动重新计算”,就可以切换“内存”窗口中的活动计算功能。
设置变量的查看格式
在某些时候,“监视”窗口或“快速监视”对话框所显示值的格式不符合我们的需求。例如,当程序中断到函数print的第二条语句时,如果使用“监视”窗口或“快速监视”对话框查看变量“i”的值,显示的内容将是整数“97”。
void print(char c)
{
int i = c;
printf("ASCII code of character
'%c' is %d.\n", c, i);
}
int main(int argc, char* argv[])
{
print('a');
return 0;
}
清单 4 设置变量的查看格式代码示例
如果我们又想看看变量“i”的内容究竟对应于哪个英文字符,那该怎么办呢?许多人可能会想到将变量“i”转型为char,这样就可以使用表达式“(char)i”来查看它究竟对应于哪个英文字符。这种方法完全可行,不过Visual
Studio提供了更加便捷的方法——格式说明符。
在变量名之后添加格式说明符 “d”,可以将变量解释为需要的格式。例如,在前面的示例中使用“i,c”就可以将变量“i”的内容解释为一个字符,“监视”窗口或“快速监视”对话框中显示的值变为“97
’a’”。表 2显示了Visual Studio支持的格式说明符。
说明符 |
格式 |
表达式 |
显示的值 |
d,i |
signed 十进制整数 |
0xF000F065, d |
-268373915 |
u |
unsigned 十进制整数 |
0x0065, u |
101 |
o |
unsigned 八进制整数 |
0xF065, o |
0170145 |
x,X |
十六进制整数 |
61541, x |
0x0000F065 |
l,h |
用于 d、i、u、o、x、X 的 long 或 short
前缀 |
00406042,hx |
0x0c22 |
f |
signed 浮点型 |
(3./2.), f |
1.500000 |
e |
signed 科学计数法 |
(3./2.), e |
1.500000e+000 |
g |
signed 浮点型或 signed 科学计数法,显示其中较短的数 |
(3./2.), g |
1.5 |
c |
单个字符 |
0x0065, c |
101 'e' |
s |
字符串 |
0x0012fde8, s |
"Hello world" |
su |
Unicode 字符串 |
0x0012fde8, su |
"Hello world" |
s8 |
UTF-8 字符串 |
0x0012fde8, s8 |
"Hello world" |
hr |
HRESULT 或 Win32 错误代码。 (调试器自动将
HRESULT 解码,因此这些情况下不需要该说明符。) |
0x00000000L, hr |
S_OK |
wc |
窗口类标志。 |
0x00000040, wc |
WC_DEFAULTCHAR |
wm |
Windows 消息数字 |
0x0010, wm |
WM_CLOSE |
! |
原始格式,忽略任何数据类型视图自定义项 |
i ! |
4 |
表 2 Visual Studio支持的格式说明符
如果要将格式说明符应用于数组元素或对象成员,必须将其直接应用于每个元素或成员。
不能将其整体应用于数组或对象。 例如,假设有数组 “array”,并且想看字符格式的第二个元素。 应在“监视”窗口或“快速监视”对话框中输入表达式“array[1],c”。
Visual Studio还支持内存位置格式化符,表 3显示了Visual
Studio支持的内存位置格式化符。
符号 |
格式 |
表达式 |
显示的值 |
ma |
64 个 ASCII 字符 |
ptr, ma |
0x0012ffac .4...0...".0W&.......1W&.0.:W..1...."..1.JO&.1.2.."..1...0y....1 |
m |
以十六进制表示的 16 个字节,后跟 16 个 ASCII
字符 |
ptr, m |
0x0012ffac B3 34 CB 00 84 30 94
80 FF 22 8A 30 57 26 00 00 .4...0...".0W&.. |
mb |
以十六进制表示的 16 个字节,后跟 16 个 ASCII
字符 |
ptr, mb |
0x0012ffac B3 34 CB 00 84 30 94
80 FF 22 8A 30 57 26 00 00 .4...0...".0W&.. |
mw |
8 个字 |
ptr, mw |
0x0012ffac 34B3 00CB 3084 8094
22FF 308A 2657 0000 |
md |
4 个双倍长字 |
ptr, md |
0x0012ffac 00CB34B3 80943084 308A22FF
00002657 |
mq |
2 个四倍长字 |
ptr, mq |
0x0012ffac 7ffdf00000000000 5f441a790012fdd4 |
mu |
2 字节字符 (Unicode) |
ptr, mu |
0x0012fc60 8478 77f4 ffff ffff
0000 0000 0000 0000 |
表 3 Visual Studio支持的内存位置格式化符号
查看Windows消息
在创建Windows平台上的应用程序时,我们经常会用到Windows消息。例如,有时候为了防止误操作退出程序,我们会屏蔽对话框上的回车键。这时,我们可能会创建类似清单
5所示的代码,覆盖(Override)方法PreTranslateMessage来屏蔽回车键。
BOOL CMyDialog::PreTranslateMessage(MSG*
pMsg)
{
if(pMsg->message == WM_KEYDOWN &&
pMsg->wParam == VK_RETURN)
return TRUE;
return CDialog::PreTranslateMessage(pMsg);
}
清单 5 查看Windows消息代码示例
当程序中断到清单 5所示的函数,使用“监视”窗口或“快速监视”对话框查看变量“pMsg->message”的值(即当前的Windows消息)时,我们看到的将是一个整数,因为在Windows消息的数据类型无符号整数(unsigned
int)。相信多数人不记得每个Windows消息对应的整数值,所以只能打开文件“WinUser.h”查找这个整数究竟对应于哪个Windows消息。如果使用格式说明符“wm”(请参考表
2),这件事情就会变得非常简单。在“监视”窗口或“快速监视”中输入“pMsg->message,wm”(或“(*pMsg).message,wm”),这时显示结果就不再是一个整数,而是Windows消息的名称(如WM_KEYDOWN)。
查看函数的返回值
在多数情况下,如果某个函数有返回值,我们会将函数的返回值赋给某个临时变量。这样,我们就可以通过这个临时观察函数的返回值。但是,有些时候我们不会这样做。例如,像清单
6所示的代码一样,直接使用将另一个函数(fclose)的返回值作为当前函数(main)的返回值。在这种情况下,如果要查看函数的返回值该怎么办呢?
#include <stdio.h>
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
if (argc != 2)
{
cout << "Please input a
file name!" << endl;
return -1;
}
FILE* fp = NULL;
char line[1000];
if((fp = fopen(argv[1], "r"))
!= NULL)
{
while(fgets(line, 1000, fp) != NULL)
cout << line;
}
else
return -1;
return fclose(fp);
}
清单 6 查看函数的返回值代码示例
许多人首选的办法就是修改代码,将函数的返回值赋给某个临时变量。这种方案确实工作,但是过于麻烦。有没有更好的办法呢?当然有。
事实上,大多数编译器使用类似的方式传递函数的返回值。表 4列出了在x86平台的32位编译器下各种类型函数返回值的存储方式。
返回值类型 |
保存方式 |
小于等于4字节的整数、字符或指针 |
保存到EAX寄存器。 |
超过4字节但是少于8字节的整数 |
保存低4字节到EAX寄存器,其余部分到EDX寄存器。 |
结构或类 |
分配一个临时变量作为隐含的参数传递给被调用函数,被调用函数将返回值复制到这个隐含参数之中,并且将其地址赋给EAX寄存器。 |
浮点类型 |
通过专门的浮点指令使用栈来传递。 |
表 4 不同类型函数返回值的保存方式列表
那么,对于清单 6所示的代码,如果要查看函数fclose的返回值,只需要在调用fclose的那一行设置一个断点,运行程序到该行,单步执行该行,查看寄存器EAX的值。这个值就是函数fclose的返回值。
如果使用Visual Studio调试清单 6所示的代码,那么可以使用“自动”窗口来显示函数返回值。只需要在调用fclose的那一行设置一个断点,运行程序到该行,单步执行该行,然后打开“自动”窗口,就可以看到如图
11所示的结果,“fclose returned”正是函数fclose的返回值。
图 11 使用“自动”窗口查看函数的返回值
查看被调试进程的环境变量
有时候,我们希望能够查看当前进程的环境变量。例如,清单 7所示的代码会为当前进程增加一个新的环境变量“MYPROG”。如果想查看该环境变量是否设置成功该怎么办呢?
#include <stdlib.h>
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
_putenv("MYPROM=testenv");
cout << getenv("MYPROM ")
<< endl;
return 0;
}
清单 7 查看环境变量代码示例
启动调试后,在Visual Studio的“即时”窗口中输入“$env=0”,就可以看到被调试进程的所有环境变量。可能许多人看了这个技巧后会大惑不解。“$env”是什么东西?为什么在
“即时”窗口中输入“$env=0”会显示被调试进程的所有环境变量?
“$env”是一个伪变量。伪变量是用于在“局部变量”窗口或“快速监视”窗口中显示某些信息的术语。您可以像输入普通变量那样输入伪变量。但伪变量不是变量,它不与程序中的变量名相对应。表
5列出了在Visual Studio中调试C/C++程序时可以使用的所有伪变量。
伪变量 |
功能 |
$handles |
显示应用程序中分配的句柄数。 |
$vframe |
显示当前堆栈帧的地址。 |
$TID |
显示当前线程的线程 ID。 |
$ENV |
显示环境块的大小。 |
$CMDLINE |
显示启动程序的命令行字符串的大小。 |
$ 寄存器名
或者
@ 寄存器名 |
显示寄存器 寄存器名 的内容。
通常,只需输入寄存器名便可以显示寄存器的内容。仅在寄存器名重载变量名时才
需要使用此语法。如果寄存器名与当前范围内的某个变量名同名,
则调试器将该名称解释为变量名。这时就需要使用 $寄存器名 或 @寄存器名。 |
$clk |
以时钟形式显示时间。 |
$user |
显示一个结构,在该结构中含有应用程序运行于的帐户的帐户信息。出于安全原因,不显示密码信息。 |
表 5 伪变量列表
例如,如果你希望看到被调试程序中已经分配的句柄数,那么可以在“监视”窗口的输入伪变量“$handles”,就可以得到已经分配的句柄数。
对于伪变量“$env”,如果试图在“即时”窗口中修改它的值,那么就会将所有的环境变量显示出来,而不是显示环境块的大小。所以,在“即时”窗口中输入“$env=1”也会显示所有环境变量,等于号后面的那个数字没有什么意义。
对于伪变量“$CMDLINE”,如果试图在“即时”窗口中修改它的值,那么就会将启动程序的命令行字符串显示出来,而不是字符串的大小。
|