Visual C++.NET 2010开发实践:基于C++/CLI
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.5 C++/CLI数组

与标准C++可以自己维护堆不同,C++/CLI中动态分配的内存是由CLR来维护的。当不需要堆时,CLR自动将其删除并回收,同时CLR还能自动压缩内存堆以避免产生不必要的内存碎片。C++/CLI的这种机制能够避免内存泄露和产生内存碎片,被称为垃圾回收。而由CLR管理的这种堆被称为CLR堆,它是由gcnew操作符创建的。

由于垃圾回收机制会改变堆中对象的地址,如果使用指针,则指针将不再有效,因此不能在CLR堆中使用普通C++指针。为此,CLR提供了跟踪句柄和跟踪引用来安全地访问堆中的对象。

1.5.1 跟踪句柄

跟踪句柄类似于本地标准C++中的指针,其中存储着某个对象的地址。但是与C++指针不同的是,当CLR压缩堆过程中时将改变该对象的实际地址,由垃圾回收器自动更新跟踪句柄所包含对象的地址。所以,在程序中不能够像本地C++指针那样执行地址的算术运算,也不允许对跟踪句柄进行强制类型转换。

跟踪句柄所引用的对象是在CLR堆中被创建的,并且所有属于引用类型的对象都存储在堆中,因此为了能够引用这些对象而创建的变量都必须是该类型对象的跟踪句柄。例如,为了能够引用一个String类型的对象,必须创建一个String类型的跟踪句柄。

在声明一个跟踪句柄时,可以将符号“^”添加到类型名称的后面用于指定该类型的句柄变量。当声明某个句柄时,系统会自动将该句柄初始化为空值,以表示该句柄未引用任何对象。同时可以通过关键字nullptr显式地将跟踪句柄初始化为空值,还可以将跟踪句柄初始化为指定的值。例如:

        String^ name;               // 声明名称为name的String类型的句柄,初始值为空
        String^ words = nullptr;   // 声明名称为words的句柄, 并显示初始化为空
        String^ saying = L"Hello World!";  // 声明名称为saying的句柄, 并指定初始化值

如果在基本数据类型后添加“^”符号,那么将创建一个值类型的跟踪句柄,并且将在CLR堆上创建该类型的变量。由于此时定义的变量相当于本地C++中的指针,所以此变量不能够参与算术运算,而需要通过“*”运算符对地址求值。例如:

        int^ value = 99;
        int result = 2 * (*value) + 15;   // result = 2*99+15=213

同时,如果当value作为左值时,可以直接对value指向的变量赋值而不需要通过“*”运算符对地址求值。但是在这种情况下,作为左值的跟踪句柄必须已经实际定义过,如果仅仅是一个声明,则会在运行时产生错误。例如:

        int^ result1 = 0;               // 警告: 不能用0来初始化句柄
        result1 = 2 * (*value) + 15;   // 或者: *result1 = 2 * (*value) + 15
        int^ result2;
        *result2 = 2 * (*value) + 15;  // 错误, resuilt2并未引用实际的对象

因此,凡是在CLR堆上创建的对象都必须被跟踪句柄所引用,其中,这些对象中包括了用gcnew操作符显式地创建在堆上的对象和所引用的数据类型。然而需要注意的是,所有分配在堆上的对象都不能在全局范围内被创建。例如,在程序中不能够创建一个String类型的全局变量。

跟踪引用类似于本地C++引用,用于表示某对象的别名。可以给堆栈上的值对象、CLR堆上的跟踪句柄创建跟踪引用。与本地C++定义引用的方式不同,C++/CLI通过“%”符号定义句柄的跟踪引用。例如:

        int value = 10;
        int% trackValue = value;
        trackValue *= 5;
        Console::WriteLine(value);     // value = 50

跟踪引用本身永远是在堆栈上创建的,如果垃圾回收移动了被引用的对象,则跟踪引用将被自动更新。

