4.6.1 跟踪句柄
跟踪句柄类似于本地C++(www.cppentry.com)指针,但也有明显不同的地方。跟踪句柄确实存储着某个地址,如果堆压缩过程中移动了句柄引用的对象,垃圾回收器将自动更新句柄包含的地址。但是,我们不能像使用本地指针那样使用跟踪句柄执行地址的算术操作,跟踪句柄的强制类型转换也是不允许的。
在CLR堆中创建的所有对象都必须被跟踪句柄引用。所有属于引用类类型的对象都存储在堆中,因此为引用这些对象而创建的变量都必须是跟踪句柄。例如,String类属于引用类类型,因此引用String对象的变量必须是跟踪句柄。供数值类类型使用的内存默认是在堆栈上分配的,但我们也可以通过使用gcnew运算符选择将数值存储在堆上。此刻又是一个很好的机会来回忆一下第2章曾经提到的一点:在CLR堆上分配的变量-- 其中包括所有CLR引用类型,都不能在全局作用域中声明。
声明跟踪句柄
我们通过将符号^(通常被称为"帽子")放在类型名称的后面来指定该类型的句柄。例如,下面这条语句就声明了一个可以存储String类型对象的地址、名为proverb的跟踪句柄:
- String^ proverb;
该语句定义的proverb变量是String^类型的跟踪句柄。当我们声明某个句柄时,系统自动将其初始化为空值,因此该句柄将不引用任何对象。为显式地将某个句柄设置为空值,我们可以像下面这样使用关键字nullptr:
- proverb = nullptr; // Set handle to null
注意,我们在这里不能像使用本地指针时那样,使用0来表示空值。如果用0来初始化句柄,则数值0将被转换为该句柄引用的对象的类型,而这个新对象的地址将被存入该句柄中。
当然,我们可以在声明时显式地初始化句柄。下面这条语句定义的也是指向String对象的句柄:
- String^ saying = L"I used to think I was
indecisive but now I'm not so sure";
该语句在堆上创建一个包含等号右边字符串的String对象,该新建对象的地址被存入saying中。注意,字符串字面值的类型是const wchar_t*,而非String。定义String类的方式使这样的字面值可以用来创建String类型的对象。
下面这条语句给出为数值类型创建句柄的方法:
- int^ value = 99;
该语句创建一个int^类型的句柄value,并将堆内该句柄指向的数值初始化为99。记住,我们创建的是一种指针,因此在没有解除引用的情况下value不能参与算术运算。为了解除对跟踪句柄的引用,我们以与使用本地指针时相同的方式使用*运算符。例如,下面这条语句就在算术运算中使用了跟踪句柄指向的值:
- int result = 2*(*value)+15;
圆括弧内的表达式*value访问该跟踪句柄指向的地址存储的整数,因此变量result的结果是213。
注意,当我们在等号左边使用句柄时,不需要显式解除引用就可以用来存储结果,编译器将替我们处理好一切。例如:
- int^ result = 0;
- result = 2*(*value)+15;
在此首先创建一个指向数值0的句柄result。注意,第一行将使编译器产生一条警告消息,因为编译器认为我们可能打算将句柄初始化为空值,但这种写法是错误的。因为在下一条语句中result出现在等号左边,而右边的结果是数值,所以编译器能够判定必须解除对result的引用才能存储数值。当然,我们也可以将这条语句写成下面的显式形式:
- *result = 2*(*value)+15;
注意,仅当真正定义过result之后,这条语句才能工作。如果只是声明了result,则执行上述代码时将产生运行时错误。例如:
- int^ result; // Declaration but no definition
- *result = 2*(*value)+15; // Error message - unhandled exception
第二条语句要解除对result的引用,这意味着被指向的对象已经存在。但实际上并不然,所以程序将产生运行时错误。第一条语句是句柄result的声明,该句柄默认情况下被设置为空值,而我们不能解除对空句柄的引用。如果我们不在第二条语句中显式解除对result的引用,则一切都将像预期的那样正常工作。因为等号右边表达式的结果是数值类类型,所以其地址被存入result中。