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

C++/CLI 中的数组

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (48投票s)

2004年7月13日

Ms-PL

5分钟阅读

viewsIcon

589208

本文介绍了 C++/CLI 中用于声明和使用 CLI 数组的新数组语法。

引言

托管数组在 CLI 堆上分配,而原生数组在非托管 C++ 堆上分配。这意味着它们是典型的垃圾回收的 .NET 对象。在本文中,“数组”一词将指代托管数组,如果提到原生数组,将明确称之为“原生数组”。有些人可能已经看过我关于使用旧 MC++ 语法中的托管数组的文章,我敢肯定有些人会对那些非常奇怪的语法发出痛苦的呻吟。嗯,那些人应该会很高兴地知道,C++/CLI 使用比旧语法更直观的语法支持托管数组。

基本特性

以下是 C++/CLI 中托管数组的一些基本特性:

  • 语法类似于 C++ 模板
  • System::Array 是所有托管数组的自动基类型。
  • 在 CLR 堆上分配(意味着它们会被垃圾回收)
  • 数组的秩不必是 1(秩为 1 的数组称为一维数组,秩大于 1 的数组称为多维数组)。
  • 轻松支持交错数组(与旧语法不同,交错数组更容易声明和使用)。
  • 隐式转换为 System::Array BCL 类,以及从 System::Array 显式转换。
  • 一旦实例化了一个数组,其秩和维数就不能更改。

数组类型的伪模板

C++/CLI 中数组类型的声明和使用似乎使用了虚拟的模板类型(当然没有这样的模板,这都是 VC++ 编译器魔术):

namespace stdcli::language
{
    template<typename T, int rank = 1>
        ref class array : System::Array {};
}

array 声明在 stdcli::language 命名空间内,以避免与现有源代码冲突。

一维数组使用

引用类型的数组

ref class R
{
public:
    void Test1(int x)
    {
        array<String^>^ strarray = gcnew array<String^>(x);
        for(int i=0; i<x; i++)
            strarray[i] = String::Concat("Number ",i.ToString());
        for(int i=0; i<x; i++)
            Console::WriteLine(strarray[i]);
    }
};

语法确实与原生数组不同。使用过模板的 C++ 程序员应该会觉得比没有 C++ 背景的人更容易理解,但最终他们会习惯的。

值类型的数组

ref class R
{
public:
    void Test2(int x)
    {
        array<int>^ strarray = gcnew array<int>(x);
        for(int i=0; i<x; i++)
            strarray[i] = i * 10;
        for(int i=0; i<x; i++)
            Console::WriteLine(strarray[i]);
    }
};

与旧语法不同,值类型的数组语法与托管类型的数组语法完全相同。

多维数组使用

多维数组是秩大于 1 的托管数组。它们不是数组的数组(那是交错数组)。

ref class R
{
public:
    void Test3()
    {
        array<String^,2>^ names = gcnew array<String^,2>(4,2);
        names[0,0] = "John";
        names[1,0] = "Tim";
        names[2,0] = "Nancy";
        names[3,0] = "Anitha";
        for(int i=0; i<4; i++)
            if(i%2==0)
                names[i,1] = "Brown";
            else
                names[i,1] = "Wilson";
        for(int i=0; i<4; i++)
            Console::WriteLine("{0} {1}",names[i,0],names[i,1]);
    }    
};

交错数组

交错数组是非矩形的,实际上是数组的数组。新的模板风格数组语法简化了交错数组的声明和使用,这比旧语法是一个重大的改进,在旧语法中,交错数组必须由开发人员人为模拟。

ref class R
{
public:
  void Test()
  {
    array<array<int>^>^ arr = gcnew array<array<int>^> (5); 

    for(int i=0, j=10; i<5; i++, j+=10)
    {
      arr[i] = gcnew array<int> (j);
    }

    Console::WriteLine("Rank = {0}; Length = {1}",
      arr->Rank,arr->Length);
    /*
      Output :-
        Rank = 1; Length = 5
    */

    for(int i=0; i<5; i++)
      Console::WriteLine("Rank = {0}; Length = {1}",
        arr[i]->Rank,arr[i]->Length);
    /*
      Output :-
        Rank = 1; Length = 10
        Rank = 1; Length = 20
        Rank = 1; Length = 30
        Rank = 1; Length = 40
        Rank = 1; Length = 50
    */
  }
};

使用 typedef 简化交错数组的使用

typedef array<array<int>^> JaggedInt32Array;
typedef array<array<String^>^> JaggedStringArray;

ref class R
{
public:
    void Test()
    {
        JaggedInt32Array^ intarr = gcnew JaggedInt32Array(10);
        JaggedStringArray^ strarr = gcnew JaggedStringArray(15);        
    }
};

直接初始化数组

新语法允许轻松地直接初始化数组。

ref class R1
{
public:
  void Test()
  {
    //Single dimensional arrays
    array<String^>^ arr1 = gcnew array<String^> {"Nish", "Colin"};
    array<String^>^ arr2 = {"Nish", "Smitha"};
    
    //Multi dimensional arrays
    array<Object^,2> ^ multiobarr = {{"Nish", 100}, {"Jambo", 200}};
  }
};

