C++/CLI 中的数组






4.93/5 (48投票s)
本文介绍了 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::Sort
和 System::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 对象作为数组成员以及使用指针直接访问数组的部分来更新文章。