1.5.2 CLR数组

CLR数组与标准C++数组不同,CLR数组所占用的内存是在可回收垃圾堆上分配的,并且CLR数组提供了一些内置功能用于取得数组的相关信息和操作数组等。

在创建CLR数组时,需要通过关键字array定义数组对象的跟踪句柄,并且需要在array关键字后通过尖括号(“< >”)为数组中的元素指定数据类型。例如:

        array<int>^ value;               // 声明一个数组元素为int类型的数组跟踪句柄
        array<String^>^ lines = nullptr;// 声明一个数组元素为字符串的数组,并初始化为空

在声明数组变量的同时,可以通过gcnew运算符在堆上创建CLR数组,并且还可以通过圆括号(“( )”)指定新创建的数组所能够包含元素的数目。并且,在CLR数组中包含Length属性,该属性为32位的整数值,用于记录数组中所能包含元素的数量(即数值的长度),而CLR数组中的LongLength属性将以64位表示数组长度。可以通过“->”运算符访问数组对象中的属性。例如:

        array<int>^ data = gcnew array<int>(100);   // 创建一个长度为100的整形数组
        Console::WriteLine(L"数组的长度为:{0}",data->Length);// 输出数组的长度为100

数组可以在创建时通过元素列表初始化,也可以通过gcnew显式地创建数组对象时初始化数组。例如:

        array<double>^ sample1 = {3.4, 2.3, 6.8, 1.2, 5.5, 4.9, 7.4, 1.6};
        array<double>^ sample2 = gcnew array<double>{3.4, 2.3, 6.8, 1.2, 5.5, 4.9, 7.4, 1.6}
        array<String^>^names={L"Jack",L"John",L"Joe",L"Jessica",L"Jim",L"Joanna"};

与标准C++数组相同,CLR数组中元素的索引值同样以0开始,可以通过“[ ]”运算符访问数组指定位置的元素,也可以通过for each循环遍历数组中的所有元素。例如:

        array<int>^ value = {3, 5, 6, 8, 6};
        for each(int item in value)        // 遍历数组
            Console::Write(L"{0, 5}, ", item);
        Console::WriteLine();

C++/CLI提供的for each循环语句用于重复处理一组特定类型对象中的所有对象。在for each循环中,首先需要定义一个与集合中元素类型相同的迭代变量,该变量作为一个只读的局部变量,用来依次获得集合中的每个元素。然后,通过in关键字指定需要被遍历的集合对象。

【例1.8】 一维数组的使用示例。创建一个元素类型为double类型的一维数组,并将其中的元素初始化为随机数,然后从数组中找出其中的最大值。代码如下:

        // Ex1_8.cpp : 主项目文件
        #include "stdafx.h"
        using namespace System;
        int main(array<System::String ^> ^args)
        {
            Random^ random = gcnew Random();        // Random对象,用于产生随机数
            array<double>^ numbers = gcnew array<double>(50);
            // 产生一个随机数
            Console::WriteLine(L"产生一个随机数.");
            for ( int i = 0; i<numbers->Length; i++)
              numbers[i] = random->NextDouble()*100.0;//产生一个0~100.0之间的随机数
            // 输出数组中的元素
          Console::WriteLine(L"数组中的元素为: ");
          for (int i = 0; i<numbers->Length; i++)
          {
            Console::Write(L"{0, 10:F2}", numbers[i]);//右对齐,保留两位有效小数位输出
              if ((i+1) % 5 == 0) Console::WriteLine();
          }
          // 找出数组中的最大值
          double max = 0.0;
          for each (double value in numbers)
              max = value > max ? value : max;
          Console::WriteLine(L"数组中的最大值为: {0:F2}", max);
          return 0;
      }

在Ex1_8项目中输出数组的元素和最大值时,将在每行输出5个数组元素,其中每个元素占10个字符的宽度,并保留两位有效小数位。Ex1_8项目的运行结果如图1.12所示。

