Visual Studio 调试器包括表达式计算器,当您在**“快速监视”对话框、“监视”窗口或“即时”窗口中输入表达式时,这些计算器可以对其进行计算。 这些表达式计算器还可以在“断点”**窗口和调试器中的许多其他位置使用。
常见的表达式计算器功能
试器中常见的各种表达式计算器功能,这些功能仅因语言不同而不同。
隐式变量
在 Visual Basic 和 C# 中,通过使用表达式计算器可以创建隐式变量。 这些隐式变量永远不会超出范围,可以作为任何其他变量一样处理。在 C# 中,通过在表达式计算器中声明隐式变量可创建隐式变量。 例如,您可以在**“即时”**窗口中输入下列 C# 代码:
int b = 100;
在**“即时”窗口中执行此代码时,新的隐式变量将显示在“局部变量”**窗口中,其变量名称前有一个 $ 符号,在本例中,为 $b。
在 Visual Basic 中,不能在表达式计算器中声明隐式变量。 但是,如果在 Visual Basic 表达式计算器中使用未声明的变量,将会自动创建隐式变量。 在 Visual Basic 中,隐式变量不会列在**“局部变量”**窗口中。
断点
如果使用“即时”窗口计算包含断点的 Visual Basic 或 C# 方法或函数,将命中该断点并在**“调用堆栈”**上显示一个新框架。 下面是一个 C# 示例:
class Program { static void Main(string[] args) { // Breakpoint here: int a = 20; } }
如果在注释指示的位置设置一个断点,然后按 F5 编译并执行该程序,将以常规方式命中该断点。 如果现在通过将 Program.Main(null) 键入**“即时”窗口来计算 Main 方法,该断点将再次被命中,“调用堆栈”**上将有一个该方法的项。
在“监视”窗口中计算
为避免出现不该有的副作用,调试器每次步进时都不会自动计算函数或方法。 相反,您可以通过一个图标来手动更新结果。 该图标显示在**“值”**列中。 这样就可以手动计算调用。
对象标识
此功能对 Visual Basic 不可用。某些应用程序会创建一个类的许多实例。 在这些应用程序中,通过使用标识符来区分类的给定实例通常十分有用。 例如,如果类的特定实例的行为与预期不同或者已将特定实例多次插入应仅包含它一次的集合中,这么做就很有用。
本机对象标识
调试非托管代码时,可以通过地址来唯一地标识对象。 这么做很重要,原因有两点:
-
只需使用对象的地址即可跟踪对象。 这包括使用地址执行以下操作的能力:
-
查看位于该地址的对象的值。
-
检查是否相等。 通常情况下,您可以像使用对象变量本身那样使用对象的地址。
-
-
您可以使用对象(实例)的地址在该特定实例中的方法上设置断点。
例如,假定您有一个对象,它是 CMyType 类的实例,地址为 0xcccccccc。 您可以在该实例的 aMethod 方法上指定一个函数断点,如下所示:
((CMyType *) 0xcccccccc)->aMethod
托管对象标识
对于托管代码,不能使用对象的地址来标识该对象。 相反,需要使用由公共语言运行时 (CLR) 调试服务生成的并与该对象关联的整数(称为对象 ID)。 该数字是一个由 CLR 调试服务生成的正整数。 对象 ID 值除唯一地标识对象外,别无他用。
对象句柄显示为可变长度的十进制整数,数字后跟一个井号 (#),且不带任何前导零,例如 5#。 句柄显示在不同调试器数据窗口中的**“值”**列中。
若要为变量创建对象 ID,请右击该变量,然后选择**“创建对象 ID”。 调试器将显示一个后跟井号 (#) 的数字,如 123#。 若要删除对象 ID,请右击该变量,然后选择“删除对象 ID”**。
命中断点时,可以在**“监视”**窗口中键入变量的句柄。 调试器显示对象 ID 的值,您可以展开并检查它,就像对任何其他变量一样。
您可以使用对象 ID 在特定实例的方法上设置断点。 例如,假定您有一个对象,它是 CMyType 类的一个实例,该实例的对象 ID 为 5#。 类 CMyType 包含一个方法 aMethod。 您可以在实例 5# 的 aMethod 方法上设置一个函数断点,如下所示:
((CMyType) 5#).aMethod
还可以在断点条件中使用对象 ID。 下面的示例演示如何在条件中测试对象 ID。
this == 5#
本机 C++ 中的表达式
格式说明符
在**“监视”窗口或“快速监视”**对话框中调试本机代码时,您会使用格式说明符更改值的显示格式。
(大多数格式说明符仅适用于本机代码,但是 Visual C# 包含有限的一组格式说明符)。 (有关信息,请参阅 C# 中的格式说明符。)
您还可以在即时窗口、命令窗口甚至是源窗口中使用格式说明符。 如果将光标悬停在这些窗口中的表达式上,结果将在数据提示中显示。 数据提示将在数据提示的显示内容中反映格式说明符。
例子:
假设 nVar 是整数变量,并且“监视”窗口显示其包含值 0x0065。 若要看到表示为字符而不是整数的值,请在“名称”列,在变量名之后添加字符格式说明符 c:
nVar,c
“值”列现在不显示整数值 0x0065,而显示字符值 101 'e'。
如果要将格式说明符应用于数组元素或对象成员,必须将其直接应用于每个元素或成员。 不能将其整体应用于数组或对象。 例如,假设有数组 nArray,并且想看字符格式的前四个元素。 应在**“监视”**窗口输入下列表达式:
nArray[0],c
nArray[1],c
nArray[2],c
nArray[3],c
下表说明调试器可识别的格式说明符。
说明符 |
Format |
表达式 |
显示的值 |
---|---|---|---|
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 |
String |
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 |
下表包含用于内存位置的格式化符号。 可以使用带有计算为位置的任何值或表达式的内存位置说明符。
符号 |
Format |
表达式 |
显示的值 |
---|---|---|---|
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 |
数组形式的指针大小说明符
如果对于要以数组形式查看的对象,有一个指向它的指针,则可以使用一个整数来指定数组元素的数量:
ptr,10
上下文运算符
上下文运算符是由本机调试器提供的附加运算符。 调试本机代码时,可使用上下文运算符限定断点位置、变量名称或表达式:
-
{[函数],[源],[模块] } location
-
{[函数],[源],[模块] } variable_name
-
{[函数],[源],[模块] } 表达式
大括号可以包含函数名、源文件路径和模块(可执行文件或 DLL)路径的任意组合。 上下文运算符对于某些目的很有用,如指定来自外部范围的、但被本地名称隐藏的名称。
例子:
在 EXAMPLE.CPP 的 301 行设置断点:
{,EXAMPLE.CPP,}@301
如果省略 函数 或 模块,则不能省略两个逗号。 因此,下面的语法无效:
{File.c, File.exe} @143 // Invalid syntax
但是,如果将 源 和 模块 都省略掉,则可以省略逗号。 下面的语法有效:
{Fun} @143
如果 源 或 模块 路径包括逗号、嵌入空格或大括号,则必须在路径两边使用引号,以便上下文分析器能够正确识别该字符串。 单引号被视为 Windows 文件名的一部分,因此必须使用双引号。 例如,
{,"a long, long, name.c", } .143
当表达式计算器遇到表达式中的符号时,它按下列顺序搜索该符号:
-
词法范围向外,从当前块开始(括在大括号中的一系列语句),然后从该封闭块继续向外。 当前块是包含当前位置(指令指针地址)的代码。
-
函数范围。 当前函数。
-
类范围,如果当前位置在 C++ 成员函数内。 类范围包含所有的基类。 表达式计算器使用正常域控制规则。
-
当前模块。
-
全局符号。
-
其他模块。
-
程序中的公共符号。
使用上下文运算符,可以指定搜索的起始点并跳过当前位置。 不能指定类,但是可以指定类的成员函数,并让表达式计算器向外搜索。
不支持的运算符和附加运算符
调试器接受大多数 Microsoft 和 ANSI C/C++ 表达式;
在本机 C++ 中,调试器表达式不支持下列运算符:
-
逗号运算符:
Expr1 , Expr2
-
条件运算符:
Expr1 ? Expr2 : Expr3
在本机 C++ 中,调试器表达式不支持下列附加运算符:
-
指定符号的上下文的上下文运算符 ({ })。
-
用于访问内存的内存运算符(BY、WO 和 DW)。 在所有运算符中,内存运算符的优先级最低。 内存运算符主要在调试汇编语言代码时有用。
本机 C++ 表达式的限制
当在调试器窗口中输入 C/C++ 表达式时,将受到下列常规限制:
访问控制
调试器可以访问所有的类成员而不用考虑访问控制。 可以检查任一类对象成员,包括基类和嵌入成员对象。
不明确的引用
如果调试器表达式引用不明确的成员名称,必须使用类名称来限定它。 例如,如果 CObject 是 CClass 实例,后者从 AClass 和 BClass 二者中继承了名为 expense 的成员函数,则 CObject.expense 是不明确的。 可以按如下方式化解多义性:
CObject.BClass::expense
若要化解多义性,表达式计算器应用关于成员名称的正常域控制规则。
匿名命名空间
本机 C++ 表达式计算器不支持匿名命名空间。 例如,假定您具有下列代码:
#include "stdafx.h"
namespace mars
{
namespace
{
int test = 0;
}
}
int main()
{
// Adding a watch on test does not work.
mars::test++;
return 0;
}
本示例中,唯一一种监视符号 test 的方式是使用修饰名:
(int*)?test@?A0xccd06570@mars@@3HA
构造函数、析构函数和转换
不能使用要求构造临时对象的表达式显式或隐式地为对象调用构造函数或析构函数。 例如,下列表达式显式调用构造函数并导致错误信息:
Date( 2, 3, 1985 )
如果转换的目标是类,则不能调用转换函数。 这种转换涉及到构造对象。 例如,如果 myFraction 是 CFraction 的实例,后者定义了转换函数运算符 FixedPoint,下列表达式导致错误:
(FixedPoint)myFraction
但是,如果转换的目标是内置类型,则可以调用转换函数。 如果 CFraction 定义转换函数 operator float,在调试器中以下表达式是合法的:
(float)myFraction
可以调用返回对象或声明局部对象的函数。
不能调用 new 或 delete 运算符。 下列表达式在调试器中不能运行:
new Date(2,3,1985)
Inheritance
当使用调试器显示具有虚拟基类的类对象时,为每个继承路径显示虚拟基类的成员,即使只存储了那些成员中的一个实例也是如此。
虚函数调用将被表达式计算器正确地处理。 例如,假定类 CEmployee 定义虚拟函数 computePay,该函数在从 CEmployee 继承的类中重新定义。 可以通过指向 CEmployee 的指针调用 computePay,并执行正确的函数:
empPtr->computePay()
可以将指向派生类对象的指针转换为指向基类对象的指针。 不允许反向转换。
内部和内联函数
调试器表达式不能调用内部或内联函数,除非该函数至少作为正常函数出现一次。
数值常数
调试器表达式可以使用八进制、十六进制或十进制格式的整数常数。 默认情况下,调试器需要十进制常数。 此设置可以在**“调试”选项卡的“常规”**页上更改。
可以使用前缀或后缀符号表示另一基中的数字。 下表显示了可以使用的格式。
语法 |
示例(十进制 100) |
基数 |
---|---|---|
digits |
100 或 64 |
十进制或十六进制,具体取决于当前的设置。 |
0digits |
0144 |
八进制(以 8 为基数) |
0ndigits |
0n100 |
十进制(以 10 为基数) |
0xdigits |
0x64 |
十六进制(以 16 为基数) |
digitsh |
64h |
十六进制(以 16 为基数) |
运算符函数
调试器表达式可以隐式或显式地调用类的运算符函数。 例如,假设 myFraction 和 yourFraction 都是定义 operator+ 的类的实例。 可以使用下列表达式显示这两个对象的和:
myFraction + yourFraction
如果将运算符函数定义为友元函数,则可以使用与调用成员函数相同的语法隐式调用它,或者显式调用它,如:
operator+( myFraction, yourFraction )
与一般函数一样,不能调用带有要求转换(涉及到对象构造)的参数的运算符函数。
调试器不支持同时具有常量和非常量版本的重载运算符。 带有 const 和非 const 版本的重载运算符常用在标准模板库中。
重载
如果存在精确匹配或者匹配不要求涉及对象构造的转换,则调试器表达式可以调用重载函数。 例如,如果函数 calc 将 CFraction 对象作为参数,并且 CFraction 类定义接受整数的单一参数构造函数,则下列表达式将导致错误:
calc( 23 )
即使存在将整数转换为 calc 期望的 CFraction 对象的合法转换,但这样的转换涉及到对象的创建,因此不受支持。
优先级
在调试器表达式中,C++ 范围运算符 (::) 比其在源代码中具有更低的优先级。 在 C++ 源代码中,该运算符具有最高的优先级。 在调试器中,其优先级介于基数与后缀运算符(->、++、--)和一元运算符(!、&、* 及其他)之间。
符号格式
如果符号所在的模块是用完全调试信息(/Zi 或 /ZI)编译的,则输入的包含符号的调试器表达式与源代码中使用的格式相同。 如果输入的表达式包含公共符号(即在库中或者在用 /Zd 编译的模块中可以找到的符号),则必须使用符号的修饰名(即在对象代码中使用的格式)。 有关更多信息,请参见 /Z7、/Zd、/Zi、/ZI(调试信息格式)。
使用 LINK /MAP 选项可以获得所有修饰和未修饰格式的名称列表。 有关更多信息,请参见 /MAP(生成映射文件)。
名称修饰是用于强制类型安全链接的机制。 这意味着只有拼写、大小写、调用约定和类型精确匹配的名称和引用才链接在一起。
用 C 调用约定声明(使用 _cdecl 关键字显式或隐式声明)的名称以下划线 (_) 开头。 例如,函数 main 可显示为 _main。 声明为 _fastcall 的名称以 @ 符号开头。
对于 C++,修饰名除了对调用约定进行编码外还对符号类型进行编码。 这种格式的名称会很长而且难以读取。 该名称以至少一个问号 (?) 开头。 对于 C++ 函数,修饰包括函数范围、函数参数的类型和函数返回类型。
类型强制转换
如果转换为类型,调试器必须已知该类型。 在程序中必须有该类型的另外一个对象。 不支持使用 typedef 语句创建的类型。
汇编语言表达式
调试器可以正确计算汇编语言表达式,但有某些限制。 用于某些汇编语言表达式的语法不同于用于汇编语言开发系统(如 Microsoft Macro Assembler (MASM))中的语法。
内存运算符
内存运算符是返回直接内存操作结果的一元运算符。 这些运算符主要用于调试汇编语言代码。
{BY | WO | DW} address
BY 运算符返回包含地址处的第一个字节的短整型。 该运算符模拟 BYTE PTR。
WO 运算符返回包含地址处的字的值(两个字节)的短整型。 该运算符模拟 Microsoft Macro Assembler 的 WORD PTR 操作。 DW 运算符返回包含地址处的前四个字节的值的长整型。 此运算符模拟 DWORD PTR。
用在一些示例中的 x 格式说明符导致结果以十六进制显示。
示例
-
显示位于变量 sum 地址处的第一个字节:
BY sum
-
显示位于变量 new_set 地址处的第一个字:
WO new_set
-
显示位于 sum 地址处的双倍长字:
DW sum
-
显示位移为 6 的 EBP 寄存器指向的字节:
BY ebp+6,x
-
显示堆栈指针指向的字(压入堆栈的最后一个字):
WO esp,x
-
显示 ESI 寄存器指向的双倍长字:
DW esi,x
寄存器间接寻址
调试器不识别指示寄存器所指向的内存位置的方括号 ([ ])。 相反,请使用 BY、WO 和 DW 运算符引用相应的字节、字或双字的值。
MASM 表达式 |
调试器表达式 |
C++ 表达式 |
---|---|---|
BYTE PTR [bx] |
BY ebx |
*(unsigned char) ebx |
WORD PTR [bp] |
WO ebp |
*(unsigned short *) ebp |
DWORD PTR [bp] |
DW ebp |
*(unsigned long *) ebp |
带置换的寄存器间接寻址
若要执行带位移的基本、索引的或基本索引的间接寻址模式操作,请使用带加法运算符的 BY、WO 和 DW 运算符。
MASM 表达式 |
调试器表达式 |
---|---|
BYTE PTR [edi+6] |
BY edi+6 |
BYTE PTR Test[ebx] |
BY &Test+ebx |
WORD PTR [esi][ebp+6] |
WO esi+ebp+6 |
DWORD PTR [ebx][esi] |
DW ebx+esi |
变量地址
使用 C address-of 运算符 (&),而不是 MASM OFFSET 运算符。
MASM 表达式 |
调试器表达式 |
---|---|
OFFSET Var |
&Var |
PTR 运算符
将 address-of 运算符 (&) 与类型强制转换或 BY、WO 和 DW 运算符结合使用,以替换汇编语言 PTR 运算符。
MASM 表达式 |
调试器表达式 |
---|---|
BYTE PTR Var |
BY &Var |
*(unsigned char*) |
&Var |
WORD PTR Var |
WO &Var |
DWORD PTR Var |
DW &Var |
*(unsigned long*) |
&Var |
汇编语言字符串
在变量名之后添加字符串格式说明符 ,s。
MASM 表达式 |
调试器表达式 |
---|---|
String |
String,s |
因为 C 字符串以空字符 (ASCII 0) 结尾,因此当请求字符串显示时,调试器显示从变量的第一个子节直到内存中下一个空字节之间的所有字符。 如果打算调试汇编语言程序,并且要在**“监视”窗口中查看字符串,应当用一个空字符分隔字符串变量。 一种查看以空终止或未终止的字符串的简便方法是使用“内存”**窗口。
数组和结构元素
用 address-of 运算符 (&) 做数组名称的前缀并添加所需的偏移量。 偏移量可以是表达式、数字、寄存器名称或变量。
下面的示例说明如何为字节、字和双倍长字的数组执行该操作。
MASM 表达式 |
调试器表达式 |
---|---|
String[12] |
BY &String+12*(&String+12) |
aWords[bx+di] |
WO &aWords+bx+di*(unsigned*)(&aWords+bx+di) |
aDWords[bx+4] |
DW &aDWords+bx+4*(unsigned long*)(&aDWords+bx+4) |