数组作为函数参数

ref class R
{
public:
  void ShowStringArray(array<String^>^ strarr)
  {
    for(int i=0; i<strarr->Length; i++)
      Console::WriteLine(strarr[i]);
  }
  void Show2DInt32Array(array<int,2>^ arr)
  {    
    for(int i=0; i<arr->GetLength(0); i++)
    {
      Console::WriteLine("{0} times 2 is {1}",arr[i,0],arr[i,1]);
    }
  }
};

void _tmain()
{
  R^ r = gcnew R();
  r->ShowStringArray(gcnew array<String^> {"hello", "world"});
  array<int,2>^ arr = { {1,2}, {2,4}, {3,6}, {4,8} };
  r->Show2DInt32Array(arr);
}

//Output :-

/*
  hello
  world
  1 times 2 is 2
  2 times 2 is 4
  3 times 2 is 6
  4 times 2 is 8
*/

从函数返回数组

ref class R
{
public:
    array<String^>^ GetNames(int count)
    {
        array<String^>^ arr = gcnew array<String^>(count);
        for(int i=0; i<count; i++)
        {
            arr[i] = String::Concat("Mr. ",(i+1).ToString());
        }
        return arr;
    }

    void ShowNames(array<String^>^ arr)
    {
        for(int i=0; i<arr->Length; i++)
            Console::WriteLine(arr[i]);
    }
};

void _tmain()
{
    R^ r = gcnew R();
    array<String^>^ arr = r->GetNames(7);
    r->ShowNames(arr);
}

数组协变性

您可以将类型 R 的数组分配给类型 B 的数组,其中 B 是 R 的直接或间接父类,并且 R 是一个 ref 类。

ref class R1
{
    //...
};

ref class R2 : R1
{
    //...
};

void _tmain()
{
    array<R1^>^ arr1 = gcnew array<R1^>(4);
    array<R2^>^ arr2 = gcnew array<R2^>(4);    
    
    array<R1^>^ arr3 = arr2;    
    array<R1^>^ arr4 = gcnew array<R2^>(4);    
}

参数数组

C++/CLI 支持参数数组。每个函数只能有一个此类参数数组,并且它必须是最后一个参数。

ref class R
{
public:
    void Test(String^ s, [ParamArray] array<int>^ arr )    
    {
        Console::Write(s);
        for(int i=0; i<arr->Length; i++)
            Console::Write(" {0}",arr[i]);
        Console::WriteLine();
    }
};

void _tmain()
{
    R^ r = gcnew R();
    r->Test("Nish");
    r->Test("Nish",1);
    r->Test("Nish",1,15);
    r->Test("Nish",1,25,100);
}

目前唯一支持的语法是使用 ParamArray 属性,但最终的语法也将支持下面显示的更简单的样式:

  • void Test(String^ s, ... array<int>^ arr )

调用 System::Array 方法

由于每个 C++/CLI 数组隐式都是一个 System::Array 对象,我们可以对 CLI 数组使用 System::Array 方法。

ref class R
{
public:
  bool CheckName(array<String^>^ strarr, String^ str)
  {
    Array::Sort(strarr);
    return Array::BinarySearch(strarr,str) < 0 ? false : true;
  }
};

void _tmain()
{
  R^ r = gcnew R();
  array<String^>^ strarr = {"Nish","Smitha",
    "Colin","Jambo","Kannan","David","Roger"};  
  Console::WriteLine("Nish is {0}",r->CheckName(strarr,"Nish") ? 
    gcnew String("Present") : gcnew String("Absent"));
  Console::WriteLine("John is {0}",r->CheckName(strarr,"John") ? 
    gcnew String("Present") : gcnew String("Absent"));
}

我在上面的示例中使用了 System::SortSystem::BinarySearch

这是另一个清晰展示隐式转换为 System::Array 的代码片段。

ref class R
{
public:
    void ShowRank(Array^ a)
    {
        Console::WriteLine("Rank is {0}",a->Rank);
    }
};

void _tmain()
{
    R^ r = gcnew R();    
    r->ShowRank( gcnew array<int>(10) );
    r->ShowRank( gcnew array<String^,2>(10,2) );
    r->ShowRank( gcnew array<double,3>(10,3,2) );    
    r->ShowRank( gcnew array<int> {1,2,3} );
    r->ShowRank( gcnew array<int,2> {{1,2}, {2,3}, {3,4}} );
}

非 CLI 对象的数组

您可以声明 C++/CLI 数组,其中数组类型是非 CLI 对象。唯一的限制是该类型必须是指针类型。考虑以下原生 C++ 类:

#define Show(x) Console::WriteLine(x)

class N
{
public:
   N()
   {
      Show("N::ctor");
   }
   ~N()
   {
      Show("N::dtor");
   }
};

现在,这是声明此类数组的方式:

