C++/CLI 中的内部指针概述






4.89/5 (28投票s)
2004年11月29日
5分钟阅读

121847
试图解释 C++/CLI 中内部指针的语法、用法和行为。
引言
现在已过时的 C++ 受管扩展的一个非常令人困惑的方面是其指针使用语法,其中 T*
可以是原生指针、受管引用或内部指针。在新的 C++/CLI 语法中,受管引用使用 ^
标点符号( Redmondians 称之为 hat,我第一次看到它时错误地称为 cap),从而避免了与原生指针的任何混淆。Herb Sutter 及其团队很可能出于同样的意图,为我们提供了一种更直观的模板式语法来表示内部指针,类似于表示 CLI 数组的新语法。在本文中,我将讨论什么是内部指针,它们可以在哪里使用以及如何使用它们。
什么是内部指针?
内部指针本质上是进入 CLI 堆(不一定总是指向 CLI 堆)的指针,指向受管对象或受管对象的成员。使用原生指针指向受管对象或受管对象的成员对象的基本问题在于,垃圾回收器可能会在 GC/内存压缩周期中移动 CLI 堆中的对象。因此,在 GC/内存压缩周期之后,位于 0x00BB0010
的受管对象可能会移至 0x00BBC010
。如果您在 GC 周期之前有一个指向受管对象的原生指针 p
,您可以想象会发生什么。在 GC 周期之前,指针 p
将继续指向 0x00BB0010
(受管对象的位置),但现在受管对象位于 0x00BBC010
,而 0x00BB0010
只是 CLI 堆中的一个随机位置,以后可能会被其他对象占用。如果现在使用原生指针 p
来修改 0x00BB0010
的内容,我们实际上是在破坏数据——正如您所见,这是非常不希望发生的!
这就是内部指针发挥作用的地方。CLR 知道内部指针,并且每次垃圾回收器重新定位由内部指针变量指向的对象时,CLR 都会自动更新内部指针的值以反映新位置。因此,我们可以继续使用内部指针,并且完全不必关心 GC 在 CLI 堆中移动我们指向的对象。下面是一些代码来说明这一点:
这是我们带有 int
成员的测试类。
ref class Test
{
public:
int m_i;
};
这是一个尝试填满 CLI 堆的 Generation-0 内存的函数。[如果您没有得到预期的效果,可能需要将循环计数(对我来说是 10,000)修改为其他值]
void DoLotsOfAllocs()
{
for(int i=0; i<10000; i++)
{
gcnew Test();
}
}
这是测试代码。
void _tmain()
{
DoLotsOfAllocs();
Test^ t = gcnew Test();
t->m_i = 99;
interior_ptr<int> p = &t->m_i;
printf("%p %d\r\n",p,*p);
DoLotsOfAllocs();
printf("%p %d\r\n",p,*p);
我的机器上的输出 [地址对您来说将不同]
******* Output ******
00AC9B3C 99
00AA8D18 99
好了,同一个内部指针变量 p
,它之前指向 0x00AC9B3C
,之后指向 0x00AA8D18
。如果使用的是原生指针,运行时将无法更新其值(指向的地址);如果想尝试一下,那就不用费心了,因为编译器不允许您使用原生指针指向受管对象的成员。
使用内部指针按引用传递
当需要带有按引用传递参数的函数时,内部指针的一个便捷用途。让我们看一个简单地计算传入整数平方的函数:
ref class Test
{
public:
int m_i;
};
void Square(interior_ptr<int> pNum)
{
*pNum *= *pNum;
}
现在看这段代码。
void _tmain()
{
Test^ t = gcnew Test();
t->m_i = 99;
interior_ptr<int> p = &t->m_i;
printf("%d\r\n",*p);
Square(p);
printf("%d\r\n",*p);
int a = 10;
Square(&a);
printf("%d\r\n",a);
输出:
******* Output ******
99
9801
100
您可以看到我们如何将内部指针和原生指针都传递给同一个函数;这是因为原生指针会自动转换为内部指针。[请注意,内部指针不能转换为原生指针]
既然我们已经看到了内部指针如何用于通过引用传递值,那么应该在这里说明,使用跟踪引用也可以同样轻松地完成。请看下面一个使用跟踪引用作为参数执行相同操作的函数。
void Square2(int% pNum)
{
pNum *= pNum;
}
以及相应的调用者代码。
void _tmain()
{
Test^ t = gcnew Test();
t->m_i = 99;
printf("%d\r\n",t->m_i);
Square2(t->m_i);
printf("%d\r\n",t->m_i);
int a = 10;
Square2(a);
printf("%d\r\n",a);
跟踪引用和内部指针本质上非常相似,尽管您可以使用内部指针进行更多操作,例如指针算术和指针比较。
内部指针的指针算术
这是一些遍历并对 int
数组求和的示例代码。
void ArrayStuff()
{
array<int>^ arr = gcnew array<int> {2,4,6,8,3,5,7};
interior_ptr<int> p = &arr[0];
int s = 0;
while(p != &arr[0] + arr->Length)
{
s += *p;
p++;
}
printf("Sum = %d\r\n",s);
}
这是一些直接操作 System::String
的代码。
String^ str = "hello";
interior_ptr<Char> ptxt = const_cast< interior_ptr<Char> >(
PtrToStringChars(str));
for(int i=0; i<str->Length; i++)
*(ptxt+i) = *(ptxt+i) + 1;
Console::WriteLine(str);
如果您有自虐倾向,可以将 for
循环更改为
for(; (*ptxt++)++; *ptxt);
并获得相同的结果。它生成的代码不是很易读,但它确实证明了您可以对内部指针进行指针比较(在上面的示例中隐式检查 nullptr
)。
关注点
您不能让类成员成为内部指针,我猜这是为了保持语言互操作性。我的意思是,如果您的类充满了内部指针成员对象,您就不能让 VB 用户使用您的类,对吧?(如果我猜错了,请随时纠正我)
您不能使用内部指针指向 ref
对象,但可以指向 ref
对象的句柄。因此, interior_pointer<System::String>
是不允许的,但 interior_ptr<System::String^>
是合法的。
除非您明确为其提供其他默认值,否则所有内部指针都将隐式初始化为 nullptr
。
编译器将发出一个 modopt
来区分内部指针和跟踪引用。目前,这个 modopt
是 Microsoft::VisualC::IsCXXPointerModifier
(在 Microsoft.VisualC.dll 中找到),但在最终版本中,它将是 System::Runtime::CompilerServices::IsExplicitlyDereferenced
(在 Whidbey 及更高版本的 mscorlib.dll 中找到)。
对于 value
类,this
指针是一个内部指针。我猜是这样的,因为 value
类可以是受管类的成员,如果是这样,假设 value
类使用了 this
指针,如果该指针不是内部指针,可能会致命危险。(如果我猜错了,请随时纠正我)
value class V
{
void A()
{
interior_ptr<V> pV1 = this;
V* pV2 = this; //won't compile
}
};
结论
我希望我已经相当全面地介绍了内部指针及其在 C++/CLI 中的用法。请提交您的反馈,以便我能有机会改进本文。谢谢。