图1.12 Ex1_8项目的运行结果

1.5.3 数组的排序及查找

事实上,所有CLR数组都隐式地继承于System::Array类。该类提供了创建、操作、搜索和排序数组的基本方法,因而作为所有CLR数组的基类。Array类的常用方法及说明如表1.6所示。

表1.6 Array类的常用方法及说明

1. 数组排序

Array类提供了一组Sort静态方法,分别用于对数组中的部分元素或整个数组进行排序。Sort方法使用快速排序(Quick Sort)算法对数组中的元素进行排序,因此该方法执行的是不稳定排序。也就是说,如果数组中两个元素相等,则其所处的顺序可能会与排序前的顺序不同。Array类的Sort方法及重载方法的声明如下:

      static void Sort(array<T>^array);
      static void Sort(array<T>^array,int index,int length);
      static void Sort(array<T1>^keys,array<T2>^items);
      static void Sort(array<T1>^keys,array<T2>^items,int index,int length);

其中,array、keys和items参数表示需要排序的数组,并且T、T1和T2分别表示数组中元素的类型;index参数指定了数组中排序范围的起始索引,而需要排序的元素个数由length参数指定。

另外,Sort方法还可以对两个相关的数组进行排序。其中,第一个数组中的元素作为第二个数组对应元素的键,当对第一个数组中的元素排序后,将对第二个数组中元素的顺序进行相应的调整,以使其与第一个数组中的元素相对应。例如:

        array<double>^ grades = { … };// 若grades中值为: 3.0, 4.0, 2.0, 1.0
        array<String^>^ names = { … };// 若names中值为: "A", "B", "C", "D"
        Array::Sort(grades, names);  // 排序后, grade中元素顺序为: 1.0, 2.0, 3.0, 4.0
                                      // 排序后, names中元素顺序为: "D", "C", "A", "B"

由于Sort方法采用快速排序算法对数组进行排序,所以,在一般情况下该方法执行的时间复杂度为O(nlogn)(其中n为数组的长度);而在最坏的情况下,Sort方法执行的时间复杂度为O(n2)。

【例1.9】 一维数组的排序示例。首先对包含学生姓名的数组进行排序,并为这些学生分别产生一个随机的成绩。然后,将包含成绩信息和学生姓名的数组作为两个相关数组,并按照成绩的高低对这两个相关数组进行排序,并输出排序的结果。代码如下:

        // Ex1_9.cpp : 主项目文件
        #include "stdafx.h"
        using namespace System;
        int main(array<System::String ^> ^args)
        {
            array<String^>^ names = { L"林一帆", L"张薇", L"赵琳", L"孙研", L"马琳琳" }
            Console::WriteLine(L"\n排序前的学生姓名为: ");
            for each (String^ name in names)
              Console::Write(L"\t{0}", name);
            Array::Sort(names);
            Console::WriteLine(L"\n排序后的学生姓名为: ");
            for each (String^ name in names)
              Console::Write(L"\t{0}", name);
          Random^ random = gcnew Random();
          array<double>^ grades = gcnew array<double>(5);
          for (int i = 0; i<grades->Length; i++)
              grades[i] = random->NextDouble() * 100.0;
          Console::WriteLine(L"\n学生对应的成绩为: ");
          for each (double grade in grades)
              Console::Write(L"\t{0:F2}", grade);
          Array::Sort(grades, names);
          Console::WriteLine(L"\n按成绩排序后的成绩及学生姓名:");
          for each (double grade in grades)
              Console::Write(L"\t{0:F2}", grade);
          Console::WriteLine();
          for each (String^ name in names)
              Console::Write(L"\t{0}", name);
          Console::WriteLine();
          return 0;
      }

Ex1_9项目运行后,当对包含成绩信息和学生姓名的两个相关数组进行排序时,学生姓名数组中元素的顺序将按照成绩数组的排序而排列。Ex1_9项目的运行结果如图1.13所示。