ref class R
{
public:   
   void Test()
   {
      array<N*>^ arr = gcnew array<N*>(3);
      for(int i=0; i<arr->Length; i++)   
         arr[i] = new N();
   }
};

使用以下测试代码来使用这个类:

void _tmain()
{
   R^ r = gcnew R();
   r->Test();
   Show("Press any key...");
   Console::ReadKey();
}

/* Output:

N::ctor
N::ctor
N::ctor
Press any key...

*/

好了,这样可以了。当然,数组元素的析构函数没有被调用,事实上,即使发生垃圾回收并且数组对象被清理,它们也不会被调用。由于它们是在标准 C++ 堆上的原生对象,因此需要单独对它们调用 `delete`。

ref class R
{
public:   
   void Test()
   {
      array<N*>^ arr = gcnew array<N*>(3);
      for(int i=0; i<arr->Length; i++)   
         arr[i] = new N();
      for(int i=0; i<arr->Length; i++)   
         delete arr[i];
   }
   //...

/* Output

N::ctor
N::ctor
N::ctor
N::dtor
N::dtor
N::dtor
Press any key...

*/

好的,现在情况好多了。或者,如果您想避免对每个对象调用 `delete`,您可以选择这样做:

ref class R
{
public:   
   void Test()
   {
      N narr[3];
      array<N*>^ arr = gcnew array<N*>(3);
      for(int i=0; i<arr->Length; i++)
         arr[i] = &narr[i];   
   }   
};

这会产生与上面代码片段类似的结果。或者,您可以在其包含类的构造函数中初始化数组成员,并在析构函数中删除它们,然后将包含类用作自动变量(C++/CLI 支持确定性析构)。

使用原生指针直接操作 CLI 数组

这是一段代码,展示了如何使用原生指针直接操作数组的内容。第一个示例是一维数组,第二个示例是交错数组。

原生访问一维数组

void Test1()    
{
    array<int>^ arr = gcnew array<int>(3);
    arr[0] = 100;
    arr[1] = 200;
    arr[2] = 300;

    Console::WriteLine(arr[0]);
    Console::WriteLine(arr[1]);
    Console::WriteLine(arr[2]);

    /*
    Output :-
      100
      200
      300
    */

    // Modifying the array using a native int* 
    // that points to a pinned pointer in GC'd heap
    pin_ptr<int> p1 = &arr[0];
    int* p2 = p1;
    while(*p2)
    {           
        (*p2)++;
        p2++;
    }

    Console::WriteLine(arr[0]);
    Console::WriteLine(arr[1]);
    Console::WriteLine(arr[2]);

    /*
    Output :-
      101
      201
      301
    */
} 

原生访问交错数组

void Test2()
{
    array<array<int>^>^ arr = gcnew array<array<int>^>(2);
    arr[0] = gcnew array<int>(2);
    arr[1] = gcnew array<int>(2);
    arr[0][0] = 10;
    arr[0][1] = 100;
    arr[1][0] = 20;
    arr[1][1] = 200;

    Console::WriteLine(arr[0][0]);
    Console::WriteLine(arr[0][1]);
    Console::WriteLine(arr[1][0]);
    Console::WriteLine(arr[1][1]);

    /*
    Output:
      10
      100
      20
      200
    */


    // Copying the managed jagged array to
    // a native array of pointers and accessing
    // the members using the native array
    pin_ptr<int> p1 = &arr[0][0];
    pin_ptr<int> p2 = &arr[1][0];
    int* p3[2];
    p3[0] = p1;
    p3[1] = p2;

    Console::WriteLine(p3[0][0]);
    Console::WriteLine(p3[0][1]);
    Console::WriteLine(p3[1][0]);
    Console::WriteLine(p3[1][1]);

    /*
    Output:
      10
      100
      20
      200
    */

    // Assigning the native array of pointers
    // to a native int** and modifying the array
    int** p4 = p3;
    for(int i=0; i<2; i++)
        for(int j=0; j<2; j++)
            p4[i][j] += 3;          

    Console::WriteLine(arr[0][0]);
    Console::WriteLine(arr[0][1]);
    Console::WriteLine(arr[1][0]);
    Console::WriteLine(arr[1][1]);

    /*
    Output:
      13
      103
      23
      203
    */
}

本质上,我们使用一个固定指针指向 GC 分配的堆,然后将强制转换后的原生指针视为指向原生数组。这为我们提供了一种非常快速的操作数组内容的方法!

结论

C++/CLI 中的数组语法与旧的 MC++ 语法相比,绝对是一种美学上的改进,并且它还带来了之前严重缺乏的语法一致性。模板风格的语法应该让 C++ 程序员感觉自然,尽管可能需要一些时间才能完全习惯。一如既往,我请求您随时发布您对我的任何评论、建议、批评、赞扬等。

历史

  • 2004 年 7 月 13 日 - 首次发布
  • 2004 年 8 月 12 日 - 通过添加关于使用非 CLI 对象作为数组成员以及使用指针直接访问数组的部分来更新文章。
© . All rights reserved.