5.1.4 动态数组
在传统的Pascal中,数组的大小是固定的,并且在声明数据类型时限制了元素的数量。然而,Object Pascal支持动态数组的直接和本地实现。
注解:“直接实现动态数组” 与使用指针和动态内存分配来获得类似效果的方法截然不同… 后者代码非常复杂且容易出错。 顺便说一句,动态数组是大多数现代编程语言中唯一的一种结构形式。
动态数组是动态分配的,并进行引用计数(使得参数传递更快,因为只传递引用,而不是完整数组的副本)。当您完成使用数组时,可以通过将其变量设置为nil或将长度设置为零来清除数组,由于动态数组是引用计数的,编译器将自动释放内存。请注意,这仅适用于数组项使用的内存:如果数组保存对其他位置内存的引用(如对象引用),您需要确保在释放数组本身之前清理这些对象使用的内存。
使用动态数组时,您可以声明一个数组类型而不指定元素的数量,然后使用SetLength过程设置数组的大小:
var
Array1: array of Integer;
begin
// 这将导致运行时范围检查错误
// Array1[0] := 100;
SetLength(Array1, 10);
Array1[0] := 100; // 这是可以的
end;
在为数组设定长度并在堆上分配所需的内存之前,你不能使用数组。如果你这样做,要么会出现范围检查错误(如果相应的编译器选项处于激活状态),要么会在 Windows 平台上出现访问违规(Access Violation),或者在其他平台上出现类似的内存访问错误。SetLength 调用会将所有的值设置为零。数组初始化以后,你就可以立即开始读写数组值,而不必担心内存错误(除非超越了数组边界)。
如果确实需要显式分配内存,你也不必直接释放内存。在上面的代码片段中,当代码结束且 Array1 变量退出作用域时,编译器会自动释放其内存(在本例中是已分配的 10 个整数)。因此,虽然可以将动态数组变量赋值为 nil 或调用 SetLength 时赋值为 0,但一般不需要这样做(也很少这样做)。
请注意,SetLength 过程也可以用来调整数组的大小,如果要增大数组,则不会丢失当前的内容;如果要缩小数组,则会丢失一些元素。由于在最初的 SetLength 调用中只指定了数组的元素个数,动态数组的索引总是从 0 开始,直到元素个数减 1。换句话说,动态数组不支持经典静态 Pascal 数组的两个特性:非零低限和非整数索引。同时,动态数组与大多数基于 C 语法的语言中数组的工作方式更为相像。
要查询动态数组的当前大小,与静态数组一样,你可以使用 Length、High 和 Low 函数。但是,对于动态数组,Low 总是返回 0,而 High 总是返回长度减 1。这意味着,对于一个空数组,High 返回-1(仔细想想,这是一个奇怪的值,因为它比 Low 返回的值低)。
因此,在 DynArray
示例中,我使用自适应循环从动态数组中填充和提取信息。这是类型和变量定义:
type
TIntegersArray = array of Integer;
var
IntArray1: TIntegersArray;
使用以下循环,为数组分配内存并用匹配索引的值填充:
var
I: Integer;
begin
SetLength(IntArray1, 20);
for I := Low(IntArray1) to High(IntArray1) do
IntArray1[I] := I;
end;
第二个按钮的代码既显示每个值又计算平均值,类似于先前示例中的代码,但包含在一个循环中:
var
I: Integer;
Total: Integer;
begin
Total := 0;
for I := Low(IntArray1) to High(IntArray1) do
begin
Inc(Total, IntArray1[I]);
Show(I.ToString + ': ' + IntArray1[I].ToString);
end;
Show('Average: ' + (Total / Length(IntArray1)).ToString);
end;
这段代码的输出是相当明显的(大部分被省略):
0: 0
1: 1
2: 2
3: 3
...
17: 17
18: 18
19: 19
Average: 9.5
除了Length、SetLength、Low和High之外,还有其他一些常见的过程可用于数组,比如Copy函数,它允许您复制数组的一部分(或全部)。请注意,您还可以将一个数组从一个变量分配给另一个变量,但在这种情况下,您不是在进行完全复制,而是使两个变量引用相同的内存中的同一个数组。
仅在DynArray示例的最后部分中有略微复杂的代码,它以两种不同的方式将一个数组复制到另一个数组:
- 使用Copy函数,该函数在新的数据结构中使用单独的内存区域复制数组数据
- 使用赋值运算符,它实际上创建了一个别名,即一个新变量,引用相同的内存中的相同数组
在这一点上,如果您修改新数组的元素之一,您将会影响原始版本,或者根据复制的方式而不影响它。这是完整的代码:
var
IntArray2: TIntegersArray;
IntArray3: TIntegersArray;
begin
// 别名
IntArray2 := IntArray1;
// 单独的复制
IntArray3 := Copy(IntArray1, Low(IntArray1), Length(IntArray1));
// 修改项目
IntArray2[1] := 100;
IntArray3[2] := 100;
// 检查每个数组的值
Show(Format('[%d] %d -- %d -- %d', [1, IntArray1[1], IntArray2[1], IntArray3[1]]));
Show(Format('[%d] %d -- %d -- %d', [2, IntArray1[2], IntArray2[2], IntArray3[2]]));
end;
您将得到的输出如下:
[1] 100 -- 100 -- 1
[2] 2 -- 2 -- 100
对IntArray2的更改会波及到IntArray1,因为它们只是对同一物理数组的两个引用;对IntArray3的更改是独立的,因为它有数据的独立副本。
动态数组的本地操作
动态数组在Delphi XE7中引入了对常量数组的赋值和连接的支持。以下是一个示例,演示了这些操作:
var
DI: array of Integer;
I: Integer;
begin
DI := [1, 2, 3]; // 初始化
DI := DI + DI; // 连接
DI := DI + [4, 5]; // 混合连接
for I in DI do
begin
Show(I.ToString);
end;
end;
注意此代码中使用for-in
循环遍历数组元素,这是DynArrayConcat示例的一部分。这些数组可以基于任何数据类型,从简单的整数到记录和类。
除了赋值和连接之外,还可以在动态数组上使用对字符串常见的Insert和Delete等函数。
以下是使用Insert和Delete的示例:
var
DI: array of Integer;
I: Integer;
begin
DI := [1, 2, 3, 4, 5, 6];
Insert([8, 9], DI, 4);
Delete(DI, 2, 1); // 删除第三个项目(zero-based index)
end;