65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (28投票s)

2004年11月29日

5分钟阅读

viewsIcon

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 来区分内部指针和跟踪引用。目前,这个 modoptMicrosoft::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 中的用法。请提交您的反馈,以便我能有机会改进本文。谢谢。

C++/CLI 中的内部指针概述 - CodeProject - 代码之家
© . All rights reserved.