图1.13 Ex1_9项目的运行结果

2. 查找元素

Array类还提供了一组BinarySearch静态方法,可以通过二分查找法从一维数组的所有或部分元素中搜索特定的元素,并返回该元素在数组中的索引。BinarySearch方法的声明如下:

      static int BinarySearch(array<T>^array,T value);
      static int BinarySearch(array<T>^array,int index,int length,T value);

其中,array参数是被搜索数组的句柄,需要查找的元素由value参数指定。如果需要指定搜索范围,那么指定index参数为搜索的起始索引,并指定length参数为搜索的元素数量。

BinarySearch方法返回一个int类型的整数值,如果其返回值小于0,则说明该元素未包含在数组中。然而需要说明的是,在通过BinarySearch方法查找元素时,其查找的数组中的元素必须是顺序排列的,因此在搜索之前必须对数组进行排序。例如:

        Array::Sort(data);
        int pos = Array::BinarySearch(data, value);

另外,当通过BinarySearch方法未查找到目标元素时,该方法并非返回一个任意的负数,而是返回第一个大于目标元素的数组元素的索引值的补码;如果数组中不包含目标元素且不含有任何大于目标元素的数组元素,那么该方法将返回数组中最后一个数组元素的索引值加1的补码。

【例1.10】 从数组中查找指定的元素。在main函数中创建两个数组,其中一个数组用于保存学生的姓名,而另一个数组保存对应的成绩。然后通过Array类的BinarySearch方法从姓名数组中查找指定的学生姓名,并输出该学生的成绩。代码如下:

          // Ex1_10.cpp : 主项目文件
          #include "stdafx.h"
          using namespace System;
          int main(array<System::String ^> ^args)
          {
              array<String^>^ names = { L"林一帆", L"张薇", L"赵琳", L"孙研", L"马琳琳" };
              array<String^>^ found = { L"张薇", L"梦帆", L"孙研", L"赵琳" };
              Random^ random = gcnew Random();
              array<double>^ grades = gcnew array<double>(5);
              for (int i = 0; i<grades->Length; i++)
                grades[i] = random->NextDouble() * 100.0;
              Array::Sort(names, grades);
              Console::WriteLine(L"学生的姓名和成绩为: ");
              for each (String^ name in names)
                Console::Write(L"\t{0}", name);
              Console::WriteLine();
              for each (double grade in grades)
                Console::Write(L"\t{0:F2}", grade);
              Console::WriteLine();
              int i = -1;
              for each (String^ name in found)
              {
                i = Array::BinarySearch(names, name);
                if (i < 0) Console::WriteLine(L"未查找到学生: {0}", name);
                else Console::WriteLine(L"学生{0} 的成绩为: {1:F2}",
                                          name, grades[i]);
              }
              return 0;
          }

Ex1_10项目运行后,在进行从学生姓名数组中查找指定姓名的操作前,需要对该数组中的元素进行排序。如果Array::BinarySearch方法返回的值小于0,则说明数组中不包含该学生的姓名。Ex1_10项目运行结果如图1.14所示。

图1.14 Ex1_10项目的运行结果

1.5.4 多维数组

在C++/CLI中可以创建二维或多维数组,其中数组的最大维数为32。然而与标准C++多维数组不同的是,CLR数组是真正意义上的多维数组,而非数组的数组。

在创建CLR数组时,如果在尖括号(“< >”)中未指定数组的维数,那么默认创建一维数组。而在创建CLR多维数组时,需要在尖括号内元素类型后面指定数组的维数,维数中间用逗号(“,”)隔开。例如:

        array<int, 2>^ data = gcnew array<int, 2>(4, 5);//创建一个二维数组,且为4行5列

访问CLR多维数组中元素的方法是分别通过多个用逗号分隔的索引值访问每一个元素,而不能用一个索引值访问整个一行。例如,如果需要访问CLR二维数组中第i行、第j列的元素,那么需要通过[i, j]的方式来访问。例如:

        for (int i = 0; i<rows; i++) {
            for (int j = 0; j<cols; j++)
              Console::Write(L"{0,5}, ", data[i,j]);  // 输出第i行、第j列的元素
            Console::WriteLine();
        }

【例1.11】 多维数组的使用示例。在main函数中创建一个二维数组,并将9×9乘法表保存到该数组中,然后输出该二维数组。代码如下:

        // Ex1_11.cpp : 主项目文件
        #include "stdafx.h"
        using namespace System;
        int main(array<System::String ^> ^args)
        {
            array<int, 2>^ table = gcnew array<int, 2>(9, 9);
            for (int i = 0; i<9; i++)
              for (int j = 0; j<9; j++)
                  table[i, j] = (i+1) * (j+1);
            Console::WriteLine(L"二维数组中的9×9乘法表:");
            for (int i = 0; i<9; i++)
          {
              for (int j = 0; j<=i; j++)
                Console::Write(L"{0,3}  |", table[i, j]);
              Console::WriteLine();
          }
          return 0;
      }

Ex1_11项目运行后,其执行结果如图1.15所示。

图1.15 Ex1_11项目的运行结果

1.5.5 数组的数组

由于在创建CLR数组时,可以将数组元素指定为任意的对象类型,所以如果将数组元素指定为一个数组的跟踪句柄类型,那么也就创建了数组的数组,即所谓的锯齿形数组。例如:

        array< array< String^ >^ >^ grades = gcnew array< array<String^>^ >(5);

从上面的代码中可以看出,跟踪句柄grades引用了一个包含5个元素的数组。其中每个元素实际上也是数组的跟踪句柄,并且它们都引用一个元素为String^类型的数组。那么可以通过以下方式来初始化grades所引用的数组。例如:

        grades[0] = gcnew array<String^>{L"王林",L"程明"};
        grades[1] = gcnew array<String^>{L"韦平平",L"李方",L"林一凡"};
        grades[2] = gcnew array<String^>{L"赵琳",L"张强民",L"王敏",L"张薇"};
        grades[3] = gcnew array<String^>{L"李计",L"马琳琳",L"孙研",L"孙祥庆",L"罗林琳"};
        grades[4] = gcnew array<String^>{L"李红庆", L"吴薇华"};

其中,grades[i]将访问grades所引用的数组中的第i个元素,而各元素为指向String^类型数组的句柄。因此上面的语句将创建一个String对象句柄的数组,并将被创建数组的地址赋值给了grades数组中的每个元素。

【例1.12】 数组的数组的使用示例。创建一个元素为array<String^>^类型的数组,在main函数中遍历整个数组,并输出每个数组元素中的学生姓名。代码如下:

        // Ex1_12.cpp: 主项目文件
        #include "stdafx.h"
        using namespace System;
        int main(array<System::String ^> ^args)
        {
            array<array<String^>^>^ grades =
            {
              gcnew array<String^>{L"王林", L"程明"},
              gcnew array<String^>{L"韦平平", L"李方", L"林一凡"},
              gcnew array<String^>{L"赵琳", L"张强民", L"王敏", L"张薇"},
              gcnew array<String^>{L"李计", L"马琳琳", L"孙研", L"孙祥庆",L"罗林琳"},
              gcnew array<String^>{L"李红庆", L"吴薇华"}
            };
            wchar_t gradeLetter = 'A';
            for each(array<String^>^ grade in grades)
            {
              Console::WriteLine(L"成绩等级为{0} 学生:", gradeLetter++);
              for each(String^ student in grade)
                  Console::Write("\t{0}", student);
              Console::WriteLine();
            }
            return 0;
        }

Ex1_12项目运行后,其执行结果如图1.16所示。

图1.16 Ex1_12项目的运行结果